import json
import shlex
import urllib.request
import urllib.error
import urllib.parse
import http.client
from contextlib import closing
from datetime import datetime

API = "http://127.0.0.1:7001/v1"


def response_error(status, response):
    try:
        error = json.load(response)["error"]
        return RuntimeError(f"HTTP {status} {error['code']}: {error['message']}")
    except (ValueError, KeyError, TypeError):
        return RuntimeError(f"HTTP {status}: unexpected error response")


def call(method, path, body=None):
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(
        API + path, data=data, method=method,
        headers={"content-type": "application/json"} if data is not None else {},
    )
    try:
        with urllib.request.urlopen(req) as res:
            return json.load(res) if res.status != 204 else None
    except urllib.error.HTTPError as exc:
        with exc:
            raise response_error(exc.code, exc) from None


if not call("GET", "/auth")["authorized"]:
    raise RuntimeError("Create or select an account before running this example.")

SPACE = call("POST", "/spaces", {"name": "Python notebook"})["id"]
OBJ = call("POST", f"/spaces/{SPACE}/objects", {
    "type": "page", "initialProperties": {"any": {"name": "Reading list"}},
})["objectId"]
print("Space:", SPACE, "Object:", OBJ)

query = {"filter": {"any.type": "page"}, "sort": ["-modifiedAt", "id"], "limit": 20}
page = call("POST", f"/spaces/{SPACE}/objects/query", {**query, "includeTotal": True})
print("Pages:", page["total"], [r["any"]["name"] for r in page["records"]])


def display(records):
    def order(row):
        modified = datetime.fromisoformat(row["modifiedAt"]["$date"].replace("Z", "+00:00"))
        return (-modified.timestamp(), row["id"])
    print("Live:", [r["any"]["name"] for r in sorted(records, key=order)])

setup = call("POST", "/catalog/general-chat/setup", {"spaceId": SPACE})
CHAT = setup["bundles"][0]["bundle"]["rootId"]
receipt = call("POST", f"/spaces/{SPACE}/objects/{CHAT}/chat/messages", {"text": "hello"})
print("Message:", receipt["recordIds"][0])

msgs = call("POST", f"/spaces/{SPACE}/query", {
    "objectId": CHAT, "dataset": "chat_messages", "sort": ["-_ver.id"], "limit": 50,
})
print("Messages:", len(msgs["records"]))

if msgs["records"]:
    oldest = msgs["records"][-1]["_ver"]["id"]
    older = call("POST", f"/spaces/{SPACE}/query", {
        "objectId": CHAT, "dataset": "chat_messages", "sort": ["-_ver.id"], "limit": 50,
        "filter": {"_ver.id": {"$lt": oldest}},
    })
    print("Older messages:", len(older["records"]))

def sse(path, body):
    url = urllib.parse.urlsplit(API + path)
    conn = http.client.HTTPConnection(url.hostname, url.port)
    res = None
    try:
        target = url.path + ("?" + url.query if url.query else "")
        conn.request("POST", target, body=json.dumps(body), headers={
            "content-type": "application/json", "accept": "text/event-stream",
        })
        res = conn.getresponse()
        if res.status != 200:
            raise response_error(res.status, res)
        event, data = "message", []
        for raw in res:
            line = raw.decode().rstrip("\r\n")
            if line.startswith("event:"):
                event = line[6:].strip()
            elif line.startswith("data:"):
                data.append(line[5:].removeprefix(" "))
            elif line == "":
                if data:
                    yield event, json.loads("\n".join(data))
                event, data = "message", []
        if data:
            yield event, json.loads("\n".join(data))
    finally:
        if res is not None:
            res.close()
        conn.close()

rename = ["curl", "-fsS", f"{API}/spaces/{SPACE}/properties/{OBJ}/set/any",
          "-H", "content-type: application/json", "-d",
          json.dumps({"patch": {"name": "Reading list 2026"}})]
print("After the first Live output, paste this command into another terminal:")
print(shlex.join(rename), flush=True)

window = {}
try:
    with closing(sse(f"/spaces/{SPACE}/objects/query/subscribe", query)) as frames:
        for event, data in frames:
            if event == "snapshot":
                window = {r["id"]: r for r in data["records"]}
            elif event == "changes":
                for change in data:
                    for row in change.get("added", []) + change.get("updated", []):
                        window[row["id"]] = row["doc"]
                    for row in change.get("removed", []):
                        window.pop(row["id"], None)
            elif event == "closed":
                print("Subscription closed:", data["reason"])
                break
            else:
                continue
            display(window.values())
except KeyboardInterrupt:
    print("Stopped.")

