"""Strict r71b one-use Topic-59 maintenance documents and decisions."""

from __future__ import annotations

import hashlib
import json
import re
from datetime import date, datetime, timedelta
from enum import StrEnum
from typing import ClassVar, Final, Literal, Self

from pydantic import BaseModel, ConfigDict, Field, JsonValue, model_validator

_AUTHORITY_ID: Final = "nutricoach-v150-v15-runtime-authority-r71b-maintenance"
_HEX64: Final = re.compile(r"[0-9a-f]{64}")
_KST_OFFSET: Final = timedelta(hours=9)
_SCOPE_DOMAIN: Final = b"nutricoach-r71b-maintenance-scope-v1"
_BINDING_DOMAIN: Final = b"nutricoach-r71b-package-binding-v1"
_HOLD_ID_DOMAIN: Final = b"nutricoach-r71b-maintenance-hold-id-v1"
_FINAL_DOMAIN: Final = b"nutricoach-r71b-final-package-v1"


class Topic59MaintenanceContractError(Exception):
    """A maintenance document is outside its closed contract."""


class _Document(BaseModel):
    model_config: ClassVar[ConfigDict] = ConfigDict(
        frozen=True, extra="forbid", strict=True,
    )


class Topic59MaintenanceScopeV1(_Document):
    """Complete package-bound runtime maintenance scope."""

    schema_: Literal["nutricoach-r71b-maintenance-scope-v1"] = Field(
        default="nutricoach-r71b-maintenance-scope-v1", alias="schema",
    )
    product_generation: Literal["r71"] = "r71"
    package_namespace: Literal["r71b-maintenance"] = "r71b-maintenance"
    authority_id: Literal[
        "nutricoach-v150-v15-runtime-authority-r71b-maintenance"
    ] = _AUTHORITY_ID
    credential_name: Literal[
        "nutricoach-topic59-maintenance-r71b.json"
    ] = "nutricoach-topic59-maintenance-r71b.json"
    purpose: Literal["r71_cron_health_recovery"] = "r71_cron_health_recovery"
    max_consumptions: Literal[1] = 1
    candidate_digest: str
    config_digest: str
    route_digest: str
    customer_identity_digest: str
    card_slot: str
    kst_day: date
    not_before: datetime
    expires_at: datetime
    active_hold_relative_path: Literal[
        "data/topic59-maintenance-r71b/hold.json"
    ] = "data/topic59-maintenance-r71b/hold.json"
    consuming_hold_relative_path: Literal[
        "data/topic59-maintenance-r71b/hold.consuming.json"
    ] = "data/topic59-maintenance-r71b/hold.consuming.json"
    terminal_audit_relative_path: Literal[
        "data/topic59-maintenance-r71b/skip-audit.json"
    ] = "data/topic59-maintenance-r71b/skip-audit.json"
    lock_relative_path: Literal[
        "data/topic59-maintenance-r71b/maintenance.lock"
    ] = "data/topic59-maintenance-r71b/maintenance.lock"

    @model_validator(mode="after")
    def _contract(self) -> Self:
        for value in (
            self.candidate_digest, self.config_digest, self.route_digest,
            self.customer_identity_digest, self.card_slot,
        ):
            if _HEX64.fullmatch(value) is None:
                raise Topic59MaintenanceContractError()
        if (
            self.not_before.utcoffset() != _KST_OFFSET
            or self.expires_at.utcoffset() != _KST_OFFSET
            or self.not_before.microsecond != 0
            or self.expires_at.microsecond != 0
            or self.not_before.date() != self.kst_day
            or self.expires_at.date() != self.kst_day
        ):
            raise Topic59MaintenanceContractError()
        duration = self.expires_at - self.not_before
        if not timedelta(minutes=5) <= duration <= timedelta(minutes=30):
            raise Topic59MaintenanceContractError()
        return self


class Topic59MaintenanceHoldV1(_Document):
    schema_: Literal["nutricoach-topic59-maintenance-hold-v1"] = Field(
        default="nutricoach-topic59-maintenance-hold-v1", alias="schema",
    )
    hold_id: str
    package_binding_digest: str
    scope: Topic59MaintenanceScopeV1

    @model_validator(mode="after")
    def _contract(self) -> Self:
        expected = derive_hold_id(
            self.package_binding_digest, maintenance_scope_digest(self.scope),
        )
        if (
            _HEX64.fullmatch(self.package_binding_digest) is None
            or self.hold_id != expected
        ):
            raise Topic59MaintenanceContractError()
        return self

    @property
    def authority_id(self) -> str:
        return self.scope.authority_id

    @property
    def candidate_digest(self) -> str:
        return self.scope.candidate_digest

    @property
    def config_digest(self) -> str:
        return self.scope.config_digest

    @property
    def route_digest(self) -> str:
        return self.scope.route_digest

    @property
    def customer_identity_digest(self) -> str:
        return self.scope.customer_identity_digest

    @property
    def card_slot(self) -> str:
        return self.scope.card_slot

    @property
    def kst_day(self) -> date:
        return self.scope.kst_day

    @property
    def not_before(self) -> datetime:
        return self.scope.not_before

    @property
    def expires_at(self) -> datetime:
        return self.scope.expires_at


class Topic59MaintenanceAuthorityV1(_Document):
    schema_: Literal["nutricoach-topic59-maintenance-authority-v1"] = Field(
        default="nutricoach-topic59-maintenance-authority-v1", alias="schema",
    )
    authority_id: Literal[
        "nutricoach-v150-v15-runtime-authority-r71b-maintenance"
    ] = _AUTHORITY_ID
    product_generation: Literal["r71"] = "r71"
    package_namespace: Literal["r71b-maintenance"] = "r71b-maintenance"
    package_binding_digest: str
    hold_sha256: str
    hold: Topic59MaintenanceHoldV1

    @model_validator(mode="after")
    def _contract(self) -> Self:
        if (
            _HEX64.fullmatch(self.package_binding_digest) is None
            or _HEX64.fullmatch(self.hold_sha256) is None
            or self.hold.package_binding_digest != self.package_binding_digest
            or hashlib.sha256(canonical_document(self.hold)).hexdigest()
            != self.hold_sha256
        ):
            raise Topic59MaintenanceContractError()
        return self


class Topic59MaintenanceSkipAuditV1(_Document):
    schema_: Literal["nutricoach-topic59-maintenance-skip-audit-v1"] = Field(
        default="nutricoach-topic59-maintenance-skip-audit-v1", alias="schema",
    )
    hold_id: str
    hold_sha256: str
    authority_id: Literal[
        "nutricoach-v150-v15-runtime-authority-r71b-maintenance"
    ] = _AUTHORITY_ID
    package_binding_digest: str
    candidate_digest: str
    config_digest: str
    route_digest: str
    customer_identity_digest: str
    card_slot: str
    kst_day: date
    recorded_at: datetime
    terminal_state: Literal["maintenance_skipped"] = "maintenance_skipped"
    disposition: Literal["noop_maintenance"] = "noop_maintenance"
    sent: Literal[False] = False
    delivered: Literal[False] = False
    message_id: None = None
    provider_receipt: None = None
    provider_calls: Literal[0] = 0
    network_calls: Literal[0] = 0
    row_digest: str

    @model_validator(mode="after")
    def _contract(self) -> Self:
        if (
            self.recorded_at.utcoffset() != _KST_OFFSET
            or self.recorded_at.date() != self.kst_day
            or _HEX64.fullmatch(self.package_binding_digest) is None
            or _HEX64.fullmatch(self.row_digest) is None
            or self.row_digest != audit_row_digest(self)
        ):
            raise Topic59MaintenanceContractError()
        return self


class Topic59MaintenanceDecisionKind(StrEnum):
    ALLOW = "allow"
    NOOP_MAINTENANCE = "noop_maintenance"
    DENY = "deny"


class Topic59MaintenanceFailure(StrEnum):
    AUTHORITY_MISSING_FOR_TARGET_DAY = "authority_missing_for_target_day"
    AUTHORITY_CORRUPT = "authority_corrupt"
    BINDING_MISMATCH = "binding_mismatch"
    HOLD_MISSING = "hold_missing"
    HOLD_CORRUPT = "hold_corrupt"
    HOLD_NOT_YET_VALID = "hold_not_yet_valid"
    HOLD_EXPIRED = "hold_expired"
    HOLD_REPLAYED = "hold_replayed"
    AUDIT_CORRUPT = "audit_corrupt"
    FILESYSTEM_CONTRACT = "filesystem_contract"


class Topic59MaintenanceDecision(BaseModel):
    model_config: ClassVar[ConfigDict] = ConfigDict(frozen=True, strict=True)
    kind: Topic59MaintenanceDecisionKind
    audit_digest: str | None = None
    reason: Topic59MaintenanceFailure | None = None


def canonical_json(value: JsonValue) -> bytes:
    """Encode a digest preimage without a document newline."""
    return json.dumps(
        value, ensure_ascii=False, sort_keys=True, separators=(",", ":"),
        allow_nan=False,
    ).encode("utf-8")


def canonical_document(document: BaseModel) -> bytes:
    """Encode one canonical UTF-8 JSON document with its required newline."""
    encoded = canonical_json(document.model_dump(mode="json", by_alias=True)) + b"\n"
    if len(encoded) > 8192:
        raise Topic59MaintenanceContractError()
    return encoded


def _domain_digest(domain: bytes, value: JsonValue) -> str:
    return hashlib.sha256(domain + b"\0" + canonical_json(value)).hexdigest()


def maintenance_scope_digest(scope: Topic59MaintenanceScopeV1) -> str:
    """Derive the normative maintenance-scope digest."""
    return _domain_digest(
        _SCOPE_DOMAIN, scope.model_dump(mode="json", by_alias=True),
    )


def package_binding_digest(binding_inputs: JsonValue) -> str:
    """Derive the placeholder-free first-stage package digest."""
    return _domain_digest(_BINDING_DOMAIN, binding_inputs)


def derive_hold_id(binding_digest: str, scope_digest: str) -> str:
    """Derive the deterministic one-use hold identity."""
    preimage: JsonValue = {
        "schema": "nutricoach-r71b-maintenance-hold-id-preimage-v1",
        "package_binding_digest": binding_digest,
        "maintenance_scope_digest": scope_digest,
    }
    return _domain_digest(_HOLD_ID_DOMAIN, preimage)


def final_package_digest(payload: JsonValue) -> str:
    """Derive the controller and approval package digest."""
    return _domain_digest(_FINAL_DOMAIN, payload)


def audit_row_digest(audit: Topic59MaintenanceSkipAuditV1) -> str:
    """Hash canonical audit fields excluding the self-authenticating digest."""
    fields = audit.model_dump(mode="json", by_alias=True, exclude={"row_digest"})
    return hashlib.sha256(canonical_json(fields)).hexdigest()
