"""Common lifecycle vocabulary around specialized capability implementations.

This module deliberately owns orchestration vocabulary only. Policy remains in each
bounded context (auth, forms, feature packs, and so on).
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import StrEnum
from typing import Any, Protocol

from pydantic import BaseModel, ConfigDict, Field

from src.domain.capabilities.runtime import CapabilityRuntimeRequirements


class CapabilityState(StrEnum):
    DECLARED = "declared"
    PLANNED = "planned"
    APPLYING = "applying"
    READY = "ready"
    DRIFTED = "drifted"
    BLOCKED = "blocked"
    SHADOW = "shadow"
    ENABLED = "enabled"


class IssueSeverity(StrEnum):
    ERROR = "error"
    WARNING = "warning"


class CapabilityIssue(BaseModel):
    """One structured, non-secret reason a capability isn't (fully) ready.

    Codes are capability-defined strings (e.g. ``AUTH_PUBLIC_KEY_MISSING``) so a
    caller can branch on *why*, not just parse ``blocked_reason`` prose. Never put
    a secret value in ``message``/``detail`` -- only what's missing or wrong.
    """

    model_config = ConfigDict(extra="forbid", frozen=True)

    code: str
    message: str
    severity: IssueSeverity = IssueSeverity.ERROR
    repairable: bool = False


class PlanAction(StrEnum):
    """What ``apply()`` would do if run now, independent of why (``ProvisionPlan``'s
    ``changes``/``blockers`` carry the why)."""

    NOOP = "noop"
    PROVISION = "provision"
    RECONCILE = "reconcile"
    UPGRADE = "upgrade"
    BLOCKED = "blocked"


class CapabilityStatus(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)

    capability: str
    version: int
    state: CapabilityState
    desired_revision: int | None = None
    applied_revision: int | None = None
    drift: bool = False
    blocked_reason: str | None = None
    details: dict[str, Any] = Field(default_factory=dict)
    # Separate from ``state`` (Core's own provisioning state): whether the runtime
    # projection (hosting/BFF wiring) for this capability is actually serving
    # traffic. None means "not yet tracked" -- existing callers that never set this
    # keep working unchanged.
    runtime_state: CapabilityState | None = None
    issues: tuple[CapabilityIssue, ...] = ()


@dataclass(frozen=True)
class ProvisionPlan:
    capability: str
    version: int
    desired_revision: int | None = None
    changes: tuple[str, ...] = ()
    blockers: tuple[str, ...] = ()
    action: PlanAction = PlanAction.NOOP


@dataclass(frozen=True)
class ProvisionResult:
    capability: str
    version: int
    state: CapabilityState
    applied_revision: int | None = None
    blockers: tuple[str, ...] = ()
    drift: bool = False
    details: dict[str, Any] = field(default_factory=dict)


class CapabilityProvisioner(Protocol):
    capability: str
    version: int

    async def declare(self, project: Any, desired_state: dict[str, Any]) -> dict[str, Any]: ...

    async def plan(self, project: Any) -> ProvisionPlan: ...

    async def apply(self, project: Any, plan: ProvisionPlan) -> ProvisionResult: ...

    async def reconcile(self, project: Any) -> ProvisionResult: ...

    async def status(self, project: Any) -> CapabilityStatus: ...

    async def runtime_requirements(self, project: Any) -> CapabilityRuntimeRequirements:
        """Hosting-neutral runtime requirements for this capability (routes, platform
        services, config requirements) -- never secret values, never a hosting
        decision. See ``src.domain.capabilities.runtime``."""
        ...
