"""Pull Granola meetings via API (using decrypted local WorkOS token) into Darsh's Vault."""
from __future__ import annotations

import gzip
import html
import json
import re
import sys
import time
import urllib.error
import urllib.request
from html.parser import HTMLParser
from pathlib import Path

from granola_decrypt import (
    VAULT_MEETINGS,
    decrypt_granola_files,
    GRANOLA_DIR,
    pm_to_md,
    safe_filename,
    parse_dt,
)

API_BASE = "https://api.granola.ai"
USER_AGENT = "Granola/7.155.1 (Windows)"


class _HTMLToMD(HTMLParser):
    def __init__(self) -> None:
        super().__init__(convert_charrefs=True)
        self.parts: list[str] = []
        self._li_depth = 0
        self._skip = False

    def handle_starttag(self, tag, attrs):
        if tag in ("script", "style"):
            self._skip = True
            return
        if tag in ("h1", "h2", "h3", "h4", "h5", "h6"):
            level = int(tag[1])
            self.parts.append("\n\n" + "#" * level + " ")
        elif tag == "p":
            self.parts.append("\n\n")
        elif tag == "br":
            self.parts.append("\n")
        elif tag == "ul":
            self.parts.append("\n")
        elif tag == "ol":
            self.parts.append("\n")
        elif tag == "li":
            self.parts.append("\n" + ("  " * self._li_depth) + "- ")
            self._li_depth += 1
        elif tag in ("strong", "b"):
            self.parts.append("**")
        elif tag in ("em", "i"):
            self.parts.append("*")
        elif tag == "code":
            self.parts.append("`")
        elif tag == "a":
            href = dict(attrs).get("href", "")
            self.parts.append("[")
            self._href = href
        elif tag == "blockquote":
            self.parts.append("\n\n> ")

    def handle_endtag(self, tag):
        if tag in ("script", "style"):
            self._skip = False
            return
        if tag in ("h1", "h2", "h3", "h4", "h5", "h6", "p"):
            self.parts.append("\n\n")
        elif tag == "li":
            self._li_depth = max(0, self._li_depth - 1)
        elif tag in ("strong", "b"):
            self.parts.append("**")
        elif tag in ("em", "i"):
            self.parts.append("*")
        elif tag == "code":
            self.parts.append("`")
        elif tag == "a":
            href = getattr(self, "_href", "")
            self.parts.append(f"]({href})" if href else "]")

    def handle_data(self, data):
        if self._skip:
            return
        self.parts.append(data)

    def get_md(self) -> str:
        text = "".join(self.parts)
        text = html.unescape(text)
        text = re.sub(r"\n{3,}", "\n\n", text)
        return text.strip()


def html_to_md(raw: str) -> str:
    parser = _HTMLToMD()
    parser.feed(raw)
    parser.close()
    return parser.get_md()


def load_access_token() -> str:
    _, sb = decrypt_granola_files(GRANOLA_DIR)
    wt = sb.get("workos_tokens")
    if isinstance(wt, str):
        wt = json.loads(wt)
    token = (wt or {}).get("access_token")
    if not token:
        raise RuntimeError("No access_token in decrypted supabase.json.enc")
    return token


def api_post(endpoint: str, body: dict | None, token: str, timeout: int = 30):
    url = API_BASE + endpoint
    data = json.dumps(body or {}).encode()
    req = urllib.request.Request(
        url,
        data=data,
        headers={
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
            "Accept": "application/json",
            "Accept-Encoding": "gzip",
            "User-Agent": USER_AGENT,
        },
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            raw = resp.read()
            if resp.headers.get("Content-Encoding") == "gzip":
                raw = gzip.decompress(raw)
            text = raw.decode("utf-8", errors="replace")
    except urllib.error.HTTPError as e:
        try:
            err = e.read()
            if e.headers.get("Content-Encoding") == "gzip":
                err = gzip.decompress(err)
            err_text = err.decode("utf-8", errors="replace")
        except Exception:
            err_text = ""
        raise RuntimeError(f"HTTP {e.code} {endpoint}: {err_text[:500]}") from e
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        return text


def list_all_documents(token: str) -> list[dict]:
    all_docs: list[dict] = []
    offset = 0
    page_size = 100
    while True:
        page = api_post("/v2/get-documents", {"limit": page_size, "offset": offset}, token)
        docs = page.get("docs") or []
        all_docs.extend(docs)
        print(f"  fetched page offset={offset} count={len(docs)}")
        if len(docs) < page_size:
            break
        offset += page_size
        time.sleep(0.2)
    seen = set()
    unique = []
    for d in all_docs:
        did = d.get("id")
        if did in seen:
            continue
        seen.add(did)
        unique.append(d)
    return unique


def get_transcript(doc_id: str, token: str) -> list:
    out = api_post("/v1/get-document-transcript", {"document_id": doc_id}, token)
    return out if isinstance(out, list) else []


def get_panels(doc_id: str, token: str) -> list:
    out = api_post("/v1/get-document-panels", {"document_id": doc_id}, token)
    return out if isinstance(out, list) else []


def content_to_md(content) -> str:
    if content is None:
        return ""
    if isinstance(content, (dict, list)):
        return pm_to_md(content).strip()
    if isinstance(content, str):
        s = content.strip()
        if not s:
            return ""
        if s.startswith("{") or s.startswith("["):
            try:
                return pm_to_md(json.loads(s)).strip()
            except Exception:
                pass
        if "<" in s and any(tag in s for tag in ("<h", "<p", "<ul", "<li", "<div")):
            return html_to_md(s)
        return s
    return str(content).strip()


def transcript_to_md(segs: list) -> str:
    lines = []
    for seg in segs:
        if isinstance(seg, str):
            lines.append(seg)
            continue
        if not isinstance(seg, dict):
            continue
        speaker = seg.get("speaker") or seg.get("source") or seg.get("name") or ""
        text = seg.get("text") or seg.get("content") or seg.get("transcript") or ""
        if not text:
            continue
        lines.append(f"**{speaker}**: {text}" if speaker else text)
    return "\n\n".join(lines)


def panels_to_md(panels: list) -> str:
    parts = []
    for panel in panels:
        if not isinstance(panel, dict):
            continue
        title = panel.get("title") or panel.get("name") or "Summary"
        md = content_to_md(panel.get("content") or panel.get("original_content"))
        if md:
            # Avoid duplicating a top-level heading if content already starts with one
            parts.append(f"## {title}\n\n{md}")
    return "\n\n".join(parts)


def notes_field_to_md(doc: dict) -> str:
    parts = []
    for key in ("notes_markdown", "notes_plain", "notes", "summary"):
        val = doc.get(key)
        if not val:
            continue
        md = content_to_md(val)
        if md:
            parts.append(md)
            break
    return "\n\n".join(parts)


def extract_people(doc: dict) -> list[str]:
    names: list[str] = []
    people = doc.get("people")
    if isinstance(people, dict):
        creator = people.get("creator") or {}
        if isinstance(creator, dict) and creator.get("name"):
            names.append(str(creator["name"]).strip())
        for att in people.get("attendees") or []:
            if isinstance(att, str) and att.strip():
                names.append(att.strip())
            elif isinstance(att, dict):
                name = att.get("name") or att.get("email") or ""
                if not name and isinstance(att.get("details"), dict):
                    person = (att["details"].get("person") or {}).get("name") or {}
                    name = person.get("fullName") or ""
                if name:
                    names.append(str(name).strip())
    elif isinstance(people, list):
        for p in people:
            if isinstance(p, str):
                names.append(p.strip())
            elif isinstance(p, dict) and p.get("name"):
                names.append(str(p["name"]).strip())
    # unique preserve order
    seen = set()
    out = []
    for n in names:
        if n and n not in seen:
            seen.add(n)
            out.append(n)
    return out


_GENERIC_TITLES = {
    "summary",
    "notes",
    "transcript",
    "action items",
    "overview",
    "meeting summary",
    "private notes",
}


def derive_title(doc: dict, notes_md: str) -> str:
    title = (doc.get("title") or "").strip()
    if title:
        return title
    # Prefer first non-generic markdown heading in notes/panels
    for m in re.finditer(r"^#{1,4}\s+(.+)$", notes_md, re.M):
        candidate = m.group(1).strip()
        if candidate.lower() not in _GENERIC_TITLES:
            return candidate
    dt = parse_dt(doc.get("created_at")) or parse_dt(doc.get("updated_at"))
    if dt:
        return f"Meeting {dt.strftime('%Y-%m-%d')}"
    return "Untitled meeting"


def existing_by_granola_id(folder: Path) -> dict[str, Path]:
    mapping: dict[str, Path] = {}
    if not folder.exists():
        return mapping
    for p in folder.glob("*.md"):
        try:
            text = p.read_text(encoding="utf-8", errors="ignore")
        except Exception:
            continue
        m = re.search(r"^granola_id:\s*(\S+)\s*$", text, re.M)
        if m and m.group(1):
            mapping[m.group(1)] = p
    return mapping


def render(doc: dict, title: str, people: list[str], notes_md: str, transcript_md: str) -> tuple[str, str]:
    dt = parse_dt(doc.get("created_at")) or parse_dt(doc.get("updated_at"))
    date_str = dt.strftime("%Y-%m-%d") if dt else "undated"
    file_date = dt.strftime("%m-%d-%Y") if dt else "undated"

    front = [
        "---",
        "categories:",
        '  - "[[Meetings]]"',
        "type: []",
        f"date: {date_str}",
        "org:",
        "loc:",
        "people:",
    ]
    if people:
        for name in people:
            front.append(f'  - "[[{name}]]"')
    else:
        front.append("  []")
    front += [
        "topics: []",
        f"granola_id: {doc.get('id') or ''}",
        "source: granola",
        "---",
        "",
        f"# {title}",
        "",
    ]
    body_parts = []
    if notes_md:
        body_parts.append("## Notes\n\n" + notes_md)
    if transcript_md:
        body_parts.append("## Transcript\n\n" + transcript_md)
    if not body_parts:
        body_parts.append("_No notes or transcript returned from Granola._")
    content = "\n".join(front) + "\n\n".join(body_parts).strip() + "\n"
    filename = f"{file_date} - {safe_filename(title)}.md"
    return filename, content


def main() -> int:
    include_transcripts = "--no-transcripts" not in sys.argv
    force = "--force" in sys.argv
    limit = None
    for arg in sys.argv[1:]:
        if arg.startswith("--limit="):
            limit = int(arg.split("=", 1)[1])

    print("Loading WorkOS token from encrypted Granola store...")
    token = load_access_token()
    print("Listing documents...")
    docs = list_all_documents(token)
    print(f"Total documents: {len(docs)}")
    if limit is not None:
        docs = docs[:limit]
        print(f"Limiting to first {limit}")

    VAULT_MEETINGS.mkdir(parents=True, exist_ok=True)
    by_id = existing_by_granola_id(VAULT_MEETINGS)
    written = 0
    skipped = 0
    updated = 0
    errors = 0

    for i, doc in enumerate(docs, 1):
        doc_id = doc.get("id") or ""
        try:
            panels = get_panels(doc_id, token) if doc_id else []
            time.sleep(0.12)
            notes_md = panels_to_md(panels)
            inline = notes_field_to_md(doc)
            if inline:
                notes_md = (notes_md + "\n\n" + inline).strip() if notes_md else inline

            transcript_md = ""
            if include_transcripts and doc_id:
                segs = get_transcript(doc_id, token)
                time.sleep(0.12)
                transcript_md = transcript_to_md(segs)

            title = derive_title(doc, notes_md)
            people = extract_people(doc)
            filename, content = render(doc, title, people, notes_md, transcript_md)

            existing = by_id.get(doc_id)
            if existing and not force:
                skipped += 1
                print(f"[{i}/{len(docs)}] skip existing {existing.name}")
                continue

            path = VAULT_MEETINGS / filename
            if path.exists() and (not existing or existing.resolve() != path.resolve()) and doc_id:
                path = VAULT_MEETINGS / f"{path.stem} ({doc_id[:8]}).md"

            if existing and force:
                if existing.resolve() != path.resolve():
                    existing.unlink(missing_ok=True)
                path.write_text(content, encoding="utf-8")
                by_id[doc_id] = path
                updated += 1
                print(f"[{i}/{len(docs)}] updated {path.name}")
            else:
                path.write_text(content, encoding="utf-8")
                by_id[doc_id] = path
                written += 1
                print(
                    f"[{i}/{len(docs)}] wrote {path.name} "
                    f"(notes={bool(notes_md)} transcript={bool(transcript_md)})"
                )
        except Exception as e:
            errors += 1
            print(f"[{i}/{len(docs)}] ERROR {doc.get('title') or doc_id}: {e}")

    print(
        f"\nDone. wrote={written} updated={updated} skipped={skipped} "
        f"errors={errors} vault={VAULT_MEETINGS}"
    )
    return 0 if errors == 0 else 1


if __name__ == "__main__":
    raise SystemExit(main())
