"""Pure company-report business rules with no filesystem or database access."""

from __future__ import annotations

import calendar
import unicodedata
from collections import Counter, defaultdict
from dataclasses import replace
from datetime import date
from decimal import Decimal, InvalidOperation
from typing import Dict, Iterable, List, Mapping, MutableMapping, Optional, Sequence, Tuple

from company_reports.contracts import (
    COMPANY_NAMES,
    COMPANY_RULES,
    ENGLISH_MONTH_ABBREVIATIONS,
    ENGLISH_MONTH_NAMES,
    BatchSnapshot,
    BookingRoomItem,
    CompanyReport,
    DailyVersionPin,
    ErrorCode,
    FinanceFact,
    PeriodReport,
    ReportProblem,
    ReportRow,
    WarningCode,
)


def normalize_group_code(value: Optional[str]) -> str:
    """Normalize the complete RES_COMMENT value; never extract a substring."""

    if value is None:
        return ""
    return unicodedata.normalize("NFKC", str(value)).strip().upper()


def report_month_bounds(year: int, month: int) -> Tuple[date, date]:
    if year < 1900 or year > 9999 or month < 1 or month > 12:
        raise ValueError("report month is invalid")
    last_day = calendar.monthrange(year, month)[1]
    return date(year, month, 1), date(year, month, last_day)


def validate_as_of_date(year: int, month: int, as_of_date: date) -> None:
    month_start, month_end = report_month_bounds(year, month)
    allowed = {date(year, month, 10), date(year, month, 20), month_end}
    if as_of_date < month_start or as_of_date > month_end or as_of_date not in allowed:
        raise ValueError("as-of date must be the 10th, 20th, or natural month end")


def report_periods(year: int, month: int, as_of_date: date, company: str) -> Tuple[PeriodReport, ...]:
    _, month_end = report_month_bounds(year, month)
    month_label = ENGLISH_MONTH_ABBREVIATIONS[month]
    specs = ((1, 10), (11, 20), (21, month_end.day))
    periods: List[PeriodReport] = []
    for start_day, end_day in specs:
        end_date = date(year, month, end_day)
        sheet_name = f"{company} {start_day:02d}-{end_day:02d} {month_label} {year}"
        if len(sheet_name) > 31:
            raise ValueError("worksheet name exceeds Excel's 31-character limit")
        periods.append(
            PeriodReport(
                start_day=start_day,
                end_day=end_day,
                active=as_of_date >= end_date,
                sheet_name=sheet_name,
                rows=tuple(),
            )
        )
    return tuple(periods)


def company_for_fact(fact: FinanceFact) -> Optional[str]:
    """Map one fact to exactly one requested company; the merged channel wins first."""

    if fact.channel_key.strip().upper() == "DY-AI-EASY-KB":
        return "DY-AI-Easy-KB"
    normalized_company = fact.company_key.strip().upper()
    for rule in COMPANY_RULES:
        if rule.match_field == "company_key" and normalized_company == rule.match_value:
            return rule.display_name
    return None


def format_decimal(value: Decimal) -> str:
    """Use grouping separators and at most two meaningful decimal places."""

    quantized = value.quantize(Decimal("0.01"))
    rendered = f"{quantized:,.2f}"
    if "." in rendered:
        rendered = rendered.rstrip("0").rstrip(".")
    return rendered


def _decimal_or_none(value: Optional[Decimal]) -> Optional[Decimal]:
    if value is None:
        return None
    try:
        decimal_value = value if isinstance(value, Decimal) else Decimal(str(value))
    except (InvalidOperation, ValueError, TypeError):
        return None
    if not decimal_value.is_finite() or decimal_value < 0:
        return None
    return decimal_value


def _problem(
    code: str,
    stage: str,
    company: str,
    period: str,
    message: str,
    facts: Sequence[FinanceFact],
) -> ReportProblem:
    return ReportProblem(
        code=code,
        stage=stage,
        company=company,
        period=period,
        message=message,
        record_ids=tuple(sorted({fact.daily_record_id for fact in facts})),
    )


def _period_key_for_departure(departure: date, month_end_day: int) -> str:
    if departure.day <= 10:
        return "01-10"
    if departure.day <= 20:
        return "11-20"
    return f"21-{month_end_day:02d}"


def _distinct_join(values: Iterable[Optional[str]]) -> str:
    seen = set()
    ordered: List[str] = []
    for value in values:
        text = unicodedata.normalize("NFKC", str(value or "")).strip()
        if text and text not in seen:
            seen.add(text)
            ordered.append(text)
    return "；".join(ordered)


def _booking_room_text(items: Sequence[BookingRoomItem]) -> str:
    quantities: MutableMapping[str, int] = {}
    for item in sorted(items, key=lambda current: (current.segment_no, current.item_no)):
        room_type = unicodedata.normalize("NFKC", item.room_type_raw).strip()
        quantities[room_type] = quantities.get(room_type, 0) + item.quantity
    return "；".join(f"【{room_type}】{quantity}" for room_type, quantity in quantities.items())


def _price_text(facts: Sequence[FinanceFact]) -> Tuple[str, bool]:
    grouped: MutableMapping[Tuple[str, Decimal], int] = {}
    prices_by_room: Dict[str, set] = defaultdict(set)
    total = Decimal("0")
    for fact in sorted(facts, key=lambda current: current.daily_record_id):
        room_label = unicodedata.normalize("NFKC", str(fact.room_category_label or "")).strip()
        price = _decimal_or_none(fact.total_price)
        if not room_label or price is None:
            raise ValueError("invalid price detail")
        key = (room_label, price)
        grouped[key] = grouped.get(key, 0) + 1
        prices_by_room[room_label].add(price)
        total += price
    terms = [
        f"【{room_label}】{format_decimal(price)}×{count}"
        for (room_label, price), count in grouped.items()
    ]
    multi_price = any(len(prices) > 1 for prices in prices_by_room.values())
    return f"{' + '.join(terms)} = {format_decimal(total)}", multi_price


def build_company_report(
    company: str,
    year: int,
    month: int,
    as_of_date: date,
    snapshot: BatchSnapshot,
) -> CompanyReport:
    """Build one deterministic company-month payload or a privacy-minimized error set."""

    if company not in COMPANY_NAMES:
        raise ValueError(f"unsupported company: {company}")
    validate_as_of_date(year, month, as_of_date)
    month_start, month_end = report_month_bounds(year, month)
    periods = report_periods(year, month, as_of_date, company)
    facts = sorted(
        (fact for fact in snapshot.facts if company_for_fact(fact) == company),
        key=lambda current: current.daily_record_id,
    )

    errors: List[ReportProblem] = []
    eligible: List[Tuple[FinanceFact, str]] = []
    blank_booking_facts: List[FinanceFact] = []
    for fact in facts:
        generic_period = f"{year:04d}-{month:02d}"
        if not isinstance(fact.arrival, date) or not isinstance(fact.departure, date):
            errors.append(
                _problem(
                    ErrorCode.STAY_DATE_INVALID,
                    "source",
                    company,
                    generic_period,
                    "source stay dates are missing or invalid",
                    [fact],
                )
            )
            continue
        if fact.departure < fact.arrival:
            errors.append(
                _problem(
                    ErrorCode.STAY_DATE_INVALID,
                    "source",
                    company,
                    generic_period,
                    "source stay dates are inconsistent",
                    [fact],
                )
            )
            continue
        if fact.departure < month_start or fact.departure > month_end or fact.departure > as_of_date:
            continue
        period_key = _period_key_for_departure(fact.departure, month_end.day)
        expected_nights = (fact.departure - fact.arrival).days
        if not isinstance(fact.nights, int) or fact.nights != expected_nights:
            errors.append(
                _problem(
                    ErrorCode.NIGHTS_CONFLICT,
                    "source",
                    company,
                    period_key,
                    "source NIGHTS does not agree within the stay segment",
                    [fact],
                )
            )
            continue
        group_code = normalize_group_code(fact.res_comment)
        stored_group = normalize_group_code(fact.group_code_key)
        if not group_code and not stored_group:
            if (
                not str(fact.room_category_label or "").strip()
                or _decimal_or_none(fact.total_price) is None
            ):
                errors.append(
                    _problem(
                        ErrorCode.TOTAL_PRICE_INVALID,
                        "pricing",
                        company,
                        period_key,
                        "the static price detail is missing or invalid",
                        [fact],
                    )
                )
                continue
            blank_booking_facts.append(fact)
            continue
        if not group_code or not stored_group:
            errors.append(
                _problem(
                    ErrorCode.GROUP_CODE_MISSING,
                    "group_lookup",
                    company,
                    period_key,
                    "the source Group Code fields are inconsistent",
                    [fact],
                )
            )
            continue
        if stored_group != group_code:
            errors.append(
                _problem(
                    ErrorCode.GROUP_CODE_NOT_FOUND,
                    "group_lookup",
                    company,
                    period_key,
                    "the stored Group Code does not match normalized RES_COMMENT",
                    [fact],
                )
            )
            continue
        if not str(fact.room_category_label or "").strip() or _decimal_or_none(fact.total_price) is None:
            errors.append(
                _problem(
                    ErrorCode.TOTAL_PRICE_INVALID,
                    "pricing",
                    company,
                    period_key,
                    "the static price detail is missing or invalid",
                    [fact],
                )
            )
            continue
        eligible.append((fact, group_code))

    grouped_facts: Dict[Tuple[str, date, date], List[FinanceFact]] = defaultdict(list)
    for fact, group_code in eligible:
        grouped_facts[(group_code, fact.arrival, fact.departure)].append(fact)  # type: ignore[arg-type]

    items_by_segment: Dict[Tuple[str, date, date], List[BookingRoomItem]] = defaultdict(list)
    for item in snapshot.room_items:
        group_code = normalize_group_code(item.group_code_key)
        current_version = snapshot.group_parse_versions.get(group_code)
        if current_version is None or item.parse_version_id != current_version:
            continue
        items_by_segment[(group_code, item.arrival, item.departure)].append(item)

    warnings: List[ReportProblem] = []
    rows: List[ReportRow] = []
    used_booking_versions: Dict[str, int] = {}
    for fact in blank_booking_facts:
        total_booking_price, multi_price = _price_text([fact])
        rows.append(
            ReportRow(
                arrival=fact.arrival,  # type: ignore[arg-type]
                departure=fact.departure,  # type: ignore[arg-type]
                nights=fact.nights,  # type: ignore[arg-type]
                block_code=_distinct_join([fact.block_code]),
                res_comment=unicodedata.normalize(
                    "NFKC", str(fact.res_comment or "")
                ).strip(),
                booking_room="",
                total_booking_price=total_booking_price,
                normalized_group_code="",
                record_ids=(fact.daily_record_id,),
                multi_price_review=multi_price,
            )
        )
    for (group_code, arrival, departure), segment_facts in sorted(
        grouped_facts.items(), key=lambda item: (item[0][2], item[0][1], item[0][0])
    ):
        period_key = _period_key_for_departure(departure, month_end.day)
        nights = {fact.nights for fact in segment_facts}
        if len(nights) != 1:
            errors.append(
                _problem(
                    ErrorCode.NIGHTS_CONFLICT,
                    "aggregation",
                    company,
                    period_key,
                    "multiple NIGHTS values exist for one output stay segment",
                    segment_facts,
                )
            )
            continue
        try:
            total_booking_price, multi_price = _price_text(segment_facts)
        except ValueError:
            errors.append(
                _problem(
                    ErrorCode.TOTAL_PRICE_INVALID,
                    "pricing",
                    company,
                    period_key,
                    "the static price detail cannot be formatted",
                    segment_facts,
                )
            )
            continue
        if multi_price:
            warnings.append(
                _problem(
                    WarningCode.MULTI_PRICE_REVIEW,
                    "pricing",
                    company,
                    period_key,
                    "one room category has multiple static prices and requires review",
                    segment_facts,
                )
            )
        parse_version_id = snapshot.group_parse_versions.get(group_code)
        valid_room_items: List[BookingRoomItem] = []
        if parse_version_id is not None:
            used_booking_versions[group_code] = parse_version_id
            room_items = items_by_segment.get((group_code, arrival, departure), [])
            valid_room_items = [
                item
                for item in room_items
                if item.quantity > 0
                and bool(item.room_type_raw.strip())
                and item.nights == (departure - arrival).days
            ]
        rows.append(
            ReportRow(
                arrival=arrival,
                departure=departure,
                nights=next(iter(nights)),  # type: ignore[arg-type]
                block_code=_distinct_join(fact.block_code for fact in segment_facts),
                res_comment=unicodedata.normalize(
                    "NFKC", str(segment_facts[0].res_comment or "")
                ).strip(),
                booking_room=_booking_room_text(valid_room_items),
                total_booking_price=total_booking_price,
                normalized_group_code=group_code,
                record_ids=tuple(sorted(fact.daily_record_id for fact in segment_facts)),
                multi_price_review=multi_price,
            )
        )

    duplicate_counts = Counter(
        row.normalized_group_code for row in rows if row.normalized_group_code
    )
    rows = [
        replace(
            row,
            duplicate_group=bool(
                row.normalized_group_code
                and duplicate_counts[row.normalized_group_code] > 1
            ),
        )
        for row in rows
    ]

    rows_by_period: Dict[str, List[ReportRow]] = defaultdict(list)
    for row in rows:
        rows_by_period[_period_key_for_departure(row.departure, month_end.day)].append(row)

    populated_periods: List[PeriodReport] = []
    for period in periods:
        period_rows = tuple(
            sorted(
                rows_by_period.get(period.key, []),
                key=lambda row: (
                    row.departure,
                    row.arrival,
                    row.normalized_group_code,
                    row.record_ids,
                ),
            )
        )
        populated_periods.append(replace(period, rows=period_rows if period.active else tuple()))

    filename = f"{company}-{ENGLISH_MONTH_NAMES[month]}-{year}.xlsx"
    used_daily_ids = {
        fact.daily_version_id for fact, _ in eligible
    } | {fact.daily_version_id for fact in blank_booking_facts}
    daily_versions = tuple(
        pin
        for pin in sorted(snapshot.daily_versions, key=lambda current: current.business_date)
        if pin.daily_version_id in used_daily_ids
    )
    return CompanyReport(
        company=company,
        report_year=year,
        report_month=month,
        as_of_date=as_of_date,
        filename=filename,
        periods=tuple(populated_periods),
        warnings=tuple(warnings),
        errors=tuple(errors),
        daily_versions=daily_versions,
        booking_versions=dict(sorted(used_booking_versions.items())),
    )


def build_all_company_reports(
    year: int,
    month: int,
    as_of_date: date,
    snapshot: BatchSnapshot,
    companies: Optional[Sequence[str]] = None,
) -> Tuple[CompanyReport, ...]:
    selected = tuple(companies or COMPANY_NAMES)
    if len(set(selected)) != len(selected):
        raise ValueError("company selection contains duplicates")
    return tuple(
        build_company_report(company, year, month, as_of_date, snapshot)
        for company in selected
    )
