from __future__ import annotations

from collections.abc import Callable, Iterable, Mapping, Sequence
from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import Literal, cast

from govoplan_core.core.external_references import (
    INTEGRATION_MATURITY_ORDER,
    SOURCE_AUTHORITY_MODES,
    IntegrationMaturity,
    SourceAuthorityMode,
    integration_maturity_rank,
)


ArchitectureLayer = Literal[
    "runtime_meta",
    "institutional_foundation",
    "governance_accountability",
    "human_work_procedure",
    "communication_participation",
    "content_records_evidence",
    "data_reporting_integration",
    "domain_capability",
    "product_sector_package",
]
ModuleKind = Literal[
    "kernel",
    "foundation",
    "governance",
    "runtime",
    "editor",
    "domain",
    "integration",
    "presentation",
    "operations",
    "meta",
]
ModuleMaturity = Literal[
    "concept",
    "scaffold",
    "vertical_slice",
    "reference_ready",
    "supported",
    "lts",
]
MaturityEvidenceKind = Literal[
    "test",
    "documentation",
    "reference_process",
    "target_test",
    "migration",
    "upgrade",
    "recovery",
    "security",
    "operations",
    "accessibility",
    "privacy",
    "compatibility",
    "provider",
]
ProviderOperation = Literal[
    "discover",
    "link",
    "search",
    "read",
    "write",
    "delete",
    "publish",
    "synchronize",
    "migrate",
    "preview",
    "dry_run",
]
ProviderHealthState = Literal[
    "healthy",
    "warning",
    "error",
    "unknown",
    "inactive",
]
ProviderFreshnessState = Literal[
    "current",
    "stale",
    "unknown",
    "not_applicable",
]
ProviderConflictState = Literal[
    "clear",
    "pending",
    "blocked",
    "unknown",
    "not_applicable",
]
ProviderRecoveryState = Literal[
    "ready",
    "attention",
    "unsupported",
    "unknown",
    "not_applicable",
]


ARCHITECTURE_LAYERS: tuple[ArchitectureLayer, ...] = (
    "runtime_meta",
    "institutional_foundation",
    "governance_accountability",
    "human_work_procedure",
    "communication_participation",
    "content_records_evidence",
    "data_reporting_integration",
    "domain_capability",
    "product_sector_package",
)
MODULE_KINDS: tuple[ModuleKind, ...] = (
    "kernel",
    "foundation",
    "governance",
    "runtime",
    "editor",
    "domain",
    "integration",
    "presentation",
    "operations",
    "meta",
)
MODULE_MATURITY_ORDER: tuple[ModuleMaturity, ...] = (
    "concept",
    "scaffold",
    "vertical_slice",
    "reference_ready",
    "supported",
    "lts",
)
MATURITY_EVIDENCE_KINDS: tuple[MaturityEvidenceKind, ...] = (
    "test",
    "documentation",
    "reference_process",
    "target_test",
    "migration",
    "upgrade",
    "recovery",
    "security",
    "operations",
    "accessibility",
    "privacy",
    "compatibility",
    "provider",
)
PROVIDER_OPERATIONS: tuple[ProviderOperation, ...] = (
    "discover",
    "link",
    "search",
    "read",
    "write",
    "delete",
    "publish",
    "synchronize",
    "migrate",
    "preview",
    "dry_run",
)
PROVIDER_HEALTH_STATES: tuple[ProviderHealthState, ...] = (
    "healthy",
    "warning",
    "error",
    "unknown",
    "inactive",
)
PROVIDER_FRESHNESS_STATES: tuple[ProviderFreshnessState, ...] = (
    "current",
    "stale",
    "unknown",
    "not_applicable",
)
PROVIDER_CONFLICT_STATES: tuple[ProviderConflictState, ...] = (
    "clear",
    "pending",
    "blocked",
    "unknown",
    "not_applicable",
)
PROVIDER_RECOVERY_STATES: tuple[ProviderRecoveryState, ...] = (
    "ready",
    "attention",
    "unsupported",
    "unknown",
    "not_applicable",
)

_MATURITY_EVIDENCE_REQUIREMENTS: dict[ModuleMaturity, frozenset[str]] = {
    "concept": frozenset(),
    "scaffold": frozenset({"documentation"}),
    "vertical_slice": frozenset({"test", "documentation"}),
    "reference_ready": frozenset(
        {
            "test",
            "documentation",
            "reference_process",
            "target_test",
            "recovery",
            "security",
            "operations",
            "accessibility",
            "privacy",
        }
    ),
    "supported": frozenset(
        {
            "test",
            "documentation",
            "reference_process",
            "target_test",
            "security",
            "operations",
            "upgrade",
            "recovery",
            "accessibility",
            "privacy",
        }
    ),
    "lts": frozenset(
        {
            "test",
            "documentation",
            "reference_process",
            "target_test",
            "security",
            "operations",
            "upgrade",
            "recovery",
            "accessibility",
            "privacy",
            "compatibility",
        }
    ),
}
_OPERATION_MINIMUM_MATURITY: dict[ProviderOperation, IntegrationMaturity] = {
    "discover": "discover",
    "link": "link",
    "search": "search",
    "read": "read",
    "write": "publish",
    "delete": "publish",
    "publish": "publish",
    "synchronize": "synchronize",
    "migrate": "migrate",
    "preview": "read",
    "dry_run": "read",
}
_EFFECT_OPERATIONS = frozenset({"write", "delete", "publish", "synchronize", "migrate"})
_READ_OPERATIONS = frozenset({"search", "read", "preview", "dry_run"})


class ArchitectureDeclarationError(ValueError):
    pass


@dataclass(frozen=True, slots=True)
class ModuleMaturityEvidence:
    kind: MaturityEvidenceKind
    reference: str
    summary: str

    def __post_init__(self) -> None:
        if self.kind not in MATURITY_EVIDENCE_KINDS:
            raise ArchitectureDeclarationError(
                f"Unsupported module maturity evidence kind: {self.kind!r}."
            )
        _require_text(self.reference, "Maturity evidence reference")
        _require_text(self.summary, "Maturity evidence summary")

    def to_dict(self) -> dict[str, str]:
        return {
            "kind": self.kind,
            "reference": self.reference,
            "summary": self.summary,
        }


@dataclass(frozen=True, slots=True)
class ModuleArchitectureDocumentation:
    migration: tuple[str, ...] = ()
    upgrade: tuple[str, ...] = ()
    recovery: tuple[str, ...] = ()
    security: tuple[str, ...] = ()
    operations: tuple[str, ...] = ()

    def __post_init__(self) -> None:
        for field_name in ("migration", "upgrade", "recovery", "security", "operations"):
            _validate_text_tuple(
                getattr(self, field_name),
                f"Architecture documentation {field_name}",
            )

    def to_dict(self) -> dict[str, list[str]]:
        return {
            "migration": list(self.migration),
            "upgrade": list(self.upgrade),
            "recovery": list(self.recovery),
            "security": list(self.security),
            "operations": list(self.operations),
        }


@dataclass(frozen=True, slots=True)
class ModuleArchitectureDeclaration:
    layer: ArchitectureLayer
    kind: ModuleKind
    maturity: ModuleMaturity
    evidence: tuple[ModuleMaturityEvidence, ...] = ()
    known_limits: tuple[str, ...] = ()
    supported_authority_modes: tuple[SourceAuthorityMode, ...] = ()
    owned_concepts: tuple[str, ...] = ()
    non_owned_concepts: tuple[str, ...] = ()
    reference_packages: tuple[str, ...] = ()
    target_tested_providers: tuple[str, ...] = ()
    documentation: ModuleArchitectureDocumentation = field(
        default_factory=ModuleArchitectureDocumentation
    )
    contract_version: str = "1"

    def __post_init__(self) -> None:
        if self.contract_version != "1":
            raise ArchitectureDeclarationError(
                "Unsupported module architecture contract version."
            )
        if self.layer not in ARCHITECTURE_LAYERS:
            raise ArchitectureDeclarationError(
                f"Unsupported architecture layer: {self.layer!r}."
            )
        if self.kind not in MODULE_KINDS:
            raise ArchitectureDeclarationError(
                f"Unsupported module kind: {self.kind!r}."
            )
        if self.maturity not in MODULE_MATURITY_ORDER:
            raise ArchitectureDeclarationError(
                f"Unsupported module maturity: {self.maturity!r}."
            )
        for mode in self.supported_authority_modes:
            if mode not in SOURCE_AUTHORITY_MODES:
                raise ArchitectureDeclarationError(
                    f"Unsupported source-authority mode: {mode!r}."
                )
        for field_name in (
            "known_limits",
            "owned_concepts",
            "non_owned_concepts",
            "reference_packages",
            "target_tested_providers",
        ):
            _validate_text_tuple(getattr(self, field_name), field_name.replace("_", " "))
        if set(self.owned_concepts) & set(self.non_owned_concepts):
            raise ArchitectureDeclarationError(
                "Owned and explicitly non-owned concepts must not overlap."
            )
        evidence_keys = [(item.kind, item.reference) for item in self.evidence]
        if len(evidence_keys) != len(set(evidence_keys)):
            raise ArchitectureDeclarationError(
                "Module maturity evidence entries must be unique."
            )

    def to_dict(self) -> dict[str, object]:
        return {
            "contract_version": self.contract_version,
            "layer": self.layer,
            "kind": self.kind,
            "maturity": self.maturity,
            "evidence": [item.to_dict() for item in self.evidence],
            "known_limits": list(self.known_limits),
            "supported_authority_modes": list(self.supported_authority_modes),
            "owned_concepts": list(self.owned_concepts),
            "non_owned_concepts": list(self.non_owned_concepts),
            "reference_packages": list(self.reference_packages),
            "target_tested_providers": list(self.target_tested_providers),
            "documentation": self.documentation.to_dict(),
        }


def declared_module_architecture(
    *,
    layer: ArchitectureLayer,
    kind: ModuleKind,
    maturity: ModuleMaturity,
    documentation_ref: str,
    known_limits: Sequence[str],
    test_ref: str | None = None,
    supported_authority_modes: Sequence[SourceAuthorityMode] = (),
    owned_concepts: Sequence[str] = (),
    non_owned_concepts: Sequence[str] = (),
    reference_packages: Sequence[str] = (),
    target_tested_providers: Sequence[str] = (),
    migration_docs: Sequence[str] = (),
    upgrade_docs: Sequence[str] = (),
    recovery_docs: Sequence[str] = (),
    security_docs: Sequence[str] = (),
    operations_docs: Sequence[str] = (),
) -> ModuleArchitectureDeclaration:
    """Build concise, explicit repository-owned architecture metadata.

    The helper deliberately does not infer maturity from repository contents.
    Callers must choose the claim and provide the evidence paths that support
    it; workspace checks subsequently prove that repository-local paths exist.
    """

    evidence = [
        ModuleMaturityEvidence(
            kind="documentation",
            reference=documentation_ref,
            summary="Defines the module boundary, supported behavior, and known limits.",
        )
    ]
    if test_ref is not None:
        evidence.append(
            ModuleMaturityEvidence(
                kind="test",
                reference=test_ref,
                summary="Exercises the declared module contract and implemented slice.",
            )
        )
    return ModuleArchitectureDeclaration(
        layer=layer,
        kind=kind,
        maturity=maturity,
        evidence=tuple(evidence),
        known_limits=tuple(known_limits),
        supported_authority_modes=tuple(supported_authority_modes),
        owned_concepts=tuple(owned_concepts),
        non_owned_concepts=tuple(non_owned_concepts),
        reference_packages=tuple(reference_packages),
        target_tested_providers=tuple(target_tested_providers),
        documentation=ModuleArchitectureDocumentation(
            migration=tuple(migration_docs),
            upgrade=tuple(upgrade_docs),
            recovery=tuple(recovery_docs),
            security=tuple(security_docs),
            operations=tuple(operations_docs),
        ),
    )


@dataclass(frozen=True, slots=True)
class ProviderObjectDeclaration:
    object_type: str
    field_groups: tuple[str, ...]
    authority_modes: tuple[SourceAuthorityMode, ...]
    default_authority_mode: SourceAuthorityMode

    def __post_init__(self) -> None:
        _require_text(self.object_type, "Provider object type")
        _validate_text_tuple(self.field_groups, "Provider field groups", require=True)
        if not self.authority_modes:
            raise ArchitectureDeclarationError(
                "Provider object declarations require source-authority modes."
            )
        for mode in self.authority_modes:
            if mode not in SOURCE_AUTHORITY_MODES:
                raise ArchitectureDeclarationError(
                    f"Unsupported source-authority mode: {mode!r}."
                )
        if self.default_authority_mode not in self.authority_modes:
            raise ArchitectureDeclarationError(
                "Provider default authority mode must be one of its supported modes."
            )

    def to_dict(self) -> dict[str, object]:
        return {
            "object_type": self.object_type,
            "field_groups": list(self.field_groups),
            "authority_modes": list(self.authority_modes),
            "default_authority_mode": self.default_authority_mode,
        }


@dataclass(frozen=True, slots=True)
class ProviderBehaviorDeclaration:
    revision_tokens: str | None = None
    concurrency: str | None = None
    freshness: str | None = None
    health: str | None = None
    max_read_items: int | None = None
    idempotency: str | None = None
    retry: str | None = None
    timeout_seconds: int | None = None
    conflicts: str | None = None
    outcome_unknown: str | None = None
    outcome_unknown_supported: bool = False
    evidence: str | None = None
    audit_event_types: tuple[str, ...] = ()
    correction: str | None = None
    rollback: str | None = None
    compensation: str | None = None
    reconciliation: str | None = None
    outage: str | None = None
    classifications: tuple[str, ...] = ()
    purposes: tuple[str, ...] = ()
    retention: str | None = None
    secret_handling: str | None = None

    def __post_init__(self) -> None:
        if self.max_read_items is not None and self.max_read_items < 1:
            raise ArchitectureDeclarationError(
                "Provider max_read_items must be positive."
            )
        if self.timeout_seconds is not None and self.timeout_seconds < 1:
            raise ArchitectureDeclarationError(
                "Provider timeout_seconds must be positive."
            )
        for field_name in ("audit_event_types", "classifications", "purposes"):
            _validate_text_tuple(getattr(self, field_name), field_name.replace("_", " "))

    def to_dict(self) -> dict[str, object]:
        return {
            "revision_tokens": self.revision_tokens,
            "concurrency": self.concurrency,
            "freshness": self.freshness,
            "health": self.health,
            "max_read_items": self.max_read_items,
            "idempotency": self.idempotency,
            "retry": self.retry,
            "timeout_seconds": self.timeout_seconds,
            "conflicts": self.conflicts,
            "outcome_unknown": self.outcome_unknown,
            "outcome_unknown_supported": self.outcome_unknown_supported,
            "evidence": self.evidence,
            "audit_event_types": list(self.audit_event_types),
            "correction": self.correction,
            "rollback": self.rollback,
            "compensation": self.compensation,
            "reconciliation": self.reconciliation,
            "outage": self.outage,
            "classifications": list(self.classifications),
            "purposes": list(self.purposes),
            "retention": self.retention,
            "secret_handling": self.secret_handling,
        }


@dataclass(frozen=True, slots=True)
class ExternalProviderDeclaration:
    id: str
    module_id: str
    label: str
    maturity: IntegrationMaturity
    operations: tuple[ProviderOperation, ...]
    objects: tuple[ProviderObjectDeclaration, ...]
    behavior: ProviderBehaviorDeclaration
    capability_names: tuple[str, ...] = ()
    interface_names: tuple[str, ...] = ()
    action_keys: tuple[str, ...] = ()
    effect_keys: tuple[str, ...] = ()
    operational_check_ids: tuple[str, ...] = ()
    documentation_topic_ids: tuple[str, ...] = ()
    policy_hooks: tuple[str, ...] = ()
    ownership_resource_types: tuple[str, ...] = ()
    contract_version: str = "1"

    def __post_init__(self) -> None:
        if self.contract_version != "1":
            raise ArchitectureDeclarationError(
                "Unsupported external provider declaration contract version."
            )
        _require_text(self.id, "Provider declaration id")
        _require_text(self.module_id, "Provider declaration module id")
        _require_text(self.label, "Provider declaration label")
        if not self.id.startswith(f"{self.module_id}."):
            raise ArchitectureDeclarationError(
                "Provider declaration ids must be namespaced by their module id."
            )
        if self.maturity not in INTEGRATION_MATURITY_ORDER:
            raise ArchitectureDeclarationError(
                f"Unsupported integration maturity: {self.maturity!r}."
            )
        if not self.operations:
            raise ArchitectureDeclarationError(
                "Provider declarations require at least one operation."
            )
        for operation in self.operations:
            if operation not in PROVIDER_OPERATIONS:
                raise ArchitectureDeclarationError(
                    f"Unsupported provider operation: {operation!r}."
                )
            minimum = _OPERATION_MINIMUM_MATURITY[operation]
            if integration_maturity_rank(self.maturity) < integration_maturity_rank(
                minimum
            ):
                raise ArchitectureDeclarationError(
                    f"Provider operation {operation!r} requires integration maturity "
                    f"{minimum!r} or higher."
                )
        if len(self.operations) != len(set(self.operations)):
            raise ArchitectureDeclarationError(
                "Provider declaration operations must be unique."
            )
        if not self.objects:
            raise ArchitectureDeclarationError(
                "Provider declarations require at least one object declaration."
            )
        object_types = [item.object_type for item in self.objects]
        if len(object_types) != len(set(object_types)):
            raise ArchitectureDeclarationError(
                "Provider object types must be unique within a declaration."
            )
        for field_name in (
            "capability_names",
            "interface_names",
            "action_keys",
            "effect_keys",
            "operational_check_ids",
            "documentation_topic_ids",
            "policy_hooks",
            "ownership_resource_types",
        ):
            _validate_text_tuple(getattr(self, field_name), field_name.replace("_", " "))
        _validate_provider_authority_modes(self)
        _validate_provider_behavior(self)

    @property
    def authority_modes(self) -> tuple[SourceAuthorityMode, ...]:
        return tuple(
            dict.fromkeys(
                mode for item in self.objects for mode in item.authority_modes
            )
        )

    def to_dict(self) -> dict[str, object]:
        return {
            "contract_version": self.contract_version,
            "id": self.id,
            "module_id": self.module_id,
            "label": self.label,
            "maturity": self.maturity,
            "operations": list(self.operations),
            "objects": [item.to_dict() for item in self.objects],
            "authority_modes": list(self.authority_modes),
            "behavior": self.behavior.to_dict(),
            "capability_names": list(self.capability_names),
            "interface_names": list(self.interface_names),
            "action_keys": list(self.action_keys),
            "effect_keys": list(self.effect_keys),
            "operational_check_ids": list(self.operational_check_ids),
            "documentation_topic_ids": list(self.documentation_topic_ids),
            "policy_hooks": list(self.policy_hooks),
            "ownership_resource_types": list(self.ownership_resource_types),
        }


@dataclass(frozen=True, slots=True)
class ExternalProviderStateContext:
    """Bounded context supplied to a module-owned runtime state provider."""

    session: object | None
    tenant_id: str | None = None
    principal: object | None = None
    max_items: int = 500

    def __post_init__(self) -> None:
        if self.tenant_id is not None:
            _require_text(self.tenant_id, "Provider state tenant id")
        if not 1 <= self.max_items <= 1_000:
            raise ArchitectureDeclarationError(
                "Provider state max_items must be between 1 and 1000."
            )


@dataclass(frozen=True, slots=True)
class ExternalProviderRuntimeState:
    """One configured external binding observation without secret material."""

    provider_id: str
    observed_at: datetime
    configured: bool
    active: bool
    health: ProviderHealthState
    freshness: ProviderFreshnessState
    conflict: ProviderConflictState
    recovery: ProviderRecoveryState
    binding_ref: str | None = None
    authority_mode: SourceAuthorityMode | None = None
    last_success_at: datetime | None = None
    detail: str | None = None
    metrics: Mapping[str, str | int | float | bool | None] = field(
        default_factory=dict
    )
    contract_version: str = "1"

    def __post_init__(self) -> None:
        if self.contract_version != "1":
            raise ArchitectureDeclarationError(
                "Unsupported external provider runtime-state contract version."
            )
        _require_text(self.provider_id, "Provider runtime-state id")
        _validate_aware_datetime(self.observed_at, "Provider state observed_at")
        if self.active and not self.configured:
            raise ArchitectureDeclarationError(
                "An active provider binding must be configured."
            )
        if self.configured:
            _require_text(self.binding_ref or "", "Configured provider binding ref")
            if self.authority_mode not in SOURCE_AUTHORITY_MODES:
                raise ArchitectureDeclarationError(
                    "Configured provider bindings require a source-authority mode."
                )
        elif self.binding_ref is not None or self.authority_mode is not None:
            raise ArchitectureDeclarationError(
                "Unconfigured provider state cannot identify a binding or authority mode."
            )
        if self.health not in PROVIDER_HEALTH_STATES:
            raise ArchitectureDeclarationError(
                f"Unsupported provider health state: {self.health!r}."
            )
        if self.freshness not in PROVIDER_FRESHNESS_STATES:
            raise ArchitectureDeclarationError(
                f"Unsupported provider freshness state: {self.freshness!r}."
            )
        if self.conflict not in PROVIDER_CONFLICT_STATES:
            raise ArchitectureDeclarationError(
                f"Unsupported provider conflict state: {self.conflict!r}."
            )
        if self.recovery not in PROVIDER_RECOVERY_STATES:
            raise ArchitectureDeclarationError(
                f"Unsupported provider recovery state: {self.recovery!r}."
            )
        if self.last_success_at is not None:
            _validate_aware_datetime(
                self.last_success_at,
                "Provider state last_success_at",
            )
        if self.detail is not None and len(self.detail.strip()) > 1_000:
            raise ArchitectureDeclarationError(
                "Provider state detail is limited to 1000 characters."
            )
        if len(self.metrics) > 50:
            raise ArchitectureDeclarationError(
                "Provider state metrics are limited to 50 values."
            )
        for key, value in self.metrics.items():
            _require_text(str(key), "Provider state metric name")
            if not isinstance(value, (str, int, float, bool, type(None))):
                raise ArchitectureDeclarationError(
                    "Provider state metrics must contain scalar values."
                )

    def to_dict(self) -> dict[str, object]:
        return {
            "contract_version": self.contract_version,
            "provider_id": self.provider_id,
            "binding_ref": self.binding_ref,
            "authority_mode": self.authority_mode,
            "configured": self.configured,
            "active": self.active,
            "health": self.health,
            "freshness": self.freshness,
            "conflict": self.conflict,
            "recovery": self.recovery,
            "observed_at": self.observed_at.isoformat(),
            "last_success_at": (
                self.last_success_at.isoformat()
                if self.last_success_at is not None
                else None
            ),
            "detail": self.detail,
            "metrics": dict(self.metrics),
        }


ExternalProviderStateProvider = Callable[
    [ExternalProviderStateContext],
    Sequence[ExternalProviderRuntimeState],
]


@dataclass(frozen=True, slots=True)
class ExternalProviderStateProviderRegistration:
    module_id: str
    provider_id: str
    provider: ExternalProviderStateProvider

    def __post_init__(self) -> None:
        _require_text(self.module_id, "Provider-state module id")
        _require_text(self.provider_id, "Provider-state provider id")


def collect_external_provider_states(
    registrations: Sequence[ExternalProviderStateProviderRegistration],
    context: ExternalProviderStateContext,
) -> dict[str, dict[str, object]]:
    """Collect and aggregate module state while isolating optional failures."""

    result: dict[str, dict[str, object]] = {}
    for registration in registrations:
        if registration.provider_id in result:
            raise ArchitectureDeclarationError(
                f"Duplicate runtime state provider {registration.provider_id!r}."
            )
        try:
            states = tuple(registration.provider(context))
            if len(states) > context.max_items:
                states = states[: context.max_items]
                truncated = True
            else:
                truncated = False
            if any(item.provider_id != registration.provider_id for item in states):
                raise ArchitectureDeclarationError(
                    "Runtime state provider returned a state for a different provider."
                )
            binding_refs = [
                item.binding_ref for item in states if item.binding_ref is not None
            ]
            if len(binding_refs) != len(set(binding_refs)):
                raise ArchitectureDeclarationError(
                    "Runtime state provider returned duplicate binding references."
                )
            if not states:
                states = (
                    _unconfigured_provider_state(registration.provider_id),
                )
            result[registration.provider_id] = _aggregate_provider_states(
                states,
                truncated=truncated,
            )
        except Exception as exc:  # noqa: BLE001 - isolate optional provider failures.
            failure = ExternalProviderRuntimeState(
                provider_id=registration.provider_id,
                observed_at=_runtime_now(),
                configured=False,
                active=False,
                health="error",
                freshness="unknown",
                conflict="unknown",
                recovery="attention",
                detail=(
                    "Runtime state collection failed "
                    f"({type(exc).__name__})."
                ),
            )
            result[registration.provider_id] = _aggregate_provider_states(
                (failure,),
                truncated=False,
            )
    return result


def module_architecture_issues(
    declaration: ModuleArchitectureDeclaration,
    *,
    has_migrations: bool,
) -> tuple[str, ...]:
    issues: list[str] = []
    evidence_kinds = {item.kind for item in declaration.evidence}
    required = set(_MATURITY_EVIDENCE_REQUIREMENTS[declaration.maturity])
    if declaration.maturity in {"supported", "lts"} and has_migrations:
        required.add("migration")
    if (
        declaration.maturity in {"reference_ready", "supported", "lts"}
        and declaration.target_tested_providers
    ):
        required.add("provider")
    missing = sorted(required - evidence_kinds)
    if missing:
        issues.append(
            f"maturity {declaration.maturity!r} is missing evidence: "
            + ", ".join(missing)
        )
    if declaration.maturity in {
        "concept",
        "scaffold",
        "vertical_slice",
        "reference_ready",
    } and not declaration.known_limits:
        issues.append(
            f"maturity {declaration.maturity!r} must state at least one known limit"
        )
    if (
        declaration.maturity in {"reference_ready", "supported", "lts"}
        and not declaration.reference_packages
    ):
        issues.append(
            f"{declaration.maturity} maturity requires a reference package"
        )
    documentation_requirements: dict[ModuleMaturity, tuple[str, ...]] = {
        "concept": (),
        "scaffold": (),
        "vertical_slice": (),
        "reference_ready": ("recovery", "security", "operations"),
        "supported": ("upgrade", "recovery", "security", "operations"),
        "lts": ("upgrade", "recovery", "security", "operations"),
    }
    missing_documentation = tuple(
        name
        for name in documentation_requirements[declaration.maturity]
        if not getattr(declaration.documentation, name)
    )
    if has_migrations and declaration.maturity in {"supported", "lts"}:
        if not declaration.documentation.migration:
            missing_documentation = (*missing_documentation, "migration")
    if missing_documentation:
        issues.append(
            f"maturity {declaration.maturity!r} is missing documentation: "
            + ", ".join(sorted(set(missing_documentation)))
        )
    return tuple(issues)


def module_architecture_from_mapping(
    value: Mapping[str, object],
) -> ModuleArchitectureDeclaration:
    raw_documentation = value.get("documentation")
    documentation = (
        raw_documentation if isinstance(raw_documentation, Mapping) else {}
    )
    return ModuleArchitectureDeclaration(
        contract_version=str(value.get("contract_version") or "1"),
        layer=cast(ArchitectureLayer, _mapping_text(value, "layer")),
        kind=cast(ModuleKind, _mapping_text(value, "kind")),
        maturity=cast(ModuleMaturity, _mapping_text(value, "maturity")),
        evidence=tuple(
            ModuleMaturityEvidence(
                kind=cast(
                    MaturityEvidenceKind, _mapping_text(item, "kind")
                ),
                reference=_mapping_text(item, "reference"),
                summary=_mapping_text(item, "summary"),
            )
            for item in _mapping_items(value.get("evidence"), "architecture evidence")
        ),
        known_limits=_mapping_texts(value.get("known_limits"), "known limits"),
        supported_authority_modes=tuple(
            cast(SourceAuthorityMode, item)
            for item in _mapping_texts(
                value.get("supported_authority_modes"),
                "supported authority modes",
            )
        ),
        owned_concepts=_mapping_texts(
            value.get("owned_concepts"), "owned concepts"
        ),
        non_owned_concepts=_mapping_texts(
            value.get("non_owned_concepts"), "non-owned concepts"
        ),
        reference_packages=_mapping_texts(
            value.get("reference_packages"), "reference packages"
        ),
        target_tested_providers=_mapping_texts(
            value.get("target_tested_providers"), "target-tested providers"
        ),
        documentation=ModuleArchitectureDocumentation(
            migration=_mapping_texts(
                documentation.get("migration"), "migration documentation"
            ),
            upgrade=_mapping_texts(
                documentation.get("upgrade"), "upgrade documentation"
            ),
            recovery=_mapping_texts(
                documentation.get("recovery"), "recovery documentation"
            ),
            security=_mapping_texts(
                documentation.get("security"), "security documentation"
            ),
            operations=_mapping_texts(
                documentation.get("operations"), "operations documentation"
            ),
        ),
    )


def external_provider_from_mapping(
    value: Mapping[str, object],
) -> ExternalProviderDeclaration:
    raw_behavior = value.get("behavior")
    if not isinstance(raw_behavior, Mapping):
        raise ArchitectureDeclarationError(
            "External provider behavior must be an object."
        )
    return ExternalProviderDeclaration(
        contract_version=str(value.get("contract_version") or "1"),
        id=_mapping_text(value, "id"),
        module_id=_mapping_text(value, "module_id"),
        label=_mapping_text(value, "label"),
        maturity=cast(IntegrationMaturity, _mapping_text(value, "maturity")),
        operations=tuple(
            cast(ProviderOperation, item)
            for item in _mapping_texts(value.get("operations"), "provider operations")
        ),
        objects=tuple(
            ProviderObjectDeclaration(
                object_type=_mapping_text(item, "object_type"),
                field_groups=_mapping_texts(
                    item.get("field_groups"), "provider field groups"
                ),
                authority_modes=tuple(
                    cast(SourceAuthorityMode, mode)
                    for mode in _mapping_texts(
                        item.get("authority_modes"),
                        "provider authority modes",
                    )
                ),
                default_authority_mode=cast(
                    SourceAuthorityMode,
                    _mapping_text(item, "default_authority_mode"),
                ),
            )
            for item in _mapping_items(value.get("objects"), "provider objects")
        ),
        behavior=ProviderBehaviorDeclaration(
            revision_tokens=_mapping_optional_text(
                raw_behavior.get("revision_tokens")
            ),
            concurrency=_mapping_optional_text(raw_behavior.get("concurrency")),
            freshness=_mapping_optional_text(raw_behavior.get("freshness")),
            health=_mapping_optional_text(raw_behavior.get("health")),
            max_read_items=_mapping_optional_int(
                raw_behavior.get("max_read_items")
            ),
            idempotency=_mapping_optional_text(raw_behavior.get("idempotency")),
            retry=_mapping_optional_text(raw_behavior.get("retry")),
            timeout_seconds=_mapping_optional_int(
                raw_behavior.get("timeout_seconds")
            ),
            conflicts=_mapping_optional_text(raw_behavior.get("conflicts")),
            outcome_unknown=_mapping_optional_text(
                raw_behavior.get("outcome_unknown")
            ),
            outcome_unknown_supported=bool(
                raw_behavior.get("outcome_unknown_supported", False)
            ),
            evidence=_mapping_optional_text(raw_behavior.get("evidence")),
            audit_event_types=_mapping_texts(
                raw_behavior.get("audit_event_types"), "provider audit event types"
            ),
            correction=_mapping_optional_text(raw_behavior.get("correction")),
            rollback=_mapping_optional_text(raw_behavior.get("rollback")),
            compensation=_mapping_optional_text(raw_behavior.get("compensation")),
            reconciliation=_mapping_optional_text(
                raw_behavior.get("reconciliation")
            ),
            outage=_mapping_optional_text(raw_behavior.get("outage")),
            classifications=_mapping_texts(
                raw_behavior.get("classifications"), "provider classifications"
            ),
            purposes=_mapping_texts(
                raw_behavior.get("purposes"), "provider purposes"
            ),
            retention=_mapping_optional_text(raw_behavior.get("retention")),
            secret_handling=_mapping_optional_text(
                raw_behavior.get("secret_handling")
            ),
        ),
        capability_names=_mapping_texts(
            value.get("capability_names"), "provider capability names"
        ),
        interface_names=_mapping_texts(
            value.get("interface_names"), "provider interface names"
        ),
        action_keys=_mapping_texts(value.get("action_keys"), "provider action keys"),
        effect_keys=_mapping_texts(value.get("effect_keys"), "provider effect keys"),
        operational_check_ids=_mapping_texts(
            value.get("operational_check_ids"), "provider operational check ids"
        ),
        documentation_topic_ids=_mapping_texts(
            value.get("documentation_topic_ids"),
            "provider documentation topic ids",
        ),
        policy_hooks=_mapping_texts(value.get("policy_hooks"), "provider policy hooks"),
        ownership_resource_types=_mapping_texts(
            value.get("ownership_resource_types"),
            "provider ownership resource types",
        ),
    )


def _validate_provider_authority_modes(
    declaration: ExternalProviderDeclaration,
) -> None:
    operations = set(declaration.operations)
    for item in declaration.objects:
        for mode in item.authority_modes:
            if mode == "external_mirror" and "read" not in operations:
                raise ArchitectureDeclarationError(
                    "External mirror providers must support read operations."
                )
            if mode == "governed_sync":
                if "read" not in operations or not (
                    operations & {"write", "synchronize"}
                ):
                    raise ArchitectureDeclarationError(
                        "Governed-sync providers must support read and write/synchronize operations."
                    )
                if integration_maturity_rank(
                    declaration.maturity
                ) < integration_maturity_rank("synchronize"):
                    raise ArchitectureDeclarationError(
                        "Governed-sync providers require synchronize maturity."
                    )
            if mode == "external_authoritative" and not (
                operations & {"link", "search", "read", "write", "publish", "synchronize"}
            ):
                raise ArchitectureDeclarationError(
                    "External-authoritative providers must expose an external-system operation."
                )


def _validate_provider_behavior(
    declaration: ExternalProviderDeclaration,
) -> None:
    operations = set(declaration.operations)
    behavior = declaration.behavior
    if operations & _READ_OPERATIONS:
        _require_behavior_fields(
            behavior,
            "read-capable provider",
            ("freshness", "health"),
        )
        if behavior.max_read_items is None:
            raise ArchitectureDeclarationError(
                "Read-capable providers must declare max_read_items."
            )
    if operations & _EFFECT_OPERATIONS:
        _require_behavior_fields(
            behavior,
            "effect-capable provider",
            (
                "idempotency",
                "retry",
                "concurrency",
                "conflicts",
                "outcome_unknown",
                "evidence",
                "correction",
                "reconciliation",
                "outage",
                "secret_handling",
            ),
        )
        if behavior.timeout_seconds is None:
            raise ArchitectureDeclarationError(
                "Effect-capable providers must declare timeout_seconds."
            )
        if not behavior.audit_event_types:
            raise ArchitectureDeclarationError(
                "Effect-capable providers must declare audit event types."
            )
    if behavior.outcome_unknown_supported and not behavior.reconciliation:
        raise ArchitectureDeclarationError(
            "Outcome-unknown support requires a reconciliation path."
        )
    if not behavior.classifications:
        raise ArchitectureDeclarationError(
            "Provider declarations must state data classifications."
        )
    if not behavior.purposes:
        raise ArchitectureDeclarationError(
            "Provider declarations must state allowed purposes."
        )
    _require_text(behavior.retention or "", "Provider retention behavior")


def _aggregate_provider_states(
    states: Sequence[ExternalProviderRuntimeState],
    *,
    truncated: bool,
) -> dict[str, object]:
    configured = tuple(item for item in states if item.configured)
    active = tuple(item for item in configured if item.active)
    observed = active or configured or tuple(states)
    authority_modes = tuple(
        sorted(
            {
                item.authority_mode
                for item in configured
                if item.authority_mode is not None
            }
        )
    )
    last_successes = tuple(
        item.last_success_at
        for item in configured
        if item.last_success_at is not None
    )
    binding_ref = configured[0].binding_ref if len(configured) == 1 else None
    authority_mode = authority_modes[0] if len(authority_modes) == 1 else None
    payload: dict[str, object] = {
        "contract_version": "1",
        "provider_id": states[0].provider_id,
        "binding_ref": binding_ref,
        "authority_mode": authority_mode,
        "authority_modes": list(authority_modes),
        "configured": bool(configured),
        "active": bool(active),
        "health": _worst_state(
            (item.health for item in observed),
            ("healthy", "inactive", "unknown", "warning", "error"),
        ),
        "freshness": _worst_state(
            (item.freshness for item in observed),
            ("current", "not_applicable", "unknown", "stale"),
        ),
        "conflict": _worst_state(
            (item.conflict for item in observed),
            ("clear", "not_applicable", "unknown", "pending", "blocked"),
        ),
        "recovery": _worst_state(
            (item.recovery for item in observed),
            ("ready", "not_applicable", "unsupported", "unknown", "attention"),
        ),
        "observed_at": max(item.observed_at for item in observed).isoformat(),
        "last_success_at": (
            max(last_successes).isoformat() if last_successes else None
        ),
        "detail": (
            states[0].detail
            if len(states) == 1
            else f"{len(configured)} configured bindings; {len(active)} active."
        ),
        "metrics": {
            "binding_count": len(configured),
            "active_binding_count": len(active),
            "truncated": truncated,
        },
        "bindings": [item.to_dict() for item in states if item.configured],
    }
    return payload


def _worst_state(values: Iterable[str], order: tuple[str, ...]) -> str:
    ranks = {value: index for index, value in enumerate(order)}
    normalized = tuple(values)
    return max(normalized, key=lambda value: ranks.get(value, len(order)))


def _unconfigured_provider_state(
    provider_id: str,
) -> ExternalProviderRuntimeState:
    return ExternalProviderRuntimeState(
        provider_id=provider_id,
        observed_at=_runtime_now(),
        configured=False,
        active=False,
        health="inactive",
        freshness="not_applicable",
        conflict="not_applicable",
        recovery="not_applicable",
        detail="No configured binding was found in this scope.",
    )


def _runtime_now() -> datetime:
    return datetime.now(UTC)


def _validate_aware_datetime(value: datetime, label: str) -> None:
    if value.tzinfo is None or value.utcoffset() is None:
        raise ArchitectureDeclarationError(f"{label} must include a timezone.")


def _require_behavior_fields(
    behavior: ProviderBehaviorDeclaration,
    label: str,
    fields: tuple[str, ...],
) -> None:
    missing = [
        field_name
        for field_name in fields
        if not str(getattr(behavior, field_name) or "").strip()
    ]
    if missing:
        raise ArchitectureDeclarationError(
            f"{label.capitalize()} is missing behavior declarations: "
            + ", ".join(missing)
        )


def _require_text(value: str, label: str) -> None:
    if not str(value or "").strip():
        raise ArchitectureDeclarationError(f"{label} is required.")


def _validate_text_tuple(
    values: tuple[str, ...],
    label: str,
    *,
    require: bool = False,
) -> None:
    if require and not values:
        raise ArchitectureDeclarationError(f"{label.capitalize()} are required.")
    if any(not str(value or "").strip() for value in values):
        raise ArchitectureDeclarationError(
            f"{label.capitalize()} must not contain empty values."
        )
    if len(values) != len(set(values)):
        raise ArchitectureDeclarationError(
            f"{label.capitalize()} must not contain duplicates."
        )


def _mapping_text(value: Mapping[str, object], key: str) -> str:
    result = _mapping_optional_text(value.get(key))
    if result is None:
        raise ArchitectureDeclarationError(f"Architecture field {key!r} is required.")
    return result


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


def _mapping_optional_int(value: object | None) -> int | None:
    if value is None:
        return None
    try:
        return int(value)
    except (TypeError, ValueError) as exc:
        raise ArchitectureDeclarationError(
            f"Expected an integer architecture value, got {value!r}."
        ) from exc


def _mapping_texts(value: object | None, label: str) -> tuple[str, ...]:
    if value is None:
        return ()
    if isinstance(value, str) or not isinstance(value, Sequence):
        raise ArchitectureDeclarationError(f"{label.capitalize()} must be a list.")
    return tuple(str(item) for item in value)


def _mapping_items(
    value: object | None,
    label: str,
) -> tuple[Mapping[str, object], ...]:
    if value is None:
        return ()
    if isinstance(value, (str, bytes)) or not isinstance(value, Sequence):
        raise ArchitectureDeclarationError(f"{label.capitalize()} must be a list.")
    if any(not isinstance(item, Mapping) for item in value):
        raise ArchitectureDeclarationError(
            f"{label.capitalize()} entries must be objects."
        )
    return cast(tuple[Mapping[str, object], ...], tuple(value))


__all__ = [
    "ARCHITECTURE_LAYERS",
    "MATURITY_EVIDENCE_KINDS",
    "MODULE_KINDS",
    "MODULE_MATURITY_ORDER",
    "PROVIDER_OPERATIONS",
    "ArchitectureDeclarationError",
    "ArchitectureLayer",
    "ExternalProviderDeclaration",
    "ExternalProviderRuntimeState",
    "ExternalProviderStateContext",
    "ExternalProviderStateProvider",
    "ExternalProviderStateProviderRegistration",
    "MaturityEvidenceKind",
    "ModuleArchitectureDeclaration",
    "ModuleArchitectureDocumentation",
    "ModuleKind",
    "ModuleMaturity",
    "ModuleMaturityEvidence",
    "ProviderBehaviorDeclaration",
    "ProviderConflictState",
    "ProviderFreshnessState",
    "ProviderHealthState",
    "ProviderObjectDeclaration",
    "ProviderOperation",
    "ProviderRecoveryState",
    "collect_external_provider_states",
    "declared_module_architecture",
    "module_architecture_issues",
    "external_provider_from_mapping",
    "module_architecture_from_mapping",
]
