#!/usr/bin/env python3
"""Extract a claudelives.com conversation from a saved live-DOM HTML file.

The site renders user turns as <div> and assistant turns as <pre>, as direct
children of rows inside <div class="messages ...">. twind rewrites class names
to hashed atoms on save, so extraction keys on structure, not class names:
a row containing a <pre> is an assistant turn; otherwise it's a user turn.
Copy buttons contain only SVG (no text nodes), so they add nothing.

Usage: python3 extract_from_saved_html.py file.html > out.json
"""
import json
import sys
from datetime import datetime, timezone
from html.parser import HTMLParser

VOID = {"area", "base", "br", "col", "embed", "hr", "img", "input",
        "link", "meta", "param", "source", "track", "wbr"}


class Node:
    def __init__(self, tag, attrs, parent):
        self.tag = tag
        self.attrs = dict(attrs)
        self.parent = parent
        self.children = []  # Node or str

    def classes(self):
        return (self.attrs.get("class") or "").split()

    def text(self):
        out = []
        for c in self.children:
            out.append(c if isinstance(c, str) else c.text())
        return "".join(out)

    def find_all(self, pred, out=None):
        if out is None:
            out = []
        for c in self.children:
            if isinstance(c, Node):
                if pred(c):
                    out.append(c)
                c.find_all(pred, out)
        return out


class TreeBuilder(HTMLParser):
    def __init__(self):
        super().__init__(convert_charrefs=True)
        self.root = Node("#root", [], None)
        self.cur = self.root

    def handle_starttag(self, tag, attrs):
        node = Node(tag, attrs, self.cur)
        self.cur.children.append(node)
        if tag not in VOID:
            self.cur = node

    def handle_startendtag(self, tag, attrs):
        self.cur.children.append(Node(tag, attrs, self.cur))

    def handle_endtag(self, tag):
        # close nearest matching open ancestor (tolerates minor malformation)
        n = self.cur
        while n is not self.root and n.tag != tag:
            n = n.parent
        if n is not self.root:
            self.cur = n.parent

    def handle_data(self, data):
        # skip text inside script/style so app source never leaks into turns
        n = self.cur
        while n is not None:
            if n.tag in ("script", "style"):
                return
            n = n.parent
        self.cur.children.append(data)


def extract(path):
    with open(path, encoding="utf-8", errors="replace") as f:
        html = f.read()
    tb = TreeBuilder()
    tb.feed(html)

    containers = tb.root.find_all(
        lambda n: n.tag == "div" and "messages" in n.classes())
    if len(containers) != 1:
        raise SystemExit(f"expected 1 .messages container, found {len(containers)}")

    messages = []
    for row in containers[0].children:
        if not isinstance(row, Node) or row.tag != "div":
            continue
        pres = row.find_all(lambda n: n.tag == "pre")
        if pres:
            messages.append({"role": "assistant", "text": pres[0].text()})
        else:
            divs = row.find_all(lambda n: n.tag == "div")
            if divs:
                messages.append({"role": "user", "text": divs[0].text()})
    return {
        "venue": "https://claudelives.com/ — free public Claude 3 Sonnet interface",
        "source_file": path.split("/")[-1],
        "extracted_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
        "extraction": "structural parse of saved live DOM (assistant=<pre>, user=<div>); see extract_from_saved_html.py",
        "messages": messages,
    }


if __name__ == "__main__":
    print(json.dumps(extract(sys.argv[1]), ensure_ascii=False, indent=2))
