#!/usr/bin/env python3
"""Send to CRM (phase 2): write one Diary conversation as a real NOTE onto a real HubSpot contact/deal.

RecapMuse build spec 03 phase 2 (the live CRM API write, gated behind the Recall edition in the
Swift shell). Phase 1 (crm_export.py) copies paste-ready text to the clipboard; this creates an actual
record via the HubSpot CRM v3 API:

  POST /crm/v3/objects/notes  { properties:{hs_note_body, hs_timestamp},
                                associations:[{ to:{id:<contactId>},
                                  types:[{associationCategory:"HUBSPOT_DEFINED", associationTypeId:202}] }] }

The note body reuses crm_export.format_crm (summary + talk-ratio + topics + objections + next steps), so
Copy-for-CRM and Send-to-CRM produce identical content. The contact is resolved from an email (HubSpot
needs the numeric contact id, never the email), optionally created if missing.

Auth: a HubSpot PRIVATE APP token (scopes crm.objects.contacts.write + crm.objects.notes-or-... ). Passed
via env HUBSPOT_TOKEN (preferred; never argv) or --token. Never printed.

Pure stdlib (urllib) so it runs under the system /usr/bin/python3 with no pip installs, like the other
engines. The Swift shell reads the JSON on stdout: {ok, note_id, contact_id, url, error}.

  crm_push.py --rec-dir DIR --conv-key KEY --contact-email a@b.com [--deal-id 123] [--create-contact]
  crm_push.py --rec-dir DIR --conv-key KEY --contact-email a@b.com --dry-run   # show the request, send nothing
"""
import json
import os
import sys
import time
import urllib.error
import urllib.request

from crm_export import _find_conv, format_crm, _clean  # phase-1 body builder (same engine dir)

HS_BASE = "https://api.hubapi.com"
# HubSpot default (HUBSPOT_DEFINED) association type ids for a Note -> object.
ASSOC_NOTE_TO_CONTACT = 202
ASSOC_NOTE_TO_DEAL = 214


def _html_body(text):
    """HubSpot renders hs_note_body as HTML. Escape the plain-text export and keep line breaks."""
    esc = (text or "").replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
    return esc.replace("\n", "<br>")


def _hs(method, path, token, body=None, dry=None):
    """One HubSpot API call. On dry-run, append the planned request to `dry` and return a stub."""
    url = HS_BASE + path
    payload = json.dumps(body).encode() if body is not None else None
    if dry is not None:
        dry.append({"method": method, "url": url, "body": body})
        return 200, {"dry_run": True}
    req = urllib.request.Request(url, data=payload, method=method)
    req.add_header("Authorization", "Bearer " + token)
    req.add_header("Content-Type", "application/json")
    try:
        with urllib.request.urlopen(req, timeout=25) as r:
            raw = r.read().decode()
            return r.status, (json.loads(raw) if raw else {})
    except urllib.error.HTTPError as e:
        raw = e.read().decode()
        try:
            return e.code, json.loads(raw)
        except Exception:  # noqa: BLE001
            return e.code, {"message": raw[:300]}
    except Exception as e:  # noqa: BLE001
        return 0, {"message": str(e)[:200]}


def find_contact(token, email, dry=None):
    st, data = _hs("POST", "/crm/v3/objects/contacts/search", token, {
        "filterGroups": [{"filters": [{"propertyName": "email", "operator": "EQ", "value": email}]}],
        "properties": ["email"], "limit": 1,
    }, dry=dry)
    if dry is not None:
        return "<contactId>"
    if st == 200:
        results = data.get("results") or []
        return results[0]["id"] if results else None
    return None


def create_contact(token, email, dry=None):
    st, data = _hs("POST", "/crm/v3/objects/contacts", token, {"properties": {"email": email}}, dry=dry)
    if dry is not None:
        return "<newContactId>"
    if st in (200, 201):
        return data.get("id")
    # 409 = already exists; HubSpot returns the existing id in the error message
    if st == 409:
        msg = data.get("message", "")
        for tok in msg.replace(".", " ").split():
            if tok.isdigit():
                return tok
    return None


def create_note(token, text, contact_id, deal_id=None, dry=None):
    assoc = [{
        "to": {"id": contact_id},
        "types": [{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": ASSOC_NOTE_TO_CONTACT}],
    }]
    if deal_id:
        assoc.append({
            "to": {"id": deal_id},
            "types": [{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": ASSOC_NOTE_TO_DEAL}],
        })
    body = {
        "properties": {
            "hs_note_body": _html_body(text),
            "hs_timestamp": int(time.time() * 1000),  # required; ms epoch
        },
        "associations": assoc,
    }
    st, data = _hs("POST", "/crm/v3/objects/notes", token, body, dry=dry)
    if dry is not None:
        return "<noteId>", 200, data
    return data.get("id"), st, data


def _portal_id(token, dry=None):
    if dry is not None:
        return None
    st, data = _hs("GET", "/account-info/v3/details", token)
    return data.get("portalId") if st == 200 else None


def run(rec_dir, key, token, contact_email, deal_id="", create_missing=False, dry_run=False):
    if not contact_email:
        return {"ok": False, "error": "need_contact_email"}
    if not token and not dry_run:
        return {"ok": False, "error": "no_hubspot_token"}
    diary_path = os.path.join(os.path.expanduser(rec_dir), "diary.json")
    if not os.path.exists(diary_path):
        return {"ok": False, "error": "no diary yet (organize your recordings first)"}
    try:
        diary = json.load(open(diary_path))
    except Exception as e:  # noqa: BLE001
        return {"ok": False, "error": "could not read diary.json: %s" % (str(e)[:120])}
    cv, day = _find_conv(diary, key)
    if not cv:
        return {"ok": False, "error": "conversation not found: %s" % key}
    text = format_crm(cv, day)

    dry = [] if dry_run else None
    contact_id = find_contact(token, contact_email, dry=dry)
    if not contact_id:
        if create_missing:
            contact_id = create_contact(token, contact_email, dry=dry)
            if not contact_id:
                return {"ok": False, "error": "could_not_create_contact"}
        else:
            return {"ok": False, "error": "contact_not_found", "email": contact_email}
    note_id, st, data = create_note(token, text, contact_id, deal_id or None, dry=dry)

    if dry_run:
        return {"ok": True, "dry_run": True, "note_body": text, "requests": dry,
                "note_body_html": _html_body(text)}
    if st not in (200, 201) or not note_id:
        return {"ok": False, "error": "note_create_failed", "status": st,
                "detail": (data or {}).get("message", "")[:200]}
    pid = _portal_id(token)
    url = ("https://app.hubspot.com/contacts/%s/record/0-1/%s" % (pid, contact_id)) if pid else ""
    return {"ok": True, "note_id": note_id, "contact_id": contact_id, "title": _clean(cv.get("title")), "url": url}


def main(argv):
    def opt(name, default=""):
        return argv[argv.index(name) + 1] if name in argv else default
    token = opt("--token") or os.environ.get("HUBSPOT_TOKEN", "")
    res = run(
        rec_dir=opt("--rec-dir"),
        key=opt("--conv-key"),
        token=token,
        contact_email=opt("--contact-email"),
        deal_id=opt("--deal-id"),
        create_missing=("--create-contact" in argv),
        dry_run=("--dry-run" in argv),
    )
    sys.stdout.write(json.dumps(res))


if __name__ == "__main__":
    main(sys.argv[1:])
