from __future__ import annotations

from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from datetime import datetime
from typing import Literal, Protocol, runtime_checkable

from govoplan_core.core.external_references import (
    SOURCE_AUTHORITY_MODES,
    SourceAuthorityMode,
)


CAPABILITY_DATASOURCE_CATALOGUE = "datasources.catalogue"
CAPABILITY_DATASOURCE_LIFECYCLE = "datasources.lifecycle"
CAPABILITY_DATASOURCE_PUBLICATION = "datasources.publication"
CAPABILITY_DATASOURCE_ORIGINS = "connectors.datasourceOrigins"

DatasourceMode = Literal["live", "cached", "static"]
DatasourceKind = Literal[
    "upload",
    "database",
    "http",
    "rest",
    "directory",
    "file",
    "feed",
    "custom",
]
DatasourceShape = Literal["tabular", "document", "binary", "directory", "stream"]
DatasourceConsistency = Literal["current", "live", "frozen"]


class DatasourceError(ValueError):
    """Stable base error for provider-neutral datasource operations."""


class DatasourceNotFoundError(DatasourceError):
    pass


class DatasourceAccessError(DatasourceError):
    pass


class DatasourceValidationError(DatasourceError):
    pass


class DatasourceUnavailableError(DatasourceError):
    pass


@dataclass(frozen=True, slots=True)
class DatasourceField:
    name: str
    data_type: str
    nullable: bool = True


@dataclass(frozen=True, slots=True)
class DatasourceGovernance:
    """Provider-neutral governance facts attached to a datasource revision."""

    owner_ref: str | None = None
    steward_ref: str | None = None
    responsible_organization_ref: str | None = None
    responsible_function_ref: str | None = None
    authoritative_source_ref: str | None = None
    authority_mode: SourceAuthorityMode = "linked_reference"
    legal_basis_refs: tuple[str, ...] = ()
    purposes: tuple[str, ...] = ()
    semantic_definition: str | None = None
    schema_owner_ref: str | None = None
    official_keys: tuple[str, ...] = ()
    classification: str = "internal"
    privacy_profile_ref: str | None = None
    retention_policy_ref: str | None = None
    hold_refs: tuple[str, ...] = ()
    publication_state: str = "draft"
    transfer_agreement_ref: str | None = None
    freshness_policy: Mapping[str, object] = field(default_factory=dict)
    quality_policy: Mapping[str, object] = field(default_factory=dict)
    known_limits: tuple[str, ...] = ()
    correction_procedure_ref: str | None = None
    affected_refs: tuple[str, ...] = ()
    dependency_refs: tuple[str, ...] = ()

    def __post_init__(self) -> None:
        if self.authority_mode not in SOURCE_AUTHORITY_MODES:
            raise DatasourceValidationError(
                f"Unsupported datasource authority mode: {self.authority_mode!r}."
            )
        if not self.classification.strip():
            raise DatasourceValidationError("Datasource classification is required.")
        if not self.publication_state.strip():
            raise DatasourceValidationError("Datasource publication state is required.")
        for field_name in (
            "legal_basis_refs",
            "purposes",
            "official_keys",
            "hold_refs",
            "known_limits",
            "affected_refs",
            "dependency_refs",
        ):
            values = getattr(self, field_name)
            if any(not value.strip() for value in values):
                raise DatasourceValidationError(
                    f"Datasource governance {field_name} cannot contain empty values."
                )
            if len(values) != len(set(values)):
                raise DatasourceValidationError(
                    f"Datasource governance {field_name} cannot contain duplicates."
                )

    @classmethod
    def from_mapping(cls, value: Mapping[str, object] | None) -> "DatasourceGovernance":
        source = value or {}
        return cls(
            owner_ref=_optional_governance_text(source.get("owner_ref")),
            steward_ref=_optional_governance_text(source.get("steward_ref")),
            responsible_organization_ref=_optional_governance_text(
                source.get("responsible_organization_ref")
            ),
            responsible_function_ref=_optional_governance_text(
                source.get("responsible_function_ref")
            ),
            authoritative_source_ref=_optional_governance_text(
                source.get("authoritative_source_ref")
            ),
            authority_mode=str(
                source.get("authority_mode") or "linked_reference"
            ),  # type: ignore[arg-type]
            legal_basis_refs=_governance_texts(source.get("legal_basis_refs")),
            purposes=_governance_texts(source.get("purposes")),
            semantic_definition=_optional_governance_text(
                source.get("semantic_definition")
            ),
            schema_owner_ref=_optional_governance_text(source.get("schema_owner_ref")),
            official_keys=_governance_texts(source.get("official_keys")),
            classification=str(source.get("classification") or "internal"),
            privacy_profile_ref=_optional_governance_text(
                source.get("privacy_profile_ref")
            ),
            retention_policy_ref=_optional_governance_text(
                source.get("retention_policy_ref")
            ),
            hold_refs=_governance_texts(source.get("hold_refs")),
            publication_state=str(source.get("publication_state") or "draft"),
            transfer_agreement_ref=_optional_governance_text(
                source.get("transfer_agreement_ref")
            ),
            freshness_policy=_governance_mapping(source.get("freshness_policy")),
            quality_policy=_governance_mapping(source.get("quality_policy")),
            known_limits=_governance_texts(source.get("known_limits")),
            correction_procedure_ref=_optional_governance_text(
                source.get("correction_procedure_ref")
            ),
            affected_refs=_governance_texts(source.get("affected_refs")),
            dependency_refs=_governance_texts(source.get("dependency_refs")),
        )

    def to_dict(self) -> dict[str, object]:
        return {
            "owner_ref": self.owner_ref,
            "steward_ref": self.steward_ref,
            "responsible_organization_ref": self.responsible_organization_ref,
            "responsible_function_ref": self.responsible_function_ref,
            "authoritative_source_ref": self.authoritative_source_ref,
            "authority_mode": self.authority_mode,
            "legal_basis_refs": list(self.legal_basis_refs),
            "purposes": list(self.purposes),
            "semantic_definition": self.semantic_definition,
            "schema_owner_ref": self.schema_owner_ref,
            "official_keys": list(self.official_keys),
            "classification": self.classification,
            "privacy_profile_ref": self.privacy_profile_ref,
            "retention_policy_ref": self.retention_policy_ref,
            "hold_refs": list(self.hold_refs),
            "publication_state": self.publication_state,
            "transfer_agreement_ref": self.transfer_agreement_ref,
            "freshness_policy": dict(self.freshness_policy),
            "quality_policy": dict(self.quality_policy),
            "known_limits": list(self.known_limits),
            "correction_procedure_ref": self.correction_procedure_ref,
            "affected_refs": list(self.affected_refs),
            "dependency_refs": list(self.dependency_refs),
        }


@dataclass(frozen=True, slots=True)
class DatasourceDescriptor:
    ref: str
    source_name: str
    name: str
    kind: DatasourceKind
    mode: DatasourceMode
    shape: DatasourceShape
    status: str = "active"
    description: str | None = None
    provider: str | None = None
    provider_ref: str | None = None
    schema: tuple[DatasourceField, ...] = ()
    schema_version: str = "1"
    fingerprint: str = ""
    current_materialization_ref: str | None = None
    row_count: int | None = None
    byte_count: int | None = None
    updated_at: datetime | None = None
    capabilities: tuple[str, ...] = ("read",)
    provenance: Mapping[str, object] = field(default_factory=dict)
    metadata: Mapping[str, object] = field(default_factory=dict)
    governance: DatasourceGovernance = field(default_factory=DatasourceGovernance)


@dataclass(frozen=True, slots=True)
class DatasourceMaterialization:
    ref: str
    datasource_ref: str
    revision: int
    state: str
    fingerprint: str
    schema: tuple[DatasourceField, ...] = ()
    row_count: int | None = None
    byte_count: int | None = None
    frozen_at: datetime | None = None
    frozen_label: str | None = None
    source_timestamp: datetime | None = None
    created_at: datetime | None = None
    provenance: Mapping[str, object] = field(default_factory=dict)
    metadata: Mapping[str, object] = field(default_factory=dict)
    governance: DatasourceGovernance = field(default_factory=DatasourceGovernance)


@dataclass(frozen=True, slots=True)
class DatasourceStage:
    ref: str
    name: str
    source_name: str
    kind: DatasourceKind
    mode: DatasourceMode
    shape: DatasourceShape
    state: str
    target_datasource_ref: str | None = None
    fingerprint: str = ""
    schema: tuple[DatasourceField, ...] = ()
    row_count: int | None = None
    byte_count: int | None = None
    validation: Mapping[str, object] = field(default_factory=dict)
    created_at: datetime | None = None
    promoted_at: datetime | None = None
    promoted_materialization_ref: str | None = None
    provenance: Mapping[str, object] = field(default_factory=dict)
    metadata: Mapping[str, object] = field(default_factory=dict)
    governance: DatasourceGovernance = field(default_factory=DatasourceGovernance)


@dataclass(frozen=True, slots=True)
class DatasourceReadRequest:
    datasource_ref: str
    materialization_ref: str | None = None
    consistency: DatasourceConsistency = "current"
    limit: int = 250
    offset: int = 0
    columns: tuple[str, ...] = ()
    expected_fingerprint: str | None = None


@dataclass(frozen=True, slots=True)
class DatasourceReadResult:
    datasource: DatasourceDescriptor
    rows: tuple[Mapping[str, object], ...]
    total_rows: int
    truncated: bool
    materialization: DatasourceMaterialization | None = None


@dataclass(frozen=True, slots=True)
class DatasourceStageInput:
    name: str
    source_name: str
    kind: DatasourceKind
    mode: DatasourceMode
    shape: DatasourceShape
    rows: tuple[Mapping[str, object], ...]
    description: str | None = None
    target_datasource_ref: str | None = None
    provider: str | None = None
    provider_ref: str | None = None
    provenance: Mapping[str, object] = field(default_factory=dict)
    metadata: Mapping[str, object] = field(default_factory=dict)
    governance: DatasourceGovernance | None = None


@dataclass(frozen=True, slots=True)
class DatasourcePublicationRequest:
    producer_module: str
    producer_run_ref: str
    idempotency_key: str
    rows: tuple[Mapping[str, object], ...]
    target_datasource_ref: str | None = None
    name: str | None = None
    source_name: str | None = None
    description: str | None = None
    freeze: bool = False
    frozen_label: str | None = None
    set_current: bool = True
    source_timestamp: datetime | None = None
    provenance: Mapping[str, object] = field(default_factory=dict)
    metadata: Mapping[str, object] = field(default_factory=dict)
    governance: DatasourceGovernance | None = None


@dataclass(frozen=True, slots=True)
class DatasourcePublicationResult:
    ref: str
    status: str
    datasource: DatasourceDescriptor
    materialization: DatasourceMaterialization
    replayed: bool = False


@dataclass(frozen=True, slots=True)
class DatasourceOrigin:
    ref: str
    source_name: str
    name: str
    kind: DatasourceKind
    shape: DatasourceShape
    supported_modes: tuple[DatasourceMode, ...]
    provider: str
    description: str | None = None
    schema: tuple[DatasourceField, ...] = ()
    schema_version: str = "1"
    fingerprint: str = ""
    row_count: int | None = None
    byte_count: int | None = None
    updated_at: datetime | None = None
    capabilities: tuple[str, ...] = ("read",)
    metadata: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class DatasourceOriginReadRequest:
    origin_ref: str
    limit: int = 250
    offset: int = 0
    columns: tuple[str, ...] = ()
    expected_fingerprint: str | None = None


@dataclass(frozen=True, slots=True)
class DatasourceOriginReadResult:
    origin: DatasourceOrigin
    rows: tuple[Mapping[str, object], ...]
    total_rows: int
    truncated: bool


@runtime_checkable
class DatasourceCatalogueProvider(Protocol):
    def list_datasources(
        self,
        session: object,
        principal: object,
        *,
        query: str = "",
        limit: int = 100,
        authority_mode: str | None = None,
        classification: str | None = None,
        publication_state: str | None = None,
        owner_ref: str | None = None,
        responsible_organization_ref: str | None = None,
        affected_ref: str | None = None,
        dependency_ref: str | None = None,
    ) -> Sequence[DatasourceDescriptor]:
        ...

    def get_datasource(
        self,
        session: object,
        principal: object,
        *,
        datasource_ref: str,
    ) -> DatasourceDescriptor | None:
        ...

    def read_datasource(
        self,
        session: object,
        principal: object,
        *,
        request: DatasourceReadRequest,
    ) -> DatasourceReadResult:
        ...

    def list_materializations(
        self,
        session: object,
        principal: object,
        *,
        datasource_ref: str,
    ) -> Sequence[DatasourceMaterialization]:
        ...


@runtime_checkable
class DatasourceLifecycleProvider(Protocol):
    def list_stages(
        self,
        session: object,
        principal: object,
        *,
        limit: int = 100,
    ) -> Sequence[DatasourceStage]:
        ...

    def create_stage(
        self,
        session: object,
        principal: object,
        *,
        stage: DatasourceStageInput,
    ) -> DatasourceStage:
        ...

    def promote_stage(
        self,
        session: object,
        principal: object,
        *,
        stage_ref: str,
        freeze: bool = False,
        frozen_label: str | None = None,
    ) -> tuple[DatasourceDescriptor, DatasourceMaterialization]:
        ...

    def register_origin(
        self,
        session: object,
        principal: object,
        *,
        origin_ref: str,
        name: str,
        source_name: str,
        mode: DatasourceMode,
        description: str | None = None,
        governance: DatasourceGovernance | None = None,
    ) -> DatasourceDescriptor:
        ...

    def update_datasource_governance(
        self,
        session: object,
        principal: object,
        *,
        datasource_ref: str,
        governance: DatasourceGovernance,
    ) -> DatasourceDescriptor:
        ...

    def refresh_datasource(
        self,
        session: object,
        principal: object,
        *,
        datasource_ref: str,
    ) -> tuple[DatasourceDescriptor, DatasourceMaterialization]:
        ...

    def freeze_datasource(
        self,
        session: object,
        principal: object,
        *,
        datasource_ref: str,
        label: str | None = None,
    ) -> DatasourceMaterialization:
        ...

    def retire_datasource(
        self,
        session: object,
        principal: object,
        *,
        datasource_ref: str,
    ) -> DatasourceDescriptor:
        ...


@runtime_checkable
class DatasourcePublicationProvider(Protocol):
    def publish_rows(
        self,
        session: object,
        principal: object,
        *,
        request: DatasourcePublicationRequest,
    ) -> DatasourcePublicationResult:
        ...


@runtime_checkable
class DatasourceOriginProvider(Protocol):
    def list_origins(
        self,
        session: object,
        principal: object,
        *,
        query: str = "",
        limit: int = 100,
    ) -> Sequence[DatasourceOrigin]:
        ...

    def get_origin(
        self,
        session: object,
        principal: object,
        *,
        origin_ref: str,
    ) -> DatasourceOrigin | None:
        ...

    def read_origin(
        self,
        session: object,
        principal: object,
        *,
        request: DatasourceOriginReadRequest,
    ) -> DatasourceOriginReadResult:
        ...


def datasource_catalogue(registry: object | None) -> DatasourceCatalogueProvider | None:
    capability = _capability(registry, CAPABILITY_DATASOURCE_CATALOGUE)
    return capability if isinstance(capability, DatasourceCatalogueProvider) else None


def datasource_lifecycle(registry: object | None) -> DatasourceLifecycleProvider | None:
    capability = _capability(registry, CAPABILITY_DATASOURCE_LIFECYCLE)
    return capability if isinstance(capability, DatasourceLifecycleProvider) else None


def datasource_publication(
    registry: object | None,
) -> DatasourcePublicationProvider | None:
    capability = _capability(registry, CAPABILITY_DATASOURCE_PUBLICATION)
    return capability if isinstance(capability, DatasourcePublicationProvider) else None


def datasource_origins(registry: object | None) -> DatasourceOriginProvider | None:
    capability = _capability(registry, CAPABILITY_DATASOURCE_ORIGINS)
    return capability if isinstance(capability, DatasourceOriginProvider) else None


def _capability(registry: object | None, name: str) -> object | None:
    if (
        registry is None
        or not hasattr(registry, "has_capability")
        or not hasattr(registry, "capability")
        or not registry.has_capability(name)
    ):
        return None
    return registry.capability(name)


def _optional_governance_text(value: object) -> str | None:
    if value is None:
        return None
    cleaned = str(value).strip()
    return cleaned or None


def _governance_texts(value: object) -> tuple[str, ...]:
    if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
        return ()
    return tuple(str(item).strip() for item in value)


def _governance_mapping(value: object) -> Mapping[str, object]:
    if not isinstance(value, Mapping):
        return {}
    return {str(key): item for key, item in value.items()}


__all__ = [
    "CAPABILITY_DATASOURCE_CATALOGUE",
    "CAPABILITY_DATASOURCE_LIFECYCLE",
    "CAPABILITY_DATASOURCE_ORIGINS",
    "DatasourceAccessError",
    "DatasourceCatalogueProvider",
    "DatasourceConsistency",
    "DatasourceDescriptor",
    "DatasourceError",
    "DatasourceField",
    "DatasourceGovernance",
    "DatasourceKind",
    "DatasourceLifecycleProvider",
    "DatasourceMaterialization",
    "DatasourceMode",
    "DatasourceNotFoundError",
    "DatasourceOrigin",
    "DatasourceOriginProvider",
    "DatasourceOriginReadRequest",
    "DatasourceOriginReadResult",
    "DatasourceReadRequest",
    "DatasourceReadResult",
    "DatasourceShape",
    "DatasourceStage",
    "DatasourceStageInput",
    "DatasourceUnavailableError",
    "DatasourceValidationError",
    "datasource_catalogue",
    "datasource_lifecycle",
    "datasource_origins",
]
