"""Descriptor-bound seven-calendar-day aggregate builder."""

from __future__ import annotations

import os
import stat
from dataclasses import dataclass
from datetime import date, datetime, time, timedelta
from decimal import InvalidOperation, ROUND_HALF_UP, Decimal
from typing import Final, assert_never
from zoneinfo import ZoneInfo

from .store import CanonicalEventSnapshot
from .weekly_operations import (
    DayState,
    WeeklyOperationRow,
    WeeklyOperationsConflict,
    WeeklyOperationsInputError,
)
from .weekly_operations_customer_authority import CanonicalCheckinCustomerAuthority
from .weekly_operations_layout import customer_data_name
from .weekly_operations_lineage import (
    InvalidCanonical,
    InvalidLineage,
    MissingLineage,
    SelectedLineage,
    canonical_prefix_pin,
    resolve_canonical_lineage,
)
from .weekly_operations_store import WeeklyOperationsStore
from .weekly_operations_summary_types import (
    PriorWeekComparison,
    WeeklyOperationsSummary,
    WeightTrend,
)

__all__: Final = ("build_weekly_operations_summary",)

_KST: Final = ZoneInfo("Asia/Seoul")
_CALENDAR_DAYS: Final = 7
_WEIGHT_STABLE_DELTA_KG: Final = Decimal("0.3")
_WEIGHT_MAX_KG: Final = Decimal("500")


def _canonical_weight_kg(value: float) -> Decimal:
    if type(value) is not float:
        raise WeeklyOperationsConflict("weekly summary body weight is invalid")
    try:
        decimal_value = Decimal(str(value))
    except (InvalidOperation, ValueError) as error:
        raise WeeklyOperationsConflict("weekly summary body weight is invalid") from error
    if not decimal_value.is_finite() or decimal_value <= 0 or decimal_value > _WEIGHT_MAX_KG:
        raise WeeklyOperationsConflict("weekly summary body weight is invalid")
    return decimal_value


@dataclass(frozen=True, slots=True)
class _WeekAggregate:
    submitted_count: int
    late_count: int
    missed_count: int
    adherence_percent: float
    weight_trend: WeightTrend
    reminder_sent_count: int
    reminder_incident_count: int
    has_sidecar_rows: bool

    @property
    def completed_days(self) -> int:
        return self.submitted_count + self.late_count


def _adherence_percent(completed_days: int) -> float:
    value = Decimal(completed_days) * Decimal(100) / Decimal(_CALENDAR_DAYS)
    return float(value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))


def _require_confirmed_sidecar(store: WeeklyOperationsStore) -> tuple[WeeklyOperationRow, ...]:
    path = store.authority.observed_path / customer_data_name(store.customer_identity_digest)
    try:
        info = os.stat(path, follow_symlinks=False)
    except FileNotFoundError as error:
        raise WeeklyOperationsConflict("weekly summary sidecar is missing") from error
    if not stat.S_ISREG(info.st_mode) or stat.S_IMODE(info.st_mode) != 0o600:
        raise WeeklyOperationsConflict("weekly summary sidecar is unsafe")
    rows = store.read()
    try:
        observed = os.stat(path, follow_symlinks=False)
    except FileNotFoundError as error:
        raise WeeklyOperationsConflict("weekly summary sidecar disappeared") from error
    if (observed.st_dev, observed.st_ino, observed.st_mode) != (
        info.st_dev,
        info.st_ino,
        info.st_mode,
    ):
        raise WeeklyOperationsConflict("weekly summary sidecar changed during read")
    return rows


def _validate_canonical_pins(
    snapshot: CanonicalEventSnapshot, rows: tuple[WeeklyOperationRow, ...]
) -> None:
    for row in rows:
        if row.canonical_sequence > len(snapshot.sequence_rows):
            raise WeeklyOperationsConflict("weekly summary canonical pin is stale")
        expected = canonical_prefix_pin(snapshot, row.canonical_sequence)
        if expected.sequence != row.canonical_sequence or expected.digest != row.canonical_digest:
            raise WeeklyOperationsConflict("weekly summary canonical digest disagrees")


def _aggregate_week(
    snapshot: CanonicalEventSnapshot,
    rows: tuple[WeeklyOperationRow, ...],
    starts_on: date,
) -> _WeekAggregate:
    ends_on = starts_on + timedelta(days=_CALENDAR_DAYS - 1)
    week_rows = tuple(row for row in rows if starts_on <= row.kst_day <= ends_on)
    active_rows = {row.kst_day: row for row in week_rows}
    submitted_count = 0
    late_count = 0
    missed_count = 0
    weights: list[Decimal] = []
    for offset in range(_CALENDAR_DAYS):
        day = starts_on + timedelta(days=offset)
        row = active_rows.get(day)
        if row is None:
            continue
        resolution = resolve_canonical_lineage(snapshot, day)
        match resolution:
            case InvalidCanonical():
                raise WeeklyOperationsConflict("weekly summary canonical snapshot is invalid")
            case InvalidLineage():
                raise WeeklyOperationsConflict("weekly summary canonical lineage is invalid")
            case MissingLineage():
                if row.source_event_id is not None:
                    raise WeeklyOperationsConflict("weekly summary sidecar source is stale")
            case SelectedLineage(source=source, source_digest=digest):
                if row.source_event_id is None:
                    if row.state is not DayState.MISSED:
                        raise WeeklyOperationsConflict("weekly summary sidecar source is missing")
                elif row.source_event_id != source.event_id or row.source_event_digest != digest:
                    raise WeeklyOperationsConflict("weekly summary sidecar source disagrees")
                if row.state is not DayState.MISSED:
                    check_in = source.check_in
                    if check_in is None:
                        raise WeeklyOperationsConflict("weekly summary active source is incomplete")
                    if check_in.body_weight_kg is not None:
                        weights.append(_canonical_weight_kg(check_in.body_weight_kg))
            case _:
                assert_never(resolution)
        match row.state:
            case DayState.SUBMITTED:
                submitted_count += 1
            case DayState.LATE_SUBMITTED:
                late_count += 1
            case DayState.MISSED:
                missed_count += 1
            case _:
                assert_never(row.state)
    if len(weights) < 2:
        weight_trend = WeightTrend.INSUFFICIENT_DATA
    elif abs(weights[-1] - weights[0]) < _WEIGHT_STABLE_DELTA_KG:
        weight_trend = WeightTrend.STABLE
    elif weights[-1] > weights[0]:
        weight_trend = WeightTrend.INCREASING
    else:
        weight_trend = WeightTrend.DECREASING
    sent_audits = {row.reminder_audit_id for row in week_rows if row.reminder_audit_id}
    reservations = {
        row.reminder_reservation_id
        for row in week_rows
        if row.reminder_reservation_id
    }
    audited_reservations = {
        row.reminder_reservation_id
        for row in week_rows
        if row.reminder_reservation_id and row.reminder_audit_id
    }
    return _WeekAggregate(
        submitted_count,
        late_count,
        missed_count,
        _adherence_percent(submitted_count + late_count),
        weight_trend,
        len(sent_audits),
        len(reservations - audited_reservations),
        bool(week_rows),
    )


def build_weekly_operations_summary(
    source: CanonicalCheckinCustomerAuthority | None,
    sidecar: WeeklyOperationsStore | None,
    starts_on: date,
) -> WeeklyOperationsSummary:
    """Join one registered canonical authority with its confirmed sidecar."""
    if source is None:
        raise WeeklyOperationsConflict("weekly summary canonical authority is missing")
    if sidecar is None:
        raise WeeklyOperationsConflict("weekly summary sidecar authority is missing")
    if starts_on.weekday() != 0:
        raise WeeklyOperationsInputError("weekly summary must start on Monday")
    if source.customer_identity_digest != sidecar.customer_identity_digest:
        raise WeeklyOperationsConflict("weekly summary customer authority disagrees")
    if source.registry_authority_binding_digest != sidecar.authority.binding.binding_digest:
        raise WeeklyOperationsConflict("weekly summary registry authority disagrees")
    with source.read_locked() as snapshot:
        rows = _require_confirmed_sidecar(sidecar)
        _validate_canonical_pins(snapshot, rows)
        current = _aggregate_week(snapshot, rows, starts_on)
        prior = _aggregate_week(snapshot, rows, starts_on - timedelta(days=_CALENDAR_DAYS))
    comparison = (
        None
        if not prior.has_sidecar_rows
        else PriorWeekComparison(
            current.submitted_count - prior.submitted_count,
            current.late_count - prior.late_count,
            current.missed_count - prior.missed_count,
            current.completed_days - prior.completed_days,
            round(current.adherence_percent - prior.adherence_percent, 2),
        )
    )
    ends_on = starts_on + timedelta(days=_CALENDAR_DAYS - 1)
    return WeeklyOperationsSummary(
        starts_on,
        ends_on,
        datetime.combine(starts_on, time.min, _KST),
        datetime.combine(ends_on, time(23, 59, 59), _KST),
        current.submitted_count,
        current.late_count,
        current.missed_count,
        current.completed_days,
        _CALENDAR_DAYS,
        current.adherence_percent,
        current.weight_trend,
        current.reminder_sent_count,
        current.reminder_incident_count,
        comparison,
    )
