"""project_summary (rpcs §2.3) + the per-description expansion — the
/projects page's data.

FK grouping (rebuild delta #4): one row per project entity; unassigned
rows group by their raw_* triple ("(missing)" is presentation). The
lower(name) entity-join chain is gone by construction — adjustments and
income ride the project row the group IS.

The expansion deliberately fixes rpcs §2.4's staleness (binding): ONE
canonical status predicate, ONE hours definition (duration_seconds),
so the expansion sums to its parent row (T-RPT-004). Pin params are
distinct from the contains-filters (fixes the B20 __null__ collision).
"""

from __future__ import annotations

import uuid
from decimal import Decimal

from sqlalchemy import distinct, func
from sqlalchemy.orm import Session

from app.models import TimeEntry
from app.services.authz.scope import scope
from app.services.reporting.filters import EntryFilters, EntryJoins, apply_filters
from app.services.reporting.money import MixedCurrencyError, Money, quantize2


def _dec(value) -> Decimal:
    return Decimal(value) if value is not None else Decimal(0)


def _money(amount: Decimal, currency: str) -> dict:
    return Money(amount=str(quantize2(amount)), currency=currency).as_dict()


def _adjusted(billout: Decimal, pct, amount) -> Decimal:
    raw = billout * (1 + (_dec(pct)) / 100) + _dec(amount)
    return quantize2(raw)


def project_summary(
    session: Session, actor, f: EntryFilters, *, zero_currency: str = "CAD"
) -> dict:
    joins = EntryJoins()
    p = joins.project

    stmt = (
        joins.select_from(
            p.id.label("project_id"),
            joins.operator_expr.label("operator"),
            joins.client_expr.label("client"),
            joins.project_expr.label("project"),
            p.income.label("income"),
            p.income_currency.label("income_ccy"),
            p.billout_adjustment_pct.label("adj_pct"),
            p.billout_adjustment_amount.label("adj_amount"),
            func.count().label("row_count"),
            func.coalesce(func.sum(TimeEntry.duration_seconds), 0).label("secs"),
            func.sum(TimeEntry.cost_amount).label("cost"),
            func.count(distinct(TimeEntry.cost_currency)).label("cost_ccys"),
            func.min(TimeEntry.cost_currency).label("cost_ccy"),
            func.sum(TimeEntry.billout_amount).label("billout"),
            func.count(distinct(TimeEntry.billout_currency)).label("billout_ccys"),
            func.min(TimeEntry.billout_currency).label("billout_ccy"),
        ).group_by(
            p.id,
            joins.operator_expr,
            joins.client_expr,
            joins.project_expr,
            p.income,
            p.income_currency,
            p.billout_adjustment_pct,
            p.billout_adjustment_amount,
        )
    )
    stmt = apply_filters(scope(actor, "entries:read", stmt), joins, f)

    can_see_income = actor.allowed("can_see_income")
    rows: list[dict] = []
    headline = {
        "project_count": 0,
        "entry_count": 0,
        "seconds": 0,
        "cost": Decimal(0),
        "income": Decimal(0),
        "income_rows": 0,
        "cost_of_income_rows": Decimal(0),
    }
    currency = zero_currency

    for r in session.execute(stmt).mappings():
        if r["cost_ccys"] > 1 or r["billout_ccys"] > 1:
            raise MixedCurrencyError(
                "project_summary group", max(r["cost_ccys"], r["billout_ccys"])
            )
        currency = r["billout_ccy"] or r["cost_ccy"] or r["income_ccy"] or currency
        cost = quantize2(_dec(r["cost"]))
        row: dict = {
            "project_id": str(r["project_id"]) if r["project_id"] else None,
            "operator": r["operator"],
            "client": r["client"],
            "project": r["project"],
            "row_count": r["row_count"],
            "seconds": int(r["secs"]),
            "cost": _money(cost, r["cost_ccy"] or currency),
        }
        headline["project_count"] += 1
        headline["entry_count"] += r["row_count"]
        headline["seconds"] += row["seconds"]
        headline["cost"] += cost

        if can_see_income:
            billout_ccy = r["billout_ccy"] or currency
            billout = _dec(r["billout"])
            adjusted = _adjusted(billout, r["adj_pct"], r["adj_amount"])
            income = Decimal(r["income"]) if r["income"] is not None else None
            row["billout"] = _money(billout, billout_ccy)
            row["billout_adjusted"] = _money(adjusted, billout_ccy)
            row["billout_adjustment_pct"] = (
                str(r["adj_pct"]) if r["adj_pct"] is not None else None
            )
            row["billout_adjustment_amount"] = (
                _money(Decimal(r["adj_amount"]), billout_ccy)
                if r["adj_amount"] is not None
                else None
            )
            if income is not None:
                income_ccy = r["income_ccy"] or currency
                if income_ccy != (r["cost_ccy"] or income_ccy):
                    raise MixedCurrencyError("project_summary margin", 2)
                row["income"] = _money(income, income_ccy)
                # margin = income - cost, display arithmetic (ADR #040:
                # billout/adjustments deliberately NOT in margin)
                row["margin"] = _money(income - cost, income_ccy)
                headline["income"] += quantize2(income)
                headline["income_rows"] += 1
                headline["cost_of_income_rows"] += cost
            else:
                row["income"] = None
                row["margin"] = None
        rows.append(row)

    summary: dict = {
        "project_count": headline["project_count"],
        "entry_count": headline["entry_count"],
        "seconds": headline["seconds"],
        "cost": _money(headline["cost"], currency),
    }
    if can_see_income:
        summary["income_rows"] = headline["income_rows"]
        if headline["income_rows"]:
            summary["income"] = _money(headline["income"], currency)
            # B7 parity: total margin excludes the cost of income-less rows
            summary["margin"] = _money(
                headline["income"] - headline["cost_of_income_rows"], currency
            )
    return {"rows": rows, "summary": summary}


def entries_by_description(
    session: Session,
    actor,
    f: EntryFilters,
    *,
    project_id: uuid.UUID | None,
    raw_operator: str | None = None,
    raw_client: str | None = None,
    raw_project: str | None = None,
    zero_currency: str = "CAD",
) -> list[dict]:
    """Expand one /projects group into its per-description breakdown.

    Pin: a project id, OR (project_id=None) the raw_* triple with
    NULL-safe equality — so "(missing)" groups expand correctly."""
    joins = EntryJoins()
    stmt = joins.select_from(
        TimeEntry.description.label("description"),
        func.count().label("row_count"),
        func.coalesce(func.sum(TimeEntry.duration_seconds), 0).label("secs"),
        func.sum(TimeEntry.cost_amount).label("cost"),
        func.count(distinct(TimeEntry.cost_currency)).label("cost_ccys"),
        func.min(TimeEntry.cost_currency).label("cost_ccy"),
    ).group_by(TimeEntry.description)

    if project_id is not None:
        stmt = stmt.where(TimeEntry.project_id == project_id)
    else:
        stmt = stmt.where(
            TimeEntry.project_id.is_(None),
            _null_safe_eq(TimeEntry.raw_operator, raw_operator),
            _null_safe_eq(TimeEntry.raw_client, raw_client),
            _null_safe_eq(TimeEntry.raw_project, raw_project),
        )

    stmt = apply_filters(scope(actor, "entries:read", stmt), joins, f)
    stmt = stmt.order_by(
        func.coalesce(func.sum(TimeEntry.duration_seconds), 0).desc(),
        TimeEntry.description,
    )

    rows = []
    for r in session.execute(stmt).mappings():
        if r["cost_ccys"] > 1:
            raise MixedCurrencyError("entries_by_description group", r["cost_ccys"])
        rows.append(
            {
                "description": r["description"],
                "row_count": r["row_count"],
                "seconds": int(r["secs"]),
                "cost": _money(_dec(r["cost"]), r["cost_ccy"] or zero_currency),
            }
        )
    return rows


def _null_safe_eq(column, value: str | None):
    return column.is_(None) if value is None else column == value
