"""Fixture provider base class and built-in providers."""

from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, ClassVar

from django.apps import apps
from django.db import transaction

if TYPE_CHECKING:
    from collections.abc import Callable

    from django.db.models import Model



@dataclass
class ProviderResult:
    provider_name: str
    created: int = 0
    updated: int = 0
    deleted: int = 0
    skipped: int = 0
    details: dict[str, Any] = field(default_factory=dict)


class FixtureProvider(ABC):
    """Base class for all data providers in the provisioning engine.

    Subclass and override `provide()` to supply specific data.
    """

    name: ClassVar[str] = ""
    description: ClassVar[str] = ""
    tags: ClassVar[tuple[str, ...]] = ()
    depends_on: ClassVar[tuple[str, ...]] = ()
    scope: ClassVar[str] = "tenant"  # global | tenant | workspace

    @abstractmethod
    def provide(self, context: dict[str, Any]) -> ProviderResult:
        """Generate and persist fixture data.

        Args:
            context: Dict with keys like 'tenant', 'workspace', 'org_node', 'faker'.

        Returns:
            ProviderResult summarizing what was created/updated.
        """

    def reset(self, context: dict[str, Any]) -> ProviderResult:
        """Remove data created by this provider (if reversible)."""
        return ProviderResult(provider_name=self.name)

    def dry_run(self, context: dict[str, Any]) -> dict[str, Any]:
        """Return a preview of what would be created without persisting."""
        return {"provider": self.name, "estimated_items": 0}


class DjangoModelProvider(FixtureProvider):
    """Provider that creates Django model instances from a data specification."""

    description = "Generic Django model instance provider"
    tags = ("django", "model")

    def __init__(
        self,
        name: str = "django_model",
        model_class: type[Model] | str | None = None,
        data: list[dict[str, Any]] | Callable[[dict[str, Any]], list[dict[str, Any]]] | None = None,
        unique_fields: tuple[str, ...] | None = None,
        depends_on: tuple[str, ...] = (),
        scope: str = "tenant",
        reset_on_regenerate: bool = True,
    ) -> None:
        self._name = name
        self._model_class = model_class
        self._data = data
        self._unique_fields = unique_fields or ("id",)
        self.depends_on = depends_on
        self.scope = scope
        self._reset_on_regenerate = reset_on_regenerate

    @property
    def name(self) -> str:
        return self._name

    def _resolve_model(self) -> type[Model]:
        mc = self._model_class
        if mc is None:
            raise ValueError(f"Provider {self.name!r}: no model_class specified")
        if isinstance(mc, str):
            return apps.get_model(mc)
        return mc

    def _resolve_data(self, context: dict[str, Any]) -> list[dict[str, Any]]:
        d = self._data
        if d is None:
            return []
        if callable(d):
            return d(context)
        return d

    @transaction.atomic
    def provide(self, context: dict[str, Any]) -> ProviderResult:
        model_cls = self._resolve_model()
        specs = self._resolve_data(context)
        created = 0
        updated = 0

        for spec in specs:
            lookup = {}
            for field_name in self._unique_fields:
                if field_name in spec:
                    lookup[field_name] = spec[field_name]
            if not lookup:
                obj = model_cls.objects.create(**spec)
                created += 1
            else:
                obj, was_created = model_cls.objects.get_or_create(
                    defaults=spec,
                    **lookup,
                )
                if was_created:
                    created += 1
                else:
                    dirty = False
                    for key, val in spec.items():
                        if getattr(obj, key) != val:
                            setattr(obj, key, val)
                            dirty = True
                    if dirty:
                        obj.save()
                    updated += 1 if not was_created else 0

        return ProviderResult(
            provider_name=self.name,
            created=created,
            updated=updated,
        )

    @transaction.atomic
    def reset(self, context: dict[str, Any]) -> ProviderResult:
        model_cls = self._resolve_model()
        qs = model_cls.objects.all()
        tenant = context.get("tenant")
        if tenant and hasattr(model_cls, "tenant_id"):
            qs = qs.filter(tenant_id=tenant.pk)
        deleted, _ = qs.delete()
        return ProviderResult(
            provider_name=self.name,
            deleted=deleted,
        )


class RawSQLProvider(FixtureProvider):
    """Provider that executes raw SQL statements."""

    description = "Raw SQL fixture provider"
    tags = ("sql",)

    def __init__(
        self,
        name: str = "raw_sql",
        statements: list[str] | None = None,
        depends_on: tuple[str, ...] = (),
        scope: str = "global",
    ) -> None:
        self._name = name
        self._statements = statements or []
        self.depends_on = depends_on
        self.scope = scope

    @property
    def name(self) -> str:
        return self._name

    def provide(self, context: dict[str, Any]) -> ProviderResult:
        from django.db import connection

        with connection.cursor() as cursor:
            for stmt in self._statements:
                cursor.execute(stmt)
        return ProviderResult(
            provider_name=self.name,
            created=len(self._statements),
        )


class CallbackProvider(FixtureProvider):
    """Provider that delegates to a callable function."""

    def __init__(
        self,
        name: str,
        callback: Callable[[dict[str, Any]], ProviderResult],
        depends_on: tuple[str, ...] = (),
        scope: str = "tenant",
        description: str = "",
    ) -> None:
        self._name = name
        self._callback = callback
        self.depends_on = depends_on
        self.scope = scope
        self.description = description or f"Callback provider: {name}"

    @property
    def name(self) -> str:
        return self._name

    def provide(self, context: dict[str, Any]) -> ProviderResult:
        return self._callback(context)


class CompositeProvider(FixtureProvider):
    """Provider that combines multiple sub-providers into one logical group."""

    def __init__(
        self,
        name: str,
        providers: list[FixtureProvider],
        depends_on: tuple[str, ...] = (),
        scope: str = "tenant",
    ) -> None:
        self._name = name
        self._providers = providers
        self.depends_on = depends_on
        self.scope = scope

    @property
    def name(self) -> str:
        return self._name

    def provide(self, context: dict[str, Any]) -> ProviderResult:
        total = ProviderResult(provider_name=self.name)
        for p in self._providers:
            r = p.provide(context)
            total.created += r.created
            total.updated += r.updated
            total.deleted += r.deleted
            total.skipped += r.skipped
        return total

    def reset(self, context: dict[str, Any]) -> ProviderResult:
        total = ProviderResult(provider_name=self.name)
        for p in reversed(self._providers):
            r = p.reset(context)
            total.deleted += r.deleted
        return total
