import asyncio
import os
from typing import List, Type, Optional

import nest_asyncio
from asgiref.sync import sync_to_async
from django.db import models
from django.contrib import admin

from lex.lex_app.simple_history_config import get_model_exclusion_reason

import logging

logger = logging.getLogger(__name__)


class ModelRegistration:
    """
    Handles registration of Django models with admin sites and history tracking.

    This class manages the registration of different types of models including:
    - HTML Report models
    - Process models
    - Standard models with bitemporal history tracking
    - CalculationModel instances with aborted calculation handling
    """

    @classmethod
    def register_models(
        cls,
        models: List[Type[models.Model]],
        untracked_models: Optional[List[str]] = None,
    ) -> None:
        """
        Register a list of Django models with appropriate admin sites and history tracking.

        Args:
            models: List of Django model classes to register
            untracked_models: Optional list of model names (lowercase) that should
                            not have history tracking. Defaults to empty list.
        """
        from lex.core.models.Process import Process
        from lex.core.models.HTMLReport import HTMLReport
        from lex.core.models.CalculationModel import CalculationModel
        from django.contrib.auth.models import User

        if untracked_models is None:
            untracked_models = []

        # Configure User model display name
        def get_username(self):
            return f"{self.first_name} {self.last_name}"

        User.add_to_class("__str__", get_username)

        for model in models:
            try:
                if issubclass(model, HTMLReport):
                    cls._register_html_report(model)
                elif issubclass(model, Process):
                    cls._register_process_model(model)
                elif not issubclass(model, type) and not model._meta.abstract:
                    cls._register_standard_model(model, untracked_models)

                    if issubclass(model, CalculationModel):
                        cls._handle_calculation_model_reset(model)
            except Exception as e:
                raise RuntimeError(
                    f"Failed to register model {model.__name__}: {str(e)}"
                ) from e

    @classmethod
    def _register_html_report(cls, model: Type[models.Model]) -> None:
        from lex.process_admin.settings import processAdminSite

        model_name = model.__name__.lower()
        processAdminSite.registerHTMLReport(model_name, model)
        processAdminSite.register([model])

    @classmethod
    def _register_process_model(cls, model: Type[models.Model]) -> None:
        from lex.process_admin.settings import processAdminSite

        model_name = model.__name__.lower()
        processAdminSite.registerProcess(model_name, model)
        processAdminSite.register([model])

    @classmethod
    def _register_standard_model(
        cls, model: Type[models.Model], untracked_models: List[str]
    ) -> None:
        """
        Register a standard model with both admin sites and bitemporal history.

        This sets up the full 3-layer architecture:
          1. Level 1: Standard History  (valid_from / valid_to)
          2. Level 2: Meta History      (sys_from / sys_to)
          3. Signal handlers for chaining, sync, and scheduling
        """
        from lex.process_admin.settings import processAdminSite, adminSite
        from simple_history import register
        from simple_history.exceptions import MultipleRegistrationsError
        from lex.core.services.StandardHistory import StandardHistory
        from lex.core.services.MetaHistory import MetaLevelHistoricalRecords
        from lex.core.services.signal_registry import connect_bitemporal_signals

        model_name = model.__name__.lower()
        processAdminSite.register([model])

        exclusion_reason = get_model_exclusion_reason(model)
        is_already_tracked = exclusion_reason == "Already has history tracking"
        should_track = (
            (exclusion_reason is None or is_already_tracked)
            and model_name not in untracked_models
        )

        if should_track:
            try:
                # ── Level 1: Standard History ──
                try:
                    register(model, records_class=StandardHistory)
                except MultipleRegistrationsError:
                    pass

                if not hasattr(model, "history"):
                    logger.error(
                        f"Failed to retrieve history model for {model.__name__} "
                        f"after registration attempt."
                    )
                    return

                historical_model = model.history.model
                processAdminSite.register([historical_model])

                # ── Level 2: Meta History ──
                history = MetaLevelHistoricalRecords(
                    app=model._meta.app_label,
                    table_name=f"{model._meta.db_table}_meta_history",
                    custom_model_name=lambda x: f"Meta{x}",
                )

                try:
                    history.contribute_to_class(historical_model, "meta_history")
                except MultipleRegistrationsError:
                    pass
                except Exception as e:
                    if not hasattr(historical_model, "meta_history"):
                        logger.warning(
                            f"Could not attach meta_history to "
                            f"{historical_model.__name__}: {e}"
                        )

                try:
                    history.finalize(sender=historical_model)
                except Exception:
                    pass

                meta_historical_model = historical_model.meta_history.model

                # ── Connect all bitemporal signals ──
                connect_bitemporal_signals(
                    main_model=model,
                    historical_model=historical_model,
                    meta_historical_model=meta_historical_model,
                )

                processAdminSite.register([meta_historical_model])

                if is_already_tracked:
                    print(
                        f"✓ History hooks attached to existing history "
                        f"for: {model.__name__}"
                    )
                else:
                    print(
                        f"✓ History and Meta-History enabled "
                        f"for: {model.__name__}"
                    )

            except Exception as e:
                print(
                    f"⚠ Failed to register history for {model.__name__}: "
                    f"{type(e).__name__} - {e}"
                )
        else:
            exclusion_reason = get_model_exclusion_reason(model)
            if exclusion_reason:
                print(
                    f"⊘ History tracking skipped for {model.__name__}: "
                    f"{exclusion_reason}"
                )
            elif model_name in untracked_models:
                print(
                    f"⊘ History tracking skipped for {model.__name__}: "
                    f"In untracked_models list"
                )

        if not adminSite.is_registered(model):
            try:
                adminSite.register([model])
            except admin.exceptions.AlreadyRegistered:
                pass

    @classmethod
    def _handle_calculation_model_reset(cls, model: Type[models.Model]) -> None:
        """
        Reset CalculationModel instances left in IN_PROGRESS state on startup.
        """
        from lex.core.models.CalculationModel import CalculationModel

        if not os.getenv("CALLED_FROM_START_COMMAND"):
            return

        @sync_to_async
        def reset_instances_with_aborted_calculations():
            in_progress_instances = list(
                model.objects.filter(
                    is_calculated=CalculationModel.IN_PROGRESS
                ).order_by(model._meta.pk.name)
            )
            if not in_progress_instances:
                return

            for instance in in_progress_instances:
                try:
                    instance.is_calculated = CalculationModel.ABORTED
                    # Keep history rows while avoiding lifecycle hooks.
                    instance.save(skip_hooks=True)
                except Exception as exc:
                    logger.warning(
                        "Failed to mark %s(pk=%s) as ABORTED during startup reset: %s",
                        model.__name__,
                        getattr(instance, model._meta.pk.name, None),
                        exc,
                        exc_info=True,
                    )

        nest_asyncio.apply()
        loop = asyncio.get_event_loop()
        loop.run_until_complete(reset_instances_with_aborted_calculations())

    @classmethod
    def register_model_structure(cls, structure: dict):
        from lex.process_admin.settings import processAdminSite
        if structure:
            processAdminSite.register_model_structure(structure)

    @classmethod
    def register_model_styling(cls, styling: dict):
        from lex.process_admin.settings import processAdminSite
        if styling:
            processAdminSite.register_model_styling(styling)

    @classmethod
    def register_widget_structure(cls, structure):
        from lex.process_admin.settings import processAdminSite
        if structure:
            processAdminSite.register_widget_structure(structure)
