import json
from decimal import Decimal

from django.apps import apps
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
from django.db import connections, models, router
from django.db.models.fields.files import FileField, ImageField
from django.db.models.fields.related import ForeignKey, OneToOneField
from django.utils import timezone

from core.models import LotType


MASTER_VIEW_SAMPLE_ROWS = [
    {
        "PRO0": "1",
        "PRO1": "4070207560",
        "PRO1C": "NOGUCHI SEIKI",
        "PRO2": "260218548-021",
        "PRO5": "1",
        "PRO8": "35",
        "PRO9": "-",
        "PRO10": "755",
        "PRO11": "65",
        "PRO12": "355",
        "PRO13": "WA",
        "PRO14": "280",
        "PRO15": "J",
        "PRO16": "8",
        "PRO17": "B",
        "PRO18": "57A",
        "PRO21": "0",
        "PRO25": "2026-02-18 00:00:00.000",
        "PRO27": "0",
        "P2": "15578",
        "PRO6": "57000",
        "SPEED": "500",
        "PRO-TOOL": "PA54508-02BANANAS",
        "PRO4": "2100030080-220",
        "MC11": "35",
        "MC12": "DISK",
        "MC14": "755",
        "MC15": "65",
        "MC16": "355",
        "MC19": "WA",
        "MC20": "280",
        "MC21": "J",
        "MC22": "8",
        "MC23": "B",
        "MC24": "57A",
        "MP45": "OUT",
        "MP49": " ",
        "MI13": "1",
        "TC": "4070207560",
        "MI14": "01",
        "MI15": "23",
        "MI16": "280",
        "MI17": "J",
        "MI18": "23",
        "MI19": "52",
        "MI20": "0",
        "MI21": "NULL",
        "MI22": "0",
        "MI23": "0",
        "MI31": "NULL",
        "MI33": "NULL",
        "INSAGM": "NULL",
        "MARAGM": "NULL",
        "MI53": "Q195",
        "MI55": "Q195",
        "MI36": "000000000",
        "MI39": "000",
        "MI24": "0",
        "Ind1": "NULL",
        "Ind2": "NULL",
        "Ind3": "NULL",
        "AGR1": "",
        "AGR2": "",
        "ob_Condition": "RH-60Kg",
        "Japanese_CustomerName": "野口精機",
    },
    {
        "PRO0": "1",
        "PRO1": "4070207560",
        "PRO1C": "NOGUCHI SEIKI",
        "PRO2": "260218549-022",
        "PRO5": "1",
        "PRO8": "35",
        "PRO9": "-",
        "PRO10": "755",
        "PRO11": "65",
        "PRO12": "355",
        "PRO13": "WA",
        "PRO14": "280",
        "PRO15": "J",
        "PRO16": "8",
        "PRO17": "B",
        "PRO18": "57A",
        "PRO21": "0",
        "PRO25": "2026-02-18 00:00:00.000",
        "PRO27": "0",
        "P2": "15578",
        "PRO6": "57000",
        "SPEED": "500",
        "PRO-TOOL": "PA54508-02BANANAS",
        "PRO4": "2100030080-230",
        "MC11": "35",
        "MC12": "DISK",
        "MC14": "755",
        "MC15": "65",
        "MC16": "355",
        "MC19": "WA",
        "MC20": "280",
        "MC21": "J",
        "MC22": "8",
        "MC23": "B",
        "MC24": "57A",
        "MP45": "OUT",
        "MP49": " ",
        "MI13": "1",
        "TC": "4070207560",
        "MI14": "01",
        "MI15": "23",
        "MI16": "280",
        "MI17": "J",
        "MI18": "23",
        "MI19": "52",
        "MI20": "0",
        "MI21": "NULL",
        "MI22": "0",
        "MI23": "0",
        "MI31": "NULL",
        "MI33": "NULL",
        "INSAGM": "NULL",
        "MARAGM": "NULL",
        "MI53": "Q195",
        "MI55": "Q195",
        "MI36": "000000000",
        "MI39": "000",
        "MI24": "0",
        "Ind1": "NULL",
        "Ind2": "NULL",
        "Ind3": "NULL",
        "AGR1": "",
        "AGR2": "",
        "ob_Condition": "RH-60Kg",
        "Japanese_CustomerName": "野口精機",
    },
    {
        "PRO0": "1",
        "PRO1": "4070207551",
        "PRO1C": "NOGUCHI SEIKI",
        "PRO2": "260218548-019",
        "PRO5": "1",
        "PRO8": "35",
        "PRO9": "-",
        "PRO10": "755",
        "PRO11": "65",
        "PRO12": "355",
        "PRO13": "WA",
        "PRO14": "280",
        "PRO15": "K+",
        "PRO16": "8",
        "PRO17": "B",
        "PRO18": "57A",
        "PRO21": "0",
        "PRO25": "2026-02-18 00:00:00.000",
        "PRO27": "0",
        "P2": "15826",
        "PRO6": "57000",
        "SPEED": "500",
        "PRO-TOOL": "PA54508-01BANAARI",
        "PRO4": "2100030080-200",
        "MC11": "35",
        "MC12": "DISK",
        "MC14": "755",
        "MC15": "65",
        "MC16": "355",
        "MC19": "WA",
        "MC20": "280",
        "MC21": "K+",
        "MC22": "8",
        "MC23": "B",
        "MC24": "57A",
        "MP45": "OUT",
        "MP49": "NULL",
        "MI13": "1",
        "TC": "4070207551",
        "MI14": "01",
        "MI15": "23",
        "MI16": "280",
        "MI17": "K+",
        "MI18": "41",
        "MI19": "67",
        "MI20": "0",
        "MI21": "0",
        "MI22": "0",
        "MI23": "0",
        "MI31": "NULL",
        "MI33": "NULL",
        "INSAGM": "NULL",
        "MARAGM": "NULL",
        "MI53": "Q195",
        "MI55": "Q195",
        "MI36": "000000000",
        "MI39": "000",
        "MI24": "0",
        "Ind1": "NULL",
        "Ind2": "NULL",
        "Ind3": "NULL",
        "AGR1": "",
        "AGR2": "",
        "ob_Condition": "RH-60Kg",
        "Japanese_CustomerName": "野口精機",
    },
]

DATA_MS_SAMPLE_ROWS = [
    {
        "row_no": "1",
        "dsize": "205.76",
        "tsize": "19.12",
        "hsize": "0",
        "created_at": "2019-02-12 14:34:57.680",
        "updated_at": "2019-02-12 14:34:57.680",
        "lot_no": "181-38043F",
        "machine_id": "27",
        "code": "1-3668-4196-6",
        "emp_id": "20191",
        "weight": "0.152",
        "devid": "NULL",
        "tpoint1": "705.29",
        "tpoint2": "20.35",
        "tpoint3": "20.35",
        "tpoint4": "0.25",
        "tpoint1ok": "NG",
        "tpoint2ok": "NG",
        "tpoint3ok": "NG",
        "tpoint4ok": "NG",
        "tdiff": "705.04",
        "tmin": "0.25",
        "tmax": "705.29",
        "hdev": "",
        "hsizeproxy": "+1.07",
        "shift": "NULL",
        "mode": "NULL",
        "cal_mode": "0",
        "dsizeOk": "NG",
        "tsizeOk": "NG",
        "hsizeOk": "NG",
    },
    {
        "row_no": "2",
        "dsize": "5.07",
        "tsize": "0.49",
        "hsize": "38.6",
        "created_at": "2019-02-12 14:45:18.633",
        "updated_at": "2019-02-12 14:45:18.633",
        "lot_no": "181-38043F",
        "machine_id": "26",
        "code": "1-3668-4196-6",
        "emp_id": "20191",
        "weight": "8.16",
        "devid": "NULL",
        "tpoint1": "43.5",
        "tpoint2": "43.5",
        "tpoint3": "43.5",
        "tpoint4": "0.49",
        "tpoint1ok": "NG",
        "tpoint2ok": "NG",
        "tpoint3ok": "NG",
        "tpoint4ok": "NG",
        "tdiff": "43.01",
        "tmin": "0.49",
        "tmax": "43.5",
        "hdev": "",
        "hsizeproxy": "OK",
        "shift": "NULL",
        "mode": "NULL",
        "cal_mode": "0",
        "dsizeOk": "NG",
        "tsizeOk": "NG",
        "hsizeOk": "NG",
    },
    {
        "row_no": "1",
        "dsize": "25.682",
        "tsize": "25.682",
        "hsize": "25.568",
        "created_at": "2019-02-12 16:35:21.570",
        "updated_at": "2019-02-12 16:35:21.570",
        "lot_no": "11808915",
        "machine_id": "25",
        "code": "1-9000-3035-3",
        "emp_id": "20191",
        "weight": "0.368",
        "devid": "NULL",
        "tpoint1": "0",
        "tpoint2": "0",
        "tpoint3": "0",
        "tpoint4": "0",
        "tpoint1ok": "null",
        "tpoint2ok": "null",
        "tpoint3ok": "null",
        "tpoint4ok": "null",
        "tdiff": "0",
        "tmin": "0",
        "tmax": "0",
        "hdev": "NULL",
        "hsizeproxy": "NULL",
        "shift": "NULL",
        "mode": "NULL",
        "cal_mode": "0",
        "dsizeOk": "NG",
        "tsizeOk": "NG",
        "hsizeOk": "NG",
    },
]


class Command(BaseCommand):
    help = "Create missing tables for project models and seed mock rows for empty tables."

    target_app_labels = ("auth", "core", "legacy", "sysadmin")
    excluded_tables = {
        "auth_group",
        "auth_group_permissions",
        "auth_permission",
        "auth_user_groups",
        "auth_user_user_permissions",
        "django_admin_log",
        "django_content_type",
        "django_migrations",
        "django_session",
    }

    def add_arguments(self, parser):
        parser.add_argument(
            "--target-count",
            type=int,
            default=1,
            help="Minimum number of rows to ensure for each project table.",
        )

    def handle(self, *args, **options):
        self._seed_index = 1
        self._active_seed_stack = set()
        self._target_count = max(options["target_count"], 1)

        models_to_process = self.get_target_models()
        self.create_missing_tables(models_to_process)

        created_rows = 0
        for model in models_to_process:
            if model._meta.db_table in self.excluded_tables:
                continue
            created_rows += self.ensure_target_rows(model, self._target_count)

        self.stdout.write(
            self.style.SUCCESS(
                f"Seed completed. Ensured tables for {len(models_to_process)} models and created {created_rows} seed rows."
            )
        )

    def get_target_models(self):
        selected = []
        seen = set()

        user_model = get_user_model()
        selected.append(user_model)
        seen.add((user_model._meta.app_label, user_model._meta.model_name))

        for app_label in self.target_app_labels:
            app_config = apps.get_app_config(app_label)
            for model in app_config.get_models():
                key = (model._meta.app_label, model._meta.model_name)
                if key in seen:
                    continue
                selected.append(model)
                seen.add(key)

        lot_type_key = (LotType._meta.app_label, LotType._meta.model_name)
        if lot_type_key not in seen:
            selected.append(LotType)

        return selected

    def get_database_objects(self):
        cache = {}
        for alias in connections:
            connection = connections[alias]
            with connection.cursor() as cursor:
                cache[alias] = {info.name for info in connection.introspection.get_table_list(cursor)}
        return cache

    def create_missing_tables(self, models_to_process):
        existing = self.get_database_objects()
        pending = []
        for model in models_to_process:
            alias = router.db_for_write(model) or "default"
            if model._meta.db_table in self.excluded_tables:
                continue
            if model._meta.db_table not in existing.get(alias, set()):
                pending.append(model)

        while pending:
            progress = False
            next_pending = []

            for model in pending:
                alias = router.db_for_write(model) or "default"
                dependencies_ready = True
                for field in model._meta.local_fields:
                    if not isinstance(field, (ForeignKey, OneToOneField)):
                        continue
                    related_model = field.related_model
                    if not related_model:
                        continue
                    related_table = related_model._meta.db_table
                    related_alias = router.db_for_write(related_model) or "default"
                    if related_table in self.excluded_tables:
                        continue
                    if related_table not in existing.get(related_alias, set()):
                        dependencies_ready = False
                        break

                if not dependencies_ready:
                    next_pending.append(model)
                    continue

                connection = connections[alias]
                with connection.schema_editor() as schema_editor:
                    schema_editor.create_model(model)
                existing.setdefault(alias, set()).add(model._meta.db_table)
                progress = True
                self.stdout.write(f"Created table for {model._meta.label} on {alias} -> {model._meta.db_table}")

            if not progress:
                stalled = ", ".join(model._meta.label for model in next_pending)
                raise RuntimeError(f"Unable to create tables due to unresolved dependencies: {stalled}")

            pending = next_pending

    def ensure_target_rows(self, model, target_count):
        if model._meta.db_table in self.excluded_tables:
            return 0

        alias = router.db_for_write(model) or "default"
        current_count = model._default_manager.using(alias).count()
        rows_to_create = max(target_count - current_count, 0)

        if rows_to_create == 0:
            return 0

        label = model._meta.label
        if label in self._active_seed_stack:
            return 0

        self._active_seed_stack.add(label)
        try:
            created = 0
            for row_offset in range(rows_to_create):
                instance = model()
                value_index = self._seed_index

                for field in model._meta.local_fields:
                    if field.auto_created and not field.concrete:
                        continue

                    if getattr(field, "auto_now", False) or getattr(field, "auto_now_add", False):
                        continue

                    if isinstance(field, (ForeignKey, OneToOneField)):
                        related_value = self.get_related_value(field, row_offset, target_count)
                        setattr(instance, field.attname, related_value)
                        continue

                    if field.primary_key and isinstance(field, (models.AutoField, models.BigAutoField)):
                        continue

                    value = self.build_field_value(model, field, value_index)
                    if value is not None or not field.null:
                        setattr(instance, field.attname, value)

                instance.save(using=alias, force_insert=True)
                self._seed_index += 1
                created += 1

            if created:
                self.stdout.write(f"Seeded {created} rows for {label} on {alias} -> {model._meta.db_table}")
            return created
        finally:
            self._active_seed_stack.remove(label)

    def get_related_value(self, field, row_offset, target_count):
        related_model = field.related_model
        if related_model is None:
            return None

        alias = router.db_for_write(related_model) or "default"

        if related_model._meta.db_table not in self.excluded_tables:
            self.ensure_target_rows(related_model, target_count)

        queryset = related_model._default_manager.using(alias).order_by(related_model._meta.pk.attname)
        related_instances = list(queryset)
        if not related_instances:
            return None

        if isinstance(field, OneToOneField):
            index = row_offset if row_offset < len(related_instances) else len(related_instances) - 1
        else:
            index = row_offset % len(related_instances)

        related_instance = related_instances[index]
        return getattr(related_instance, related_model._meta.pk.attname)

    def get_master_sample_row(self, value_index):
        return MASTER_VIEW_SAMPLE_ROWS[(value_index - 1) % len(MASTER_VIEW_SAMPLE_ROWS)]

    def get_sample_value(self, model, field, value_index):
        model_name = model._meta.model_name

        if model_name in {"mgmasterview", "vmasterview", "belmasterview", "emasterview"}:
            row = self.get_master_sample_row(value_index)
            if field.name == "PRO0":
                return str(value_index)
            keys = [field.db_column, field.name]
            for key in keys:
                if key and key in row:
                    return self.coerce_sample_value(field, row[key])

        if model_name in {"datams", "datamslot2210062522"}:
            row = DATA_MS_SAMPLE_ROWS[(value_index - 1) % len(DATA_MS_SAMPLE_ROWS)]
            master_row = self.get_master_sample_row(value_index)
            if field.name == "lot_no":
                return self.coerce_sample_value(field, master_row["PRO2"])
            if field.name == "code":
                return self.coerce_sample_value(field, master_row["PRO1"])
            keys = [field.db_column, field.name]
            for key in keys:
                if not key:
                    continue
                for candidate_key, candidate_value in row.items():
                    if candidate_key.lower() == key.lower():
                        return self.coerce_sample_value(field, candidate_value)

        if model_name == "data":
            row = DATA_MS_SAMPLE_ROWS[(value_index - 1) % len(DATA_MS_SAMPLE_ROWS)]
            master_row = self.get_master_sample_row(value_index)
            direct_map = {
                "row_no": row.get("row_no"),
                "lot_no": master_row.get("PRO2"),
                "code": master_row.get("PRO1"),
                "created_at": row.get("created_at"),
                "updated_at": row.get("updated_at"),
                "machine_id": row.get("machine_id"),
                "r_type": "CSV",
                "grade": row.get("dsizeOk"),
                "rgrade": row.get("hsizeOk"),
                "header": "CSV",
                "ndata": "1",
                "sub_order": row.get("row_no"),
                "p1": row.get("dsize"),
                "p2": row.get("tsize"),
                "p3": row.get("hsize"),
                "p4": row.get("weight"),
                "p5": row.get("tpoint1"),
                "p6": row.get("tpoint2"),
                "p7": row.get("tpoint3"),
                "p8": row.get("tpoint4"),
                "p9": row.get("tmin"),
                "p10": row.get("tmax"),
            }
            if field.name == "avg":
                try:
                    average = (float(row["dsize"]) + float(row["tsize"]) + float(row["hsize"])) / 3
                    return average
                except Exception:
                    return None
            if field.name in direct_map:
                return self.coerce_sample_value(field, direct_map[field.name])

        return None

    def coerce_sample_value(self, field, raw_value):
        if raw_value is None:
            return None

        if isinstance(raw_value, str):
            value = raw_value.strip()
            if value.lower() in {"null", ""}:
                return None if field.null else ""
        else:
            value = raw_value

        if isinstance(field, models.DateTimeField):
            return timezone.datetime.fromisoformat(value.replace(" ", "T"))

        if isinstance(field, models.DateField):
            return timezone.datetime.fromisoformat(value.replace(" ", "T")).date()

        if isinstance(field, models.DecimalField):
            return Decimal(str(value))

        if isinstance(field, models.FloatField):
            return float(value)

        if isinstance(field, (models.IntegerField, models.BigIntegerField, models.SmallIntegerField)):
            return int(float(value))

        return value

    def build_field_value(self, model, field, value_index):
        sample_value = self.get_sample_value(model, field, value_index)
        if sample_value is not None:
            return sample_value

        if field.has_default():
            default = field.get_default()
            return default() if callable(default) else default

        if getattr(field, "choices", None):
            return field.choices[0][0]

        model_key = model._meta.model_name
        field_key = field.name.lower()

        if isinstance(field, models.JSONField):
            if "template" in field_key:
                return ["dimension", "hardness_out", "centering"]
            return {"model": model_key, "field": field.name, "seed": value_index}

        if isinstance(field, (ImageField, FileField)):
            filename = f"{model_key}-{value_index}.png" if isinstance(field, ImageField) else f"{model_key}-{value_index}.dat"
            return f"mock/{filename}"

        if isinstance(field, models.EmailField):
            return f"{model_key}{value_index}@example.com"

        if isinstance(field, models.DateTimeField):
            return timezone.now()

        if isinstance(field, models.DateField):
            return timezone.now().date()

        if isinstance(field, models.TimeField):
            return timezone.now().time().replace(microsecond=0)

        if isinstance(field, models.DecimalField):
            whole = max(field.max_digits - field.decimal_places, 1)
            number = str((value_index % (10 ** min(whole, 4))) + 1)
            decimal_part = ("12" * max(field.decimal_places, 1))[: field.decimal_places]
            return Decimal(f"{number}.{decimal_part}") if field.decimal_places else Decimal(number)

        if isinstance(field, models.FloatField):
            return float(value_index) + 0.25

        if isinstance(field, models.BooleanField):
            return True

        if isinstance(field, (models.IntegerField, models.BigIntegerField, models.SmallIntegerField)):
            return value_index

        if isinstance(field, models.TextField):
            return f"Mock {model._meta.verbose_name} text {value_index}"

        if isinstance(field, models.CharField):
            if field.primary_key:
                if field.max_length <= 12:
                    value = str(1000000000 + value_index)[-field.max_length:]
                else:
                    suffix = str(value_index)
                    prefix_length = max(field.max_length - len(suffix) - 1, 1)
                    value = f"{model_key[:prefix_length].upper()}-{suffix}"
            elif "json" in field_key:
                value = json.dumps({"seed": value_index})
            elif "email" in field_key:
                value = f"{model_key}{value_index}@example.com"
            elif "file" in field_key or "picture" in field_key or "drawing" in field_key:
                value = f"mock/{model_key}-{field.name}-{value_index}.dat"
            elif field_key == "password":
                value = "pbkdf2_sha256$600000$Xn2U8rTJlAmDG6TaLgb9WQ$ElMUGivveb2WqmckmheHTb8jaxVuu1NJ2/QAAnpvZ7w="
            elif field_key == "username":
                value = f"{model_key}_{value_index}"
            else:
                value = f"{model_key}_{field.name}_{value_index}"
            return value[: field.max_length]

        if field.null:
            return None

        return f"{model_key}_{field.name}_{value_index}"
