"""The running list: merge the items file with rian's recorded state.

Sources of truth: import/items.json (items; written only by main/scripts/items.py) and
app/models/owner.py (rian's state). Pure functions here so the merge is tested without a
database; the router and the host script both call them.

Three kinds, one list: `decide` (only rian can answer), `do` (rian does, checks or reviews),
`issue` (something to address, owned by a stream or by nobody yet). An item leaves the page
when it is closed: rian marks it done or dismissed on the page, or a session resolves it in
the file. Closed items stay visible for a short window so a decision can be read back, then
drop off; the exported markdown mirrors keep the full record. `archived` is rian's fourth
state (T9): an item, open or closed, he has put away for good; it never shows by default and
stays reachable behind the page's filter (`show=archived`, `show=all`), never on a separate
page. Reopening an archived item brings it back open.
"""

import json
from datetime import UTC, date, datetime
from pathlib import Path

KINDS = ("decide", "do", "issue")
#: rian's states on an item (owner_item_states.status); `resolved` is a session's, in the file.
STATES = ("open", "done", "dismissed", "archived")
#: What a read of the list may ask for: the default, the archived set, or everything.
SHOWS = ("recent", "archived", "all")
PRIORITIES = ("P1", "P2", "P3")
WEIGHTS = ("blocking", "costly", "info")
URGENCIES = ("now", "overtaken", "soon", "later")
CLOSED_WINDOW_DAYS = 10
# A gated task due within this many days makes its item "now" even before a session starts it.
NOW_WINDOW_DAYS = 3

_CANDIDATES = (Path("/srv/import/items.json"), Path(__file__).resolve().parents[3] / "import" / "items.json")


def items_path() -> Path | None:
    for path in _CANDIDATES:
        if path.is_file():
            return path
    return None


def load_items() -> list[dict]:
    path = items_path()
    if path is None:
        return []
    with path.open(encoding="utf-8") as fh:
        data = json.load(fh)
    return list(data.get("items", []))


def _prio_rank(p: str | None) -> int:
    return PRIORITIES.index(p) if p in PRIORITIES else len(PRIORITIES)


def urgency(item: dict, plan_tasks: dict[str, dict], today: date) -> tuple[str, list[dict], str]:
    """How soon an open item needs rian, read from the plan rather than from a date.

    An item names the plan tasks it gates (`blocks`). If one of them is in progress, or due
    within NOW_WINDOW_DAYS, the item is `now`. If every gated task is already done, the work
    went ahead under the assumption: `overtaken`, which costs more the longer it waits. Any
    other gated item is `soon`, ordered by the earliest gated due date. An item that gates
    nothing is `soon` when weighted costly and `later` when informational. A `do` item whose
    own date has arrived is `now` regardless. Returns (urgency, gates, reason).
    """
    gates = []
    for tid in item.get("blocks") or []:
        t = plan_tasks.get(tid)
        gates.append({"id": tid, "title": (t or {}).get("title", "").split(" [")[0], "status": (t or {}).get("status", "unknown"),
                      "due": (t or {}).get("due")})
    live = [g for g in gates if g["status"] not in ("done", "deferred", "unknown")]
    doing = [g for g in live if g["status"] == "doing"]
    near = []
    for g in live:
        try:
            if g["due"] and (date.fromisoformat(g["due"]) - today).days <= NOW_WINDOW_DAYS:
                near.append(g)
        except ValueError:
            pass
    own_due = item.get("due")
    own_near = False
    try:
        own_near = bool(own_due) and (date.fromisoformat(own_due) - today).days <= 1
    except ValueError:
        pass
    if doing:
        g = doing[0]
        return "now", gates, f"{g['id']} is in progress now: {g['title']}"
    if near:
        g = min(near, key=lambda g: g["due"])
        return "now", gates, f"blocks {g['id']} ({g['title']}), due {g['due']}"
    if own_near and item.get("kind") == "do":
        return "now", gates, f"due {own_due}"
    if gates and not live and any(g["status"] == "done" for g in gates):
        return "overtaken", gates, "the work it gated has gone ahead under the assumption; settling it now avoids a costlier change later"
    if live:
        dated = [g for g in live if g["due"]]
        g = min(dated, key=lambda g: g["due"]) if dated else live[0]
        return "soon", gates, f"before {g['id']} ({g['title']})" + (f", due {g['due']}" if g["due"] else "")
    if item.get("weight") == "costly":
        return "soon", gates, "no task waits on it, but it gets costlier to change the longer it stands"
    return "later", gates, "nothing waits on it; it may be better informed later"


def merge(items: list[dict], states: dict[str, dict], today: date | None = None, plan_tasks: dict[str, dict] | None = None) -> list[dict]:
    """Attach rian's state to each item and decide what the page shows.

    `states` maps item_id -> {status, decision, acted_by, acted_at}; `plan_tasks` maps a plan
    task id to its progress.json row (status, title, due). Returns every item with `status` in
    open|done|dismissed|resolved, `closed_on`, `visible`, and for open items `urgency`,
    `gates` and `reason`.
    """
    today = today or datetime.now(UTC).date()
    plan_tasks = plan_tasks or {}
    out: list[dict] = []
    for raw in items:
        item = dict(raw)
        st = states.get(item["id"])
        resolved = item.get("resolved") or None
        if st is not None:
            item["status"] = st.get("status") or "open"
            item["decision"] = st.get("decision")
            item["acted_by"] = st.get("acted_by")
            acted_at = st.get("acted_at")
            item["closed_on"] = (acted_at.date().isoformat() if isinstance(acted_at, datetime) else acted_at) if item["status"] != "open" else None
        elif resolved:
            item["status"] = "resolved"
            item["decision"] = resolved.get("note")
            item["acted_by"] = resolved.get("by")
            item["closed_on"] = resolved.get("on")
        else:
            item["status"] = "open"
            item["decision"] = None
            item["acted_by"] = None
            item["closed_on"] = None
        if item["status"] == "open":
            item["visible"] = True
        elif item["status"] == "archived":
            item["visible"] = False  # put away for good; the page's filter is the way back
        else:
            try:
                age = (today - date.fromisoformat(item["closed_on"])).days if item["closed_on"] else 0
            except ValueError:
                age = 0
            item["visible"] = age <= CLOSED_WINDOW_DAYS
        item["thread_key"] = f"item:{item['id']}"
        if item["status"] == "open":
            item["urgency"], item["gates"], item["reason"] = urgency(item, plan_tasks, today)
        else:
            item["urgency"], item["gates"], item["reason"] = None, [], None
        out.append(item)

    def gate_due(i: dict) -> str:
        dues = [g["due"] for g in i.get("gates", []) if g.get("due") and g.get("status") not in ("done", "deferred")]
        return min(dues) if dues else (i.get("due") or "9999")

    def key(i: dict):
        urg = URGENCIES.index(i["urgency"]) if i.get("urgency") in URGENCIES else len(URGENCIES)
        kind_rank = KINDS.index(i["kind"]) if i["kind"] in KINDS else len(KINDS)
        return (urg, gate_due(i), kind_rank, _prio_rank(i.get("priority")), i.get("created") or "")

    out.sort(key=key)
    return out


def shown(merged: list[dict], show: str = "recent") -> list[dict]:
    """The rows a read returns: `recent` is the page's default (open, and closed within the
    window); `archived` the put-away set; `all` everything, closed at any age included."""
    if show == "all":
        return list(merged)
    if show == "archived":
        return [i for i in merged if i["status"] == "archived"]
    return [i for i in merged if i["visible"]]


def counts(merged: list[dict]) -> dict:
    c = {k: 0 for k in KINDS}
    c.update({u: 0 for u in URGENCIES})
    closed = archived = 0
    for i in merged:
        if i["status"] == "open":
            c[i["kind"]] = c.get(i["kind"], 0) + 1
            c[i["urgency"]] = c.get(i["urgency"], 0) + 1
        elif i["status"] == "archived":
            archived += 1
        elif i["visible"]:
            closed += 1
    c["closed"] = closed
    c["archived"] = archived
    return c
