#!/usr/bin/env python3
"""PageHawk Medical-Record Review — 100% ON-DEVICE (Ollama). NEVER uploads PHI.

Turns a 500-2,000-page medical packet (one PDF, or a folder of records) into three
things a PI / workers-comp lawyer pays $300-800/matter to outsource today:

  1. a CHRONOLOGICAL TREATMENT TIMELINE  (date -> provider/facility -> what happened -> page)
  2. an INJURY SUMMARY                   (body regions, diagnoses, mechanism, course, status)
  3. a PROVIDER INDEX                    (every provider/facility, role, visit count, dates, pages)

Same engine seam as pagehawk_ai (local LLM via Ollama, on-device OCR fallback). Degrades
gracefully: if Ollama is not running it returns a clear, actionable error, and the
deterministic parts (page extraction, date parsing, provider rollup) still run.

CLI:  python pagehawk_medical.py review  --doc FILE_OR_FOLDER [--report] [--max-pages N]
      python pagehawk_medical.py status
"""
import os, re, sys, json, time

# Shared on-device plumbing (text extraction + OCR fallback + Ollama). Importable because this
# module ships next to pagehawk_ai.py in the app's bundled engine dir.
try:
    import pagehawk_ai as ai
except Exception:                                   # pragma: no cover - standalone fallback
    ai = None

_NORUN = ("Local AI is not running. Open Terminal and run  ollama serve  "
          "(and once:  ollama pull qwen2.5-coder:14b ). Everything stays on your Mac.")

_EVENT_TYPES = ("office_visit", "imaging", "procedure", "surgery", "therapy",
                "medication", "diagnostic", "ER", "hospitalization", "other")


# ---------------------------------------------------------------- date normalization
_MONTHS = {m: i for i, m in enumerate(
    ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"], 1)}


def parse_date(s):
    """Best-effort normalize a written date to ISO (YYYY-MM-DD) for sorting. Returns None
    if unparseable, so unknown-date events sort last but are never dropped."""
    if not s:
        return None
    s = s.strip()
    m = re.search(r"\b(\d{4})-(\d{1,2})-(\d{1,2})\b", s)               # 2026-03-09
    if m:
        y, mo, d = map(int, m.groups())
        return _iso(y, mo, d)
    m = re.search(r"\b(\d{1,2})[/\-.](\d{1,2})[/\-.](\d{2,4})\b", s)   # 03/09/2026, 3-9-26
    if m:
        mo, d, y = int(m.group(1)), int(m.group(2)), int(m.group(3))
        if y < 100:
            y += 2000 if y < 50 else 1900
        return _iso(y, mo, d)
    m = re.search(r"\b([A-Za-z]{3,9})\.?\s+(\d{1,2}),?\s+(\d{4})\b", s)  # March 9, 2026
    if m and m.group(1)[:3].lower() in _MONTHS:
        return _iso(int(m.group(3)), _MONTHS[m.group(1)[:3].lower()], int(m.group(2)))
    m = re.search(r"\b(\d{1,2})\s+([A-Za-z]{3,9})\.?\s+(\d{4})\b", s)    # 9 March 2026
    if m and m.group(2)[:3].lower() in _MONTHS:
        return _iso(int(m.group(3)), _MONTHS[m.group(2)[:3].lower()], int(m.group(1)))
    return None


def _iso(y, mo, d):
    if not (1 <= mo <= 12 and 1 <= d <= 31 and 1900 <= y <= 2100):
        return None
    return "%04d-%02d-%02d" % (y, mo, d)


# ---------------------------------------------------------------- packet -> page text
def _pages_with_source(path, max_pages):
    """[(source_file, page_number, text), ...] across one PDF or a whole folder."""
    out = []
    if ai is None:
        return out
    if os.path.isdir(path):
        files = sorted(f for f in os.listdir(path)
                       if f.lower().endswith(ai._DOC_EXTS) and not f.startswith("."))
        for f in files:
            for i, pg in enumerate(ai.extract_pages(os.path.join(path, f))):
                if pg.strip():
                    out.append((f, i + 1, pg))
                if len(out) >= max_pages:
                    return out
    else:
        base = os.path.basename(path)
        for i, pg in enumerate(ai.extract_pages(path)):
            if pg.strip():
                out.append((base, i + 1, pg))
            if len(out) >= max_pages:
                break
    return out


def _windows(pages, per=5):
    """Group pages into ~per-page windows with explicit page markers so the model can cite
    the page a fact came from. Yields (source_file, [page_numbers], windowed_text)."""
    i = 0
    while i < len(pages):
        grp = pages[i:i + per]
        src = grp[0][0]
        nums = [p[1] for p in grp]
        text = "\n\n".join("===== %s p.%d =====\n%s" % (p[0], p[1], p[2][:3500]) for p in grp)
        yield src, nums, text
        i += per


# ---------------------------------------------------------------- LLM event extraction
def extract_events(path, max_pages=2000, timeout=300, per_window=5, progress=None):
    """Pull every dated treatment event from the packet, on-device. Returns
    [{date, date_iso, provider, facility, event_type, description, source, page}, ...]."""
    if ai is None or not ai.pick_model():
        return None
    model = ai.pick_model()
    pages = _pages_with_source(path, max_pages)
    if not pages:
        return []
    events, total = [], 0
    windows = list(_windows(pages, per_window))
    for wi, (src, nums, text) in enumerate(windows):
        if progress:
            progress(wi + 1, len(windows))
        prompt = (
            "You are a medical-records analyst working OFFLINE on the user's Mac, preparing a "
            "personal-injury treatment chronology. From the records below, extract EVERY dated "
            "clinical event (visits, imaging, procedures, surgeries, therapy/PT, ER, medications "
            "started, diagnoses made). Use ONLY what is in the text. Return STRICT JSON, shape:\n"
            '{"events":[{"date":"as written","provider":"clinician name or empty",'
            '"facility":"clinic/hospital or empty","event_type":"one of %s",'
            '"description":"one concise clinical line","page":<the page number from the nearest '
            "===== marker>}]}\nNo prose outside the JSON. If there are no dated events, "
            'return {"events":[]}.\n\nRECORDS:\n%s' % ("/".join(_EVENT_TYPES), text))
        try:
            raw = ai._generate(prompt, model, num_predict=1200, temperature=0.0,
                               fmt="json", timeout=timeout)
            data = json.loads(raw or "{}")
        except Exception:
            continue
        for e in data.get("events", []) or []:
            if not isinstance(e, dict):
                continue
            pg = e.get("page")
            try:
                pg = int(pg)
            except Exception:
                pg = nums[0] if nums else None
            if pg not in nums:                       # keep citations honest to this window
                pg = nums[0] if nums else pg
            events.append({
                "date": (e.get("date") or "").strip(),
                "date_iso": parse_date(e.get("date") or ""),
                "provider": (e.get("provider") or "").strip(),
                "facility": (e.get("facility") or "").strip(),
                "event_type": (e.get("event_type") or "other").strip(),
                "description": (e.get("description") or "").strip(),
                "source": src,
                "page": pg,
            })
            total += 1
    # chronological: known dates first (ascending), unknown-date events appended in encounter order
    dated = sorted([e for e in events if e["date_iso"]], key=lambda x: x["date_iso"])
    undated = [e for e in events if not e["date_iso"]]
    return dated + undated


# ---------------------------------------------------------------- provider index (deterministic)
def provider_index(events):
    """Roll the events up into a deduped provider/facility index — no LLM, pure aggregation."""
    idx = {}
    for e in events:
        key = (e["provider"] or e["facility"] or "Unknown").strip()
        if not key:
            key = "Unknown"
        r = idx.setdefault(key, {"provider": e["provider"], "facilities": set(),
                                 "event_types": set(), "pages": set(), "dates": set(), "visits": 0})
        if e["facility"]:
            r["facilities"].add(e["facility"])
        if e["event_type"]:
            r["event_types"].add(e["event_type"])
        if e["page"]:
            r["pages"].add(e["page"])
        if e["date_iso"]:
            r["dates"].add(e["date_iso"])
        r["visits"] += 1
    out = []
    for name, r in idx.items():
        dates = sorted(r["dates"])
        out.append({"name": name, "facilities": sorted(r["facilities"]),
                    "event_types": sorted(r["event_types"]), "visits": r["visits"],
                    "first_seen": dates[0] if dates else "", "last_seen": dates[-1] if dates else "",
                    "pages": sorted(r["pages"])})
    out.sort(key=lambda x: (-x["visits"], x["name"]))
    return out


# ---------------------------------------------------------------- injury summary (LLM synthesis)
def injury_summary(events, timeout=300):
    """Synthesize an injury summary across the whole timeline, on-device."""
    if ai is None or not ai.pick_model():
        return ""
    model = ai.pick_model()
    lines = []
    for e in events[:140]:                            # cap context; timeline is already chronological
        lines.append("%s | %s | %s | %s (p.%s)" % (
            e["date"] or "undated", e["event_type"],
            e["provider"] or e["facility"] or "", e["description"], e["page"]))
    prompt = (
        "You are a personal-injury case analyst working OFFLINE on the user's Mac. From this "
        "chronological list of treatment events, write a tight INJURY SUMMARY for the attorney. "
        "Cover, as plain-text bullets each starting with '- ': injured body regions; the working "
        "diagnoses; the mechanism of injury if stated; the arc of treatment (ER -> specialists -> "
        "imaging -> therapy -> surgery if any); and the current/last-known status. Be precise, "
        "cite page numbers like (p. 12) where you can, and never invent facts not in the list.\n\n"
        "TREATMENT EVENTS:\n%s\n\nINJURY SUMMARY:" % "\n".join(lines))
    try:
        return ai._generate(prompt, model, num_predict=750, temperature=0.1, timeout=timeout).strip()
    except Exception as e:
        return "Local AI error: %s" % e


# ---------------------------------------------------------------- orchestrate
def review(path, max_pages=2000, timeout=300, progress=None):
    t0 = time.time()
    if ai is None or not ai.pick_model():
        return {"error": _NORUN, "events": [], "providers": [], "summary": ""}
    events = extract_events(path, max_pages=max_pages, timeout=timeout, progress=progress)
    if events is None:
        return {"error": _NORUN, "events": [], "providers": [], "summary": ""}
    providers = provider_index(events)
    summary = injury_summary(events, timeout=timeout)
    dates = [e["date_iso"] for e in events if e["date_iso"]]
    return {"model": ai.pick_model(), "events": events, "providers": providers,
            "summary": summary, "event_count": len(events), "provider_count": len(providers),
            "date_range": [dates[0], dates[-1]] if dates else [],
            "ms": int((time.time() - t0) * 1000)}


# ---------------------------------------------------------------- markdown report
def to_markdown(path, r):
    if r.get("error"):
        return "# Medical-Record Review\n\n%s\n" % r["error"]
    name = os.path.basename(path.rstrip("/")) or path
    dr = r.get("date_range") or []
    out = ["# Medical-Record Review — %s" % name, "",
           "_Prepared 100% on-device. Nothing left this Mac._", "",
           "- Events extracted: **%d**  |  Providers: **%d**%s" % (
               r["event_count"], r["provider_count"],
               ("  |  Span: **%s to %s**" % (dr[0], dr[1])) if len(dr) == 2 else ""), ""]
    out += ["## Injury summary", "", r.get("summary") or "_(none)_", "",
            "## Treatment timeline", "", "| Date | Type | Provider / Facility | Summary | Page |",
            "|---|---|---|---|---|"]
    for e in r["events"]:
        who = e["provider"] or e["facility"] or ""
        if e["provider"] and e["facility"]:
            who = "%s, %s" % (e["provider"], e["facility"])
        desc = (e["description"] or "").replace("|", "/")
        out.append("| %s | %s | %s | %s | %s |" % (
            e["date"] or "undated", e["event_type"], who.replace("|", "/"), desc, e["page"] or ""))
    out += ["", "## Provider index", "",
            "| Provider / Facility | Role (types) | Visits | First | Last | Pages |",
            "|---|---|---|---|---|---|"]
    for p in r["providers"]:
        fac = ", ".join(p["facilities"])
        nm = p["name"] + (("  (%s)" % fac) if fac and fac != p["name"] else "")
        out.append("| %s | %s | %d | %s | %s | %s |" % (
            nm.replace("|", "/"), ", ".join(p["event_types"]), p["visits"],
            p["first_seen"], p["last_seen"], ", ".join(str(x) for x in p["pages"][:12])))
    return "\n".join(out) + "\n"


# ---------------------------------------------------------------- CLI
if __name__ == "__main__":
    import argparse
    ap = argparse.ArgumentParser(description="PageHawk on-device medical-record review")
    ap.add_argument("cmd", choices=["review", "status"])
    ap.add_argument("--doc")
    ap.add_argument("--report", action="store_true", help="emit a markdown report instead of JSON")
    ap.add_argument("--max-pages", type=int, default=2000)
    a = ap.parse_args()
    if a.cmd == "status":
        print(json.dumps(ai.status() if ai else {"running": False})); sys.exit(0)
    if not a.doc or not os.path.exists(a.doc):
        print(json.dumps({"error": "Not found: %s" % a.doc})); sys.exit(1)
    def _prog(i, n): print("  window %d/%d…" % (i, n), file=sys.stderr)
    r = review(a.doc, max_pages=a.max_pages, progress=_prog)
    print(to_markdown(a.doc, r) if a.report else json.dumps(r, indent=2))
