"""Cost/billout stamping — the ONLY place stamps are computed
(04-architecture "Money paths"; ADR #014 invariant: locked at write
time; judgment #19: NULL = never priced, 0.00 = priced at zero).

Semantics (imports.md B64–B65 re-expressed on the R1 model):

  - Worker resolution: entry email -> person party (party.email,
    lowercased) holding an ACTIVE worker engagement (subcontract /
    freelance, ended_on IS NULL) in the tenant. No match -> the entry
    stays unresolved: worker/engagement NULL, both stamps NULL
    (T-CLK-018 port*).
  - Rates come from ``compensation_terms`` **in force at write time**
    (effective_from <= today <= effective_to, hourly model), the
    project-scoped term beating the engagement default (02 §2), the
    latest effective_from winning among survivors.
  - Billout additionally consults ``rate_overrides`` (judgment #25)
    with the legacy precedence: worker-specific override >
    project-wide override > term. An absolute override IS the rate; a
    pct override modifies the term-resolved default (NULL default +
    pct -> NULL, "no rate available" — T-ADJ-008).
  - Amount = source hours × rate, cents, ROUND_HALF_UP (T-CLK-023);
    NULL duration or NULL rate -> NULL amount (T-CLK-022b). The
    currency rides WITH the amount (02 §3: the currency is part of
    the stamp) — never one without the other.
"""

from __future__ import annotations

import uuid
from dataclasses import dataclass
from datetime import date
from decimal import Decimal

from sqlalchemy import func, or_, select
from sqlalchemy.orm import Session

from app.models import CompensationTerm, Engagement, Party, RateOverride
from app.services.reporting.money import quantize2

#: Worker engagement types (02 §4: subcontract / freelance grant the
#: worker role; other types never attribute time).
WORKER_ENGAGEMENT_TYPES = ("subcontract", "freelance")


@dataclass(frozen=True)
class StampRates:
    """The per-(engagement, project) rate resolution — memoize on that
    pair, exactly like the legacy effRateCache."""

    cost_rate: Decimal | None
    cost_currency: str | None
    billout_rate: Decimal | None
    billout_currency: str | None


@dataclass(frozen=True)
class Stamp:
    cost_amount: Decimal | None
    cost_currency: str | None
    billout_amount: Decimal | None
    billout_currency: str | None


def resolve_worker(
    session: Session, tenant_id: uuid.UUID, email: str
) -> tuple[uuid.UUID, uuid.UUID] | None:
    """email -> (worker_party_id, via_engagement_id) or None. The
    earliest-created active worker engagement wins when several match."""
    if not email:
        return None
    row = session.execute(
        select(Engagement.party_b_id, Engagement.id)
        .join(Party, Party.id == Engagement.party_b_id)
        .where(
            Engagement.tenant_id == tenant_id,
            Engagement.type.in_(WORKER_ENGAGEMENT_TYPES),
            Engagement.ended_on.is_(None),
            Party.kind == "person",
            func.lower(Party.email) == email.strip().lower(),
        )
        .order_by(Engagement.created_at)
    ).first()
    if row is None:
        return None
    return row[0], row[1]


def _term_in_force(
    session: Session,
    engagement_id: uuid.UUID,
    kind: str,
    project_id: uuid.UUID | None,
    on: date,
) -> CompensationTerm | None:
    """Hourly term in force on ``on``: project-scoped beats the
    engagement default; latest effective_from wins among survivors."""
    terms = (
        session.scalars(
            select(CompensationTerm).where(
                CompensationTerm.engagement_id == engagement_id,
                CompensationTerm.kind == kind,
                CompensationTerm.model == "hourly",
                CompensationTerm.effective_from <= on,
                or_(
                    CompensationTerm.effective_to.is_(None),
                    CompensationTerm.effective_to >= on,
                ),
            )
        )
        .unique()
        .all()
    )
    pool = [t for t in terms if project_id is not None and t.project_id == project_id]
    if not pool:
        pool = [t for t in terms if t.project_id is None]
    if not pool:
        return None
    return max(pool, key=lambda t: t.effective_from)


def _billout_override(
    session: Session,
    tenant_id: uuid.UUID,
    project_id: uuid.UUID | None,
    worker_party_id: uuid.UUID,
) -> RateOverride | None:
    """Worker-specific beats project-wide (specificity precedence —
    judgment #25 / T-ADJ-001)."""
    if project_id is None:
        return None
    rows = session.scalars(
        select(RateOverride).where(
            RateOverride.tenant_id == tenant_id,
            RateOverride.project_id == project_id,
            or_(
                RateOverride.worker_party_id == worker_party_id,
                RateOverride.worker_party_id.is_(None),
            ),
        )
    ).all()
    specific = next((r for r in rows if r.worker_party_id == worker_party_id), None)
    wide = next((r for r in rows if r.worker_party_id is None), None)
    return specific or wide


def resolve_rates(
    session: Session,
    tenant_id: uuid.UUID,
    engagement_id: uuid.UUID,
    worker_party_id: uuid.UUID,
    project_id: uuid.UUID | None,
    on: date | None = None,
) -> StampRates:
    on = on or date.today()

    cost_term = _term_in_force(session, engagement_id, "cost", project_id, on)
    cost_rate = (
        Decimal(cost_term.rate_amount)
        if cost_term and cost_term.rate_amount is not None
        else None
    )
    cost_currency = cost_term.currency if cost_term else None

    billout_term = _term_in_force(session, engagement_id, "billout", project_id, on)
    default_rate = (
        Decimal(billout_term.rate_amount)
        if billout_term and billout_term.rate_amount is not None
        else None
    )
    default_currency = billout_term.currency if billout_term else None

    billout_rate: Decimal | None = default_rate
    billout_currency: str | None = default_currency
    override = _billout_override(session, tenant_id, project_id, worker_party_id)
    if override is not None:
        if override.override_rate is not None:
            billout_rate = Decimal(override.override_rate)
            billout_currency = override.currency
        elif override.override_pct is not None:
            # pct modifies the term-resolved default; no default -> no
            # rate available (T-ADJ-008), never coalesced to 0
            if default_rate is None:
                billout_rate, billout_currency = None, None
            else:
                pct = Decimal(override.override_pct)
                billout_rate = default_rate * (1 + pct / Decimal(100))
                billout_currency = default_currency

    return StampRates(
        cost_rate=cost_rate,
        cost_currency=cost_currency if cost_rate is not None else None,
        billout_rate=billout_rate,
        billout_currency=billout_currency if billout_rate is not None else None,
    )


def compute_amount(duration_seconds: int | None, rate: Decimal | None) -> Decimal | None:
    """seconds/3600 × rate, cents, ROUND_HALF_UP — 2dp at computation
    time (T-CLK-023); either input None -> None (T-CLK-022b)."""
    if duration_seconds is None or rate is None:
        return None
    return quantize2(Decimal(duration_seconds) / Decimal(3600) * rate)


def stamp_entry(duration_seconds: int | None, rates: StampRates) -> Stamp:
    """Fold resolved rates into the (amount, currency) stamp pairs.
    NULL rate -> NULL stamp (judgment #19); a 0 rate stamps 0.00 (Gary,
    the zero-cost billable worker — T-TEAM-08)."""
    cost_amount = compute_amount(duration_seconds, rates.cost_rate)
    billout_amount = compute_amount(duration_seconds, rates.billout_rate)
    return Stamp(
        cost_amount=cost_amount,
        cost_currency=rates.cost_currency if cost_amount is not None else None,
        billout_amount=billout_amount,
        billout_currency=rates.billout_currency if billout_amount is not None else None,
    )
