"""Export Engine — registry for entity-type export specifications.

Usage (in a module's ``apps.py`` ``ready()`` hook)::

    from simorgh.apps.platform_core.export_registry import (
        export_registry,
        ExportSpec,
        ColumnExportSpec,
    )

    export_registry.register(ExportSpec(
        entity_type="crm.lead",
        columns=[
            ColumnExportSpec(key="full_name",  label="Full Name"),
            ColumnExportSpec(key="email",       label="Email"),
            ColumnExportSpec(key="phone",       label="Phone"),
        ],
        queryset_fn=my_leads_queryset,
        row_fn=my_lead_to_dict,
        display_name="CRM Lead",
    ))

``queryset_fn`` signature::

    def my_leads_queryset(*, tenant, actor, filters: dict) -> QuerySet | Iterable:
        ...

``row_fn`` signature::

    def my_lead_to_dict(obj) -> dict[str, Any]:
        ...

Both must be pure / side-effect-free.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Callable, Iterable

__all__ = [
    "ColumnExportSpec",
    "ExportSpec",
    "ExportRegistry",
    "export_registry",
]


@dataclass(frozen=True, slots=True)
class ColumnExportSpec:
    """Metadata for a single exportable column."""

    key: str
    label: str
    # Optional callable to transform the raw value before writing.
    # Receives (value, obj) and returns a serialisable scalar.
    transform: Callable[[Any, Any], Any] | None = None


@dataclass
class ExportSpec:
    """Schema + data-fetcher for one entity type."""

    entity_type: str
    # Ordered list of columns that will appear in the export file.
    columns: list[ColumnExportSpec]
    # Callable that returns an iterable of objects to export.
    queryset_fn: Callable[..., Iterable[Any]]
    # Callable that converts a single object to a plain dict keyed by
    # ``ColumnExportSpec.key``.
    row_fn: Callable[[Any], dict[str, Any]]
    # Optional human-readable display name (falls back to entity_type)
    display_name: str = ""

    def column_labels(self) -> list[str]:
        return [c.label for c in self.columns]

    def column_keys(self) -> list[str]:
        return [c.key for c in self.columns]


class ExportRegistry:
    """Singleton registry mapping entity_type → ExportSpec."""

    def __init__(self) -> None:
        self._specs: dict[str, ExportSpec] = {}

    # ------------------------------------------------------------------
    # Registration
    # ------------------------------------------------------------------

    def register(self, spec: ExportSpec) -> None:
        """Register *spec*.  Raises ``ValueError`` if already registered."""
        if spec.entity_type in self._specs:
            raise ValueError(
                f"ExportSpec for entity type {spec.entity_type!r} is already registered. "
                "Use replace() if intentional."
            )
        self._specs[spec.entity_type] = spec

    def replace(self, spec: ExportSpec) -> None:
        """Register or replace the spec for *entity_type*."""
        self._specs[spec.entity_type] = spec

    def unregister(self, entity_type: str) -> None:
        """Remove *entity_type* from registry.  No-op if not registered."""
        self._specs.pop(entity_type, None)

    # ------------------------------------------------------------------
    # Lookup
    # ------------------------------------------------------------------

    def get(self, entity_type: str) -> ExportSpec | None:
        return self._specs.get(entity_type)

    def get_or_raise(self, entity_type: str) -> ExportSpec:
        spec = self.get(entity_type)
        if spec is None:
            raise KeyError(
                f"No ExportSpec registered for entity type {entity_type!r}. "
                f"Available: {sorted(self._specs)}"
            )
        return spec

    def all_entity_types(self) -> list[str]:
        return sorted(self._specs)

    def all_specs(self) -> list[ExportSpec]:
        return [self._specs[k] for k in sorted(self._specs)]

    def __contains__(self, entity_type: str) -> bool:
        return entity_type in self._specs

    def __len__(self) -> int:
        return len(self._specs)

    # ------------------------------------------------------------------
    # Row serialisation helpers
    # ------------------------------------------------------------------

    def build_row(self, spec: ExportSpec, obj: Any) -> dict[str, Any]:
        """Convert *obj* to an ordered dict keyed by column keys."""
        raw = spec.row_fn(obj)
        result: dict[str, Any] = {}
        for col in spec.columns:
            value = raw.get(col.key)
            if col.transform is not None:
                value = col.transform(value, obj)
            result[col.key] = value
        return result


# Module-level singleton — import this in application code.
export_registry = ExportRegistry()
