"""Pure aggregation rules for the frozen channel BI contract 1.2."""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from decimal import Decimal, InvalidOperation
from typing import Any, Dict, Iterable, List, Optional, Tuple


DASHBOARD_VERSION = "1.2"
DETAIL_VERSION = "1.0"
UNLABELED_ROOM_TYPE = "未标注房型"
MONTH_RE = re.compile(r"^(\d{4})-(\d{2})$")
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")


class AnalyticsError(RuntimeError):
    def __init__(self, code: str, safe_message: str):
        super().__init__(safe_message)
        self.code = code
        self.safe_message = safe_message


@dataclass(frozen=True)
class AggregatedRoomFact:
    worksheet: str
    room_type: str
    rooms_sold: int
    total_price: Decimal
    room_nights: int
    reservation_rows: int


@dataclass
class _RoomMetrics:
    rooms_sold: int = 0
    total_price: Decimal = Decimal(0)
    room_nights: int = 0
    reservation_rows: int = 0

    def add(self, fact: AggregatedRoomFact) -> None:
        self.rooms_sold += fact.rooms_sold
        self.total_price += fact.total_price
        self.room_nights += fact.room_nights
        self.reservation_rows += fact.reservation_rows


@dataclass
class _ScopeMetrics:
    rooms: Dict[str, _RoomMetrics] = field(default_factory=dict)

    def add(self, fact: AggregatedRoomFact) -> None:
        room_type = fact.room_type.strip() or UNLABELED_ROOM_TYPE
        self.rooms.setdefault(room_type, _RoomMetrics()).add(fact)

    @property
    def rooms_sold(self) -> int:
        return sum(item.rooms_sold for item in self.rooms.values())

    @property
    def total_price(self) -> Decimal:
        return sum((item.total_price for item in self.rooms.values()), Decimal(0))

    @property
    def room_nights(self) -> int:
        return sum(item.room_nights for item in self.rooms.values())

    @property
    def reservation_rows(self) -> int:
        return sum(item.reservation_rows for item in self.rooms.values())


def validate_month_key(month_key: str) -> Tuple[int, int]:
    match = MONTH_RE.fullmatch(month_key)
    if not match:
        raise AnalyticsError("ANALYTICS_MONTH_INVALID", "month must use YYYY-MM")
    year, month = int(match.group(1)), int(match.group(2))
    if year < 1900 or month < 1 or month > 12:
        raise AnalyticsError("ANALYTICS_MONTH_INVALID", "month is invalid")
    return year, month


def _integer(value: Any, field_name: str) -> int:
    if isinstance(value, bool):
        raise AnalyticsError("ANALYTICS_SOURCE_INVALID", f"{field_name} is invalid")
    try:
        number = Decimal(str(value))
    except (InvalidOperation, TypeError, ValueError):
        raise AnalyticsError("ANALYTICS_SOURCE_INVALID", f"{field_name} is invalid") from None
    if not number.is_finite() or number < 0 or number != number.to_integral_value():
        raise AnalyticsError("ANALYTICS_SOURCE_INVALID", f"{field_name} is invalid")
    return int(number)


def _decimal(value: Any, field_name: str) -> Decimal:
    try:
        number = Decimal(str(value))
    except (InvalidOperation, TypeError, ValueError):
        raise AnalyticsError("ANALYTICS_SOURCE_INVALID", f"{field_name} is invalid") from None
    if not number.is_finite() or number < 0:
        raise AnalyticsError("ANALYTICS_SOURCE_INVALID", f"{field_name} is invalid")
    return number


def json_number(value: Decimal) -> int | float:
    return int(value) if value == value.to_integral_value() else float(value)


def normalized_fact(fact: AggregatedRoomFact) -> AggregatedRoomFact:
    if not isinstance(fact.worksheet, str) or not fact.worksheet.strip():
        raise AnalyticsError("ANALYTICS_SOURCE_INVALID", "worksheet is invalid")
    if not isinstance(fact.room_type, str):
        raise AnalyticsError("ANALYTICS_SOURCE_INVALID", "room type is invalid")
    normalized = AggregatedRoomFact(
        worksheet=fact.worksheet,
        room_type=fact.room_type.strip() or UNLABELED_ROOM_TYPE,
        rooms_sold=_integer(fact.rooms_sold, "rooms_sold"),
        total_price=_decimal(fact.total_price, "total_price"),
        room_nights=_integer(fact.room_nights, "room_nights"),
        reservation_rows=_integer(fact.reservation_rows, "reservation_rows"),
    )
    if normalized.reservation_rows == 0 and (
        normalized.rooms_sold or normalized.room_nights or normalized.total_price
    ):
        raise AnalyticsError(
            "ANALYTICS_SOURCE_INVALID",
            "aggregate row count does not match its values",
        )
    return normalized


def _room_rows(scope: _ScopeMetrics) -> List[Dict[str, Any]]:
    denominator = scope.rooms_sold
    return [
        {
            "room_type": room_type,
            "rooms_sold": metrics.rooms_sold,
            "rooms_share": metrics.rooms_sold / denominator if denominator else 0,
            "total_price": json_number(metrics.total_price),
            "room_nights": metrics.room_nights,
            "reservation_rows": metrics.reservation_rows,
        }
        for room_type, metrics in sorted(
            scope.rooms.items(),
            key=lambda item: (-item[1].rooms_sold, item[0]),
        )
    ]


def _totals(scope: _ScopeMetrics, channel_count: Optional[int] = None) -> Dict[str, Any]:
    payload: Dict[str, Any] = {
        "rooms_sold": scope.rooms_sold,
        "total_price": json_number(scope.total_price),
        "room_nights": scope.room_nights,
        "reservation_rows": scope.reservation_rows,
        "room_type_count": len(scope.rooms),
    }
    if channel_count is not None:
        payload["channel_count"] = channel_count
    return payload


def build_dashboard(
    month_key: str,
    updated_at: str,
    max_arrival_date: Optional[str],
    source_monthly_sha256: str,
    channel_names: Iterable[str],
    facts: Iterable[AggregatedRoomFact],
    min_arrival_date: Optional[str] = None,
) -> Dict[str, Any]:
    validate_month_key(month_key)
    names = tuple(channel_names)
    if (
        not names
        or len(set(names)) != len(names)
        or any(not isinstance(name, str) or not name.strip() for name in names)
        or SHA256_RE.fullmatch(source_monthly_sha256) is None
    ):
        raise AnalyticsError("ANALYTICS_SOURCE_INVALID", "dashboard manifest is invalid")
    channels = {name: _ScopeMetrics() for name in names}
    overall = _ScopeMetrics()
    for raw_fact in facts:
        fact = normalized_fact(raw_fact)
        if fact.worksheet not in channels:
            raise AnalyticsError(
                "ANALYTICS_SOURCE_INVALID",
                "aggregate contains a channel outside the report manifest",
            )
        channels[fact.worksheet].add(fact)
        overall.add(fact)
    channel_payload = [
        {
            "worksheet": name,
            "totals": _totals(channels[name]),
            "room_types": _room_rows(channels[name]),
        }
        for name in names
    ]
    if (
        overall.rooms_sold != sum(item["totals"]["rooms_sold"] for item in channel_payload)
        or overall.room_nights != sum(item["totals"]["room_nights"] for item in channel_payload)
        or overall.reservation_rows
        != sum(item["totals"]["reservation_rows"] for item in channel_payload)
        or overall.total_price
        != sum(
            (Decimal(str(item["totals"]["total_price"])) for item in channel_payload),
            Decimal(0),
        )
    ):
        raise AnalyticsError("ANALYTICS_SOURCE_INVALID", "dashboard totals do not balance")
    return {
        "version": DASHBOARD_VERSION,
        "month_key": month_key,
        "updated_at": updated_at,
        "min_arrival_date": min_arrival_date,
        "max_arrival_date": max_arrival_date,
        "source_monthly_sha256": source_monthly_sha256,
        "overall": {
            "totals": _totals(overall, channel_count=len(names)),
            "room_types": _room_rows(overall),
        },
        "channels": channel_payload,
    }
