# -*- coding: utf-8 -*-
"""Generate NUMU_API_ENDPOINTS_REFERENCE.docx — full endpoint reference."""
from docx import Document
from docx.shared import Pt, RGBColor, Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.oxml.ns import qn
from docx.oxml import OxmlElement

MONO = "Consolas"
BLUE = RGBColor(0x1F, 0x4E, 0x79)
GREEN = RGBColor(0x1E, 0x7A, 0x33)
GREY = RGBColor(0x55, 0x55, 0x55)
METHOD_COLORS = {"GET": RGBColor(0x2E,0x7D,0x32), "POST": RGBColor(0xB7,0x6E,0x00),
                 "PUT": RGBColor(0x15,0x65,0xC0), "PATCH": RGBColor(0x6A,0x1B,0x9A),
                 "DELETE": RGBColor(0xC6,0x28,0x28)}

doc = Document()
style = doc.styles["Normal"]
style.font.name = "Calibri"
style.font.size = Pt(10.5)

def shade(cell, hexcolor):
    tcPr = cell._tc.get_or_add_tcPr()
    sh = OxmlElement("w:shd"); sh.set(qn("w:val"), "clear"); sh.set(qn("w:fill"), hexcolor)
    tcPr.append(sh)

def code_block(text):
    p = doc.add_paragraph()
    p.paragraph_format.left_indent = Inches(0.15)
    p.paragraph_format.space_before = Pt(2); p.paragraph_format.space_after = Pt(6)
    for i, line in enumerate(text.split("\n")):
        run = p.add_run(("\n" if i else "") + line)
        run.font.name = MONO; run.font.size = Pt(9)
        run.font.color.rgb = RGBColor(0x1a,0x1a,0x1a)
    # light grey shading
    pPr = p._p.get_or_add_pPr()
    sh = OxmlElement("w:shd"); sh.set(qn("w:val"),"clear"); sh.set(qn("w:fill"),"F3F4F6")
    pPr.append(sh)
    return p

def endpoint(method, url, desc, inputs=None, output=None, ex_req=None, ex_res=None, rollback=None):
    h = doc.add_paragraph(); h.paragraph_format.space_before = Pt(10); h.paragraph_format.space_after = Pt(2)
    m = h.add_run(method + "  "); m.bold = True; m.font.size = Pt(11)
    m.font.color.rgb = METHOD_COLORS.get(method, BLUE); m.font.name = MONO
    u = h.add_run(url); u.bold = True; u.font.size = Pt(11); u.font.name = MONO
    d = doc.add_paragraph(); d.paragraph_format.space_after = Pt(3)
    dr = d.add_run(desc); dr.font.size = Pt(10.5)
    if rollback:
        rr = d.add_run("   ↺ " + rollback); rr.font.size = Pt(9); rr.italic = True; rr.font.color.rgb = GREEN
    if inputs:
        lbl = doc.add_paragraph(); lr = lbl.add_run("Input"); lr.bold = True; lr.font.size = Pt(9.5); lr.font.color.rgb = GREY
        lbl.paragraph_format.space_after = Pt(1)
        t = doc.add_table(rows=1, cols=4); t.style = "Light Grid Accent 1"; t.alignment = WD_TABLE_ALIGNMENT.LEFT
        hdr = t.rows[0].cells
        for i, x in enumerate(["Field","Req","Type","Notes"]):
            hdr[i].text = ""; r = hdr[i].paragraphs[0].add_run(x); r.bold = True; r.font.size = Pt(8.5)
        for (fld, req, typ, note) in inputs:
            row = t.add_row().cells
            for i, val in enumerate([fld, req, typ, note]):
                row[i].text = ""
                rr = row[i].paragraphs[0].add_run(val); rr.font.size = Pt(8.5)
                if i in (0,2): rr.font.name = MONO
        for row in t.rows:
            for c in row.cells:
                c.width = Inches(1.2 if c is row.cells[0] else (0.5 if c is row.cells[1] else (1.1 if c is row.cells[2] else 3.0)))
    if output:
        lbl = doc.add_paragraph(); lr = lbl.add_run("Output"); lr.bold = True; lr.font.size = Pt(9.5); lr.font.color.rgb = GREY
        lbl.paragraph_format.space_after = Pt(1)
        op = doc.add_paragraph(); opr = op.add_run(output); opr.font.size = Pt(9.5)
        op.paragraph_format.space_after = Pt(3)
    if ex_req is not None:
        lbl = doc.add_paragraph(); lr = lbl.add_run("Example — request"); lr.bold = True; lr.font.size = Pt(9.5); lr.font.color.rgb = GREY
        lbl.paragraph_format.space_after = Pt(1)
        code_block(ex_req)
    if ex_res is not None:
        lbl = doc.add_paragraph(); lr = lbl.add_run("Example — response"); lr.bold = True; lr.font.size = Pt(9.5); lr.font.color.rgb = GREY
        lbl.paragraph_format.space_after = Pt(1)
        code_block(ex_res)

def h1(text):
    doc.add_page_break()
    p = doc.add_heading(text, level=1)
    for r in p.runs: r.font.color.rgb = BLUE

def h2(text):
    p = doc.add_heading(text, level=2)
    for r in p.runs: r.font.color.rgb = BLUE

# ============================ TITLE ============================
t = doc.add_paragraph(); t.alignment = WD_ALIGN_PARAGRAPH.CENTER
tr = t.add_run("NUMU ANGELS — AI / MCP API"); tr.bold = True; tr.font.size = Pt(26); tr.font.color.rgb = BLUE
s = doc.add_paragraph(); s.alignment = WD_ALIGN_PARAGRAPH.CENTER
sr = s.add_run("Complete Endpoint Reference — Input · Output · Examples"); sr.font.size = Pt(13); sr.font.color.rgb = GREY
v = doc.add_paragraph(); v.alignment = WD_ALIGN_PARAGRAPH.CENTER
vr = v.add_run("Full (read+write) connector — base path /api/v1   ·   78 MCP tools   ·   v1 (2026-06-25)"); vr.font.size = Pt(10); vr.italic = True
doc.add_paragraph()

# ============================ 1. AUTH ============================
h1("1. Authentication & Conventions")
doc.add_paragraph(
    "Every request is authenticated and scoped to one connector session. Two auth schemes are accepted "
    "on the same endpoints:")
for b in [
    "OAuth 2.1 (PKCE, S256) — bearer tokens prefixed naat_. This is the path Claude's connector uses.",
    "Sanctum personal-access token — for server-to-server / Postman testing.",
]:
    doc.add_paragraph(b, style="List Bullet")
h2("Base URL & required headers")
code_block("Base URL : https://dashboard.numuangels.net\n"
           "Authorization: Bearer <naat_… or sanctum token>\n"
           "Accept: application/json\n"
           "Content-Type: application/json   (for write requests)")
h2("Scopes (hierarchical: admin ⊇ enrich ⊇ write ⊇ read)")
for b in ["numu:read — all GET/list/search/get endpoints.",
          "numu:write — create/update endpoints (Full connector).",
          "numu:enrich — gap/enrichment + asset upload.",
          "numu:admin — rollback + privileged operations."]:
    doc.add_paragraph(b, style="List Bullet")
h2("OAuth flow (one-time, per connector install)")
for b in ["GET /oauth/authorize?response_type=code&client_id=…&redirect_uri=…&code_challenge=…&code_challenge_method=S256&scope=numu:read numu:write — user approves; returns ?code=…",
          "POST /oauth/token { grant_type:authorization_code, code, code_verifier, client_id, redirect_uri } → { access_token (naat_…), refresh_token, expires_in }",
          "POST /oauth/token { grant_type:refresh_token, refresh_token, client_id } → new access_token."]:
    doc.add_paragraph(b, style="List Bullet")
h2("Standard response envelope")
doc.add_paragraph("Success — data holds the payload, meta carries request/session context:")
code_block('{\n  "data": { … endpoint payload … },\n  "meta": {\n    "request_id": "req_…",\n    "session_id": "claude-session-…",\n    "actor": { "id": 7, "email": "admin@numu…" }\n  }\n}')
doc.add_paragraph("Lists add next_cursor + has_more to meta (cursor pagination). Error:")
code_block('{\n  "error": { "code": "validation_error", "message": "…", "details": { "field": ["…"] } }\n}')
doc.add_paragraph("Write endpoints return \"changed\": true|false so a no-op (identical value) is distinguishable. "
                  "Every write is recorded in the AI activity log and most are reversible via the rollback service (↺ marks reversible endpoints below).")

# ============================ 2. MCP TRANSPORT ============================
h1("2. MCP Transport & Health")
endpoint("POST","/api/v1/ai/mcp/read   ·   /api/v1/ai/mcp/full",
         "MCP (JSON-RPC 2.0) transport endpoints. The Read tier exposes read-only tools; the Full tier exposes all 78 tools (read + write). Claude calls tools/list and tools/call here; the individual REST endpoints below are what each tool maps onto.",
         inputs=[("jsonrpc","yes","string","\"2.0\""),("method","yes","string","initialize | tools/list | tools/call"),
                 ("params","no","object","for tools/call: { name, arguments }")],
         output="JSON-RPC result — tool list, or a tool's structured result.",
         ex_req='{ "jsonrpc":"2.0", "id":1, "method":"tools/call",\n  "params": { "name":"investor_get", "arguments": { "id": 42 } } }',
         ex_res='{ "jsonrpc":"2.0", "id":1, "result": { "content":[{ "type":"text", "text":"{…investor…}" }] } }')
endpoint("GET","/api/v1/ai/_ping/read   ·   /api/v1/ai/_ping/full",
         "Health/connectivity check for each tier — confirms the token + scope resolve.",
         output="{ data: { ok: true, tier: \"full\" } }",
         ex_res='{ "data": { "ok": true, "tier": "full" }, "meta": { … } }')

# ============================ 3. INVESTORS ============================
h1("3. Investors")
endpoint("GET","/api/v1/investors   ·   /api/v1/ai/investors",
         "List investors (cursor-paginated). Optional free-text q matches name/email.",
         inputs=[("q","no","string","search term (≤120)"),("limit","no","int","1–200, default 50"),("cursor","no","string","pagination cursor")],
         output="data: [ investor summaries ]; meta.next_cursor, meta.has_more.",
         ex_req="GET /api/v1/investors?q=ali&limit=25",
         ex_res='{ "data": [ { "id":42, "display_name":"Ali Q.", "email":"ali@x.com", "is_favorite":false } ],\n  "meta": { "next_cursor":"eyJpZ…", "has_more":true } }')
endpoint("GET","/api/v1/ai/investors/search",
         "Lightweight typeahead search (id + name only). q is required.",
         inputs=[("q","yes","string","≥2 chars"),("limit","no","int","1–50, default 20")],
         output="data: [ { id, display_name, email } ]",
         ex_req="GET /api/v1/ai/investors/search?q=ali&limit=10",
         ex_res='{ "data": [ { "id":42, "display_name":"Ali Qassem" } ], "meta": { … } }')
endpoint("GET","/api/v1/ai/investors/{investor}",
         "Full investor profile — all sections, label values resolved to names, preferences, tags, notes count.",
         output="data: { id, display_name, contact, investment profile, sectors[], stages[], payment_methods[], tags[], … }",
         ex_req="GET /api/v1/ai/investors/42",
         ex_res='{ "data": { "id":42, "display_name":"Ali Qassem", "sectors":["FinTech"], "is_favorite":false }, "meta": { … } }')
endpoint("POST","/api/v1/ai/investors/{investor}/gaps",
         "Enrichment helper — returns the empty/missing fields on the investor that could be filled (drives the enrichment workflow).",
         output="data: { gaps: [ { field, label, section } ], filled_pct }",
         ex_req="POST /api/v1/ai/investors/42/gaps",
         ex_res='{ "data": { "gaps":[ { "field":"linkedin_url", "label":"LinkedIn" } ], "filled_pct":0.82 } }')
endpoint("GET","/api/v1/investors/{investor}/meetings",
         "All meetings (demos/committees) this investor is/was part of, with their attendance status.",
         inputs=[("limit","no","int","default 30"),("cursor","no","string","")],
         output="data: [ { meeting_id, type, date, attendance_status } ]",
         ex_req="GET /api/v1/investors/42/meetings",
         ex_res='{ "data": [ { "meeting_id":9, "type":"demo", "date":"2026-09-16", "attendance_status":"coming" } ] }')
endpoint("PATCH","/api/v1/investors/{investor}   ·   /api/v1/ai/investors/{investor}",
         "Update investor fields. Accepts direct columns + label fields (by id or by name). Partial — only sent fields change.",
         inputs=[("<any profile field>","no","mixed","e.g. investor_bio, action_option_id"),
                 ("tags","no","int[]","attach tag ids (does not detach)"),("rationale","no","string","audit note")],
         output="data: { changed, investor: { … } }",
         ex_req='{ "investor_bio":"Angel, ex-founder", "action_option_id": null }',
         ex_res='{ "data": { "changed":true, "investor": { "id":42, "investor_bio":"Angel, ex-founder" } } }',
         rollback="reversible (field restore)")
endpoint("POST","/api/v1/ai/investors/bulk-update",
         "Update many investors in one call (same field map as single update). Each row is validated independently.",
         inputs=[("updates","yes","object[]","[ { id, fields… } ]"),("rationale","no","string","")],
         output="data: { updated: n, results: [ { id, changed } ] }",
         ex_req='{ "updates": [ { "id":42, "tier_option_id":3 }, { "id":43, "investor_bio":"…" } ] }',
         ex_res='{ "data": { "updated":2, "results":[ { "id":42, "changed":true } ] } }',
         rollback="reversible per row")
endpoint("POST","/api/v1/ai/investors/{investor}/upload-avatar",
         "Set the investor avatar from an image URL (SSRF-checked, downloaded, stored).",
         inputs=[("image_url","yes","string","public image URL"),("rationale","no","string","")],
         output="data: { changed, avatar_path }",
         ex_req='{ "image_url":"https://…/ali.jpg" }',
         ex_res='{ "data": { "changed":true, "avatar_path":"avatars/42.jpg" } }',
         rollback="reversible (path restore)")
endpoint("PUT","/api/v1/investors/{investor}/preferences",
         "Replace the investor's multiselect preference pivots — sectors / stages / payment_methods (each is a list of label_option ids). Omit a field to leave it unchanged; each provided list replaces that pivot wholesale. Same validation + sync the dashboard's inline editor uses.",
         inputs=[("sectors","no","int[]","sector option ids"),("stages","no","int[]","preferred_stage option ids"),
                 ("payment_methods","no","int[]","payment_method option ids"),("rationale","no","string","")],
         output="data: { changed, preferences: { sectors:[…], stages:[…], payment_methods:[…] } }",
         ex_req='{ "sectors": [18, 19], "stages": [201], "payment_methods": [205] }',
         ex_res='{ "data": { "changed":true, "preferences": { "sectors":[18,19], "stages":[201], "payment_methods":[205] } } }',
         rollback="reversible (re-sync prior id sets)")
endpoint("POST","/api/v1/investors/{investor}/notes",
         "Append a note to the investor (author = the connector's owning admin). Notes are append-only + editable; never deleted via API.",
         inputs=[("note","yes","string","1–5000 chars"),("rationale","no","string","")],
         output="data: { note_id, entity, created_at }",
         ex_req='{ "note":"Interested in FinTech; follow up next week." }',
         ex_res='{ "data": { "note_id":501, "entity": { "type":"investor", "id":42 } } }')

# ============================ 4. STARTUPS ============================
h1("4. Companies (Startups)")
endpoint("GET","/api/v1/startups   ·   /api/v1/ai/startups",
         "List startups (cursor-paginated). Optional q matches company name.",
         inputs=[("q","no","string","≤120"),("limit","no","int","1–200, default 50"),("cursor","no","string","")],
         output="data: [ startup summaries ]; meta.next_cursor.",
         ex_req="GET /api/v1/startups?q=instant&limit=25",
         ex_res='{ "data": [ { "id":475, "name":"Instant", "sector":"FinTech" } ], "meta": { "has_more":false } }')
endpoint("GET","/api/v1/ai/startups/search",
         "Typeahead search (id + name). q required (≥2).",
         inputs=[("q","yes","string","≥2"),("limit","no","int","1–50, default 20")],
         output="data: [ { id, name } ]",
         ex_req="GET /api/v1/ai/startups/search?q=inst",
         ex_res='{ "data": [ { "id":475, "name":"Instant" } ] }')
endpoint("GET","/api/v1/ai/startups/{startup}",
         "Full startup profile — all sections, label values resolved, geographic focus, team members, files, tags.",
         output="data: { id, name, sector, geographic_focuses[], members[], files[], … }",
         ex_req="GET /api/v1/ai/startups/475",
         ex_res='{ "data": { "id":475, "name":"Instant", "geographic_focuses":["UAE"] } }')
endpoint("POST","/api/v1/ai/startups/{startup}/gaps",
         "Returns the empty/missing startup fields for enrichment.",
         output="data: { gaps:[…], filled_pct }",
         ex_req="POST /api/v1/ai/startups/475/gaps",
         ex_res='{ "data": { "gaps":[ { "field":"website" } ], "filled_pct":0.7 } }')
endpoint("PATCH","/api/v1/startups/{startup}   ·   /api/v1/ai/startups/{startup}",
         "Update startup fields (direct + label, by id or name). Partial. Body wraps fields in `changes`.",
         inputs=[("changes.<field>","no","mixed","direct + label fields; e.g. startup_brief, sector_option_id, asking_fund_sar"),
                 ("changes.status_option_id","no","int","PIPELINE STATUS — label_key=status option id (fires the status lifecycle event)"),
                 ("rationale","no","string","audit note")],
         output="data: { subject_id, applied: { … } }",
         ex_req='{ "changes": { "startup_brief":"AI payments", "asking_fund_sar":500000 } }',
         ex_res='{ "data": { "subject_id":475, "applied": { "startup_brief":"AI payments" } } }',
         rollback="reversible (field restore)")
endpoint("PATCH","/api/v1/startups/{startup}   (Status example)",
         "Set the startup PIPELINE STATUS — the `status_option_id` label field. Distinct from action_option_id (workflow) and group_id (pipeline group). Firing changes the StartupLabelOptionChanged event (same as the dashboard) so any armed status notification sends. Look up ids via GET /api/v1/ai/label-options?label_key=status.",
         inputs=[("changes.status_option_id","yes","int","status option id (e.g. 12=had_meeting, 11=under_review, 13=rejected)"),
                 ("rationale","no","string","")],
         output="data: { subject_id, applied: { status_option_id } }",
         ex_req='{ "changes": { "status_option_id": 12 }, "rationale": "had the intro meeting" }',
         ex_res='{ "data": { "subject_id":475, "applied": { "status_option_id":12 } } }')
endpoint("POST","/api/v1/ai/startups/bulk-update",
         "Update many startups in one call.",
         inputs=[("updates","yes","object[]","[ { id, fields… } ]"),("rationale","no","string","")],
         output="data: { updated, results:[…] }",
         ex_req='{ "updates": [ { "id":475, "sector":"FinTech" } ] }',
         ex_res='{ "data": { "updated":1 } }', rollback="reversible per row")
endpoint("POST","/api/v1/ai/startups/{startup}/upload-logo",
         "Set the startup logo from an image URL (SSRF-checked, stored in the logo slot).",
         inputs=[("image_url","yes","string","public image URL"),("rationale","no","string","")],
         output="data: { changed, logo_file_id }",
         ex_req='{ "image_url":"https://…/logo.png" }',
         ex_res='{ "data": { "changed":true, "logo_file_id":880 } }',
         rollback="reversible (deletes the created logo row)")
endpoint("POST","/api/v1/startups/{startup}/members",
         "Add a team member / founder (same fields as the dashboard Team section).",
         inputs=[("name","yes","string","≤255"),("is_founder","no","bool",""),("job_type_option_id","no","int","label id"),
                 ("country_id","no","int",""),("linkedin_url","no","url",""),("phone_number","no","string",""),
                 ("short_brief","no","string","≤2000"),("positions","no","int[]","position label ids"),("rationale","no","string","")],
         output="data: { changed, member: { id, name, positions[] } }",
         ex_req='{ "name":"Mohamed Saad", "is_founder":true, "linkedin_url":"https://linkedin.com/in/x", "positions":[] }',
         ex_res='{ "data": { "changed":true, "member": { "id":925, "name":"Mohamed Saad", "is_founder":true } } }')
endpoint("PATCH","/api/v1/startups/{startup}/members/{member}",
         "Update a team member (full replace of fields + positions). The member must belong to the startup.",
         inputs=[("name","yes","string",""),("is_founder","no","bool",""),("positions","no","int[]",""),("…","no","mixed","other member fields"),("rationale","no","string","")],
         output="data: { changed, member: { … } }",
         ex_req='{ "name":"Mohamed S.", "is_founder":false }',
         ex_res='{ "data": { "changed":true, "member": { "id":925, "name":"Mohamed S." } } }',
         rollback="reversible (field restore)")
endpoint("DELETE","/api/v1/startups/{startup}/members/{member}",
         "Soft-delete a team member (same as the remove button).",
         output="data: { changed, deleted_member_id }",
         ex_req="DELETE /api/v1/startups/475/members/925",
         ex_res='{ "data": { "changed":true, "deleted_member_id":925 } }')
endpoint("POST","/api/v1/startups/{startup}/files",
         "Upload a file into a startup slot FROM A URL (clients have no local file). The URL is SSRF-checked, size-capped per slot, and the bytes are mime-validated against the same rules as the dashboard. Single-file slots (logo, pitch_deck) replace the existing file.",
         inputs=[("file_url","yes","string","public URL (≤2048)"),
                 ("slot","yes","enum","logo | pitch_deck | other | initial_dd"),
                 ("file_name","no","string","else derived from URL"),
                 ("category","no","string","initial_dd only: legal|financial|other"),
                 ("document_type","no","string","initial_dd only: e.g. cap_table, historical_pnl, commercial_registration"),
                 ("rationale","no","string","")],
         output="data: { changed, file: { id, slot, file_name, mime_type, file_type } }",
         ex_req='{ "file_url":"https://…/captable.xlsx", "slot":"initial_dd",\n  "category":"financial", "document_type":"cap_table" }',
         ex_res='{ "data": { "changed":true, "file": { "id":893, "slot":"initial_dd", "file_type":"document" } } }')
endpoint("PATCH","/api/v1/startups/{startup}/files/{file}/slot",
         "Move a file between logo / pitch_deck / other (relocates the blob, displacing any single-file occupant).",
         inputs=[("slot","yes","enum","logo | pitch_deck | other"),("rationale","no","string","")],
         output="data: { changed, file: { id, slot } }",
         ex_req='{ "slot":"other" }',
         ex_res='{ "data": { "changed":true, "file": { "id":893, "slot":"other" } } }',
         rollback="reversible (moves back)")
endpoint("DELETE","/api/v1/startups/{startup}/files/{file}",
         "Delete a startup file + purge its blob from disk. Not auto-reversible (the blob is gone).",
         output="data: { changed, deleted_file_id }",
         ex_req="DELETE /api/v1/startups/475/files/893",
         ex_res='{ "data": { "changed":true, "deleted_file_id":893 } }')
endpoint("PUT","/api/v1/startups/{startup}/geographic-focus",
         "Replace the startup's geographic-focus multiselect with the given label_option ids (same field as the dashboard).",
         inputs=[("geographic_focuses","yes","int[]","geographic_focus option ids"),("rationale","no","string","")],
         output="data: { changed, preferences: { geographic_focuses:[…] } }",
         ex_req='{ "geographic_focuses": [39] }',
         ex_res='{ "data": { "changed":true, "preferences": { "geographic_focuses":[39] } } }',
         rollback="reversible (re-sync prior set)")

# ============================ 5. DEMOS ============================
h1("5. Demos")
endpoint("GET","/api/v1/ai/demos",
         "List demos (cursor-paginated).",
         inputs=[("limit","no","int","1–100, default 30"),("cursor","no","string","")],
         output="data: [ { id, title, meeting_date, meeting_time, status } ]",
         ex_req="GET /api/v1/ai/demos?limit=20",
         ex_res='{ "data": [ { "id":9, "title_en":"Demo Day", "meeting_date":"2026-09-16" } ] }')
endpoint("GET","/api/v1/ai/demos/{demo}",
         "Full demo — core fields, audience (investors+groups), startup roster, slots, attendance matrix, notes.",
         output="data: { id, titles, date/time/url, investors[], groups[], startups[], slots[], … }",
         ex_req="GET /api/v1/ai/demos/9",
         ex_res='{ "data": { "id":9, "title_en":"Demo Day", "startups":[475], "slots":[…] } }')
endpoint("POST","/api/v1/demos",
         "Create a demo. Month-uniqueness is enforced (422 month_taken if a demo already exists that month).",
         inputs=[("title_ar","yes*","string","* per DemoRequest"),("title_en","yes*","string",""),
                 ("meeting_date","yes","date","YYYY-MM-DD"),("meeting_time","yes","time","HH:MM"),
                 ("meeting_url","no","url",""),("rationale","no","string","")],
         output="data: { created:true, demo: { id, … } }",
         ex_req='{ "title_ar":"يوم العرض", "title_en":"Demo Day", "meeting_date":"2026-12-16", "meeting_time":"19:30" }',
         ex_res='{ "data": { "created":true, "demo": { "id":12 } } }')
endpoint("PATCH","/api/v1/demos/{demo}",
         "Update demo core fields (titles, type/mode, date/time/url). Partial. Month-uniqueness re-checked.",
         inputs=[("title_en","no","string",""),("meeting_date","no","date",""),("meeting_time","no","time",""),("meeting_url","no","url",""),("rationale","no","string","")],
         output="data: { changed, demo: { … } }",
         ex_req='{ "meeting_time":"20:00", "meeting_url":"https://teams…/demo" }',
         ex_res='{ "data": { "changed":true, "demo": { "id":9, "meeting_time":"20:00" } } }',
         rollback="reversible (field restore)")
endpoint("PUT","/api/v1/demos/{demo}/audience",
         "Set the demo audience — pinned investors + investor groups (each list syncs wholesale).",
         inputs=[("investor_ids","no","int[]",""),("group_ids","no","int[]",""),("rationale","no","string","")],
         output="data: { changed, effective_investor_count }",
         ex_req='{ "investor_ids": [1,2], "group_ids": [3] }',
         ex_res='{ "data": { "changed":true, "effective_investor_count":28 } }',
         rollback="reversible (re-sync prior audience)")
endpoint("PUT","/api/v1/demos/{demo}/slots",
         "Set the demo's startup time-slots (replaces the slot list). Validated against the demo time (no slot before the demo, no overlap, no duplicate startup).",
         inputs=[("slots","yes","object[]","[ { startup_id, start_time, end_time } ]"),("rationale","no","string","")],
         output="data: { changed, slot_count, roster }   (422 with field errors on overlap/duplicate)",
         ex_req='{ "slots": [ { "startup_id":475, "start_time":"19:30", "end_time":"20:00" } ] }',
         ex_res='{ "data": { "changed":true, "slot_count":1, "roster":1 } }',
         rollback="reversible (re-sync prior slots)")
endpoint("PUT","/api/v1/demos/{demo}/notes",
         "Set the demo's notes field (admin notes on the demo itself).",
         inputs=[("notes","yes","string",""),("rationale","no","string","")],
         output="data: { changed }",
         ex_req='{ "notes":"Rescheduled from Sept 9." }',
         ex_res='{ "data": { "changed":true } }', rollback="reversible")
endpoint("PATCH","/api/v1/demos/{demo}/investors/{investor}/attendance",
         "Set an investor's attendance for the demo (admin status). Guarded to the demo's effective audience.",
         inputs=[("status","yes","enum","coming | attended | not_attended"),("rationale","no","string","")],
         output="data: { changed, status }",
         ex_req='{ "status":"attended" }',
         ex_res='{ "data": { "changed":true, "status":"attended" } }',
         rollback="reversible (status restore)")
endpoint("PATCH","/api/v1/demos/{demo}/startups/{startup}/attendance",
         "Set a startup's attendance for the demo. Guarded to the demo roster.",
         inputs=[("status","yes","enum","coming | attended | not_attended"),("rationale","no","string","")],
         output="data: { changed, status }",
         ex_req='{ "status":"coming" }',
         ex_res='{ "data": { "changed":true, "status":"coming" } }', rollback="reversible")
endpoint("POST","/api/v1/ai/demos/{demo}/retry-notifications/preview",
         "Preview the delta-retry plan for a demo's notifications — which recipients/channels are not_sent/failed, no sending.",
         inputs=[("channels","no","string[]","filter"),("delivery_state","no","string","failed|not_sent")],
         output="data: { recipients, per_channel_counts }",
         ex_req='{ "delivery_state":"failed" }',
         ex_res='{ "data": { "recipients":99, "per_channel":{ "whatsapp":99 } } }')
endpoint("POST","/api/v1/ai/demos/{demo}/retry-notifications",
         "Execute the delta retry — dispatches one job per recipient for only the missing/failed channels (never re-sends a delivered channel).",
         inputs=[("channels","no","string[]",""),("delivery_state","no","string","")],
         output="data: { dispatched, recipients }",
         ex_req='{ "delivery_state":"failed" }',
         ex_res='{ "data": { "dispatched":99 } }')

# ============================ 6. COMMITTEES ============================
h1("6. Committees")
endpoint("GET","/api/v1/ai/committees","List committees (cursor-paginated).",
         inputs=[("limit","no","int","1–100, default 30"),("cursor","no","string","")],
         output="data: [ { id, meeting_date, meeting_time, status } ]",
         ex_req="GET /api/v1/ai/committees", ex_res='{ "data": [ { "id":5, "meeting_date":"2026-12-09" } ] }')
endpoint("GET","/api/v1/ai/committees/{committee}",
         "Full committee — members, startup roster, slots, evaluation matrix, attendance, notes.",
         output="data: { id, date/time/url, investors[], startups[], slots[], evaluations[], … }",
         ex_req="GET /api/v1/ai/committees/5",
         ex_res='{ "data": { "id":5, "investors":[1,2,3], "startups":[…] } }')
endpoint("POST","/api/v1/committees","Create a committee.",
         inputs=[("meeting_date","yes","date",""),("meeting_time","yes","time",""),("meeting_url","no","url",""),("rationale","no","string","")],
         output="data: { created, committee: { id } }",
         ex_req='{ "meeting_date":"2026-12-09", "meeting_time":"13:00" }',
         ex_res='{ "data": { "created":true, "committee": { "id":8 } } }')
endpoint("PATCH","/api/v1/committees/{committee}","Update committee core fields (date/time/url). Partial.",
         inputs=[("meeting_date","no","date",""),("meeting_time","no","time",""),("meeting_url","no","url",""),("rationale","no","string","")],
         output="data: { changed, committee: { … } }",
         ex_req='{ "meeting_time":"14:00" }', ex_res='{ "data": { "changed":true } }', rollback="reversible (field restore)")
endpoint("PUT","/api/v1/committees/{committee}/members",
         "Set the committee member roster (investor ids — syncs wholesale).",
         inputs=[("investor_ids","yes","int[]",""),("rationale","no","string","")],
         output="data: { changed, member_count }",
         ex_req='{ "investor_ids": [1,2,3] }', ex_res='{ "data": { "changed":true, "member_count":3 } }',
         rollback="reversible (re-sync prior roster)")
endpoint("PUT","/api/v1/committees/{committee}/evaluations",
         "Set the pass/fail evaluation matrix cells (per startup × member). Only roster-valid cells are written (UPSERT).",
         inputs=[("cells","yes","object[]","[ { startup_id, member_id, result } ]"),("rationale","no","string","")],
         output="data: { changed, written }",
         ex_req='{ "cells": [ { "startup_id":475, "member_id":1, "result":"pass" } ] }',
         ex_res='{ "data": { "changed":true, "written":1 } }')
endpoint("PUT","/api/v1/committees/{committee}/slots",
         "Set the committee's startup time-slots (replaces the list). Validated against the committee time.",
         inputs=[("slots","yes","object[]","[ { startup_id, start_time, end_time } ]"),("rationale","no","string","")],
         output="data: { changed, slot_count, roster }   (422 on overlap/duplicate)",
         ex_req='{ "slots": [ { "startup_id":475, "start_time":"13:00", "end_time":"13:30" } ] }',
         ex_res='{ "data": { "changed":true, "slot_count":1 } }', rollback="reversible (re-sync prior slots)")
endpoint("PUT","/api/v1/committees/{committee}/notes","Set the committee notes field.",
         inputs=[("notes","yes","string",""),("rationale","no","string","")],
         output="data: { changed }", ex_req='{ "notes":"Quorum met." }', ex_res='{ "data": { "changed":true } }', rollback="reversible")
endpoint("PATCH","/api/v1/committees/{committee}/investors/{investor}/attendance",
         "Set a committee member's attendance (admin status). Guarded to the roster.",
         inputs=[("status","yes","enum","coming | attended | not_attended"),("rationale","no","string","")],
         output="data: { changed, status }", ex_req='{ "status":"attended" }', ex_res='{ "data": { "changed":true } }', rollback="reversible")
endpoint("PATCH","/api/v1/committees/{committee}/startups/{startup}/attendance",
         "Set a startup's attendance for the committee. Guarded to the roster.",
         inputs=[("status","yes","enum","coming | attended | not_attended"),("rationale","no","string","")],
         output="data: { changed, status }", ex_req='{ "status":"coming" }', ex_res='{ "data": { "changed":true } }', rollback="reversible")
endpoint("POST","/api/v1/ai/committees/{committee}/retry-notifications/preview   ·   …/retry-notifications",
         "Preview / execute the committee notification delta-retry (same model as the demo retry above).",
         inputs=[("channels","no","string[]",""),("delivery_state","no","string","failed|not_sent")],
         output="preview: { recipients, per_channel }; execute: { dispatched }",
         ex_req='{ "delivery_state":"not_sent" }', ex_res='{ "data": { "dispatched":12 } }')

# ============================ 7. MEETINGS ============================
h1("7. Meetings (generic)")
endpoint("GET","/api/v1/meetings","Unified list of meetings (demos + committees) with filters.",
         inputs=[("limit","no","int","default 30"),("cursor","no","string","")],
         output="data: [ { id, type, date, time } ]",
         ex_req="GET /api/v1/meetings?limit=20", ex_res='{ "data": [ { "id":9, "type":"demo", "date":"2026-09-16" } ] }')
endpoint("GET","/api/v1/meetings/{meeting}","One meeting with its participants + attendance.",
         output="data: { id, type, participants[], … }",
         ex_req="GET /api/v1/meetings/9", ex_res='{ "data": { "id":9, "type":"demo" } }')
endpoint("PATCH","/api/v1/meetings/{meeting}/attendance",
         "Set attendance for a participant on a generic meeting (coming|attended|not_attended). Idempotent.",
         inputs=[("participant_type","yes","enum","investor | startup"),("participant_id","yes","int",""),("status","yes","enum",""),("rationale","no","string","")],
         output="data: { changed, status }",
         ex_req='{ "participant_type":"investor", "participant_id":42, "status":"attended" }',
         ex_res='{ "data": { "changed":true, "status":"attended" } }', rollback="reversible")
endpoint("PUT","/api/v1/meetings/{meeting}/note","Set the meeting note.",
         inputs=[("note","yes","string",""),("rationale","no","string","")],
         output="data: { changed }", ex_req='{ "note":"All confirmed." }', ex_res='{ "data": { "changed":true } }', rollback="reversible")

# ============================ 8. NOTES / TAGS / FAVORITES ============================
h1("8. Notes, Tags & Favorites")
endpoint("GET","/api/v1/ai/notes","List notes for an entity (investor/startup).",
         inputs=[("entity_type","yes","enum","investor | startup"),("entity_id","yes","int",""),("limit","no","int","")],
         output="data: [ { id, body, admin, created_at } ]",
         ex_req="GET /api/v1/ai/notes?entity_type=investor&entity_id=42",
         ex_res='{ "data": [ { "id":501, "body":"Follow up", "created_at":"2026-06-25T…" } ] }')
endpoint("POST","/api/v1/ai/notes","Create a note on an investor/startup (generic form).",
         inputs=[("entity_type","yes","enum","investor | startup"),("entity_id","yes","int",""),("body","yes","string","1–5000"),("rationale","no","string","")],
         output="data: { note_id, entity, created_at }",
         ex_req='{ "entity_type":"startup", "entity_id":475, "body":"Strong team." }',
         ex_res='{ "data": { "note_id":777, "entity": { "type":"startup", "id":475 } } }')
endpoint("PATCH","/api/v1/ai/notes/{note}",
         "Edit a note's body. NOTE: notes are never deleted via the API (audit-trail integrity) — owner policy.",
         inputs=[("body","yes","string","1–5000"),("rationale","no","string","")],
         output="data: { changed, note_id, updated_at }",
         ex_req='{ "body":"Updated: invested in the round." }',
         ex_res='{ "data": { "changed":true, "note_id":501 } }',
         rollback="reversible (body restore)")
endpoint("GET","/api/v1/ai/tags","List all tags.",
         output="data: [ { id, name, color } ]",
         ex_req="GET /api/v1/ai/tags", ex_res='{ "data": [ { "id":3, "name":"VIP", "color":"#f00" } ] }')
endpoint("POST","/api/v1/ai/tags/upsert",
         "Find-or-create a tag by name (case-insensitive). Idempotent — returns the existing tag if the name already exists.",
         inputs=[("name","yes","string","1–80"),("color","no","string","")],
         output="data: { tag: { id, name, color }, created }",
         ex_req='{ "name":"Hardware", "color":"#3b82f6" }',
         ex_res='{ "data": { "tag": { "id":11, "name":"Hardware" }, "created":true } }')
endpoint("POST","/api/v1/ai/tags/{tag}/attach-investor",
         "Attach an existing tag to an investor (idempotent — no-op if already attached).",
         inputs=[("investor_id","yes","int",""),("rationale","no","string","")],
         output="data: { investor_id, tag_id, newly_attached }",
         ex_req='{ "investor_id":42 }',
         ex_res='{ "data": { "investor_id":42, "tag_id":11, "newly_attached":true } }')
endpoint("PATCH","/api/v1/ai/tags/{tag}",
         "Rename / recolor an existing tag (name must stay unique). NOTE: a rename fans out to every entity carrying the tag. Tags are never deleted or detached via the API — owner policy.",
         inputs=[("name","yes","string","≤191, unique"),("color","no","string","≤32"),("rationale","no","string","")],
         output="data: { changed, tag: { id, name, color } }",
         ex_req='{ "name":"Hardware/IoT", "color":"#2266ff" }',
         ex_res='{ "data": { "changed":true, "tag": { "id":11, "name":"Hardware/IoT" } } }',
         rollback="reversible (name+color restore)")
endpoint("POST","/api/v1/favorites/toggle",
         "Toggle (or set) the favorite star on an investor or startup. Pass desired to set an explicit state idempotently; omit for a plain toggle.",
         inputs=[("entity_type","yes","enum","investor | startup"),("entity_id","yes","int",""),
                 ("desired","no","bool","explicit target; omit to toggle"),("rationale","no","string","")],
         output="data: { changed, entity, is_favorite }",
         ex_req='{ "entity_type":"investor", "entity_id":42 }',
         ex_res='{ "data": { "changed":true, "is_favorite":true } }',
         rollback="reversible (boolean restore)")

# ============================ 9. UTILITY / AUDIT ============================
h1("9. Reference, Files, Audit & Tasks")
endpoint("GET","/api/v1/ai/label-options",
         "Look up label option ids by label key — needed to set label/multiselect fields (sectors, stages, payment_method, geographic_focus, …).",
         inputs=[("label_key","no","string","e.g. sector, preferred_stage"),("q","no","string","filter by name")],
         output="data: { <label_key>: [ { id, name } ] }",
         ex_req="GET /api/v1/ai/label-options?label_key=sector",
         ex_res='{ "data": { "sector": [ { "id":18, "name":"FinTech" } ] } }')
endpoint("GET","/api/v1/ai/files/{file}",
         "Stream/download a file's bytes (e.g. a pitch deck) the connector has a file id for.",
         output="binary stream (Content-Type per file) — or 404.",
         ex_req="GET /api/v1/ai/files/893", ex_res="(binary — application/pdf …)")
endpoint("GET","/api/v1/ai/activity-logs   ·   /api/v1/ai/activity-logs/{log}",
         "List / get the AI write audit trail — every write the connector made (action, subject, old→new, actor, source). The basis for rollback.",
         inputs=[("q","no","string","filter"),("limit","no","int","1–100, default 30"),("cursor","no","string","")],
         output="data: [ { id, action_type, subject, created_at, reversible } ] | full log with old/new values.",
         ex_req="GET /api/v1/ai/activity-logs?limit=20",
         ex_res='{ "data": [ { "id":3201, "action_type":"investor.update", "reversible":true } ] }')
endpoint("GET","/api/v1/ai/notification-logs   ·   /api/v1/ai/notification-logs/{log}",
         "List / get channel-level notification delivery logs (status, channel, address, provider ids, retries).",
         inputs=[("q","no","string","address filter"),("limit","no","int","1–100"),("cursor","no","string","")],
         output="data: [ { id, channel, status, address, attempt_count } ]",
         ex_req="GET /api/v1/ai/notification-logs?limit=20",
         ex_res='{ "data": [ { "id":90, "channel":"whatsapp", "status":"delivered" } ] }')
endpoint("POST","/api/v1/ai/tasks   ·   GET /api/v1/ai/tasks",
         "Open / list an AI task — a named unit of work that groups writes so they can be summarised or rolled back together.",
         inputs=[("title","yes","string","task label"),("metadata","no","object","")],
         output="data: { task: { id, title, status } }",
         ex_req='{ "title":"Enrich Q3 investor batch" }',
         ex_res='{ "data": { "task": { "id":55, "status":"open" } } }')
endpoint("GET","/api/v1/ai/tasks/{task}/summary",
         "Summary of a task — counts of writes by type + reversible/already-rolled-back status.",
         output="data: { task, writes: n, by_action: {…}, reversible: n }",
         ex_req="GET /api/v1/ai/tasks/55/summary",
         ex_res='{ "data": { "writes":12, "by_action": { "investor.update":12 } } }')
endpoint("POST","/api/v1/ai/tasks/{task}/resume",
         "Re-open / continue a task so further writes attach to it.",
         output="data: { task: { id, status } }",
         ex_req="POST /api/v1/ai/tasks/55/resume",
         ex_res='{ "data": { "task": { "id":55, "status":"open" } } }')

# ============================ APPENDIX ============================
h1("Appendix — Rollback & Intentional Non-goals")
doc.add_paragraph("Rollback", style="Heading 2")
doc.add_paragraph("Every write is audited; endpoints marked ↺ reverse cleanly via the rollback service "
                  "(single log, whole session, whole task, or a date range). Reversal is guarded by staleness "
                  "(someone else changed it since), subject-gone, and already-rolled-back checks.")
doc.add_paragraph("Intentional non-goals (owner decisions)", style="Heading 2")
for b in ["Entity DELETE (investor / startup / demo / committee) — not exposed.",
          "Notes — create + edit only; never deleted via API (audit-trail integrity).",
          "Tags — add + rename/recolor only; never deleted, detached, or full-synced (a change fans out to every entity carrying the tag).",
          "startup_file_upload / startup_file_delete are audited but not auto-reversible (disk blob written/purged); file move IS reversible."]:
    doc.add_paragraph(b, style="List Bullet")

doc.save(r"docs/NUMU_API_ENDPOINTS_REFERENCE.docx")
print("Saved docs/NUMU_API_ENDPOINTS_REFERENCE.docx")
