"""Strict finite value types for authority-bound weekly owner facts."""

from __future__ import annotations

import hashlib
import json
import re
from dataclasses import dataclass
from datetime import date, timedelta
from decimal import ROUND_HALF_UP, Decimal
from enum import StrEnum
from typing import Final, TypeAlias, TypedDict, override

from .weekly_operations_summary_types import WeightTrend

_DIGEST: Final = re.compile(r"[0-9a-f]{64}")
GROUNDING_SCHEMA: Final = "nutricoach-grounded-weekly-facts-v3"
LOCK_TOKEN: Final = "[주간 운영 사실 잠금]"
FALLBACK_TOKEN: Final = "[결정론적 주간 대체문]"
GroundedJsonValue: TypeAlias = str | int | float | bool | None | list["GroundedJsonValue"] | dict[str, "GroundedJsonValue"]
CanonicalValue: TypeAlias = GroundedJsonValue | bool | tuple["CanonicalValue", ...]

class WeekStatus(StrEnum):
    CLOSED = "closed"
    INCOMPLETE = "incomplete"


class WeeklyDecisionOption(StrEnum):
    MAINTAIN = "maintain"
    OWNER_ADJUSTMENT_REVIEW = "owner_adjustment_review"


class WeeklyDecision(StrEnum):
    OWNER_REVIEW_REQUIRED = "owner_review_required"


class WeeklyAction(StrEnum):
    OWNER_EDIT = "owner_edit"
    OWNER_APPROVE = "owner_approve"
    OWNER_SEND = "owner_send"


@dataclass(frozen=True, slots=True)
class WeeklyGroundingError(ValueError):
    reason: str

    @override
    def __str__(self) -> str:
        return f"weekly grounding is invalid: {self.reason}"


@dataclass(frozen=True, slots=True)
class WeeklyGroundingBindings:
    candidate_digest: str
    config_digest: str
    customer_authority_digest: str
    owner_route_digest: str
    consent_digest: str

    def __post_init__(self) -> None:
        if any(_DIGEST.fullmatch(value) is None for value in self.values()):
            raise WeeklyGroundingError("binding digest")

    def values(self) -> tuple[str, ...]:
        return (
            self.candidate_digest, self.config_digest, self.customer_authority_digest,
            self.owner_route_digest, self.consent_digest,
        )


@dataclass(frozen=True, slots=True)
class GroundedPriorWeekComparison:
    submitted_count_delta: int
    late_count_delta: int
    missed_count_delta: int
    completed_days_delta: int
    adherence_percent_delta: Decimal

    def __post_init__(self) -> None:
        counts = (
            self.submitted_count_delta, self.late_count_delta,
            self.missed_count_delta, self.completed_days_delta,
        )
        if any(type(value) is not int or not -7 <= value <= 7 for value in counts):
            raise WeeklyGroundingError("comparison count")
        _require_decimal(self.adherence_percent_delta, Decimal("-100"), Decimal("100"))


class GroundedFactsPatch(TypedDict, total=False):
    week_status: WeekStatus | str
    weight_trend: WeightTrend | str
    source_aggregate_digest: str
    bound_summary_authority_digest: str
    grounding_digest: str


@dataclass(frozen=True, slots=True)
class GroundedWeeklyFacts:
    starts_on: date
    ends_on: date
    submitted_count: int
    late_count: int
    missed_count: int
    completed_days: int
    calendar_days: int
    adherence_percent: Decimal
    weight_trend: WeightTrend
    reminder_sent_count: int
    reminder_incident_count: int
    prior_week_comparison: GroundedPriorWeekComparison | None
    week_status: WeekStatus
    decision_options: tuple[WeeklyDecisionOption, ...]
    decision: WeeklyDecision
    actions: tuple[WeeklyAction, ...]
    source_aggregate_digest: str
    bound_summary_authority_digest: str
    candidate_digest: str
    config_digest: str
    customer_authority_digest: str
    owner_route_digest: str
    consent_digest: str
    grounding_digest: str

    def __post_init__(self) -> None:
        self._require_finite()
        if self.grounding_digest != grounded_facts_digest(self):
            raise WeeklyGroundingError("grounding digest")

    def _require_finite(self) -> None:
        counts = (
            self.submitted_count, self.late_count, self.missed_count,
            self.completed_days, self.calendar_days, self.reminder_sent_count,
            self.reminder_incident_count,
        )
        if any(type(value) is not int or not 0 <= value <= 7 for value in counts):
            raise WeeklyGroundingError("bounded count")
        if type(self.starts_on) is not date or type(self.ends_on) is not date:
            raise WeeklyGroundingError("date")
        if self.starts_on.weekday() != 0 or self.ends_on != self.starts_on + timedelta(days=6):
            raise WeeklyGroundingError("week window")
        expected_adherence = _adherence(self.completed_days)
        if self.calendar_days != 7 or self.completed_days != self.submitted_count + self.late_count:
            raise WeeklyGroundingError("count relation")
        if self.submitted_count + self.late_count + self.missed_count > 7:
            raise WeeklyGroundingError("week count")
        _require_decimal(self.adherence_percent, Decimal(0), Decimal(100))
        if self.adherence_percent != expected_adherence:
            raise WeeklyGroundingError("adherence")
        if type(self.weight_trend) is not WeightTrend or type(self.week_status) is not WeekStatus:
            raise WeeklyGroundingError("finite status")
        if self.decision_options != tuple(WeeklyDecisionOption):
            raise WeeklyGroundingError("decision options")
        if self.decision is not WeeklyDecision.OWNER_REVIEW_REQUIRED or self.actions != tuple(WeeklyAction):
            raise WeeklyGroundingError("owner authority")
        digests = (
            self.source_aggregate_digest, self.bound_summary_authority_digest,
            self.candidate_digest, self.config_digest, self.customer_authority_digest,
            self.owner_route_digest, self.consent_digest, self.grounding_digest,
        )
        if any(_DIGEST.fullmatch(value) is None for value in digests):
            raise WeeklyGroundingError("digest")

    def verify(self) -> bool:
        try:
            self._require_finite()
        except WeeklyGroundingError:
            return False
        return self.grounding_digest == grounded_facts_digest(self)

    def model_copy(self, *, update: GroundedFactsPatch | None = None) -> GroundedWeeklyFacts:
        weight_trend = self.weight_trend if update is None or "weight_trend" not in update else update["weight_trend"]
        week_status = self.week_status if update is None or "week_status" not in update else update["week_status"]
        aggregate = self.source_aggregate_digest if update is None or "source_aggregate_digest" not in update else update["source_aggregate_digest"]
        authority = self.bound_summary_authority_digest if update is None or "bound_summary_authority_digest" not in update else update["bound_summary_authority_digest"]
        digest = self.grounding_digest if update is None or "grounding_digest" not in update else update["grounding_digest"]
        if type(weight_trend) is not WeightTrend or type(week_status) is not WeekStatus:
            raise WeeklyGroundingError("model copy finite field")
        return GroundedWeeklyFacts(
            self.starts_on, self.ends_on, self.submitted_count, self.late_count,
            self.missed_count, self.completed_days, self.calendar_days,
            self.adherence_percent, weight_trend,
            self.reminder_sent_count, self.reminder_incident_count,
            self.prior_week_comparison, week_status,
            self.decision_options, self.decision, self.actions,
            aggregate, authority,
            self.candidate_digest, self.config_digest, self.customer_authority_digest,
            self.owner_route_digest, self.consent_digest, digest,
        )

    def machine_payload(self) -> dict[str, GroundedJsonValue]:
        return {name: value for name, value in self.digest_values()}

    def digest_values(self) -> tuple[tuple[str, GroundedJsonValue], ...]:
        comparison = self.prior_week_comparison
        compared: list[GroundedJsonValue] | None = None if comparison is None else [
            comparison.submitted_count_delta, comparison.late_count_delta,
            comparison.missed_count_delta, comparison.completed_days_delta,
            str(comparison.adherence_percent_delta),
        ]
        return (
            ("starts_on", self.starts_on.isoformat()), ("ends_on", self.ends_on.isoformat()),
            ("submitted_count", self.submitted_count), ("late_count", self.late_count),
            ("missed_count", self.missed_count), ("completed_days", self.completed_days),
            ("calendar_days", self.calendar_days), ("adherence_percent", str(self.adherence_percent)),
            ("weight_trend", self.weight_trend.value), ("reminder_sent_count", self.reminder_sent_count),
            ("reminder_incident_count", self.reminder_incident_count), ("prior_week_comparison", compared),
            ("week_status", self.week_status.value),
            ("decision_options", [item.value for item in self.decision_options]),
            ("decision", self.decision.value), ("actions", [item.value for item in self.actions]),
            ("source_aggregate_digest", self.source_aggregate_digest),
            ("bound_summary_authority_digest", self.bound_summary_authority_digest),
            ("candidate_digest", self.candidate_digest), ("config_digest", self.config_digest),
            ("customer_authority_digest", self.customer_authority_digest),
            ("owner_route_digest", self.owner_route_digest), ("consent_digest", self.consent_digest),
        )


def grounded_facts_digest(facts: GroundedWeeklyFacts) -> str:
    return hashlib.sha256(_canonical((GROUNDING_SCHEMA, facts.digest_values()))).hexdigest()


def _adherence(completed: int) -> Decimal:
    return (Decimal(completed) * Decimal(100) / Decimal(7)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)


def _require_decimal(value: Decimal, minimum: Decimal, maximum: Decimal) -> None:
    exponent = value.as_tuple().exponent
    if type(value) is not Decimal or not value.is_finite() or not minimum <= value <= maximum or type(exponent) is not int or exponent < -2:
        raise WeeklyGroundingError("decimal")


def _canonical(value: CanonicalValue) -> bytes:
    return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
