#!/usr/bin/env python3
"""
triage.py — collate every live action item across the per-area `<Area> TODO.md`
files into one ranked sheet, for a rapid human triage / feedback pass.

Reuses the canonical parser in `engine.digest` (one source of truth for how a todo
line is read — priority tier, due/check/added dates, owners), but ALSO records each
item's source file + line number so feedback can be applied surgically (check a box,
set a date, drop a line) without rewriting the file.

Usage (T = python -m alexandria_vault.engine.triage --vault X):
    T                  # human triage sheet, all areas, grouped + bucketed
    T --area Acme      # just one area (matches on substring, case-insensitive)
    T --json           # machine dump (id, area, path, line, raw, tier, due…)
    T --bucket nodate  # only OVERDUE | dated | nodate

Read-only. Never writes to the vault — it only reports. Apply feedback with the
surgical-edit conventions in AGENTS.md (`- [ ]`→`- [x]`, append a date sub-bullet…).
"""
import json, re, sys, argparse, datetime as dt
from pathlib import Path
from collections import Counter

from alexandria_vault.vault_core import fs
from alexandria_vault.engine import consume_vault_arg

# `--vault` must land in VAULT_DIR before digest freezes its locator at import.
consume_vault_arg()

from alexandria_vault.engine import digest as d  # noqa: E402

VAULT = d.VAULT_DIR
TODAY = dt.date.today().isoformat()


def collect():
    """Like digest.collect_todos(), but tags each item with its source path + line
    number (the checkbox line, 1-based) so edits can target the exact line."""
    items = []
    for path in fs.iter_paths(VAULT, "Areas/**/*TODO.md"):
        if path.name == "Completed Todos.md":
            continue
        text = path.read_text(encoding="utf-8")
        area = d.area_from(path, text)
        heading, heading_date, cur, in_open = "", None, None, False

        def flush(cur):
            if cur:
                it = d.finalize(cur)
                it["path"] = str(path.relative_to(VAULT))
                it["line"] = cur["line"]
                items.append(it)

        for i, line in enumerate(text.splitlines(), 1):
            h = d.HEADING_RE.match(line)
            if h:
                flush(cur); cur = None
                heading = d.clean(h.group(1))
                # in_open is sticky across ### subsections (mirrors digest.collect_todos):
                # only a level-2 `## ` heading enters/leaves the Open Items gaps section.
                if line[:3] != "###":
                    in_open = heading.lower().startswith("open items")
                hd = d.DATE_RE.search(h.group(1))
                heading_date = hd.group(1) if hd else None
                continue
            if in_open:
                continue
            if d.OPEN_RE.match(line):
                flush(cur)
                cur = {"area": area, "heading": heading, "heading_date": heading_date,
                       "lines": [d.OPEN_RE.match(line).group(1).strip()], "line": i}
                continue
            if d.DONE_RE.match(line):
                flush(cur); cur = None
                continue
            if cur and (line.startswith(" ") or line.startswith("\t")) and line.strip():
                cur["lines"].append(re.sub(r"^[-*+]\s+", "", line.strip()))
                continue
            if cur and line.strip():
                flush(cur); cur = None
        flush(cur)
    return items


def rank(items):
    def key(it):
        due = d.parse_date(it.get("due")); chk = d.parse_date(it.get("check"))
        eff = min([x for x in (due, chk) if x], default=dt.date(9999, 1, 1))
        return (it["tier"], eff, it.get("rank") or 999)
    items.sort(key=key)
    for n, it in enumerate(items, 1):
        it["id"] = n
        it["mine"] = d.is_mine(it)
    return items


def bucket_of(it):
    due = it.get("due"); chk = it.get("check")
    if due and due < TODAY:
        return "OVERDUE"
    if due or chk:
        return "dated"
    return "nodate"


def short(t, n=98):
    t = " ".join(t.split())
    return t if len(t) <= n else t[:n - 1] + "…"


def meta(it):
    m = []
    due, chk = it.get("due"), it.get("check")
    if due:
        m.append(("OVERDUE " if due < TODAY else "due ") + due)
    if chk:
        m.append("check " + chk)
    if not due and not chk:
        m.append("·no date·")
    if it.get("owners"):
        m.append("@" + ",".join(o.split("|")[0] for o in it["owners"]))
    return "  [" + " ".join(m) + "]"


def render(items, area_filter=None, bucket_filter=None):
    if area_filter:
        items = [i for i in items if area_filter.lower() in i["area"].lower()]
    order = {"OVERDUE": 0, "dated": 1, "nodate": 2}
    by_area = {}
    for it in items:
        by_area.setdefault(it["area"], []).append(it)
    for area in sorted(by_area):
        group = by_area[area]
        print(f"\n############ {area} ({len(group)}) ############")
        for b in ("OVERDUE", "dated", "nodate"):
            if bucket_filter and b != bucket_filter:
                continue
            sub = [i for i in group if bucket_of(i) == b]
            if not sub:
                continue
            label = {"OVERDUE": "OVERDUE", "dated": "DATED / UPCOMING",
                     "nodate": "NO DATE — needs triage"}[b]
            print(f"\n  === {label} ({len(sub)}) ===")
            for it in sorted(sub, key=lambda x: (x["tier"], x.get("due") or x.get("check") or "9")):
                print(f"  [{it['id']:>3}] P{it['tier']} {short(it['text'])}{meta(it)}")


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--area")
    ap.add_argument("--bucket", choices=["OVERDUE", "dated", "nodate"])
    ap.add_argument("--json", action="store_true")
    a = ap.parse_args()
    items = rank(collect())
    if a.json:
        out = items
        if a.area:
            out = [i for i in out if a.area.lower() in i["area"].lower()]
        print(json.dumps(out, ensure_ascii=False, indent=1))
        return
    counts = Counter(it["area"] for it in items)
    od = sum(1 for i in items if bucket_of(i) == "OVERDUE")
    nd = sum(1 for i in items if bucket_of(i) == "nodate")
    print(f"# {len(items)} live action items  ·  {od} overdue  ·  {nd} no-date  ·  {TODAY}")
    for ar, c in counts.most_common():
        print(f"  {c:>3}  {ar}")
    render(items, a.area, a.bucket)


if __name__ == "__main__":
    main()
