"""Fail-closed Topic-59 card projection and publication boundary."""

from __future__ import annotations

import hashlib
from dataclasses import dataclass
from datetime import datetime
from enum import StrEnum
from pathlib import Path
from typing import Protocol

from .nutrition_weekly_maintenance_contract import (
    Topic59MaintenanceDecision,
    Topic59MaintenanceDecisionKind,
)
from .nutrition_weekly_operations_authority import (
    WeeklyOperationsAuthorityReceipt,
    WeeklyOperationsRuntimeContext,
    weekly_operations_is_authorized,
)
from .nutrition_weekly_operations_config import WeeklyOperationsConfig
from .nutrition_weekly_operations_ledger import Topic59PublicationLedger
from .nutrition_weekly_operations_models import (
    Topic59DayCard,
    Topic59Projection,
    project_topic59_day_card,
)
from .nutrition_weekly_operations_publication_contract import (
    Topic59IncidentReason,
    Topic59LedgerAction,
    Topic59LedgerConflict,
    Topic59LedgerError,
    Topic59LedgerState,
    Topic59ProviderDelivered,
    Topic59ProviderOutcome,
)


class Topic59PublicationDisposition(StrEnum):
    REFUSED = "refused"
    SENT = "sent"
    EDITED = "edited"
    NOOP = "noop"
    NOOP_MAINTENANCE = "noop_maintenance"
    INCIDENT = "incident"


@dataclass(frozen=True, slots=True)
class Topic59PublicationRequest:
    card: Topic59DayCard
    config: WeeklyOperationsConfig
    receipt: WeeklyOperationsAuthorityReceipt
    runtime: WeeklyOperationsRuntimeContext


@dataclass(frozen=True, slots=True)
class Topic59PublicationResult:
    disposition: Topic59PublicationDisposition
    logical_key: str | None = None
    message_id: str | None = None
    reason: Topic59IncidentReason | None = None
    maintenance_audit_digest: str | None = None


class Topic59MaintenanceEvaluator(Protocol):
    def evaluate(
        self, projection: Topic59Projection, now: datetime,
    ) -> Topic59MaintenanceDecision: ...


class Topic59PublicationLedgerPort(Protocol):
    def claim(self, projection: Topic59Projection) -> Topic59LedgerAction: ...

    def record(
        self, action: Topic59LedgerAction, outcome: Topic59ProviderOutcome,
    ) -> Topic59LedgerState: ...


class Topic59Transport(Protocol):
    async def send(self, *, chat_id: str, topic_id: str, text: str) -> Topic59ProviderOutcome: ...

    async def edit(
        self, *, chat_id: str, topic_id: str, message_id: str, text: str
    ) -> Topic59ProviderOutcome: ...


class Topic59DayCardProjector:
    def __init__(
        self, ledger_path: Path,
        maintenance_gate: Topic59MaintenanceEvaluator | None = None,
        publication_ledger: Topic59PublicationLedgerPort | None = None,
    ) -> None:
        self._ledger: Topic59PublicationLedgerPort = (
            Topic59PublicationLedger(ledger_path)
            if publication_ledger is None else publication_ledger
        )
        self._maintenance_gate: Topic59MaintenanceEvaluator | None = maintenance_gate

    async def publish(
        self, request: Topic59PublicationRequest, transport: Topic59Transport
    ) -> Topic59PublicationResult:
        if not self.reserves_topic59(request):
            return Topic59PublicationResult(Topic59PublicationDisposition.REFUSED, reason=Topic59IncidentReason.AUTHORITY_REJECTED)
        route = request.config.review_route
        if route is None:
            return Topic59PublicationResult(Topic59PublicationDisposition.REFUSED, reason=Topic59IncidentReason.AUTHORITY_REJECTED)
        projection = project_topic59_day_card(request.card, config_digest=request.config.digest, route_key=route.key)
        if self._maintenance_gate is not None:
            maintenance = self._maintenance_gate.evaluate(projection, request.runtime.now)
            match maintenance.kind:
                case Topic59MaintenanceDecisionKind.ALLOW:
                    pass
                case Topic59MaintenanceDecisionKind.NOOP_MAINTENANCE:
                    return Topic59PublicationResult(
                        Topic59PublicationDisposition.NOOP_MAINTENANCE, projection.logical_key,
                        maintenance_audit_digest=maintenance.audit_digest,
                    )
                case Topic59MaintenanceDecisionKind.DENY:
                    return Topic59PublicationResult(
                        Topic59PublicationDisposition.REFUSED, projection.logical_key,
                        reason=Topic59IncidentReason.MAINTENANCE_REJECTED,
                    )
        try:
            action = self._ledger.claim(projection)
        except Topic59LedgerError:
            return Topic59PublicationResult(
                Topic59PublicationDisposition.INCIDENT, projection.logical_key, reason=Topic59IncidentReason.LEDGER_CORRUPTION
            )
        if action.kind == "noop":
            return Topic59PublicationResult(Topic59PublicationDisposition.NOOP, projection.logical_key, action.message_id)
        if action.kind == "incident":
            return Topic59PublicationResult(
                Topic59PublicationDisposition.INCIDENT, projection.logical_key, action.message_id, action.reason
            )
        outcome = await self._deliver(action, route.chat_id, transport)
        try:
            state = self._ledger.record(action, outcome)
        except Topic59LedgerError:
            return Topic59PublicationResult(
                Topic59PublicationDisposition.INCIDENT, projection.logical_key, action.message_id, Topic59IncidentReason.LEDGER_CORRUPTION
            )
        if state is Topic59LedgerState.SENT_AUDITED:
            disposition = Topic59PublicationDisposition.SENT if action.operation == "send" else Topic59PublicationDisposition.EDITED
            return Topic59PublicationResult(disposition, projection.logical_key, _receipt(action, outcome))
        return Topic59PublicationResult(
            Topic59PublicationDisposition.INCIDENT,
            projection.logical_key,
            action.message_id,
            Topic59IncidentReason.PROVIDER_TERMINAL,
        )

    @staticmethod
    def reserves_topic59(request: Topic59PublicationRequest) -> bool:
        route = request.config.review_route
        expected_identity = _customer_identity_digest(request.runtime.customer_key)
        return (
            route is not None
            and request.card.customer_identity_digest == expected_identity
            and request.card.candidate_digest == request.runtime.candidate_digest
            and weekly_operations_is_authorized(request.config, request.receipt, request.runtime)
        )

    @staticmethod
    async def _deliver(
        action: Topic59LedgerAction, chat_id: str, transport: Topic59Transport
    ) -> Topic59ProviderOutcome:
        if action.operation == "send":
            return await transport.send(chat_id=chat_id, topic_id="59", text=action.projection.text)
        if action.operation == "edit" and action.message_id is not None:
            return await transport.edit(
                chat_id=chat_id, topic_id="59", message_id=action.message_id, text=action.projection.text
            )
        raise Topic59LedgerConflict("invalid publication action")


def _customer_identity_digest(customer_key: str) -> str:
    material = f"nutricoach-weekly-operations-customer-identity-v1\0{customer_key}"
    return hashlib.sha256(material.encode("utf-8")).hexdigest()


def _receipt(action: Topic59LedgerAction, outcome: Topic59ProviderOutcome) -> str | None:
    if action.operation == "edit":
        return action.message_id
    return outcome.message_id if isinstance(outcome, Topic59ProviderDelivered) else None
