from __future__ import annotations

import inspect
import os
import sys
import sysconfig
from pathlib import Path
from typing import Any
from typing import Literal

import django
from django.apps import AppConfig
from django.apps import apps
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import models
from pydantic import BaseModel
from pydantic import field_serializer


def get_source_file_path(obj: Any) -> Path:
    target = obj if inspect.isclass(obj) else obj.__class__
    try:
        return Path(inspect.getfile(target))
    except (TypeError, OSError):
        return Path("unknown")


def is_first_party_app(app_config: AppConfig) -> bool:
    """Check if an app is first-party (project code) vs third-party (installed package).

    Uses Python's sysconfig to determine installation paths, which properly handles
    all installation scenarios (pip, conda, virtualenv, etc.) and is platform-independent.

    Args:
        app_config: Django AppConfig to check

    Returns:
        True if the app is first-party project code, False if third-party or stdlib
    """
    try:
        # Defensive check for malformed app configs (should never happen in practice)
        if app_config.module is None:  # pragma: no cover
            return False

        app_path = Path(inspect.getfile(app_config.module)).resolve()

        for lib_path in [
            Path(sysconfig.get_path("purelib")),  # site-packages (pure Python)
            Path(sysconfig.get_path("platlib")),  # site-packages (platform-specific)
            Path(sysconfig.get_path("stdlib")),  # standard library
        ]:
            try:
                if app_path.is_relative_to(lib_path):
                    return False

            # Windows-specific: paths on different drives (e.g., C:\ vs D:\)
            except ValueError:  # pragma: no cover
                pass

        return True

    # Defensive error handling for built-in modules or broken app configs
    except (TypeError, OSError):  # pragma: no cover
        return False


def filter_models(
    models: list[Any],
    include: list[str] | None = None,
    scope: Literal["project", "all"] = "project",
) -> list[Any]:
    """Filter Django models by app inclusion or scope.

    Args:
        models: List of Django model classes to filter
        include: Specific app labels to include (overrides scope)
        scope: "project" for project models, "all" for everything

    Returns:
        Filtered list of model classes
    """
    filtered_models = []

    for model in models:
        app_label = model._meta.app_label

        # If include is specified, only filter by include (ignore scope)
        if include is not None:
            if app_label in include:
                filtered_models.append(model)
        # Otherwise, filter by scope
        elif scope == "project" and is_first_party_app(apps.get_app_config(app_label)):
            filtered_models.append(model)
        elif scope == "all":
            filtered_models.append(model)

    return filtered_models


class ProjectResource(BaseModel):
    python: PythonResource
    django: DjangoResource

    @classmethod
    def from_env(cls) -> ProjectResource:
        py = PythonResource.from_sys()
        dj = DjangoResource.from_django()
        return ProjectResource(python=py, django=dj)


class PythonResource(BaseModel):
    base_prefix: Path
    executable: Path
    path: list[Path]
    platform: str
    prefix: Path
    version_info: tuple[
        int, int, int, Literal["alpha", "beta", "candidate", "final"], int
    ]

    @classmethod
    def from_sys(cls) -> PythonResource:
        return cls(
            base_prefix=Path(sys.base_prefix),
            executable=Path(sys.executable),
            path=[Path(p) for p in sys.path],
            platform=sys.platform,
            prefix=Path(sys.prefix),
            version_info=sys.version_info,
        )


class DjangoResource(BaseModel):
    apps: list[str]
    auth_user_model: str | None
    base_dir: Path
    databases: dict[str, dict[str, str]]
    debug: bool
    settings_module: str
    version: tuple[int, int, int, Literal["alpha", "beta", "rc", "final"], int]

    @classmethod
    def from_django(cls) -> DjangoResource:
        app_names = [app_config.name for app_config in apps.get_app_configs()]

        databases = {
            db_alias: {
                "engine": db_config.get("ENGINE", ""),
                "name": str(db_config.get("NAME", "")),
            }
            for db_alias, db_config in settings.DATABASES.items()
        }

        if "django.contrib.auth" in app_names:
            user_model = get_user_model()
            auth_user_model = f"{user_model.__module__}.{user_model.__name__}"
        else:
            auth_user_model = None

        return cls(
            apps=app_names,
            auth_user_model=auth_user_model,
            base_dir=Path(getattr(settings, "BASE_DIR", Path.cwd())),
            databases=databases,
            debug=settings.DEBUG,
            settings_module=os.environ.get("DJANGO_SETTINGS_MODULE", ""),
            version=django.VERSION,
        )


class AppResource(BaseModel):
    name: str
    label: str
    path: Path
    models: list[ModelResource]

    @classmethod
    def from_app(cls, app: AppConfig) -> AppResource:
        appconfig = get_source_file_path(app)
        app_path = appconfig.parent if appconfig != Path("unknown") else Path("unknown")

        app_models = (
            [
                ModelResource.from_model(model)
                for model in app.models.values()
                if not model._meta.auto_created
            ]
            if app.models
            else []
        )

        return cls(name=app.name, label=app.label, path=app_path, models=app_models)

    @field_serializer("models")
    def serialize_models(self, models: list[ModelResource]) -> list[str]:
        return [model.model_dump()["model_class"] for model in models]


class ModelResource(BaseModel):
    model_class: type[models.Model]
    import_path: str
    source_path: Path
    fields: dict[str, str]

    @classmethod
    def from_model(cls, model: type[models.Model]):
        field_types = {
            field.name: field.__class__.__name__ for field in model._meta.fields
        }

        return cls(
            model_class=model,
            import_path=f"{model.__module__}.{model.__name__}",
            source_path=get_source_file_path(model),
            fields=field_types,
        )

    @field_serializer("model_class")
    def serialize_model_class(self, klass: type[models.Model]) -> str:
        return klass.__name__


class SettingResource(BaseModel):
    key: str
    value: Any
    value_type: str

    @field_serializer("value")
    def serialize_value(self, value: Any) -> Any:
        # Handle common Django types that need conversion
        if isinstance(value, Path):
            return str(value)
        if isinstance(value, type):  # Class objects
            return f"{value.__module__}.{value.__name__}"
        return value
