"""One immutable customer/day slot and terminal publication decisions for Topic-59."""

from __future__ import annotations

from pathlib import Path
from typing import Literal, TextIO

from .nutrition_weekly_operations_ledger_history import Topic59LedgerEntry, Topic59LedgerHistory
from .nutrition_weekly_operations_models import Topic59Projection
from .nutrition_weekly_operations_publication_contract import (
    Topic59IncidentReason,
    Topic59LedgerAction,
    Topic59LedgerConflict,
    Topic59LedgerState,
    Topic59ProviderDelivered,
    Topic59ProviderKnownFailure,
    Topic59ProviderFailure,
    Topic59ProviderOutcome,
)


class Topic59PublicationLedger:
    def __init__(self, path: Path) -> None:
        self._history: Topic59LedgerHistory = Topic59LedgerHistory(path)

    def claim(self, projection: Topic59Projection) -> Topic59LedgerAction:
        with self._history.locked() as stream:
            rows = self._history.read(stream)
            slot_rows = tuple(row for row in rows if row.card_slot == projection.card_slot)
            if not slot_rows:
                return self._reserve(stream, rows, projection, "send", None)
            if any(row.state is Topic59LedgerState.AUTHORITY_MISMATCH for row in slot_rows):
                return Topic59LedgerAction("incident", projection, reason=Topic59IncidentReason.AUTHORITY_MISMATCH)
            if not _same_authority_pins(slot_rows[0], projection):
                operation = "edit" if _message_id(slot_rows) is not None else "send"
                _ = self._history.append(
                    stream, rows, projection, operation, Topic59LedgerState.AUTHORITY_MISMATCH,
                    incident_reason=Topic59IncidentReason.AUTHORITY_MISMATCH,
                )
                return Topic59LedgerAction("incident", projection, reason=Topic59IncidentReason.AUTHORITY_MISMATCH)
            latest = slot_rows[-1]
            if latest.state is Topic59LedgerState.DELIVERED:
                if latest.message_id is None:
                    return Topic59LedgerAction("incident", projection, reason=Topic59IncidentReason.MISSING_MESSAGE_ID)
                rows = self._history.append(
                    stream, rows, projection, latest.operation, Topic59LedgerState.SENT_AUDITED, latest.message_id
                )
                latest = rows[-1]
            if latest.state is Topic59LedgerState.SENT_AUDITED:
                if latest.message_id is None:
                    return Topic59LedgerAction("incident", projection, reason=Topic59IncidentReason.MISSING_MESSAGE_ID)
                if latest.logical_key == projection.logical_key:
                    return Topic59LedgerAction("noop", projection, message_id=latest.message_id)
                return self._reserve(stream, rows, projection, "edit", latest.message_id)
            return Topic59LedgerAction("incident", projection, reason=Topic59IncidentReason.UNRESOLVED_RESERVATION)

    def record(self, action: Topic59LedgerAction, outcome: Topic59ProviderOutcome) -> Topic59LedgerState:
        operation = action.operation
        if operation is None:
            raise Topic59LedgerConflict("missing publication operation")
        with self._history.locked() as stream:
            rows = self._history.read(stream)
            if not _is_pending(rows, action, operation):
                raise Topic59LedgerConflict("publication is not pending")
            if isinstance(outcome, Topic59ProviderDelivered):
                if operation == "edit" and outcome.message_id != action.message_id:
                    self._terminal(stream, rows, action, Topic59LedgerState.UNKNOWN, Topic59ProviderFailure.RECEIPT_MISMATCH)
                    return Topic59LedgerState.UNKNOWN
                rows = self._history.append(
                    stream, rows, action.projection, operation, Topic59LedgerState.DELIVERED, outcome.message_id
                )
                _ = self._history.append(
                    stream, rows, action.projection, operation, Topic59LedgerState.SENT_AUDITED, outcome.message_id
                )
                return Topic59LedgerState.SENT_AUDITED
            if isinstance(outcome, Topic59ProviderKnownFailure):
                self._terminal(stream, rows, action, Topic59LedgerState.FAILED, outcome.reason)
                return Topic59LedgerState.FAILED
            self._terminal(stream, rows, action, Topic59LedgerState.UNKNOWN, outcome.reason)
            return Topic59LedgerState.UNKNOWN

    def _reserve(
        self,
        stream: TextIO,
        rows: tuple[Topic59LedgerEntry, ...],
        projection: Topic59Projection,
        operation: Literal["send", "edit"],
        message_id: str | None,
    ) -> Topic59LedgerAction:
        rows = self._history.append(
            stream, rows, projection, operation, Topic59LedgerState.PREPARED, message_id
        )
        _ = self._history.append(stream, rows, projection, operation, Topic59LedgerState.SENDING, message_id)
        return Topic59LedgerAction(operation, projection, operation, message_id)

    def _terminal(
        self,
        stream: TextIO,
        rows: tuple[Topic59LedgerEntry, ...],
        action: Topic59LedgerAction,
        state: Topic59LedgerState,
        reason: Topic59ProviderFailure,
    ) -> None:
        operation = action.operation
        if operation is None:
            raise Topic59LedgerConflict("missing publication operation")
        _ = self._history.append(stream, rows, action.projection, operation, state, action.message_id, reason)


def _same_authority_pins(row: Topic59LedgerEntry, projection: Topic59Projection) -> bool:
    return (
        row.customer_identity_digest == projection.customer_identity_digest
        and row.kst_day == projection.kst_day.isoformat()
        and row.config_digest == projection.config_digest
        and row.candidate_digest == projection.candidate_digest
        and row.route_digest == projection.route_digest
    )


def _message_id(rows: tuple[Topic59LedgerEntry, ...]) -> str | None:
    return next((row.message_id for row in reversed(rows) if row.message_id is not None), None)


def _is_pending(
    rows: tuple[Topic59LedgerEntry, ...],
    action: Topic59LedgerAction,
    operation: Literal["send", "edit"],
) -> bool:
    matching = tuple(
        row for row in rows
        if row.card_slot == action.projection.card_slot
        and row.logical_key == action.projection.logical_key
        and row.operation == operation
    )
    return bool(matching) and matching[-1].state is Topic59LedgerState.SENDING and matching[-1].message_id == action.message_id
