"""Hosting-neutral runtime requirements a capability declares.

A ``CapabilityProvisioner`` (``lifecycle.py``) answers "what does this project's
desired state look like, and does external state match it." ``runtime_requirements()``
answers a narrower, hosting-neutral question: "what must the generated app's runtime
be able to reach for this capability to work" -- routes, platform services, and
config *requirements* (never secret values). ``merge_runtime_requirements`` combines
those across capabilities deterministically so a hosting/runtime projection adapter
(``src.domain.hosting.runtime_projection``) can turn them into concrete wiring for
W4P/shared-BFF or legacy Pages without any capability code knowing which one it is.

Nothing here performs I/O or makes a hosting decision. See
``docs/architecture/CAPABILITY_RUNTIME_PROJECTION.md`` for the full picture.
"""
from __future__ import annotations

from dataclasses import dataclass, field, replace
from typing import Any, Literal

RUNTIME_CONTRACT_VERSION = 1

ConfigVisibility = Literal["public", "trusted"]
PortAuth = Literal["public", "session", "staff", "internal"]


@dataclass(frozen=True)
class RuntimePortRequirement:
    """One logical generated-app operation required by a capability.

    ``port_id`` is the stable identity.  ``public_path`` is the generated-app
    contract; provider/internal paths are deliberately absent here.
    """

    port_id: str
    capability: str
    capability_version: int
    method: str
    public_path: str
    auth: PortAuth
    required: bool = True
    contract_ref: str | None = None

    def to_dict(self) -> dict[str, Any]:
        return {
            "port_id": self.port_id,
            "capability": self.capability,
            "capability_version": self.capability_version,
            "method": self.method,
            "public_path": self.public_path,
            "auth": self.auth,
            "required": self.required,
            "contract_ref": self.contract_ref,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "RuntimePortRequirement":
        return cls(
            port_id=data["port_id"],
            capability=data["capability"],
            capability_version=data["capability_version"],
            method=data["method"],
            public_path=data["public_path"],
            auth=data.get("auth", "public"),
            required=data.get("required", True),
            contract_ref=data.get("contract_ref"),
        )


@dataclass(frozen=True)
class RuntimeRouteRequirement:
    """One path prefix the generated app's runtime must be able to call.

    ``path_prefix`` is hosting-neutral (``/api/auth``, not ``functions/api/auth`` or
    a Cloud gateway path) -- the hosting/runtime projection adapter decides how that
    prefix is actually served for a given provider.
    """

    path_prefix: str
    purpose: str
    methods: tuple[str, ...] = ()

    def to_dict(self) -> dict[str, Any]:
        return {
            "path_prefix": self.path_prefix,
            "purpose": self.purpose,
            "methods": list(self.methods),
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> RuntimeRouteRequirement:
        return cls(
            path_prefix=data["path_prefix"],
            purpose=data["purpose"],
            methods=tuple(data.get("methods") or ()),
        )


@dataclass(frozen=True)
class RuntimeConfigRequirement:
    """A config *requirement*, never a value. ``visibility`` says where it may be
    exposed -- ``public`` config may reach the browser; ``trusted`` config may only
    reach a same-origin BFF/server context, never a browser client."""

    key: str
    visibility: ConfigVisibility
    required: bool = True
    description: str | None = None

    def to_dict(self) -> dict[str, Any]:
        return {
            "key": self.key,
            "visibility": self.visibility,
            "required": self.required,
            "description": self.description,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> RuntimeConfigRequirement:
        return cls(
            key=data["key"],
            visibility=data["visibility"],
            required=data.get("required", True),
            description=data.get("description"),
        )


@dataclass(frozen=True)
class CapabilityRuntimeRequirements:
    """Hosting-neutral runtime requirements for one capability, at one version.

    ``platform_services`` and ``feature_flags`` are opaque labels the runtime
    projection layer may key behavior off of (e.g. ``"p2hs-auth"``) -- they are not
    Cloudflare bindings, KV keys, or file paths.
    """

    capability_key: str
    capability_version: int
    routes: tuple[RuntimeRouteRequirement, ...] = ()
    ports: tuple[RuntimePortRequirement, ...] = ()
    platform_services: tuple[str, ...] = ()
    config: tuple[RuntimeConfigRequirement, ...] = ()
    health_checks: tuple[str, ...] = ()
    feature_flags: tuple[str, ...] = ()
    contract_version: int = RUNTIME_CONTRACT_VERSION

    def to_dict(self) -> dict[str, Any]:
        return {
            "capability_key": self.capability_key,
            "capability_version": self.capability_version,
            "contract_version": self.contract_version,
            "routes": [route.to_dict() for route in self.routes],
            "ports": [port.to_dict() for port in self.ports],
            "platform_services": list(self.platform_services),
            "config": [item.to_dict() for item in self.config],
            "health_checks": list(self.health_checks),
            "feature_flags": list(self.feature_flags),
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> CapabilityRuntimeRequirements:
        return cls(
            capability_key=data["capability_key"],
            capability_version=data["capability_version"],
            contract_version=data.get("contract_version", RUNTIME_CONTRACT_VERSION),
            routes=tuple(
                RuntimeRouteRequirement.from_dict(item) for item in data.get("routes", ())
            ),
            ports=tuple(
                RuntimePortRequirement.from_dict(item) for item in data.get("ports", ())
            ),
            platform_services=tuple(data.get("platform_services") or ()),
            config=tuple(
                RuntimeConfigRequirement.from_dict(item) for item in data.get("config", ())
            ),
            health_checks=tuple(data.get("health_checks") or ()),
            feature_flags=tuple(data.get("feature_flags") or ()),
        )


@dataclass(frozen=True)
class RuntimeRequirementConflict:
    """Two capabilities disagree about one route or config requirement in a way that
    cannot be silently resolved -- the merge must fail closed, not pick a winner."""

    kind: Literal[
        "route_purpose_conflict", "port_contract_conflict",
        "config_visibility_conflict", "config_requiredness_conflict"
    ]
    key: str
    capability_a: str
    capability_b: str
    detail: str


@dataclass(frozen=True)
class MergedRuntimeRequirements:
    contract_version: int
    routes: tuple[RuntimeRouteRequirement, ...]
    ports: tuple[RuntimePortRequirement, ...]
    platform_services: tuple[str, ...]
    config: tuple[RuntimeConfigRequirement, ...]
    health_checks: tuple[str, ...]
    feature_flags: tuple[str, ...]
    # path_prefix / config key -> capabilities that declared it, in first-seen order.
    route_provenance: dict[str, tuple[str, ...]] = field(default_factory=dict)
    config_provenance: dict[str, tuple[str, ...]] = field(default_factory=dict)


@dataclass(frozen=True)
class RuntimeRequirementMergeResult:
    ok: bool
    merged: MergedRuntimeRequirements | None = None
    conflicts: tuple[RuntimeRequirementConflict, ...] = ()


def merge_runtime_requirements(
    requirements: list[CapabilityRuntimeRequirements] | tuple[CapabilityRuntimeRequirements, ...],
) -> RuntimeRequirementMergeResult:
    """Combine runtime requirements from multiple capabilities deterministically.

    Deterministic in the sense that it never reorders by anything other than the
    caller's own input order (first-seen wins position; later identical entries just
    add provenance). Conflicting requirements never silently overwrite one another --
    the result comes back ``ok=False`` with every conflict found, so planning can
    surface all of them at once instead of failing one at a time.
    """
    if not requirements:
        return RuntimeRequirementMergeResult(
            ok=True,
            merged=MergedRuntimeRequirements(
                contract_version=RUNTIME_CONTRACT_VERSION,
                routes=(),
                ports=(),
                platform_services=(),
                config=(),
                health_checks=(),
                feature_flags=(),
            ),
        )

    contract_version = max(item.contract_version for item in requirements)
    conflicts: list[RuntimeRequirementConflict] = []

    routes_by_prefix: dict[str, RuntimeRouteRequirement] = {}
    route_provenance: dict[str, list[str]] = {}
    for item in requirements:
        for route in item.routes:
            existing = routes_by_prefix.get(route.path_prefix)
            if existing is None:
                routes_by_prefix[route.path_prefix] = route
                route_provenance[route.path_prefix] = [item.capability_key]
                continue
            route_provenance[route.path_prefix].append(item.capability_key)
            if existing.purpose != route.purpose:
                conflicts.append(
                    RuntimeRequirementConflict(
                        kind="route_purpose_conflict",
                        key=route.path_prefix,
                        capability_a=route_provenance[route.path_prefix][0],
                        capability_b=item.capability_key,
                        detail=(
                            f"{route.path_prefix} declared for '{existing.purpose}' and "
                            f"'{route.purpose}'"
                        ),
                    )
                )
                continue
            merged_methods = tuple(dict.fromkeys((*existing.methods, *route.methods)))
            if merged_methods != existing.methods:
                routes_by_prefix[route.path_prefix] = replace(existing, methods=merged_methods)

    ports_by_id: dict[str, RuntimePortRequirement] = {}
    for item in requirements:
        for port in item.ports:
            existing = ports_by_id.get(port.port_id)
            if existing is None:
                ports_by_id[port.port_id] = port
                continue
            if existing != port:
                conflicts.append(
                    RuntimeRequirementConflict(
                        kind="port_contract_conflict",
                        key=port.port_id,
                        capability_a=existing.capability,
                        capability_b=port.capability,
                        detail=f"{port.port_id} has incompatible logical port contracts",
                    )
                )

    config_by_key: dict[str, RuntimeConfigRequirement] = {}
    config_provenance: dict[str, list[str]] = {}
    for item in requirements:
        for cfg in item.config:
            existing = config_by_key.get(cfg.key)
            if existing is None:
                config_by_key[cfg.key] = cfg
                config_provenance[cfg.key] = [item.capability_key]
                continue
            config_provenance[cfg.key].append(item.capability_key)
            if existing.visibility != cfg.visibility:
                conflicts.append(
                    RuntimeRequirementConflict(
                        kind="config_visibility_conflict",
                        key=cfg.key,
                        capability_a=config_provenance[cfg.key][0],
                        capability_b=item.capability_key,
                        detail=(
                            f"{cfg.key} declared '{existing.visibility}' and '{cfg.visibility}'"
                        ),
                    )
                )
                continue
            if existing.required != cfg.required:
                # required=True must win rather than conflict outright: one capability
                # merely not needing a key that another requires is not a contradiction.
                if not existing.required and cfg.required:
                    config_by_key[cfg.key] = replace(existing, required=True)

    if conflicts:
        return RuntimeRequirementMergeResult(ok=False, conflicts=tuple(conflicts))

    platform_services = tuple(
        dict.fromkeys(service for item in requirements for service in item.platform_services)
    )
    health_checks = tuple(
        dict.fromkeys(check for item in requirements for check in item.health_checks)
    )
    feature_flags = tuple(
        dict.fromkeys(flag for item in requirements for flag in item.feature_flags)
    )

    merged = MergedRuntimeRequirements(
        contract_version=contract_version,
        routes=tuple(routes_by_prefix.values()),
        ports=tuple(ports_by_id.values()),
        platform_services=platform_services,
        config=tuple(config_by_key.values()),
        health_checks=health_checks,
        feature_flags=feature_flags,
        route_provenance={key: tuple(value) for key, value in route_provenance.items()},
        config_provenance={key: tuple(value) for key, value in config_provenance.items()},
    )
    return RuntimeRequirementMergeResult(ok=True, merged=merged)
