# -*- coding: utf-8 -*-
"""Generate a ready-to-import Postman v2.1 collection (+ environment) with
saved response EXAMPLES, collection-level bearer auth, and a token-capture
test script on the OAuth exchange."""
import json, uuid

BASE = "{{base_url}}"

def url(path, query=None):
    # path like "/api/v1/investors/{{investor_id}}"
    raw = BASE + path
    segs = [s for s in path.strip("/").split("/")]
    u = {"raw": raw, "host": ["{{base_url}}"], "path": segs}
    if query:
        u["raw"] = raw + "?" + "&".join(f"{k}={v}" for k, v in query)
        u["query"] = [{"key": k, "value": v} for k, v in query]
    return u

def req(method, path, body=None, query=None, headers_json=True, desc=""):
    h = []
    if body is not None and headers_json:
        h.append({"key": "Content-Type", "value": "application/json"})
    r = {"method": method, "header": h, "url": url(path, query)}
    if desc:
        r["description"] = desc
    if body is not None:
        r["body"] = {"mode": "raw", "raw": body,
                     "options": {"raw": {"language": "json"}}}
    return r

def example(name, code, status, body, original):
    return {
        "name": name, "originalRequest": original,
        "status": status, "code": code,
        "_postman_previewlanguage": "json",
        "header": [{"key": "Content-Type", "value": "application/json"}],
        "cookie": [], "body": body,
    }

def item(name, method, path, desc="", body=None, query=None,
         ex_code=200, ex_status="OK", ex_body=None, extra_examples=None):
    request = req(method, path, body=body, query=query, desc=desc)
    responses = []
    if ex_body is not None:
        responses.append(example(f"{ex_code} {ex_status}", ex_code, ex_status, ex_body, request))
    for (en, ec, es, eb) in (extra_examples or []):
        responses.append(example(en, ec, es, eb, request))
    return {"name": name, "request": request, "response": responses}

def folder(name, items, desc=""):
    f = {"name": name, "item": items}
    if desc:
        f["description"] = desc
    return f

def meta(extra=""):
    inner = '"request_id":"req_9f2","session_id":"claude-sess-1","actor":{"id":7,"email":"admin@numuangels.net"}'
    if extra:
        inner += "," + extra
    return "{" + inner + "}"

def ok(data, extra_meta=""):
    return '{\n  "data": ' + data + ',\n  "meta": ' + meta(extra_meta) + '\n}'

ERR_422 = ('422 Validation', 422, 'Unprocessable Entity',
           '{\n  "error": {\n    "code": "validation_error",\n    "message": "The given data was invalid.",\n    "details": { "field": ["…"] }\n  }\n}')
ERR_404 = ('404 Not Found', 404, 'Not Found',
           '{\n  "error": { "code": "not_found", "message": "Resource not found.", "details": {} }\n}')

# ───────────────────────── AUTHENTICATION ─────────────────────────
auth_token_test = (
    "// Auto-capture tokens into the environment after a successful exchange.\n"
    "try {\n"
    "  const j = pm.response.json();\n"
    "  if (j.access_token)  pm.environment.set('access_token', j.access_token);\n"
    "  if (j.refresh_token) pm.environment.set('refresh_token', j.refresh_token);\n"
    "  console.log('Saved access_token to environment.');\n"
    "} catch (e) { console.log('No JSON token in response.'); }\n"
)

auth_folder = folder("0 · Authentication", [
    {
        "name": "OAuth — Exchange code for token",
        "event": [{"listen": "test", "script": {"type": "text/javascript",
                  "exec": auth_token_test.split("\n")}}],
        "request": req("POST", "/oauth/token",
            body='{\n  "grant_type": "authorization_code",\n  "code": "{{auth_code}}",\n  "code_verifier": "{{code_verifier}}",\n  "client_id": "{{client_id}}",\n  "redirect_uri": "{{redirect_uri}}"\n}',
            desc="Exchange the authorization code (from /oauth/authorize) for an access token. The test script auto-saves access_token + refresh_token into the environment."),
        "response": [example("200 OK", 200, "OK",
            '{\n  "token_type": "Bearer",\n  "expires_in": 3600,\n  "access_token": "naat_abc123…",\n  "refresh_token": "natr_def456…"\n}',
            req("POST", "/oauth/token", body="{…}"))],
    },
    {
        "name": "OAuth — Refresh token",
        "event": [{"listen": "test", "script": {"type": "text/javascript",
                  "exec": auth_token_test.split("\n")}}],
        "request": req("POST", "/oauth/token",
            body='{\n  "grant_type": "refresh_token",\n  "refresh_token": "{{refresh_token}}",\n  "client_id": "{{client_id}}"\n}',
            desc="Get a fresh access token. Auto-saves the new token."),
        "response": [example("200 OK", 200, "OK",
            '{\n  "token_type": "Bearer",\n  "expires_in": 3600,\n  "access_token": "naat_new…",\n  "refresh_token": "natr_new…"\n}',
            req("POST", "/oauth/token", body="{…}"))],
    },
    item("Health — Ping (full tier)", "GET", "/api/v1/ai/_ping/full",
         desc="Confirms the token + scope resolve for the Full tier.",
         ex_body=ok('{ "ok": true, "tier": "full" }')),
], desc="Paste a Sanctum token into {{access_token}} for quick testing, OR run the OAuth exchange (auto-saves the token). All other requests inherit collection-level Bearer auth.")

# ───────────────────────── INVESTORS ─────────────────────────
investors = folder("1 · Investors", [
    item("List", "GET", "/api/v1/investors", query=[("q","ali"),("limit","25")],
         desc="Cursor-paginated list; optional free-text q.",
         ex_body=ok('[\n    { "id": 42, "display_name": "Ali Qassem", "email": "ali@x.com", "is_favorite": false }\n  ]', '"next_cursor":"eyJpZ…","has_more":true')),
    item("Search (typeahead)", "GET", "/api/v1/ai/investors/search", query=[("q","ali"),("limit","10")],
         desc="Lightweight id+name search. q ≥ 2 chars.",
         ex_body=ok('[ { "id": 42, "display_name": "Ali Qassem" } ]')),
    item("Get (full profile)", "GET", "/api/v1/ai/investors/{{investor_id}}",
         desc="Full investor profile with resolved labels, preferences, tags.",
         ex_body=ok('{ "id": 42, "display_name": "Ali Qassem", "sectors": ["FinTech"], "is_favorite": false }')),
    item("Find gaps (enrichment)", "POST", "/api/v1/ai/investors/{{investor_id}}/gaps",
         desc="Missing/empty fields to drive enrichment.",
         ex_body=ok('{ "gaps": [ { "field": "linkedin_url", "label": "LinkedIn" } ], "filled_pct": 0.82 }')),
    item("Meetings for investor", "GET", "/api/v1/investors/{{investor_id}}/meetings",
         desc="Demos/committees this investor is part of + attendance.",
         ex_body=ok('[ { "meeting_id": 9, "type": "demo", "date": "2026-09-16", "attendance_status": "coming" } ]')),
    item("Update", "PATCH", "/api/v1/investors/{{investor_id}}",
         body='{\n  "investor_bio": "Angel, ex-founder",\n  "action_option_id": null,\n  "tags": [1, 2],\n  "rationale": "profile cleanup"\n}',
         desc="Partial update — direct + label fields (by id or name); tags attach (no detach). Reversible.",
         ex_body=ok('{ "changed": true, "investor": { "id": 42, "investor_bio": "Angel, ex-founder" } }'),
         extra_examples=[ERR_422]),
    item("Bulk update", "POST", "/api/v1/ai/investors/bulk-update",
         body='{\n  "updates": [\n    { "id": 42, "tier_option_id": 3 },\n    { "id": 43, "investor_bio": "Repeat angel" }\n  ],\n  "rationale": "Q3 batch"\n}',
         desc="Update many investors in one call. Reversible per row.",
         ex_body=ok('{ "updated": 2, "results": [ { "id": 42, "changed": true } ] }')),
    item("Upload avatar (by URL)", "POST", "/api/v1/ai/investors/{{investor_id}}/upload-avatar",
         body='{ "image_url": "https://example.com/ali.jpg" }',
         desc="Set avatar from an image URL (SSRF-checked). Reversible.",
         ex_body=ok('{ "changed": true, "avatar_path": "avatars/42.jpg" }')),
    item("Set Preferences (pivots)", "PUT", "/api/v1/investors/{{investor_id}}/preferences",
         body='{\n  "sectors": [18, 19],\n  "stages": [201],\n  "payment_methods": [205]\n}',
         desc="Replace preferred sectors / stages / payment_methods (label_option ids). Omit a field to leave it. Reversible.",
         ex_body=ok('{ "changed": true, "preferences": { "sectors": [18,19], "stages": [201], "payment_methods": [205] } }'),
         extra_examples=[ERR_422]),
    item("Add note", "POST", "/api/v1/investors/{{investor_id}}/notes",
         body='{ "note": "Interested in FinTech; follow up next week." }',
         desc="Append a note (append-only; editable, never deleted via API).",
         ex_code=201, ex_status="Created",
         ex_body=ok('{ "note_id": 501, "entity": { "type": "investor", "id": 42 } }')),
])

# ───────────────────────── COMPANIES ─────────────────────────
companies = folder("2 · Companies (Startups)", [
    item("List", "GET", "/api/v1/startups", query=[("q","instant"),("limit","25")],
         ex_body=ok('[ { "id": 475, "name": "Instant", "sector": "FinTech" } ]', '"has_more":false')),
    item("Search (typeahead)", "GET", "/api/v1/ai/startups/search", query=[("q","inst")],
         ex_body=ok('[ { "id": 475, "name": "Instant" } ]')),
    item("Get (full profile)", "GET", "/api/v1/ai/startups/{{startup_id}}",
         ex_body=ok('{ "id": 475, "name": "Instant", "geographic_focuses": ["UAE"], "members": [], "files": [] }')),
    item("Find gaps", "POST", "/api/v1/ai/startups/{{startup_id}}/gaps",
         ex_body=ok('{ "gaps": [ { "field": "website" } ], "filled_pct": 0.7 }')),
    item("Update", "PATCH", "/api/v1/startups/{{startup_id}}",
         body='{\n  "changes": {\n    "startup_brief": "AI payments",\n    "sector_option_id": 22,\n    "asking_fund_sar": 500000\n  },\n  "rationale": "profile cleanup"\n}',
         desc="Partial update — direct + label fields wrapped in `changes` (by id or name). Reversible.",
         ex_body=ok('{ "subject_id": 475, "applied": { "startup_brief": "AI payments" } }'),
         extra_examples=[ERR_422]),
    item("Set Status (status_option_id)", "PATCH", "/api/v1/startups/{{startup_id}}",
         body='{\n  "changes": { "status_option_id": 12 },\n  "rationale": "had the intro meeting"\n}',
         desc=("Set the PIPELINE STATUS — the `status_option_id` label field. Distinct from action_option_id (the "
               "Approve/Reject/Move workflow → use Set Action) and group_id (pipeline group). A change fires the same "
               "StartupLabelOptionChanged event the dashboard fires, so any armed status notification sends. "
               "Look up ids via Reference · Label options (label_key=status). This env: 17 new, 11 under_review, "
               "10 meeting_requested, 12 had_meeting, 14 listed, 13 rejected, 16 featured, 15 no_action."),
         ex_body=ok('{ "subject_id": 475, "applied": { "status_option_id": 12 } }')),
    item("Bulk update", "POST", "/api/v1/ai/startups/bulk-update",
         body='{ "updates": [ { "id": 475, "sector": "FinTech" } ] }',
         ex_body=ok('{ "updated": 1 }')),
    item("Set Action (business workflow)", "POST", "/api/v1/startups/{{startup_id}}/action",
         body='{\n  "action": "committee",\n  "reason": null,\n  "metadata": { "source": "pipeline-review" }\n}',
         desc=("EXPLICIT business action — runs the SAME StartupLabelActionService as the dashboard (group move + "
               "Email/SMS/WhatsApp/in-app + Microsoft-Bookings cleanup + activity logs + automations). NO direct DB write. "
               "`action` = slug or option id; `reason` = rejection/request-info text when applicable. "
               "Common slugs (verify per environment via Reference · Label options): "
               "request_meeting, reject (with reason), committee, move_to_review, move_to_listing_app, move_to_archive, "
               "follow_up_email, initial_dd, no_action. Examples: "
               "{\"action\":\"request_meeting\"} · {\"action\":\"reject\",\"reason\":\"Not a fit\"} · "
               "{\"action\":\"committee\"} · {\"action\":\"move_to_archive\"}."),
         ex_body=ok('{ "changed": true, "action": { "from_option_id": null, "to_option_id": 7, "name": "Committee" } }'),
         extra_examples=[("422 unknown_action", 422, "Unprocessable Entity",
            '{\n  "error": {\n    "code": "unknown_action",\n    "message": "Unknown startup action. Pass a valid action option id or slug (see details.available).",\n    "details": { "available": [ { "id": 1, "slug": "reject", "name": "Reject" }, { "id": 7, "slug": "committee", "name": "Committee" } ] }\n  }\n}')]),
    item("Upload logo (by URL)", "POST", "/api/v1/ai/startups/{{startup_id}}/upload-logo",
         body='{ "image_url": "https://example.com/logo.png" }',
         desc="Set the logo from a URL. Reversible.",
         ex_body=ok('{ "changed": true, "logo_file_id": 880 }')),
    item("Member — Create", "POST", "/api/v1/startups/{{startup_id}}/members",
         body='{\n  "name": "Mohamed Saad",\n  "is_founder": true,\n  "linkedin_url": "https://linkedin.com/in/x",\n  "positions": []\n}',
         desc="Add a team member / founder.",
         ex_body=ok('{ "changed": true, "member": { "id": 925, "name": "Mohamed Saad", "is_founder": true } }')),
    item("Member — Update", "PATCH", "/api/v1/startups/{{startup_id}}/members/{{member_id}}",
         body='{ "name": "Mohamed S.", "is_founder": false }',
         desc="Full replace of member fields + positions. Reversible.",
         ex_body=ok('{ "changed": true, "member": { "id": 925, "name": "Mohamed S." } }')),
    item("Member — Delete", "DELETE", "/api/v1/startups/{{startup_id}}/members/{{member_id}}",
         desc="Soft-delete a team member.",
         ex_body=ok('{ "changed": true, "deleted_member_id": 925 }')),
    item("File — Upload by URL", "POST", "/api/v1/startups/{{startup_id}}/files",
         body='{\n  "file_url": "https://example.com/deck.pdf",\n  "slot": "pitch_deck",\n  "file_name": "deck.pdf"\n}',
         desc="Upload from a URL (SSRF-checked, size-capped, mime-validated). Single-file slots replace the existing file.",
         ex_body=ok('{ "changed": true, "file": { "id": 893, "slot": "pitch_deck", "file_type": "pdf" } }'),
         extra_examples=[ERR_422]),
    item("File — Upload Initial-DD (cap_table)", "POST", "/api/v1/startups/{{startup_id}}/files",
         body='{\n  "file_url": "https://example.com/captable.xlsx",\n  "slot": "initial_dd",\n  "category": "financial",\n  "document_type": "cap_table"\n}',
         desc="DD document upload; category + document_type required for initial_dd.",
         ex_body=ok('{ "changed": true, "file": { "id": 894, "slot": "initial_dd", "document_type": "cap_table" } }')),
    item("File — Move Slot", "PATCH", "/api/v1/startups/{{startup_id}}/files/{{file_id}}/slot",
         body='{ "slot": "other" }',
         desc="Move between logo/pitch_deck/other (relocates the blob). Reversible.",
         ex_body=ok('{ "changed": true, "file": { "id": 893, "slot": "other" } }')),
    item("File — Delete", "DELETE", "/api/v1/startups/{{startup_id}}/files/{{file_id}}",
         desc="Delete file + purge blob. Not auto-reversible.",
         ex_body=ok('{ "changed": true, "deleted_file_id": 893 }')),
    item("Set Geographic Focus", "PUT", "/api/v1/startups/{{startup_id}}/geographic-focus",
         body='{ "geographic_focuses": [39] }',
         desc="Replace geographic-focus multiselect (label_option ids). Reversible.",
         ex_body=ok('{ "changed": true, "preferences": { "geographic_focuses": [39] } }')),
])

# ───────────────────────── DEMOS ─────────────────────────
demos = folder("3 · Demos", [
    item("List", "GET", "/api/v1/ai/demos", query=[("limit","20")],
         ex_body=ok('[ { "id": 9, "title_en": "Demo Day", "meeting_date": "2026-09-16" } ]')),
    item("Get (full)", "GET", "/api/v1/ai/demos/{{demo_id}}",
         ex_body=ok('{ "id": 9, "title_en": "Demo Day", "startups": [475], "slots": [] }')),
    item("Create", "POST", "/api/v1/demos",
         body='{\n  "title_ar": "يوم العرض",\n  "title_en": "Demo Day",\n  "meeting_date": "2026-12-16",\n  "meeting_time": "19:30"\n}',
         desc="Create a demo. 422 month_taken if one already exists that month.",
         ex_body=ok('{ "created": true, "demo": { "id": 12 } }'),
         extra_examples=[("422 month_taken", 422, "Unprocessable Entity",
            '{\n  "error": { "code": "month_taken", "message": "A demo already exists for this month." }\n}')]),
    item("Update core fields", "PATCH", "/api/v1/demos/{{demo_id}}",
         body='{\n  "meeting_time": "20:00",\n  "meeting_url": "https://teams.example/demo",\n  "rationale": "reschedule"\n}',
         desc="Partial update. Reversible.",
         ex_body=ok('{ "changed": true, "demo": { "id": 9, "meeting_time": "20:00" } }')),
    item("Set Audience", "PUT", "/api/v1/demos/{{demo_id}}/audience",
         body='{ "investor_ids": [1, 2], "group_ids": [3] }',
         desc="Sync pinned investors + groups. Reversible.",
         ex_body=ok('{ "changed": true, "effective_investor_count": 28 }')),
    item("Set Slots", "PUT", "/api/v1/demos/{{demo_id}}/slots",
         body='{\n  "slots": [\n    { "startup_id": {{startup_id}}, "start_time": "19:30", "end_time": "20:00" }\n  ]\n}',
         desc="Replace startup time-slots. Validated vs demo time. Reversible.",
         ex_body=ok('{ "changed": true, "slot_count": 1, "roster": 1 }'),
         extra_examples=[("422 overlap", 422, "Unprocessable Entity",
            '{\n  "error": { "code": "validation_error", "message": "This slot overlaps with another.", "details": { "slots.0.start_time": ["…"] } }\n}')]),
    item("Set Notes", "PUT", "/api/v1/demos/{{demo_id}}/notes",
         body='{ "notes": "Rescheduled from Sept 9." }', desc="Reversible.",
         ex_body=ok('{ "changed": true }')),
    item("Investor attendance", "PATCH", "/api/v1/demos/{{demo_id}}/investors/{{investor_id}}/attendance",
         body='{ "status": "attended" }', desc="coming|attended|not_attended. Guarded to audience. Reversible.",
         ex_body=ok('{ "changed": true, "status": "attended" }')),
    item("Startup attendance", "PATCH", "/api/v1/demos/{{demo_id}}/startups/{{startup_id}}/attendance",
         body='{ "status": "coming" }', desc="Guarded to roster. Reversible.",
         ex_body=ok('{ "changed": true, "status": "coming" }')),
    item("Retry notifications — preview", "POST", "/api/v1/ai/demos/{{demo_id}}/retry-notifications/preview",
         body='{ "delivery_state": "failed" }', desc="Delta plan, no sending.",
         ex_body=ok('{ "recipients": 99, "per_channel": { "whatsapp": 99 } }')),
    item("Retry notifications — execute", "POST", "/api/v1/ai/demos/{{demo_id}}/retry-notifications",
         body='{ "delivery_state": "failed" }', desc="Dispatches only missing/failed channels.",
         ex_body=ok('{ "dispatched": 99 }')),
    item("Announce (fire creation automations)", "POST", "/api/v1/demos/{{demo_id}}/announce",
         body='{ "rationale": "audience + slots ready" }',
         desc="Fires DemoCreated → demo_created notifications to the whole audience (same as the dashboard). Call AFTER Set Audience + Set Slots. 422 not_ready if either is empty; idempotent (no re-blast).",
         ex_body=ok('{ "changed": true, "announced": true, "announced_at": "2026-06-26T10:00:00Z", "recipients": { "investors": 28, "startups_via_slots": 6 } }'),
         extra_examples=[("422 not_ready", 422, "Unprocessable Entity",
            '{\n  "error": { "code": "not_ready", "message": "Cannot announce yet — missing: audience (no effective investors — run demo_set_audience first); slots (no startup slots — run demo_set_slots first)." }\n}')]),
])

# ───────────────────────── COMMITTEES ─────────────────────────
committees = folder("4 · Committees", [
    item("List", "GET", "/api/v1/ai/committees",
         ex_body=ok('[ { "id": 5, "meeting_date": "2026-12-09" } ]')),
    item("Get (full)", "GET", "/api/v1/ai/committees/{{committee_id}}",
         ex_body=ok('{ "id": 5, "investors": [1,2,3], "startups": [] }')),
    item("Create", "POST", "/api/v1/committees",
         body='{ "meeting_date": "2026-12-09", "meeting_time": "13:00" }',
         ex_body=ok('{ "created": true, "committee": { "id": 8 } }')),
    item("Update core fields", "PATCH", "/api/v1/committees/{{committee_id}}",
         body='{ "meeting_time": "14:00" }', desc="Reversible.",
         ex_body=ok('{ "changed": true }')),
    item("Set Members", "PUT", "/api/v1/committees/{{committee_id}}/members",
         body='{ "investor_ids": [1, 2, 3] }', desc="Sync roster. Reversible.",
         ex_body=ok('{ "changed": true, "member_count": 3 }')),
    item("Set Evaluations", "PUT", "/api/v1/committees/{{committee_id}}/evaluations",
         body='{ "cells": [ { "startup_id": {{startup_id}}, "member_id": {{investor_id}}, "result": "pass" } ] }',
         desc="UPSERT the pass/fail matrix (roster-valid cells only).",
         ex_body=ok('{ "changed": true, "written": 1 }')),
    item("Set Slots", "PUT", "/api/v1/committees/{{committee_id}}/slots",
         body='{ "slots": [ { "startup_id": {{startup_id}}, "start_time": "13:00", "end_time": "13:30" } ] }',
         desc="Replace slots. Validated vs committee time. Reversible.",
         ex_body=ok('{ "changed": true, "slot_count": 1 }')),
    item("Set Notes", "PUT", "/api/v1/committees/{{committee_id}}/notes",
         body='{ "notes": "Quorum met." }', desc="Reversible.",
         ex_body=ok('{ "changed": true }')),
    item("Investor attendance", "PATCH", "/api/v1/committees/{{committee_id}}/investors/{{investor_id}}/attendance",
         body='{ "status": "attended" }', desc="Reversible.",
         ex_body=ok('{ "changed": true, "status": "attended" }')),
    item("Startup attendance", "PATCH", "/api/v1/committees/{{committee_id}}/startups/{{startup_id}}/attendance",
         body='{ "status": "coming" }', desc="Reversible.",
         ex_body=ok('{ "changed": true, "status": "coming" }')),
    item("Announce (fire creation automations)", "POST", "/api/v1/committees/{{committee_id}}/announce",
         body='{ "rationale": "members + slots ready" }',
         desc="Fires CommitteeCreated → committee_created notifications to members + startups (same as the dashboard). Call AFTER Set Members + Set Slots. 422 not_ready if either is empty; idempotent.",
         ex_body=ok('{ "changed": true, "announced": true, "recipients": { "members": 3, "startups_via_slots": 4 } }'),
         extra_examples=[("422 not_ready", 422, "Unprocessable Entity",
            '{\n  "error": { "code": "not_ready", "message": "Cannot announce yet — missing: members (no investors — run committee_set_members first); slots (no startup slots — run committee_set_slots first)." }\n}')]),
])

# ───────────────────────── MEETINGS ─────────────────────────
meetings = folder("5 · Meetings (generic)", [
    item("List", "GET", "/api/v1/meetings", query=[("limit","20")],
         ex_body=ok('[ { "id": 9, "type": "demo", "date": "2026-09-16" } ]')),
    item("Get", "GET", "/api/v1/meetings/{{meeting_id}}",
         ex_body=ok('{ "id": 9, "type": "demo", "participants": [] }')),
    item("Set attendance", "PATCH", "/api/v1/meetings/{{meeting_id}}/attendance",
         body='{ "participant_type": "investor", "participant_id": {{investor_id}}, "status": "attended" }',
         desc="Reversible.", ex_body=ok('{ "changed": true, "status": "attended" }')),
    item("Set note", "PUT", "/api/v1/meetings/{{meeting_id}}/note",
         body='{ "note": "All confirmed." }', desc="Reversible.",
         ex_body=ok('{ "changed": true }')),
])

# ───────────────────────── NOTES / TAGS / FAVORITES ─────────────────────────
ntf = folder("6 · Notes · Tags · Favorites", [
    item("Notes — list for entity", "GET", "/api/v1/ai/notes", query=[("entity_type","investor"),("entity_id","{{investor_id}}")],
         ex_body=ok('[ { "id": 501, "body": "Follow up", "created_at": "2026-06-25T10:00:00Z" } ]')),
    item("Note — create (generic)", "POST", "/api/v1/ai/notes",
         body='{ "entity_type": "startup", "entity_id": {{startup_id}}, "body": "Strong team." }',
         ex_code=201, ex_status="Created",
         ex_body=ok('{ "note_id": 777, "entity": { "type": "startup", "id": 475 } }')),
    item("Note — edit (no delete)", "PATCH", "/api/v1/ai/notes/{{note_id}}",
         body='{ "body": "Updated: invested in the round." }',
         desc="Edit body. Notes are never deleted via API (owner policy). Reversible.",
         ex_body=ok('{ "changed": true, "note_id": 501 }')),
    item("Tags — list", "GET", "/api/v1/ai/tags",
         ex_body=ok('[ { "id": 3, "name": "VIP", "color": "#f00" } ]')),
    item("Tag — upsert (find-or-create)", "POST", "/api/v1/ai/tags/upsert",
         body='{ "name": "Hardware", "color": "#3b82f6" }',
         desc="Idempotent: returns existing tag if the name already exists.",
         ex_code=201, ex_status="Created",
         ex_body=ok('{ "tag": { "id": 11, "name": "Hardware" }, "created": true }')),
    item("Tag — attach to investor", "POST", "/api/v1/ai/tags/{{tag_id}}/attach-investor",
         body='{ "investor_id": {{investor_id}} }',
         desc="Idempotent attach (no-op if already attached).",
         ex_body=ok('{ "investor_id": 42, "tag_id": 11, "newly_attached": true }')),
    item("Tag — rename/recolor (no delete)", "PATCH", "/api/v1/ai/tags/{{tag_id}}",
         body='{ "name": "Hardware/IoT", "color": "#2266ff" }',
         desc="Rename fans out to every entity carrying the tag. Never deleted/detached. Reversible.",
         ex_body=ok('{ "changed": true, "tag": { "id": 11, "name": "Hardware/IoT" } }')),
    item("Favorite — toggle", "POST", "/api/v1/favorites/toggle",
         body='{ "entity_type": "investor", "entity_id": {{investor_id}} }',
         desc="Toggle the star, or pass desired:true/false to set explicitly. Reversible.",
         ex_body=ok('{ "changed": true, "is_favorite": true }')),
])

# ───────────────────────── REFERENCE / AUDIT / TASKS ─────────────────────────
util = folder("7 · Reference · Audit · Tasks", [
    item("Label options (lookup ids)", "GET", "/api/v1/ai/label-options", query=[("label_key","sector")],
         desc="Resolve label_option ids for label/multiselect fields.",
         ex_body=ok('{ "sector": [ { "id": 18, "name": "FinTech" } ] }')),
    item("File — download/stream", "GET", "/api/v1/ai/files/{{file_id}}",
         desc="Streams the file bytes (Content-Type per file).",
         ex_code=200, ex_status="OK", ex_body="(binary — application/pdf …)"),
    item("Activity logs — list", "GET", "/api/v1/ai/activity-logs", query=[("limit","20")],
         desc="The AI write audit trail (basis for rollback).",
         ex_body=ok('[ { "id": 3201, "action_type": "investor.update", "reversible": true } ]')),
    item("Activity log — get", "GET", "/api/v1/ai/activity-logs/{{log_id}}",
         ex_body=ok('{ "id": 3201, "action_type": "investor.update", "old_values": {}, "new_values": {} }')),
    item("Notification logs — list", "GET", "/api/v1/ai/notification-logs", query=[("limit","20")],
         ex_body=ok('[ { "id": 90, "channel": "whatsapp", "status": "delivered" } ]')),
    item("Notification log — get", "GET", "/api/v1/ai/notification-logs/{{log_id}}",
         ex_body=ok('{ "id": 90, "channel": "whatsapp", "status": "delivered", "attempt_count": 1 }')),
    item("Task — open", "POST", "/api/v1/ai/tasks",
         body='{ "title": "Enrich Q3 investor batch" }',
         desc="Groups writes for summary + bulk rollback.",
         ex_body=ok('{ "task": { "id": 55, "status": "open" } }')),
    item("Tasks — list", "GET", "/api/v1/ai/tasks", query=[("limit","20")],
         ex_body=ok('[ { "id": 55, "title": "Enrich Q3 investor batch", "status": "open" } ]')),
    item("Task — summary", "GET", "/api/v1/ai/tasks/{{task_id}}/summary",
         ex_body=ok('{ "writes": 12, "by_action": { "investor.update": 12 }, "reversible": 12 }')),
    item("Task — resume", "POST", "/api/v1/ai/tasks/{{task_id}}/resume",
         ex_body=ok('{ "task": { "id": 55, "status": "open" } }')),
])

collection = {
    "info": {
        "_postman_id": str(uuid.uuid5(uuid.NAMESPACE_DNS, "numu-mcp-collection")),
        "name": "Numu Angels — AI/MCP API (Full)",
        "description": ("Ready-to-use collection for the Numu Angels AI/MCP Full connector.\n\n"
                        "SETUP: 1) Import this + the environment file. 2) Either paste a Sanctum token into "
                        "{{access_token}}, or run '0 · Authentication › OAuth — Exchange code for token' (it "
                        "auto-saves the token). 3) Set the *_id variables. All requests inherit collection-level "
                        "Bearer auth and include a saved response example.\n\n"
                        "Base path /api/v1. Envelope: { data, meta } on success, { error } on failure. Write "
                        "responses include \"changed\": true|false. Most writes are reversible via the rollback service."),
        "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
    },
    "auth": {"type": "bearer", "bearer": [{"key": "token", "value": "{{access_token}}", "type": "string"}]},
    "variable": [
        {"key": "base_url", "value": "https://dashboard.numuangels.net"},
        {"key": "access_token", "value": ""},
    ],
    "item": [auth_folder, investors, companies, demos, committees, meetings, ntf, util],
}

with open("docs/postman/NUMU_MCP_COLLECTION.json", "w", encoding="utf-8") as f:
    json.dump(collection, f, ensure_ascii=False, indent=2)

env = {
    "id": "numu-mcp-env",
    "name": "Numu MCP — Environment",
    "values": [
        {"key": "base_url", "value": "https://dashboard.numuangels.net", "type": "default", "enabled": True},
        {"key": "access_token", "value": "", "type": "secret", "enabled": True},
        {"key": "refresh_token", "value": "", "type": "secret", "enabled": True},
        {"key": "client_id", "value": "", "type": "default", "enabled": True},
        {"key": "redirect_uri", "value": "http://localhost/callback", "type": "default", "enabled": True},
        {"key": "code_verifier", "value": "", "type": "default", "enabled": True},
        {"key": "auth_code", "value": "", "type": "default", "enabled": True},
        {"key": "investor_id", "value": "1", "type": "default", "enabled": True},
        {"key": "startup_id", "value": "1", "type": "default", "enabled": True},
        {"key": "demo_id", "value": "1", "type": "default", "enabled": True},
        {"key": "committee_id", "value": "1", "type": "default", "enabled": True},
        {"key": "meeting_id", "value": "1", "type": "default", "enabled": True},
        {"key": "member_id", "value": "1", "type": "default", "enabled": True},
        {"key": "file_id", "value": "1", "type": "default", "enabled": True},
        {"key": "note_id", "value": "1", "type": "default", "enabled": True},
        {"key": "tag_id", "value": "1", "type": "default", "enabled": True},
        {"key": "task_id", "value": "1", "type": "default", "enabled": True},
        {"key": "log_id", "value": "1", "type": "default", "enabled": True},
    ],
    "_postman_variable_scope": "environment",
}
with open("docs/postman/NUMU_MCP_ENVIRONMENT.json", "w", encoding="utf-8") as f:
    json.dump(env, f, ensure_ascii=False, indent=2)

# quick stats
reqs = sum(len(fl["item"]) for fl in collection["item"])
exs = 0
for fl in collection["item"]:
    for it in fl["item"]:
        exs += len(it.get("response", []))
print(f"Collection written: {len(collection['item'])} folders, {reqs} requests, {exs} saved examples.")
print("Environment written: %d variables." % len(env["values"]))
