#!/usr/bin/env python3
"""Seed the notification gate's PENDING list from what this machine has actually emitted.

Reads the notif-recorder history, attributes each distinct summary to its emitting
program, and registers every one as PENDING. Approves nothing. Re-runnable.
"""
import json
import os
import re
import shlex
import subprocess
import sys

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from notif_gate import Registry, make_id, normalize, record_pending  # noqa: E402

HISTORY = os.path.join(
    os.environ.get("XDG_STATE_HOME") or os.path.expanduser("~/.local/state"),
    "notif-log.jsonl",
)

# summary prefix -> (program, channel). channel != notify-send means the emitter talks
# to the session bus directly and the gate cannot swallow it.
BY_SUMMARY = [
    ("pids-guard:", "~/Projects/overdeck/modules/monitor/bin/pids-guard", "notify-send"),
    ("stall-guard:", "~/Projects/overdeck/modules/monitor/bin/stall-guard", "notify-send"),
    ("cdx ", "~/Projects/overdeck/modules/monitor/bin/stall-guard", "notify-send"),
    ("Memory ", "~/Projects/overdeck/modules/monitor/bin/mem-pressure-notify", "notify-send"),
    ("Killed ", "~/Projects/overdeck/modules/monitor/bin/mem-pressure-notify", "notify-send"),
    ("Disk ", "~/Projects/overdeck/modules/monitor/bin/disk-fill-notify", "notify-send"),
    ("System Monitor", "~/Projects/overdeck/modules/monitor/slices/bin/disk-check", "notify-send"),
    ("Systray AI", "~/Projects/overdeck/modules/systray/systray_codex_switcher.py", "notify-send"),
    ("Build pool", "~/.claude/bin/buildbox-watch.sh", "notify-send"),
    ("Buildbox", "~/.claude/bin/buildbox-watch.sh", "notify-send"),
    ("system-monitor guard DOWN", "unit:agent-guard-failure-notify.service", "log-only"),
]
BY_APP = {
    "agent-guard": ("~/Projects/overdeck/modules/monitor/agent-guard/src/agent_guard/notifier.py", "gdbus"),
    "agent-reaper": ("~/.claude/bin/reaper-notifier.py", "libnotify"),
}


def resolve(program):
    if program.startswith("unit:"):
        return program
    return os.path.realpath(os.path.expanduser(program))


def attribute(rec):
    summary = rec.get("summary") or ""
    app = rec.get("app") or ""
    for prefix, program, channel in BY_SUMMARY:
        if summary.startswith(prefix):
            return resolve(program), channel
    if app in BY_APP:
        program, channel = BY_APP[app]
        return resolve(program), channel
    return f"unattributed:{app or 'unknown'}", "dbus-direct"


def notify_units():
    """Units whose ExecStart IS notify-send: sources the history cannot show until they fire."""
    out = subprocess.run(
        ["systemctl", "--user", "show", "*.service", "-p", "Id", "-p", "ExecStart"],
        capture_output=True, text=True,
    ).stdout
    candidates = []
    unit = ""
    for line in out.splitlines():
        if line.startswith("Id="):
            unit = line[3:]
        elif line.startswith("ExecStart=") and "notify-send" in line and unit:
            candidates.append(unit)
    found = {}
    for unit in candidates:
        # `show` space-joins argv and loses the quoting, so the unit file is the only
        # place the summary survives as one argument.
        text = subprocess.run(["systemctl", "--user", "cat", unit],
                              capture_output=True, text=True).stdout
        for line in text.splitlines():
            m = re.match(r"\s*ExecStart\s*=\s*[-+!@]*(.*notify-send.*)$", line)
            if not m:
                continue
            try:
                words = shlex.split(m.group(1))
            except ValueError:
                continue
            summary = next(_summary_args(words), "")
            if summary:
                found[f"unit:{unit}"] = summary
    return found


def _summary_args(words):
    """notify-send's first positional after the flags is the summary."""
    skip_value = {"-u", "--urgency", "-i", "--icon", "-t", "--expire-time",
                  "-a", "--app-name", "-c", "--category", "-h", "--hint"}
    i = 1
    while i < len(words):
        w = words[i]
        if w in skip_value:
            i += 2
            continue
        if w.startswith("-"):
            i += 1
            continue
        yield w
        return
    return


def main():
    if not os.path.isfile(HISTORY):
        print(f"no history at {HISTORY}", file=sys.stderr)
        return 1
    seen = {}
    for line in open(HISTORY):
        try:
            rec = json.loads(line)
        except ValueError:
            continue
        summary = rec.get("summary") or ""
        if not summary:
            continue
        program, channel = attribute(rec)
        key = (program, normalize(summary))
        if key not in seen:
            seen[key] = (summary, rec.get("body") or "", channel)

    for program, summary in notify_units().items():
        seen.setdefault((program, normalize(summary)), (summary, "", "notify-send"))

    reset = "--reset" in sys.argv
    with Registry() as reg:
        pend = reg.pending()
        if reset:
            dropped = [k for k, v in pend["entries"].items() if v.get("seeded")]
            for k in dropped:
                del pend["entries"][k]
            reg.store_pending(pend)
            print(f"dropped {len(dropped)} previously seeded entry(ies)")
        already = set(reg.approved()["entries"]) | set(pend["entries"])

    added = 0
    for (program, _), (summary, body, channel) in seen.items():
        nid = make_id(program, summary)
        if nid in already:
            continue
        record_pending(nid, program, summary, body, channel)
        with Registry() as reg:
            pend = reg.pending()
            pend["entries"][nid]["seeded"] = True
            reg.store_pending(pend)
        already.add(nid)
        added += 1
    print(f"seeded {added} pending source(s) from {len(seen)} distinct historical notification(s)")
    return 0


if __name__ == "__main__":
    sys.exit(main())
