from __future__ import annotations

import os
import re
from collections.abc import Callable, Iterable, Mapping, Sequence
from dataclasses import dataclass, field
from importlib import import_module
from typing import Any

from fastapi import APIRouter, FastAPI

from govoplan_core.core.modules import ModuleManifest
from govoplan_core.core.registry import PlatformRegistry
from govoplan_core.server.fastapi import LifespanFactory

ManifestFactory = Callable[[], ModuleManifest]
RouterEnabled = Callable[[object | None, PlatformRegistry], bool]
AppConfigurator = Callable[[FastAPI, PlatformRegistry, object | None], None]
DEFAULT_TRUSTED_IMPORT_PREFIXES = ("govoplan_core", "govoplan_", "app")
TRUSTED_IMPORT_PREFIXES_ENV = "GOVOPLAN_TRUSTED_IMPORT_PREFIXES"
_MODULE_PATH_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$")
_ATTRIBUTE_PATH_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)*$")


@dataclass(frozen=True, slots=True)
class RouterContribution:
    router: APIRouter
    required_module: str | None = None
    enabled: RouterEnabled | None = None

    def should_include(self, settings: object | None, registry: PlatformRegistry) -> bool:
        if self.required_module and not registry.has_module(self.required_module):
            return False
        if self.enabled is not None and not self.enabled(settings, registry):
            return False
        return True


@dataclass(frozen=True, slots=True)
class GovoplanServerConfig:
    title: str = "GovOPlaN Server"
    version: str = "0.1.0"
    settings: object | None = None
    enabled_modules: str | Iterable[str] = ("access",)
    api_prefix: str = "/api/v1"
    base_routers: Sequence[APIRouter] = ()
    post_module_routers: Sequence[APIRouter] = ()
    extra_routers: Sequence[RouterContribution] = ()
    lifespan: LifespanFactory | None = None
    cors_origins: Iterable[str] = ()
    manifest_factories: Sequence[ManifestFactory] = ()
    module_context_data: Mapping[str, Any] = field(default_factory=dict)
    app_configurators: Sequence[AppConfigurator] = ()


def import_object(path: str) -> Any:
    module_name, attribute = validate_object_path(path)
    _validate_trusted_import_prefix(module_name, trusted_import_prefixes())
    module = import_module(module_name)  # nosemgrep: python.lang.security.audit.non-literal-import.non-literal-import
    value: Any = module
    for part in attribute.split("."):
        value = getattr(value, part)
    return value


def validate_object_path(path: str) -> tuple[str, str]:
    module_name, separator, attribute = path.strip().partition(":")
    if not separator:
        raise ValueError(f"Object path must use module:attribute syntax: {path!r}")
    if not _MODULE_PATH_RE.fullmatch(module_name):
        raise ValueError(f"Object path module is not a valid Python module path: {path!r}")
    if not _ATTRIBUTE_PATH_RE.fullmatch(attribute) or any(part.startswith("_") for part in attribute.split(".")):
        raise ValueError(f"Object path attribute is not a public attribute path: {path!r}")
    return module_name, attribute


def trusted_import_prefixes() -> tuple[str, ...]:
    configured = os.getenv(TRUSTED_IMPORT_PREFIXES_ENV, "").strip()
    if not configured:
        return DEFAULT_TRUSTED_IMPORT_PREFIXES
    prefixes = tuple(item.strip() for item in configured.split(",") if item.strip())
    return prefixes or DEFAULT_TRUSTED_IMPORT_PREFIXES


def _validate_trusted_import_prefix(module_name: str, prefixes: Iterable[str]) -> None:
    if any(_module_matches_prefix(module_name, prefix) for prefix in prefixes):
        return
    raise ValueError(
        f"Object path module {module_name!r} is outside trusted import prefixes. "
        f"Set {TRUSTED_IMPORT_PREFIXES_ENV} to allow custom deployment config modules."
    )


def _module_matches_prefix(module_name: str, prefix: str) -> bool:
    cleaned = prefix.strip()
    if not cleaned:
        return False
    if cleaned.endswith((".", "_")):
        return module_name.startswith(cleaned)
    return module_name == cleaned or module_name.startswith(f"{cleaned}.")


def load_server_config(path: str | None = None) -> GovoplanServerConfig:
    config_path = path or os.getenv("GOVOPLAN_SERVER_CONFIG") or "govoplan_core.server.default_config:get_server_config"
    try:
        loaded = import_object(config_path)
    except ModuleNotFoundError:
        if path or os.getenv("GOVOPLAN_SERVER_CONFIG"):
            raise
        return GovoplanServerConfig()
    config = loaded() if callable(loaded) else loaded
    if not isinstance(config, GovoplanServerConfig):
        raise TypeError(f"{config_path!r} did not return a GovoplanServerConfig")
    return config
