"""Auditable device-operation requests and lifecycle events."""

from __future__ import annotations

from enum import StrEnum
from typing import Annotated, Literal

from pydantic import (
    AwareDatetime,
    Field,
    JsonValue,
    NonNegativeInt,
    PositiveFloat,
    model_validator,
)

from .common import (
    API_VERSION,
    ApiVersion,
    ContractModel,
    Identifier,
    JsonObject,
    LongText,
    ShortText,
)
from .session import DeviceSessionRef


class OperationSafetyClass(StrEnum):
    READ_ONLY = "read-only"
    REVERSIBLE = "reversible"
    STATE_CHANGING = "state-changing"
    SAFETY_CRITICAL = "safety-critical"


class IdempotencyMode(StrEnum):
    IDEMPOTENT = "idempotent"
    IDEMPOTENT_WITH_KEY = "idempotent-with-key"
    NON_IDEMPOTENT = "non-idempotent"


class SecretReference(ContractModel):
    """Opaque secret-vault reference; secret material is never an SDK value."""

    provider: Identifier
    reference: ShortText
    version: ShortText | None = None


class OperationPolicy(ContractModel):
    action_id: Identifier
    safety_class: OperationSafetyClass
    idempotency: IdempotencyMode
    timeout_seconds: PositiveFloat
    acknowledgement_required: bool = True
    retry_limit: NonNegativeInt = 0
    preconditions: tuple[Identifier, ...] = Field(default_factory=tuple)
    secret_fields: tuple[Identifier, ...] = Field(default_factory=tuple)

    @model_validator(mode="after")
    def policy_collections_must_be_unique(self) -> OperationPolicy:
        for label, values in (
            ("preconditions", self.preconditions),
            ("secret fields", self.secret_fields),
        ):
            if len(values) != len(set(values)):
                raise ValueError(f"{label} must be unique")
        if self.idempotency is IdempotencyMode.NON_IDEMPOTENT and self.retry_limit:
            raise ValueError("non-idempotent operations cannot declare automatic retries")
        return self


class OperationRequest(ContractModel):
    api_version: ApiVersion = API_VERSION
    kind: Literal["OperationRequest"] = "OperationRequest"
    operation_id: Identifier
    session: DeviceSessionRef
    action_id: Identifier
    requested_at: AwareDatetime
    deadline_at: AwareDatetime
    idempotency_key: Identifier | None = None
    parameters: JsonObject = Field(default_factory=dict)
    secret_refs: dict[Identifier, SecretReference] = Field(default_factory=dict)

    @model_validator(mode="after")
    def validate_request_boundaries(self) -> OperationRequest:
        if self.deadline_at <= self.requested_at:
            raise ValueError("deadline_at must be later than requested_at")
        overlap = set(self.parameters).intersection(self.secret_refs)
        if overlap:
            raise ValueError(f"secret fields cannot also appear in parameters: {sorted(overlap)}")
        return self


class AcknowledgementDisposition(StrEnum):
    ACCEPTED = "accepted"
    REJECTED = "rejected"
    DUPLICATE = "duplicate"


class OperationAcknowledgement(ContractModel):
    """Receipt/admission acknowledgement; never proof of operation completion."""

    api_version: ApiVersion = API_VERSION
    event_type: Literal["acknowledgement"] = "acknowledgement"
    operation_id: Identifier
    session: DeviceSessionRef
    sequence: NonNegativeInt
    recorded_at: AwareDatetime
    disposition: AcknowledgementDisposition
    device_receipt_id: Identifier | None = None
    message: LongText | None = None


class OperationProgress(ContractModel):
    api_version: ApiVersion = API_VERSION
    event_type: Literal["progress"] = "progress"
    operation_id: Identifier
    session: DeviceSessionRef
    sequence: NonNegativeInt
    recorded_at: AwareDatetime
    stage: Identifier
    progress: Annotated[float, Field(ge=0.0, le=1.0)] | None = None
    message: LongText | None = None
    measurements: dict[str, JsonValue] = Field(default_factory=dict)


class OperationCompletion(ContractModel):
    api_version: ApiVersion = API_VERSION
    event_type: Literal["completion"] = "completion"
    operation_id: Identifier
    session: DeviceSessionRef
    sequence: NonNegativeInt
    recorded_at: AwareDatetime
    result: JsonObject = Field(default_factory=dict)
    evidence_ids: tuple[Identifier, ...] = Field(default_factory=tuple)


class OperationTerminalState(StrEnum):
    FAILED = "failed"
    CANCELLED = "cancelled"
    TIMED_OUT = "timed-out"


class OperationError(ContractModel):
    code: Identifier
    message: LongText
    retryable: bool
    details: JsonObject = Field(default_factory=dict)


class OperationFailure(ContractModel):
    api_version: ApiVersion = API_VERSION
    event_type: Literal["failure"] = "failure"
    operation_id: Identifier
    session: DeviceSessionRef
    sequence: NonNegativeInt
    recorded_at: AwareDatetime
    terminal_state: OperationTerminalState
    error: OperationError
    evidence_ids: tuple[Identifier, ...] = Field(default_factory=tuple)


OperationEvent = Annotated[
    OperationAcknowledgement | OperationProgress | OperationCompletion | OperationFailure,
    Field(discriminator="event_type"),
]


def validate_operation_request(policy: OperationPolicy, request: OperationRequest) -> None:
    """Validate request data that depends on its declared action policy."""

    if request.action_id != policy.action_id:
        raise ValueError("operation request action_id does not match its policy")
    declared_secrets = set(policy.secret_fields)
    supplied_secrets = set(request.secret_refs)
    missing = declared_secrets - supplied_secrets
    undeclared = supplied_secrets - declared_secrets
    if missing:
        raise ValueError(f"missing secret references: {sorted(missing)}")
    if undeclared:
        raise ValueError(f"undeclared secret references: {sorted(undeclared)}")
    if policy.idempotency is IdempotencyMode.IDEMPOTENT_WITH_KEY:
        if request.idempotency_key is None:
            raise ValueError("idempotent-with-key operation requires idempotency_key")
    elif request.idempotency_key is not None:
        raise ValueError("idempotency_key is allowed only for idempotent-with-key operations")
