"""Find past forecasts similar to a draft ("analogs").

Structured similarity only — no embeddings: problem-type overlap, closeness of
likelihood / size / location for shared problems, time of season, and zone.
Danger is deliberately NOT part of the score, so analogs don't anchor the
forecaster; their danger distribution is shown separately.
"""

from __future__ import annotations

from collections import Counter
from datetime import date

from psycopg import Connection

from .db import fetchall
from .engine import BANDS

MAX_SEASON_DAYS = 60


def _jaccard(a: set, b: set) -> float:
    if not a and not b:
        return 1.0
    if not a or not b:
        return 0.0
    return len(a & b) / len(a | b)


def _season_closeness(d1: date, d2: date) -> float:
    diff = abs(d1.timetuple().tm_yday - d2.timetuple().tm_yday)
    diff = min(diff, 365 - diff)
    return max(0.0, 1 - diff / MAX_SEASON_DAYS)


def score(query: list[dict], cand: list[dict], qdate: date, cdate: date, zone_match: bool) -> float:
    qtypes = {p["type"] for p in query}
    ctypes = {p["type"] for p in cand}
    s = 3.0 * _jaccard(qtypes, ctypes)
    shared = qtypes & ctypes
    if shared:
        sub = 0.0
        for t in shared:
            q = next(p for p in query if p["type"] == t)
            c = next(p for p in cand if p["type"] == t)
            sub += _jaccard(set(q.get("locations") or []), set(c.get("locations") or []))
            if q.get("likelihood") and c.get("likelihood"):
                sub -= 0.4 * abs(int(q["likelihood"]) - int(c["likelihood"]))
            if q.get("size_max") is not None and c.get("size_max") is not None:
                sub -= 0.4 * abs(float(q["size_max"]) - float(c["size_max"]))
        s += sub / len(shared)
    s += 1.5 * _season_closeness(qdate, cdate)
    if zone_match:
        s += 0.5
    return round(s, 4)


def find_analogs(conn: Connection, center_id: int, query: list[dict], valid_date: date, zone_id: int | None = None,
                 k: int = 8, exclude_ids: set[int] | None = None) -> dict:
    """query: [{type, locations, likelihood, size_max}] for the draft."""
    rows = fetchall(
        conn,
        """SELECT f.id, f.valid_date, f.season, f.guidance_era, f.source, f.zone_id, f.title,
                  f.danger_upper, f.danger_middle, f.danger_lower, left(f.bottom_line, 600) AS bottom_line,
                  coalesce(json_agg(json_build_object('type', p.problem_type, 'rank', p.rank, 'locations', p.locations,
                      'likelihood', p.likelihood, 'size_min', p.size_min, 'size_max', p.size_max) ORDER BY p.rank)
                      FILTER (WHERE p.id IS NOT NULL), '[]') AS problems
           FROM forecasts f LEFT JOIN forecast_problems p ON p.forecast_id = f.id
           WHERE f.center_id = %s GROUP BY f.id""",
        (center_id,),
    )
    exclude_ids = exclude_ids or set()
    scored = []
    for r in rows:
        if r["id"] in exclude_ids:
            continue
        r["score"] = score(query, r["problems"], valid_date, r["valid_date"], zone_id is not None and r["zone_id"] == zone_id)
        scored.append(r)
    scored.sort(key=lambda r: (-r["score"], -r["valid_date"].toordinal()))
    top = scored[:k]
    distribution = {b: dict(Counter(r[f"danger_{b}"] for r in top if r[f"danger_{b}"] is not None)) for b in BANDS}
    return {"items": top, "danger_distribution": distribution, "candidates": len(rows)}
