"""Pure canonical check-in correlation and one-lock sidecar transaction."""

from __future__ import annotations

from dataclasses import dataclass
from datetime import date, datetime, time
from enum import StrEnum
from typing import Final, final, assert_never
from zoneinfo import ZoneInfo

from .store import CanonicalEventSnapshot
from .weekly_operations import (
    CanonicalPin,
    CustomerIdentityDigest,
    DayState,
    SourceLineage,
    WeeklyOperationInput,
    WeeklyOperationRow,
    WeeklyOperationsConflict,
)
from .weekly_operations_lineage import (
    InvalidCanonical,
    InvalidLineage,
    MissingLineage,
    SelectedLineage,
    canonical_prefix_pin,
    resolve_canonical_lineage,
)
from .weekly_operations_customer_authority import CanonicalCheckinCustomerAuthority
from .weekly_operations_store import WeeklyOperationsStore

_CUTOFF: Final = time(23, 0)
_KST: Final = ZoneInfo("Asia/Seoul")


class CorrelationAction(StrEnum):
    CHECKIN = "checkin"
    CUTOFF = "cutoff"


class CorrelationOutcome(StrEnum):
    SUBMITTED = "submitted"
    MISSED = "missed"
    LATE_SUBMITTED = "late_submitted"
    SOURCE_ADVANCED = "source_advanced"
    REPLAY = "replay"
    MISSING = "missing"
    AWAITING_CUTOFF = "awaiting_cutoff"
    INVALID_CANONICAL = "invalid_canonical"
    INVALID_LINEAGE = "invalid_lineage"


@dataclass(frozen=True, slots=True)
class CorrelationScope:
    customer_identity_digest: CustomerIdentityDigest
    kst_day: date
    action: CorrelationAction


@dataclass(frozen=True, slots=True)
class CorrelationRequest:
    scope: CorrelationScope
    source: CanonicalCheckinCustomerAuthority


@dataclass(frozen=True, slots=True)
class CorrelationProjectionInput:
    scope: CorrelationScope
    snapshot: CanonicalEventSnapshot
    rows: tuple[WeeklyOperationRow, ...]


@dataclass(frozen=True, slots=True)
class CorrelationProjection:
    operation: WeeklyOperationInput | None
    outcome: CorrelationOutcome


@dataclass(frozen=True, slots=True)
class CorrelationResult:
    outcome: CorrelationOutcome
    row: WeeklyOperationRow | None
    appended: bool


@dataclass(frozen=True, slots=True)
class _OperationDecision:
    pin: CanonicalPin
    state: DayState
    occurred_at: datetime
    source: SourceLineage | None


def _operation(
    data: CorrelationProjectionInput, decision: _OperationDecision
) -> WeeklyOperationInput:
    return WeeklyOperationInput(
        data.scope.customer_identity_digest,
        data.scope.kst_day,
        decision.state,
        decision.pin,
        decision.occurred_at,
        decision.source,
    )


def project_canonical_checkin(data: CorrelationProjectionInput) -> CorrelationProjection:
    """Project one validated canonical snapshot without performing I/O."""
    resolution = resolve_canonical_lineage(data.snapshot, data.scope.kst_day)
    match resolution:
        case InvalidCanonical():
            return CorrelationProjection(None, CorrelationOutcome.INVALID_CANONICAL)
        case InvalidLineage():
            return CorrelationProjection(None, CorrelationOutcome.INVALID_LINEAGE)
        case MissingLineage(pin=pin):
            selected = None
        case SelectedLineage(pin=pin) as selected:
            pass
        case _:
            assert_never(resolution)
    day_rows = tuple(row for row in data.rows if row.kst_day == data.scope.kst_day)
    if selected is None:
        if data.scope.action is CorrelationAction.CUTOFF and not day_rows:
            cutoff = datetime.combine(data.scope.kst_day, _CUTOFF, _KST)
            return CorrelationProjection(
                _operation(data, _OperationDecision(pin, DayState.MISSED, cutoff, None)),
                CorrelationOutcome.MISSED,
            )
        return CorrelationProjection(None, CorrelationOutcome.MISSING)
    root_time = datetime.fromisoformat(selected.root.occurred_at_kst)
    source = SourceLineage(selected.source.event_id, selected.source_digest)
    if not day_rows:
        if root_time.time() <= _CUTOFF:
            return CorrelationProjection(
                _operation(data, _OperationDecision(pin, DayState.SUBMITTED, root_time, source)),
                CorrelationOutcome.SUBMITTED,
            )
        match data.scope.action:
            case CorrelationAction.CHECKIN:
                return CorrelationProjection(None, CorrelationOutcome.AWAITING_CUTOFF)
            case CorrelationAction.CUTOFF:
                cutoff = root_time.replace(hour=23, minute=0, second=0, microsecond=0)
                root_sequence = next(
                    index
                    for index, row in enumerate(data.snapshot.sequence_rows, start=1)
                    if row.get("event_id") == selected.root.event_id
                )
                absence_pin = canonical_prefix_pin(data.snapshot, root_sequence - 1)
                return CorrelationProjection(
                    _operation(data, _OperationDecision(absence_pin, DayState.MISSED, cutoff, None)),
                    CorrelationOutcome.MISSED,
                )
            case _:
                assert_never(data.scope.action)
    latest = day_rows[-1]
    if latest.source_event_id == source.event_id:
        source_matches = latest.source_event_digest == source.event_digest
        sequence_advances = pin.sequence > latest.canonical_sequence
        exact_pin = (
            pin.sequence == latest.canonical_sequence
            and pin.digest == latest.canonical_digest
        )
        if not source_matches or (not sequence_advances and not exact_pin):
            raise WeeklyOperationsConflict("canonical source replay authority drift")
        return CorrelationProjection(None, CorrelationOutcome.REPLAY)
    match latest.state:
        case DayState.MISSED:
            state, outcome = DayState.LATE_SUBMITTED, CorrelationOutcome.LATE_SUBMITTED
        case DayState.SUBMITTED | DayState.LATE_SUBMITTED:
            state, outcome = latest.state, CorrelationOutcome.SOURCE_ADVANCED
        case _:
            assert_never(latest.state)
    return CorrelationProjection(_operation(data, _OperationDecision(pin, state, root_time, source)), outcome)


@final
class CanonicalCheckinCorrelationTransaction:
    """Linearize canonical correlation and one sidecar append."""

    def __init__(self, store: WeeklyOperationsStore, request: CorrelationRequest) -> None:
        self._store: WeeklyOperationsStore = store
        self._request: CorrelationRequest = request

    def _decide(
        self, snapshot: CanonicalEventSnapshot, rows: tuple[WeeklyOperationRow, ...]
    ) -> tuple[WeeklyOperationInput | None, CorrelationProjection]:
        projection = project_canonical_checkin(
            CorrelationProjectionInput(self._request.scope, snapshot, rows)
        )
        self._request.source.verify()
        return projection.operation, projection

    def commit(self) -> CorrelationResult:
        """Commit at most one legal Todo 3 row under the customer lock."""
        self._request.source.verify()
        identities = (
            self._request.scope.customer_identity_digest,
            self._request.source.customer_identity_digest,
            self._store.customer_identity_digest,
        )
        if len(set(identities)) != 1:
            raise WeeklyOperationsConflict("canonical and sidecar customer authority disagree")
        registry = self._request.source.registry_authority_binding_digest
        if registry != self._store.authority.binding.binding_digest:
            raise WeeklyOperationsConflict("canonical registry and sidecar authority disagree")
        with self._request.source.read_locked() as snapshot:
            def decide(
                rows: tuple[WeeklyOperationRow, ...],
            ) -> tuple[WeeklyOperationInput | None, CorrelationProjection]:
                return self._decide(snapshot, rows)

            appended, projection = self._store.transact(decide)
            if appended is None:
                return CorrelationResult(projection.outcome, None, False)
            return CorrelationResult(projection.outcome, appended.row, appended.appended)
