"""Durable account-bound conversation registry."""

from __future__ import annotations

import fcntl
import json
import os
import re
import unicodedata
from dataclasses import dataclass
from pathlib import Path

STATE = Path.home() / ".overdeck" / "gptbridge"
PATH = STATE / "conversations.jsonl"
LOCK = STATE / "conversations.lock"
CONVERSATION_ID_RE = re.compile(
    r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")


class RegistryError(RuntimeError):
    pass


@dataclass(frozen=True)
class Conversation:
    account: str
    conversation_id: str
    title: str
    url: str = ""

    def record(self) -> dict:
        data = {"account": self.account, "conversation_id": self.conversation_id,
                "title": self.title}
        if self.url:
            data["url"] = self.url
        return data


def _has_control(value: str) -> bool:
    return any(unicodedata.category(char) == "Cc" for char in value)


def _canonical_conversation_id(value: object, line: int) -> str:
    if not isinstance(value, str) or _has_control(value):
        raise RegistryError(f"conversation registry line {line} has invalid conversation_id")
    conversation_id = value.strip().lower()
    if not CONVERSATION_ID_RE.fullmatch(conversation_id):
        raise RegistryError(f"conversation registry line {line} has invalid conversation_id")
    return conversation_id


def _record(value: object, line: int) -> Conversation:
    if not isinstance(value, dict):
        raise RegistryError(f"conversation registry line {line} is not an object")
    raw_account = value.get("account")
    raw_title = value.get("title")
    url = value.get("url", "")
    fields = {"account": raw_account, "title": raw_title, "url": url}
    for name, item in fields.items():
        if not isinstance(item, str):
            raise RegistryError(f"conversation registry line {line} has invalid {name}")
        if _has_control(item):
            raise RegistryError(f"conversation registry line {line} {name} contains a control character")
    account = raw_account.strip().lower()
    title = raw_title.strip()
    url = url.strip()
    if not account or not title:
        raise RegistryError(f"conversation registry line {line} lacks account, conversation_id, or title")
    conversation_id = _canonical_conversation_id(value.get("conversation_id"), line)
    return Conversation(account, conversation_id, title, url)


def _private_descriptor(path: Path, flags: int) -> int:
    descriptor = os.open(path, flags, 0o600)
    try:
        os.fchmod(descriptor, 0o600)
    except BaseException:
        os.close(descriptor)
        raise
    return descriptor


def _read_unlocked() -> list[Conversation]:
    if not PATH.exists():
        return []
    descriptor = _private_descriptor(PATH, os.O_RDONLY)
    with os.fdopen(descriptor, "r", encoding="utf-8") as source:
        lines = source.read().splitlines()
    latest: dict[str, Conversation] = {}
    for number, raw in enumerate(lines, start=1):
        if not raw.strip():
            continue
        try:
            item = _record(json.loads(raw), number)
        except json.JSONDecodeError as exc:
            raise RegistryError(f"conversation registry line {number} is invalid JSON") from exc
        old = latest.get(item.conversation_id)
        if old is not None and old.account != item.account:
            raise RegistryError(
                f"conversation registry assigns {item.conversation_id} to both {old.account} and {item.account}")
        latest.pop(item.conversation_id, None)
        latest[item.conversation_id] = item
    return list(reversed(latest.values()))


def _held(exclusive: bool):
    STATE.mkdir(mode=0o700, parents=True, exist_ok=True)
    descriptor = _private_descriptor(LOCK, os.O_RDWR | os.O_CREAT)
    handle = os.fdopen(descriptor, "a+", encoding="utf-8")
    fcntl.flock(handle, fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH)
    return handle


def conversations() -> list[Conversation]:
    handle = _held(False)
    try:
        return _read_unlocked()
    finally:
        fcntl.flock(handle, fcntl.LOCK_UN)
        handle.close()


def lookup(conversation_id: str) -> Conversation | None:
    canonical = _canonical_conversation_id(conversation_id, 0)
    return next((item for item in conversations() if item.conversation_id == canonical), None)


def append(account: str, conversation_id: str, title: str, url: str = "") -> Conversation:
    item = _record({"account": account, "conversation_id": conversation_id,
                    "title": title, "url": url}, 0)
    handle = _held(True)
    try:
        old = next((row for row in _read_unlocked() if row.conversation_id == item.conversation_id), None)
        if old is not None and old.account != item.account:
            raise RegistryError(
                f"conversation {item.conversation_id} belongs to {old.account}, not {item.account}")
        descriptor = _private_descriptor(PATH, os.O_WRONLY | os.O_APPEND | os.O_CREAT)
        with os.fdopen(descriptor, "a", encoding="utf-8") as output:
            output.write(json.dumps(item.record(), separators=(",", ":")) + "\n")
            output.flush()
            os.fsync(output.fileno())
        return item
    finally:
        fcntl.flock(handle, fcntl.LOCK_UN)
        handle.close()
