"""Factory-based data generation using factory_boy integration.

The FactoryProvider wraps factory_boy factories and adapts them as FixtureProviders,
enabling batch generation of model instances with realistic randomized data.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from django.db import transaction

from simorgh.apps.provisioning.providers import FixtureProvider, ProviderResult

if TYPE_CHECKING:
    from collections.abc import Callable


class FactoryProvider(FixtureProvider):
    """A FixtureProvider backed by a factory_boy factory class.

    Usage:
        import factory
        from simorgh.apps.provisioning.factories import FactoryProvider

        class UserFactory(factory.django.DjangoModelFactory):
            class Meta:
                model = "accounts.User"

            email = factory.Faker("email")
            first_name = factory.Faker("first_name")

        provider = FactoryProvider(
            name="demo_users",
            factory_class=UserFactory,
            count=10,
            overrides={"is_active": True},
        )
    """

    def __init__(
        self,
        name: str,
        factory_class: type | str,
        count: int = 1,
        overrides: dict[str, Any] | Callable[[int], dict[str, Any]] | None = None,
        depends_on: tuple[str, ...] = (),
        scope: str = "tenant",
        batch_size: int = 100,
    ) -> None:
        self._name = name
        self._factory_class = factory_class
        self._count = count
        self._overrides = overrides
        self.depends_on = depends_on
        self.scope = scope
        self._batch_size = batch_size

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

    def _get_factory(self) -> type:
        fc = self._factory_class
        if isinstance(fc, str):
            from factory_boy import factories

            return factories.get(fc)
        return fc

    def _build_overrides(self, index: int, context: dict[str, Any]) -> dict[str, Any]:
        """Resolve overrides, supporting callables per index."""
        ov = self._overrides
        if ov is None:
            return {}
        if callable(ov):
            return ov(index)
        overrides = dict(ov)
        tenant = context.get("tenant")
        if tenant and "tenant" not in overrides and "tenant_id" not in overrides:
            overrides["tenant"] = tenant
        if "tenant_id" not in overrides and tenant:
            overrides["tenant_id"] = tenant.pk
        return overrides

    @transaction.atomic
    def provide(self, context: dict[str, Any]) -> ProviderResult:
        factory_cls = self._get_factory()
        created = 0

        for batch_start in range(0, self._count, self._batch_size):
            batch_end = min(batch_start + self._batch_size, self._count)
            for i in range(batch_start, batch_end):
                kwargs = self._build_overrides(i, context)
                factory_cls.create(**kwargs)
                created += 1

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

    def to_specs(self, context: dict[str, Any]) -> list[dict[str, Any]]:
        """Generate dict specs instead of creating instances (for dry-run/preview)."""
        factory_cls = self._get_factory()
        specs: list[dict[str, Any]] = []
        for i in range(min(self._count, 10)):
            kwargs = self._build_overrides(i, context)
            instance = factory_cls.build(**kwargs)
            specs.append({f: getattr(instance, f) for f in dir(instance) if not f.startswith("_")})
        return specs


def create_factory_provider(
    name: str,
    factory_class: type | str,
    count: int = 1,
    overrides: dict[str, Any] | None = None,
    depends_on: tuple[str, ...] = (),
    scope: str = "tenant",
) -> FactoryProvider:
    """Convenience function to create a FactoryProvider."""
    return FactoryProvider(
        name=name,
        factory_class=factory_class,
        count=count,
        overrides=overrides,
        depends_on=depends_on,
        scope=scope,
    )
