from duck_domain.django_tools.soft_delete.soft_delete_model import SoftDeleteModel
from duck_domain.core.providers.i_include_provider import IIncludeProvider
from django.db.models import ForeignKey, OneToOneField, ManyToManyField
from duck_domain.core.handdlers.query_response import QueryResponse
from duck_domain.core.errors.handdlers.app_error import AppError
from typing import ClassVar, Generic, Type, TypeVar, cast, Any
from duck_domain.core.crud.dtos.where_dto import WhereDto
from django.db.models.fields.related import RelatedField
from duck_domain.core.types.base_dto import BaseDto
from collections import defaultdict

MODEL = TypeVar("MODEL", bound=SoftDeleteModel)
RESPONSE = TypeVar("RESPONSE", bound=BaseDto)
WHERE = TypeVar("WHERE", bound=WhereDto)

class SoftDeleteDjangoIncludeProvider(Generic[MODEL, WHERE, RESPONSE], IIncludeProvider[WHERE, RESPONSE]):
    """
    A generic Django include provider that enriches flat DTOs with forward
    relational data using deterministic, bulk-loading strategies.

    This provider operates strictly on DTOs returned by repositories, never
    on ORM instances, and is designed to populate relational fields *after*
    the repository query phase. It supports only forward relations defined
    on the root Django model and guarantees predictable behavior by avoiding
    implicit inference.

    Supported relation types:
        - ForeignKey (forward)
        - OneToOneField (forward)
        - ManyToManyField (forward), strictly via `through_fields`

    For ManyToMany relations, this provider requires the Django model to define
    `through_fields` on the `ManyToManyField`. No heuristic or automatic
    inference is performed. If `through_fields` is missing, the provider
    fails explicitly with an application error.

    All relational data is fetched using bulk queries (no per-item database
    access), preserving performance and avoiding N+1 query patterns. The
    original `QueryResponse` metadata (object name and filters) is preserved.

    Type Parameters:
        MODEL (SoftDeleteModel):
            Root Django model whose relations are being included.
        WHERE (WhereDto):
            DTO describing the filtering criteria of the original query.
        RESPONSE (BaseDto):
            DTO type being enriched with relational data.

    Required Class Attributes:
        model (Type[MODEL]):
            Root Django model associated with the DTOs.
        relation_name (str):
            Name of the forward relation on the root model to be included.
        dto_field_name (str):
            Name of the field on the DTO where the related data will be injected.

    Design Constraints:
        - Repositories and model `as_dto()` methods must always return flat DTOs.
        - Includes are applied exclusively at the provider layer.
        - Reverse relations and relation paths (`__`) are not supported.
        - ORM instances are never exposed beyond this layer.
    """

    model: Type[MODEL]
    relation_name: ClassVar[str]
    dto_field_name: ClassVar[str]

    def include(self, dtos: QueryResponse[RESPONSE, WHERE]) -> QueryResponse[RESPONSE, WHERE]:
        if not dtos.all:
            return dtos

        current = self._include_relation(dtos.all, self.dto_field_name, self.relation_name)
        return QueryResponse(current, dtos.object_name, dtos.where)

    def _include_relation(self, items: list[RESPONSE], dto_field: str, relation_name: str) -> list[RESPONSE]:
        field = self._get_forward_relation_field(relation_name)

        if isinstance(field, (ForeignKey, OneToOneField)):
            return self._include_fk(items, dto_field, relation_name, field)

        if isinstance(field, ManyToManyField):
            return self._include_m2m(items, dto_field, relation_name, field)

        raise AppError(
            class_pointer=self,
            title="Invalid include relation",
            message="Only forward FK/O2O and forward M2M relations are supported by this include provider.",
            details={
                "root_model": self.model.__name__,
                "relation": relation_name,
                "dto_field": dto_field,
                "field_type": field.__class__.__name__,
            },
            code=400,
        )

    def _get_forward_relation_field(self, relation_name: str) -> RelatedField:
        if "__" in relation_name:
            raise AppError(
                class_pointer=self,
                title="Invalid include relation",
                message="Relation paths with '__' are not supported in this provider version.",
                details={"relation": relation_name},
                code=400,
            )

        try:
            field = self.model._meta.get_field(relation_name)
        except Exception as e:
            raise AppError(
                class_pointer=self,
                title="Invalid include relation",
                message="Relation was not found on root model.",
                details={"root_model": self.model.__name__, "relation": relation_name, "error": str(e)},
                code=400,
            )

        if not isinstance(field, (ForeignKey, OneToOneField, ManyToManyField)):
            raise AppError(
                class_pointer=self,
                title="Unsupported include relation",
                message="Only forward FK/O2O and forward M2M relations are supported.",
                details={
                    "root_model": self.model.__name__,
                    "relation": relation_name,
                    "field_type": field.__class__.__name__,
                },
                code=400,
            )

        return cast(RelatedField, field)


    def _include_fk(
        self,
        items: list[RESPONSE],
        dto_field: str,
        relation_name: str,
        rel_field: ForeignKey | OneToOneField,
    ) -> list[RESPONSE]:
        fk_id_attr = f"{relation_name}_id"

        fk_ids = set()
        for dto in items:
            if hasattr(dto, fk_id_attr):
                fk_id = getattr(dto, fk_id_attr)
                if fk_id is not None:
                    fk_ids.add(fk_id)
                continue

            raise AppError(
                class_pointer=self,
                title="Include FK id missing",
                message="DTO does not expose the foreign key id attribute required to resolve this include.",
                details={
                    "dto_field": dto_field, 
                    "relation_name": relation_name, 
                    "expected_fk_id_attr": fk_id_attr,
                },
                code=500,
            )
         
        if not fk_ids:
            return [self._dto_set(dto, dto_field, None) for dto in items]

        related_model = cast(Type[SoftDeleteModel], rel_field.related_model)
        relation_primary_key = related_model._meta.pk.name

        found_relations = related_model.objects.filter(
            **{f"{relation_primary_key}__in": list(fk_ids)}
        ).filter(deleted_at__isnull=True)

        relations = {
            getattr(obj, relation_primary_key): cast(BaseDto, obj.as_dto())
            for obj in found_relations
        }

        out: list[RESPONSE] = []
        for dto in items:
            fk_id = getattr(dto, fk_id_attr)
            value = relations.get(fk_id) if fk_id is not None else None
            out.append(self._dto_set(dto, dto_field, value))

        return out

    def _include_m2m(
        self,
        items: list[RESPONSE],
        dto_field: str,
        relation_name: str,
        rel_field: ManyToManyField,
    ) -> list[RESPONSE]:
        """
        Deterministic forward M2M include using Django's `through_fields`.
    
        Contract:
        - The ManyToManyField MUST define `through_fields=(root_fk, related_fk)`.
        - No inference is performed. If missing, an AppError is raised.
        - DTOs must expose `id` and have a slot `dto_field` to receive a list of related DTOs.
        """

        if not items:
            return items
    
        root_ids_ordered: list[Any] = []
        root_ids_set: set[Any] = set()
    
        for dto in items:
            if not hasattr(dto, "id"):
                raise AppError(
                    class_pointer=self,
                    title="Include requires DTO id",
                    message="M2M include requires root DTOs to expose an 'id' field.",
                    details={"dto_field": dto_field, "relation_name": relation_name},
                    code=500,
                )
            rid = getattr(dto, "id")
            root_ids_ordered.append(rid)
            if rid is not None:
                root_ids_set.add(rid)
    
        through_raw = rel_field.remote_field.through
        if not through_raw:
            raise AppError(
                class_pointer=self,
                title="Invalid M2M relation",
                message="ManyToMany relation does not expose a through table.",
                details={
                    "root_model": self.model.__name__,
                    "relation": relation_name,
                    "field": rel_field.name,
                },
                code=500,
            )
    
        through = cast(type[SoftDeleteModel], through_raw)
        through_fields = getattr(rel_field.remote_field, "through_fields", None)
        if not through_fields or len(through_fields) != 2 or not through_fields[0] or not through_fields[1]:
            raise AppError(
                class_pointer=self,
                title="Missing through_fields on M2M relation",
                message="To perform deterministic M2M includes (no inference), define through_fields on the ManyToManyField.",
                details={
                    "root_model": self.model.__name__,
                    "relation": relation_name,
                    "field": rel_field.name,
                    "through_model": through.__name__,
                    "example": "through_fields=('root_fk_field', 'related_fk_field')",
                },
                code=500,
            )
    
        root_fk_name, related_fk_name = through_fields
        related_model = cast(Type[SoftDeleteModel], rel_field.related_model)
        related_pk = related_model._meta.pk.name
    
        through_qs = (
            through.objects
            .filter(**{f"{root_fk_name}_id__in": list(root_ids_set)})
            .filter(deleted_at__isnull=True)
        )
    
        pairs = list(through_qs.values_list(f"{root_fk_name}_id", f"{related_fk_name}_id"))
        related_ids = {rel_id for _, rel_id in pairs if rel_id is not None}
    
        if not related_ids:
            return [self._dto_set(dto, dto_field, []) for dto in items]
    
        rel_dto_map = {
            getattr(obj, related_pk): cast(BaseDto, obj.as_dto())
            for obj in related_model.objects.filter(
                **{f"{related_pk}__in": list(related_ids)}
            ).filter(deleted_at__isnull=True)
        }
    
        grouped = defaultdict(list)
        for root_id, rel_id in pairs:
            rel_dto = rel_dto_map.get(rel_id)
            if rel_dto is not None:
                grouped[root_id].append(rel_dto)
    
        return [
            self._dto_set(dto, dto_field, grouped.get(rid, []))
            for dto, rid in zip(items, root_ids_ordered)
        ]

    def _dto_set(self, dto: RESPONSE, field: str, value: Any) -> RESPONSE:
        if hasattr(dto, "model_copy"):
            return cast(RESPONSE, dto.model_copy(update={field: value}))
        setattr(dto, field, value)
        return dto

