"""lfcs-feedback-aggregator@v1 — turn override patterns into Rate Template + Common Misses proposals.

Per Stage 4 § 3.3 + Stage 5 milestone 1 § 4. Cron-driven (daily 4am AEST) — runs
diversity-criteria check across `Bid_Line_Item_Overrides` and surfaces proposals
to Rocky for accept/reject. Auto-update is NEVER applied without Rocky confirmation.

Diversity criteria (the rule, restated for human-readable code):
  - >= 3 overrides on the same Rate Template (or >= 2 for Common Misses Pattern auto-derive)
  - From at least 2 distinct Jobs
  - Spanning at least 30 days (max(created_at) - min(created_at) >= 30 days)
  - Reason codes spanning at least 2 of the 9 domains

Without ALL of these, the trigger does not fire. Library sharpens on signal, not noise.

Output: writes proposed Rate Template adjustments with `confidence=Needs review`
+ appends notification to `_Brain/Open-Threads.md` (vault) for Rocky's review.
Old base_rate_aud preserved until Rocky confirms (no auto-overwrite).
"""

from __future__ import annotations

import datetime as dt
import statistics
from collections import defaultdict
from dataclasses import dataclass

from . import airtable_client as at
from .config import (
    BASE_ID,
    COMMON_MISSES_TRIGGER,
    FEEDBACK_TRIGGER,
    FIELD,
    REASON_CODES,
    TBL,
)


@dataclass
class RateTemplateProposal:
    template_id: str  # methodology Rate Template record id
    template_name: str  # canonical_id (e.g., "formwork-bridge-abutment-hcb-pattern")
    current_base_rate: float
    proposed_base_rate: float  # median of override_rate_ex_gst across qualifying overrides
    override_count: int
    distinct_job_count: int
    days_span: int
    distinct_reason_domains: list[str]
    qualifying_override_ids: list[str]
    notes: str

    def to_md(self) -> str:
        return (
            f"\n### Proposal: {self.template_name}\n"
            f"- current base_rate_aud: ${self.current_base_rate:.2f}\n"
            f"- proposed base_rate_aud: ${self.proposed_base_rate:.2f}\n"
            f"- delta: ${self.proposed_base_rate - self.current_base_rate:+.2f}\n"
            f"- evidence: {self.override_count} overrides across {self.distinct_job_count} jobs, "
            f"{self.days_span} days span, reason domains {self.distinct_reason_domains}\n"
            f"- qualifying override IDs: {self.qualifying_override_ids}\n"
            f"- notes: {self.notes}\n"
        )


@dataclass
class CommonMissesPatternProposal:
    reason_codes_combo: tuple[str, ...]
    override_count: int
    distinct_job_count: int
    days_span: int
    sum_dollars: float
    median_pct: float
    qualifying_override_ids: list[str]
    description: str


def _parse_iso(s: str | None) -> dt.datetime | None:
    if not s:
        return None
    try:
        return dt.datetime.fromisoformat(s.replace("Z", "+00:00"))
    except ValueError:
        return None


def _rec_field(rec: dict, table_key: str, field_name: str):
    return rec.get("fields", {}).get(FIELD[table_key][field_name])


def _override_record_to_signal(rec: dict, jobs_lookup: dict[str, str]) -> dict:
    """Flatten an Override record to the fields the aggregator needs.

    `jobs_lookup` maps Bid Line Item record IDs → Job record IDs (for distinct-job count).
    """
    f = FIELD["Bid_Line_Item_Overrides"]
    bid_line_items = rec["fields"].get(f["bid_line_item"], [])
    bli_id = bid_line_items[0] if bid_line_items else None
    rate_template_links = rec["fields"].get(f["linked_rate_template"], [])
    rate_template_id = rate_template_links[0] if rate_template_links else None
    job_id = jobs_lookup.get(bli_id) if bli_id else None
    return {
        "id": rec["id"],
        "rate_template_id": rate_template_id,
        "bli_id": bli_id,
        "job_id": job_id,
        "override_rate": rec["fields"].get(f["override_rate_ex_gst"]),
        "original_rate": rec["fields"].get(f["original_rate_ex_gst"]),
        "delta_dollars": rec["fields"].get(f["delta_dollars"]),
        "delta_pct": rec["fields"].get(f["delta_pct"]),
        "reason_codes": rec["fields"].get(f["reason_codes"], []),
        "created_at": _parse_iso(rec["fields"].get(f["created_at"])),
        "session_ref": rec["fields"].get(f["session_ref"]),
    }


def _build_jobs_lookup() -> dict[str, str]:
    """Map Bid Line Item record IDs → Job record ID via Bid → Job traversal.

    Walked once per aggregator run; for ~hundreds of BLIs this is a couple of
    Airtable list calls and is cheap. Result is in-memory only.
    """
    bid_to_job: dict[str, str] = {}
    for bid in at.list_records(BASE_ID, TBL["Bids"]):
        job_links = bid["fields"].get(FIELD["Bids"]["Job"], [])
        if job_links:
            bid_to_job[bid["id"]] = job_links[0]
    bli_to_job: dict[str, str] = {}
    for bli in at.list_records(BASE_ID, TBL["Bid_Line_Items"]):
        bid_links = bli["fields"].get(FIELD["Bid_Line_Items"]["Bid"], [])
        if bid_links:
            bid_id = bid_links[0]
            job_id = bid_to_job.get(bid_id)
            if job_id:
                bli_to_job[bli["id"]] = job_id
    return bli_to_job


def _evaluate_diversity(
    signals: list[dict], trigger_spec: dict
) -> tuple[bool, dict]:
    """Apply diversity criteria. Returns (passes, evidence)."""
    distinct_jobs = {s["job_id"] for s in signals if s["job_id"]}
    domains = set()
    for s in signals:
        for code in s["reason_codes"] or []:
            domains.add(code)
    dates = sorted([s["created_at"] for s in signals if s["created_at"]])
    days_span = (dates[-1] - dates[0]).days if len(dates) >= 2 else 0
    sum_dollars = sum(abs(s["delta_dollars"] or 0.0) for s in signals)
    pct_values = [abs(s["delta_pct"]) for s in signals if s["delta_pct"] is not None]
    median_pct = statistics.median(pct_values) if pct_values else 0.0

    evidence = {
        "override_count": len(signals),
        "distinct_jobs": len(distinct_jobs),
        "distinct_domains": sorted(domains),
        "days_span": days_span,
        "sum_dollars": sum_dollars,
        "median_pct": median_pct,
    }

    passes = (
        len(signals) >= trigger_spec["min_overrides"]
        and len(distinct_jobs) >= trigger_spec["min_distinct_jobs"]
        and days_span >= trigger_spec["min_days_span"]
        and len(domains) >= trigger_spec["min_distinct_reason_domains"]
    )
    if "min_total_dollars" in trigger_spec:
        passes = passes and sum_dollars >= trigger_spec["min_total_dollars"]
    if "min_median_pct" in trigger_spec:
        passes = passes and median_pct >= trigger_spec["min_median_pct"]

    return passes, evidence


def evaluate_rate_template_proposals(
    overrides: list[dict] | None = None,
) -> list[RateTemplateProposal]:
    """Group overrides by linked Rate Template; emit proposals where diversity criteria pass.

    `overrides` parameter is optional for testing (dependency-injection); production
    callers omit it and the function fetches from Airtable.
    """
    jobs_lookup = _build_jobs_lookup()
    if overrides is None:
        overrides = list(at.list_records(BASE_ID, TBL["Bid_Line_Item_Overrides"]))
    signals = [_override_record_to_signal(r, jobs_lookup) for r in overrides]

    by_template: dict[str, list[dict]] = defaultdict(list)
    for s in signals:
        if s["rate_template_id"]:
            by_template[s["rate_template_id"]].append(s)

    proposals: list[RateTemplateProposal] = []
    for template_id, group in by_template.items():
        passes, evidence = _evaluate_diversity(group, FEEDBACK_TRIGGER)
        if not passes:
            continue
        rates = [s["override_rate"] for s in group if s["override_rate"] is not None]
        if not rates:
            continue
        proposed = statistics.median(rates)
        # Fetch current base_rate_aud + canonical_id
        tmpl = at.get_record(BASE_ID, TBL["_methodology_Rate_Templates"], template_id)
        current_rate = tmpl["fields"].get("base_rate_aud", 0.0)
        canonical_id = tmpl["fields"].get("template_id", template_id)
        proposals.append(
            RateTemplateProposal(
                template_id=template_id,
                template_name=canonical_id,
                current_base_rate=float(current_rate),
                proposed_base_rate=float(proposed),
                override_count=evidence["override_count"],
                distinct_job_count=evidence["distinct_jobs"],
                days_span=evidence["days_span"],
                distinct_reason_domains=evidence["distinct_domains"],
                qualifying_override_ids=[s["id"] for s in group],
                notes=(
                    f"Diversity criteria met; sum_dollars={evidence['sum_dollars']:.2f}, "
                    f"median_pct={evidence['median_pct']:.2f}"
                ),
            )
        )
    return proposals


def evaluate_common_misses_proposals(
    overrides: list[dict] | None = None,
) -> list[CommonMissesPatternProposal]:
    """Group overrides by reason_codes combination; emit pattern proposals."""
    jobs_lookup = _build_jobs_lookup()
    if overrides is None:
        overrides = list(at.list_records(BASE_ID, TBL["Bid_Line_Item_Overrides"]))
    signals = [_override_record_to_signal(r, jobs_lookup) for r in overrides]

    by_combo: dict[tuple[str, ...], list[dict]] = defaultdict(list)
    for s in signals:
        combo = tuple(sorted(s["reason_codes"] or []))
        if combo:
            by_combo[combo].append(s)

    proposals: list[CommonMissesPatternProposal] = []
    for combo, group in by_combo.items():
        passes, evidence = _evaluate_diversity(group, COMMON_MISSES_TRIGGER)
        if not passes:
            continue
        proposals.append(
            CommonMissesPatternProposal(
                reason_codes_combo=combo,
                override_count=evidence["override_count"],
                distinct_job_count=evidence["distinct_jobs"],
                days_span=evidence["days_span"],
                sum_dollars=evidence["sum_dollars"],
                median_pct=evidence["median_pct"],
                qualifying_override_ids=[s["id"] for s in group],
                description=(
                    f"{evidence['override_count']} overrides combining reason codes "
                    f"{combo} across {evidence['distinct_jobs']} jobs over "
                    f"{evidence['days_span']} days. Median delta_pct={evidence['median_pct']:.1f}."
                ),
            )
        )
    return proposals
