"""content-c6153-000 — classify every FY2026 de-obligation on an AI-keyword contract against the award's OWN
transaction history, so "money taken back" is not read as "programme cancelled".

For each negative transaction: pull /api/v2/transactions/ (POST, award_id = the row's generated_internal_id) and ask
  OFFSET       — a positive line of the same magnitude (within 1%) on the same award within `--days` either side
  TERMINATION  — the action type is E (terminate for default) or F (terminate for convenience), per the API's own
                 action_type_description, and nothing re-obligates after it
  CLOSEOUT     — action type K (close out)
  STANDING     — none of the above: the money went back and did not come back in this window

Every code's meaning is read from `action_type_description` in the government's own record, never from memory.
Read-only. Run from the repository root:
  python deob_classify.py --census <census json> --out <json>
"""
import argparse
import collections
import json
import time
import urllib.error
import urllib.request

API = "https://api.usaspending.gov"
UA = {"Content-Type": "application/json", "User-Agent": "vibeagentmaking.com research"}


def _post(path, body, tries=3):
    for i in range(tries):
        try:
            req = urllib.request.Request(API + path, data=json.dumps(body).encode("utf-8"), headers=UA)
            with urllib.request.urlopen(req, timeout=90) as r:
                return json.loads(r.read().decode("utf-8"))
        except (urllib.error.URLError, TimeoutError, json.JSONDecodeError):
            if i == tries - 1:
                raise
            time.sleep(2 * (i + 1))


def award_transactions(gid, cap_pages=6):
    out, page = [], 1
    while page <= cap_pages:
        doc = _post("/api/v2/transactions/", {"award_id": gid, "limit": 100, "page": page})
        rows = doc.get("results") or []
        out += rows
        if not (doc.get("page_metadata") or {}).get("hasNext") or not rows:
            break
        page += 1
        time.sleep(0.25)
    return out


def _days(a, b):
    from datetime import date
    ya, ma, da = (int(x) for x in str(a)[:10].split("-"))
    yb, mb, db = (int(x) for x in str(b)[:10].split("-"))
    return abs((date(ya, ma, da) - date(yb, mb, db)).days)


def classify(neg_row, hist, window_days):
    amt = float(neg_row["Transaction Amount"])
    when = str(neg_row["Action Date"])[:10]
    code = str(neg_row.get("Action Type") or "")
    offsets = [t for t in hist
               if float(t.get("federal_action_obligation") or 0) > 0
               and abs(float(t["federal_action_obligation"]) + amt) <= abs(amt) * 0.01
               and _days(t.get("action_date"), when) <= window_days]
    later = [t for t in hist
             if str(t.get("action_date") or "")[:10] > when and float(t.get("federal_action_obligation") or 0) > 0]
    words = {str(t.get("action_type") or ""): t.get("action_type_description") for t in hist if t.get("action_type")}
    if offsets:
        o = sorted(offsets, key=lambda t: t["action_date"])[0]
        return "OFFSET", "same award re-obligated %.2f on %s (%s)" % (float(o["federal_action_obligation"]),
                                                                     str(o["action_date"])[:10],
                                                                     o.get("action_type_description")), words
    if code in ("E", "F"):
        return "TERMINATION", "%s; %d later positive line(s)" % (words.get(code, "(no description)"), len(later)), words
    if code == "K":
        return "CLOSEOUT", words.get("K", "(no description)"), words
    return "STANDING", "no offsetting line within %d days; %d later positive line(s)" % (window_days, len(later)), words


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--census", required=True)
    ap.add_argument("--out", default="")
    ap.add_argument("--days", type=int, default=30)
    ap.add_argument("--top", type=int, default=40)
    a = ap.parse_args()
    doc = json.load(open(a.census, encoding="utf-8"))
    negs = sorted(doc["negatives"], key=lambda r: float(r["Transaction Amount"]))[:a.top]
    print("classifying %d de-obligations (of %d) · offset window %d days" % (len(negs), len(doc["negatives"]), a.days))
    out, kinds, money = [], collections.Counter(), collections.defaultdict(float)
    all_words = {}
    for i, r in enumerate(negs, 1):
        gid = r.get("generated_internal_id")
        try:
            hist = award_transactions(gid) if gid else []
        except Exception as e:                                        # noqa: BLE001
            print("  %2d. lookup failed %s: %s" % (i, str(gid)[:40], type(e).__name__))
            hist = []
        kind, why, words = classify(r, hist, a.days) if hist else ("UNREAD", "no transaction history returned", {})
        all_words.update({k: v for k, v in words.items() if v})
        amt = float(r["Transaction Amount"])
        kinds[kind] += 1
        money[kind] += amt
        out.append(dict(r, verdict=kind, why=why, n_history=len(hist)))
        print("  %2d. %14.2f %-10s %-30s %-24s %s" % (i, amt, kind, str(r.get("Recipient Name"))[:30],
                                                      str(r.get("Awarding Agency"))[:24], why[:70]))
        time.sleep(0.25)
    print("\nverdicts: " + " · ".join("%s %d ($%.0f)" % (k, n, money[k]) for k, n in kinds.most_common()))
    print("action type descriptions seen (the API's own words):")
    for c, d in sorted(all_words.items()):
        print("  %-3s %s" % (c, d))
    if a.out:
        json.dump({"pulled_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "offset_window_days": a.days,
                   "verdicts": {k: {"n": n, "amount": money[k]} for k, n in kinds.items()},
                   "action_type_descriptions": all_words, "rows": out},
                  open(a.out, "w", encoding="utf-8"), indent=1)
        print("-> %s" % a.out)


if __name__ == "__main__":
    main()
