from __future__ import annotations

from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from threading import RLock

from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request, status

from govoplan_core.core.module_management import ModuleManagementError, REQUIRED_PLATFORM_MODULES, plan_desired_enabled_modules
from govoplan_core.core.modules import ModuleContext, ModuleManifest
from govoplan_core.core.registry import PlatformRegistry
from govoplan_core.core.runtime import configure_runtime
from govoplan_core.core.workflows import (
    workflow_definition_contribution_provider,
)
from govoplan_core.server.route_validation import validate_router_can_mount


@dataclass(frozen=True, slots=True)
class ModuleLifecycleResult:
    enabled_modules: tuple[str, ...]
    activated_modules: tuple[str, ...]
    deactivated_modules: tuple[str, ...]
    mounted_modules: tuple[str, ...]
    migrations_applied: bool = False


def require_module_active(module_id: str):
    def dependency(request: Request) -> None:
        registry = getattr(request.app.state, "govoplan_registry", None)
        if isinstance(registry, PlatformRegistry) and registry.has_module(module_id):
            return
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Module is disabled: {module_id}")

    return dependency


class ModuleLifecycleManager:
    def __init__(
        self,
        *,
        registry: PlatformRegistry,
        available_modules: Mapping[str, ModuleManifest],
        settings: object | None,
        api_prefix: str = "/api/v1",
        manifest_factories: Sequence[object] = (),
        module_context_data: Mapping[str, object] | None = None,
    ) -> None:
        self.registry = registry
        self.available_modules = dict(available_modules)
        self.settings = settings
        self.api_prefix = api_prefix
        self.manifest_factories = tuple(manifest_factories)
        self.context = ModuleContext(registry=registry, settings=settings, data=dict(module_context_data or {}))
        self._app: FastAPI | None = None
        self._mounted_modules: set[str] = set()
        self._lock = RLock()

    def attach_app(self, app: FastAPI) -> None:
        self._app = app
        app.state.govoplan_lifecycle = self

    def configure_runtime(self) -> None:
        configure_runtime(self.context)
        self.registry.configure_capability_context(self.context)

    def active_module_ids(self) -> tuple[str, ...]:
        return tuple(manifest.id for manifest in self.registry.manifests())

    def mounted_module_ids(self) -> tuple[str, ...]:
        return tuple(sorted(self._mounted_modules))

    def live_apply_enabled(self) -> bool:
        configured = getattr(
            self.settings,
            "module_live_apply_enabled",
            None,
        )
        if configured is not None:
            return bool(configured)
        app_env = str(getattr(self.settings, "app_env", "dev")).casefold()
        return app_env in {"dev", "development", "local", "test", "testing"}

    def apply_enabled_modules(
        self,
        requested_enabled: Sequence[str],
        *,
        migrate: bool = True,
        protected_modules: Sequence[str] = REQUIRED_PLATFORM_MODULES,
    ) -> ModuleLifecycleResult:
        with self._lock:
            plan = plan_desired_enabled_modules(
                requested_enabled,
                self.available_modules,
                protected_modules=protected_modules,
            )
            previous = self.active_module_ids()
            previous_set = set(previous)
            next_set = set(plan.enabled_modules)
            activated = tuple(module_id for module_id in plan.enabled_modules if module_id not in previous_set)
            deactivated = tuple(module_id for module_id in previous if module_id not in next_set)

            if migrate:
                self._migrate(plan.enabled_modules)

            mounted = tuple(module_id for module_id in plan.enabled_modules if self._mount_module_router(module_id))

            old_manifests = {manifest.id: manifest for manifest in self.registry.manifests()}
            for module_id in deactivated:
                hook = old_manifests[module_id].on_deactivate
                if hook is not None:
                    hook(self.context)

            self.registry.replace(self.available_modules[module_id] for module_id in plan.enabled_modules)
            self.configure_runtime()

            for module_id in activated:
                hook = self.available_modules[module_id].on_activate
                if hook is not None:
                    hook(self.context)

            self.reconcile_workflow_definitions()

            if self._app is not None:
                self._app.openapi_schema = None

            return ModuleLifecycleResult(
                enabled_modules=plan.enabled_modules,
                activated_modules=activated,
                deactivated_modules=deactivated,
                mounted_modules=mounted,
                migrations_applied=migrate,
            )

    def reconcile_workflow_definitions(self) -> Mapping[str, object]:
        if not any(
            manifest.workflow_definitions for manifest in self.registry.manifests()
        ):
            return {"skipped": True, "reason": "no_contributions"}
        provider = workflow_definition_contribution_provider(self.registry)
        if provider is None:
            return {"skipped": True, "reason": "provider_unavailable"}

        from govoplan_core.db.session import get_database

        with get_database().session() as session:
            result = provider.reconcile(session)
            session.commit()
        if self._app is not None:
            self._app.state.govoplan_workflow_reconciliation = dict(result)
        return dict(result)

    def _mount_module_router(self, module_id: str) -> bool:
        if module_id in self._mounted_modules:
            return False
        app = self._app
        if app is None:
            raise ModuleManagementError("Module lifecycle manager is not attached to the FastAPI app.")
        manifest = self.available_modules.get(module_id)
        if manifest is None or manifest.route_factory is None:
            self._mounted_modules.add(module_id)
            return False

        module_router = manifest.route_factory(self.context)
        guarded = APIRouter(dependencies=[Depends(require_module_active(module_id))])
        guarded.include_router(module_router)
        validate_router_can_mount(app, guarded, prefix=self.api_prefix, owner=f"module {module_id!r}")
        app.include_router(guarded, prefix=self.api_prefix)
        app.openapi_schema = None
        self._mounted_modules.add(module_id)
        return True

    def _migrate(self, enabled_modules: Sequence[str]) -> None:
        from govoplan_core.db.migrations import migrate_database

        database_url = str(getattr(self.settings, "database_url", "") or "")
        migrate_database(
            database_url=database_url or None,
            enabled_modules=enabled_modules,
            manifest_factories=self.manifest_factories,
        )
