#!/usr/bin/env python3
"""MTD completed encounters report via AMD REST API + explicit appt-type ID mapping.

Filters:
- COMPLETED = chargesposted=true on the scheduler appointment record (charges actually billed).
  This naturally excludes no-show CNS99 charge code, plus status 4/5/6 (cancelled/no-show/
  rescheduled) which the REST list endpoint omits anyway.
- EXCLUDE DANA = appt types 7020 (DANA-INTEPRETATION) and 7022 (DANA-ADMINISTRATION).
  Note: "DANA" in the original brief turned out to be an appointment type (DANA Brain Vital
  cognitive screening), not a provider name.
- INCLUDE only med mgmt, therapy, TMS appt types (explicit allowlists below).
"""
import json, re, sys
from datetime import datetime
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import HTTPError

CREDS = json.loads(Path("/Users/exult/claude-workspace/config/credentials/advancedmd.json").read_text())
api_user = CREDS["api_users"][0]
USER = api_user["user"]; PSW = api_user["password"]
OFFICE = CREDS["office_key"]; APP = api_user.get("appname", "ABS-AVMD")
PARTNER = "https://partnerlogin.advancedmd.com/practicemanager/xmlrpc/processrequest.aspx"
REST_BASE = "https://pm-api-137.advancedmd.com/api"

# Explicit appt-type ID classification (verified via XMLRPC getappttypes 2026-06-16).
MED_MGMT_IDS = {
    6610: "IP-MED MGMT NEW",
    6611: "IP-MED MGMT F/U",
    6612: "IP-MED MGMT HOS DISCHARGE",
    6613: "TH-MED MGMT NEW",
    6614: "TH-MED MGMT F/U",
    6615: "TH-MED MGMT HOS DISCHARGE",
    6642: "ZMEDMGMT IP ONBOARD",
    6644: "ZMEDMGMT TH ONBOARD",
}
THERAPY_IDS = {
    6605: "IP-THERAPY NEW PT",
    6606: "TH-THERAPY NEW PT",
    6607: "IP-THERAPY FAMILY/COUPLE",
    6608: "IP-THERAPY INDIVIDUAL",
    6609: "TH-THERAPY INDIVIDUAL",
    6616: "IP-THERAPY CRISIS",
    6617: "TH-THERAPY CRISIS",
    6626: "TH-THERAPY FAMILY/COUPLE",
    6647: "TH-SOP",
    6660: "IP-SOP",
    6649: "ZTHERAPY IP ONBOARDING",
    6650: "ZTHERAPY TH ONBOARD",
}
TMS_IDS = {
    6632: "TH-TMS INIT EVAL",
    6645: "IP-TMS INIT EVAL",
    6654: "IP-TMS REMAPPING",
    7014: "IP-TMS F/U",
    7015: "IP-TMS MAPPING",
    6619: "HOLD TMS EVALUATION",  # likely admin hold; include if charged
    6620: "HOLD TMS MAPPING",
}
DANA_IDS = {
    7020: "DANA-INTEPRETATION",
    7022: "DANA-ADMINISTRATION",
}

def now(): return datetime.now().strftime("%m/%d/%Y %I:%M:%S %p")
def esc(s): return s.replace("&","&amp;").replace("<","&lt;").replace(">","&gt;").replace('"',"&quot;").replace("'","&apos;")

def post(url, body):
    req = Request(url, data=body.encode("utf-8"), method="POST", headers={"Content-Type":"text/xml"})
    try:
        with urlopen(req, timeout=60) as r: return r.read().decode("utf-8")
    except HTTPError as e: return e.read().decode("utf-8")

def login_xmlrpc():
    body = (f'<ppmdmsg action="login" class="login" msgtime="{esc(now())}" '
            f'username="{esc(USER)}" psw="{esc(PSW)}" '
            f'officecode="{esc(OFFICE)}" appname="{esc(APP)}"/>')
    t1 = post(PARTNER, body)
    ws = re.search(r'webserver="([^"]+)"', t1).group(1)
    t2 = post(f"{ws}/xmlrpc/processrequest.aspx", body)
    return re.search(r'<usercontext[^>]*?>([^<]+)</usercontext>', t2, re.IGNORECASE).group(1).strip()

def rest_get(token, path):
    req = Request(f"{REST_BASE}{path}", method="GET", headers={
        "Authorization": f"Bearer {token}",
        "Accept": "application/vnd.advancedmd.api.v2+json,application/json",
        "Referer": "https://static-100.advancedmd.com/",
    })
    try:
        with urlopen(req, timeout=60) as r: return json.loads(r.read().decode("utf-8")), r.status
    except HTTPError as e: return e.read().decode("utf-8"), e.code

def get_appttypeid(a):
    ids = a.get("appointmenttypeids")
    if isinstance(ids, list) and ids:
        v = ids[0]
        if isinstance(v, dict): return v.get("id")
        try: return int(v)
        except: return None
    at = a.get("appointmenttype")
    if isinstance(at, dict): return at.get("id")
    return None

def classify(tid):
    if tid in MED_MGMT_IDS: return "med_mgmt"
    if tid in THERAPY_IDS:  return "therapy"
    if tid in TMS_IDS:      return "tms"
    if tid in DANA_IDS:     return "excluded_dana"
    return "other"

def main():
    start = sys.argv[1] if len(sys.argv) > 1 else "2026-06-01"
    end   = sys.argv[2] if len(sys.argv) > 2 else "2026-06-16"
    print(f"Logging in...", file=sys.stderr)
    token = login_xmlrpc(); print("Token OK", file=sys.stderr)
    cols, st = rest_get(token, "/scheduler/columns"); assert st == 200
    print(f"Found {len(cols)} active columns", file=sys.stderr)

    all_appts = []
    for col in cols:
        cid = col["id"]; head = col["heading"]
        path = f"/scheduler/appointments?columnId={cid}&startDate={start}&endDate={end}&forView=month"
        data, st = rest_get(token, path)
        if st != 200: print(f"  col {cid} {head}: FAIL {st}", file=sys.stderr); continue
        appts = data if isinstance(data, list) else []
        for a in appts: a["_col_heading"] = head; a["_col_id"] = cid
        all_appts.extend(appts)
        print(f"  col {cid:5} {head:30} -> {len(appts):4} appts", file=sys.stderr)

    completed = [a for a in all_appts if a.get("chargesposted")]
    print(f"\nTotal appts in range: {len(all_appts)}", file=sys.stderr)
    print(f"chargesposted=true:   {len(completed)}", file=sys.stderr)

    # Classify completed by appt type
    buckets = {"med_mgmt": [], "therapy": [], "tms": [], "excluded_dana": [], "other": []}
    type_breakdown = {}
    for a in completed:
        tid = get_appttypeid(a)
        cat = classify(tid)
        tname = (MED_MGMT_IDS.get(tid) or THERAPY_IDS.get(tid) or TMS_IDS.get(tid)
                 or DANA_IDS.get(tid) or f"typeid:{tid}")
        type_breakdown.setdefault(cat, {}).setdefault(tname, 0)
        type_breakdown[cat][tname] += 1
        buckets[cat].append({
            "id": a.get("id"), "appttypeid": tid, "appttype_name": tname,
            "date": a.get("startdatetime"), "col": a["_col_heading"],
            "provider": a.get("provider"), "duration": a.get("duration"),
            "patient": f"{a.get('lastname','')},{a.get('firstname','')}",
        })

    result = {
        "date_range": [start, end],
        "definition": "Completed encounter = chargesposted=true on AMD scheduler appointment record.",
        "total_appts_in_range": len(all_appts),
        "total_completed": len(completed),
        "filter_rules": {
            "cns99_no_show_excluded": "REST list endpoint omits status 4/5/6 (cancelled/no-show/rescheduled); chargesposted=true additionally requires actual posted billable charge (CNS99 no-show code doesn't generate chargesposted).",
            "dana_excluded": "Appt type IDs 7020 (DANA-INTEPRETATION) and 7022 (DANA-ADMINISTRATION). Note: DANA is an appt type (cognitive screening), NOT a provider name.",
            "include_appt_types": {
                "med_mgmt": MED_MGMT_IDS,
                "therapy":  THERAPY_IDS,
                "tms":      TMS_IDS,
            },
        },
        "counts": {k: len(v) for k,v in buckets.items()},
        "total_in_scope": len(buckets["med_mgmt"]) + len(buckets["therapy"]) + len(buckets["tms"]),
        "type_breakdown": type_breakdown,
    }
    Path("/tmp/amd-encounters-result.json").write_text(json.dumps(result, indent=2, default=str))

    print("\n=== FINAL REPORT ===")
    print(f"Date range:        {start} -> {end} (MTD through {end})")
    print(f"Total appts pulled: {len(all_appts)}")
    print(f"Total completed:   {len(completed)} (chargesposted=true)")
    print()
    print(f"Med mgmt:          {result['counts']['med_mgmt']}")
    for tname, n in sorted(type_breakdown.get('med_mgmt',{}).items(), key=lambda x:-x[1]):
        print(f"    {n:3} {tname}")
    print(f"Therapy:           {result['counts']['therapy']}")
    for tname, n in sorted(type_breakdown.get('therapy',{}).items(), key=lambda x:-x[1]):
        print(f"    {n:3} {tname}")
    print(f"TMS:               {result['counts']['tms']}")
    for tname, n in sorted(type_breakdown.get('tms',{}).items(), key=lambda x:-x[1]):
        print(f"    {n:3} {tname}")
    print(f"DANA (excluded):   {result['counts']['excluded_dana']}")
    for tname, n in sorted(type_breakdown.get('excluded_dana',{}).items(), key=lambda x:-x[1]):
        print(f"    {n:3} {tname}")
    print(f"Other (excluded):  {result['counts']['other']}")
    for tname, n in sorted(type_breakdown.get('other',{}).items(), key=lambda x:-x[1])[:15]:
        print(f"    {n:3} {tname}")
    print()
    print(f"TOTAL IN SCOPE:    {result['total_in_scope']}")
    print(f"Full JSON -> /tmp/amd-encounters-result.json")

if __name__ == "__main__":
    main()
