#!/usr/bin/env python3
"""Minimal lobstrd prototype (v0 draft).

Zero-dependency HTTP server implementing a subset of docs/specs/LOBSTRD_API_V0_DRAFT.md.
"""

from __future__ import annotations

import hashlib
import hmac
import json
import os
import secrets
import time
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Any, Dict, List, Optional

from lobstr.core.nostr_crypto import (
    pubkey_gen_xonly,
    schnorr_sign,
    schnorr_verify,
)
from lobstr.core.relay_client import (
    FetchResult,
    RelayClient,
    RelayFailureDetail,
    RelayPool,
    RelayPublishResult,
    fetch_nip11_info,
    make_relay_client,
    ws_to_http_url,
)

def _is_hex(s: str, length: int) -> bool:
    if not isinstance(s, str) or len(s) != length:
        return False
    try:
        int(s, 16)
        return True
    except ValueError:
        return False


def _json_dumps(obj: Any) -> str:
    return json.dumps(obj, separators=(",", ":"), sort_keys=False)


def _now() -> int:
    return int(time.time())


def _compute_event_id(pubkey: str, created_at: int, kind: int, tags: List[List[str]], content: str) -> str:
    preimage = [0, pubkey, created_at, kind, tags, content]
    return hashlib.sha256(_json_dumps(preimage).encode("utf-8")).hexdigest()


class LobstrState:
    def __init__(self) -> None:
        secret_hex = os.environ.get("LOBSTRD_SECRET_KEY", "")
        if secret_hex and _is_hex(secret_hex, 64):
            self.secret_key = bytes.fromhex(secret_hex)
        else:
            self.secret_key = secrets.token_bytes(32)
            print(f"WARNING: LOBSTRD_SECRET_KEY not set — using ephemeral random key. "
                  f"Set LOBSTRD_SECRET_KEY to a 64-char hex string for a stable identity.")
        self.pubkey = pubkey_gen_xonly(self.secret_key).hex()
        self.relay_url = os.environ.get("LOBSTRD_RELAY_URL", "wss://relay.example")
        relay_urls_env = os.environ.get("LOBSTRD_RELAY_URLS", "")
        self.relay_urls: List[str] = (
            [u.strip() for u in relay_urls_env.split(",") if u.strip()]
            if relay_urls_env else [self.relay_url]
        )
        self.relay_url = self.relay_urls[0]
        self.fanout_min_success = int(os.environ.get("LOBSTRD_FANOUT_MIN_SUCCESS", "1"))
        self.relay_backend = os.environ.get("LOBSTRD_RELAY_BACKEND", "memory")
        self.relay_client: RelayClient = make_relay_client(
            self.relay_backend, self.relay_url,
            relay_urls=self.relay_urls,
            fanout_min_success=self.fanout_min_success,
        )
        self.last_relay_error: Optional[str] = None
        self.events: List[Dict[str, Any]] = []
        self.seen_event_ids: set[str] = set()
        self.subscriptions: Dict[str, Dict[str, Any]] = {}
        # Presence state
        self.presence_profile: Optional[Dict[str, Any]] = None
        self.presence_announced: bool = False
        self.presence_last_announced_at: Optional[int] = None
        self.presence_event_ids: Dict[str, Optional[str]] = {}

    def sign_event(self, kind: int, tags: List[List[str]], content: str) -> Dict[str, Any]:
        created_at = _now()
        event_id = _compute_event_id(self.pubkey, created_at, kind, tags, content)
        sig = schnorr_sign(bytes.fromhex(event_id), self.secret_key).hex()
        if not schnorr_verify(bytes.fromhex(event_id), bytes.fromhex(self.pubkey), bytes.fromhex(sig)):
            raise ValueError("generated signature failed verification")
        return {
            "id": event_id,
            "pubkey": self.pubkey,
            "created_at": created_at,
            "kind": kind,
            "tags": tags,
            "content": content,
            "sig": sig,
        }

    def set_relay_url(self, relay_url: str) -> None:
        self.relay_url = relay_url
        self.relay_client.set_relay_url(relay_url)

    def ingest_event(self, event: Dict[str, Any]) -> bool:
        err = _validate_signed_event(event)
        if err:
            return False
        recomputed = _compute_event_id(
            event["pubkey"],
            event["created_at"],
            event["kind"],
            event["tags"],
            event["content"],
        )
        if recomputed != event["id"]:
            return False
        if not schnorr_verify(
            bytes.fromhex(event["id"]),
            bytes.fromhex(event["pubkey"]),
            bytes.fromhex(event["sig"]),
        ):
            return False
        if event["id"] in self.seen_event_ids:
            return False
        self.seen_event_ids.add(event["id"])
        self.events.append(_normalize_event(event))
        return True

    def sync_from_relay(self, local_filter: Dict[str, Any]) -> Optional[FetchResult]:
        nostr_filter = _to_nostr_filter(local_filter)
        if isinstance(self.relay_client, RelayPool):
            result = self.relay_client.fetch_with_status(nostr_filter)
            for ev in result.events:
                self.ingest_event(ev)
            return result
        events = self.relay_client.fetch(nostr_filter)
        for ev in events:
            self.ingest_event(ev)
        return None


STATE = LobstrState()
AUTH_TOKEN = os.environ.get("LOBSTRD_AUTH_TOKEN", "")


def _parse_extension(content: str) -> Optional[Dict[str, Any]]:
    try:
        obj = json.loads(content)
    except (ValueError, TypeError):
        return None
    if not isinstance(obj, dict):
        return None
    required = ["spec", "type", "created_at", "content_type", "content"]
    if any(k not in obj for k in required):
        return None
    if obj.get("spec") != "lobstr/0":
        return None
    if not isinstance(obj.get("type"), str):
        return None
    if not isinstance(obj.get("created_at"), int):
        return None
    if not isinstance(obj.get("content_type"), str):
        return None
    return obj


def _normalize_event(event: Dict[str, Any]) -> Dict[str, Any]:
    return {
        "event_id": event["id"],
        "relay_url": STATE.relay_url,
        "received_at": _now(),
        "event": {
            "kind": event["kind"],
            "pubkey": event["pubkey"],
            "created_at": event["created_at"],
            "tags": event["tags"],
            "content": event["content"],
        },
        "extension": _parse_extension(event["content"]),
    }


def _validate_event_draft(event: Dict[str, Any]) -> Optional[str]:
    if not isinstance(event, dict):
        return "field 'event' must be an object"
    if "kind" not in event:
        return "field 'event.kind' is required"
    if not isinstance(event["kind"], int):
        return "field 'event.kind' must be an integer"
    if "content" not in event:
        return "field 'event.content' is required"
    if not isinstance(event["content"], str):
        return "field 'event.content' must be a string"
    tags = event.get("tags", [])
    if not isinstance(tags, list):
        return "field 'event.tags' must be an array"
    for i, tag in enumerate(tags):
        if not isinstance(tag, list) or not all(isinstance(x, str) for x in tag):
            return f"field 'event.tags[{i}]' must be an array of strings"
    return None


def _validate_signed_event(event: Dict[str, Any]) -> Optional[str]:
    if not isinstance(event, dict):
        return "field 'event' must be an object"
    required = ["id", "pubkey", "created_at", "kind", "tags", "content", "sig"]
    for key in required:
        if key not in event:
            return f"field 'event.{key}' is required"

    if not _is_hex(event["id"], 64):
        return "field 'event.id' must be 64-char hex"
    if not _is_hex(event["pubkey"], 64):
        return "field 'event.pubkey' must be 64-char hex"
    if not _is_hex(event["sig"], 128):
        return "field 'event.sig' must be 128-char hex"
    if not isinstance(event["created_at"], int):
        return "field 'event.created_at' must be an integer"
    if not isinstance(event["kind"], int):
        return "field 'event.kind' must be an integer"
    if not isinstance(event["content"], str):
        return "field 'event.content' must be a string"
    if not isinstance(event["tags"], list):
        return "field 'event.tags' must be an array"
    for i, tag in enumerate(event["tags"]):
        if not isinstance(tag, list) or not all(isinstance(x, str) for x in tag):
            return f"field 'event.tags[{i}]' must be an array of strings"
    return None


def _event_matches_filter(record: Dict[str, Any], filt: Dict[str, Any]) -> bool:
    ev = record["event"]
    ext = record["extension"]

    kinds = filt.get("kinds")
    if kinds and ev["kind"] not in kinds:
        return False

    authors = filt.get("authors")
    if authors and ev["pubkey"] not in authors:
        return False

    since = filt.get("since")
    if since is not None and ev["created_at"] < since:
        return False

    for fk, fv in filt.items():
        if fk.startswith("#") and len(fk) == 2 and isinstance(fv, list):
            tag_name = fk[1]
            tag_values = [t[1] for t in ev["tags"] if len(t) > 1 and t[0] == tag_name]
            if not any(v in fv for v in tag_values):
                return False

    ext_only = bool(filt.get("extension_only", False))
    if ext_only and ext is None:
        return False

    ext_types = filt.get("extension_types")
    if ext_types and (ext is None or ext.get("type") not in ext_types):
        return False

    return True


def _serialize_failed_relays(details: list[RelayFailureDetail]) -> list[Dict[str, Any]]:
    return [
        {"url": d.url, "reason": d.reason, "ok_prefix": d.ok_prefix,
         "nip11_url": ws_to_http_url(d.url)}
        for d in details
    ]


def _relay_error_response(signed: Dict[str, Any], result: RelayPublishResult) -> tuple[int, Dict[str, Any]]:
    """Build a standard relay failure response for publish/send endpoints."""
    STATE.last_relay_error = result.error or "relay publish failed"
    return HTTPStatus.BAD_GATEWAY, {
        "error": {"code": "RELAY_UNAVAILABLE", "message": STATE.last_relay_error, "retryable": True},
        "event_id": signed["id"],
        "published_to": result.published_to,
        "failed_relays": _serialize_failed_relays(result.failed_relays),
    }


def _error(status: int, code: str, message: str, retryable: bool = False) -> tuple[int, Dict[str, Any]]:
    return status, {"error": {"code": code, "message": message, "retryable": retryable}}


# ---------------------------------------------------------------------------
# Presence helpers
# ---------------------------------------------------------------------------


def _validate_presence_profile(body: Optional[Dict[str, Any]]) -> Optional[str]:
    """Validate required fields for a profile set via API. Returns error string or None."""
    if not isinstance(body, dict):
        return "request body must be a JSON object"
    name = body.get("name")
    if not isinstance(name, str) or not name:
        return "field 'name' is required and must be a non-empty string"
    about = body.get("about")
    if not isinstance(about, str) or not about:
        return "field 'about' is required and must be a non-empty string"
    caps = body.get("capabilities")
    if not isinstance(caps, list) or len(caps) == 0 or not all(isinstance(c, str) for c in caps):
        return "field 'capabilities' must be a non-empty array of strings"
    return None


def _build_presence_kind0(profile: Dict[str, Any]) -> Dict[str, Any]:
    """Build and sign a kind 0 Nostr profile event from presence profile."""
    content: Dict[str, Any] = {
        "name": profile.get("name", ""),
        "about": profile.get("about", ""),
        "bot": True,
    }
    for field in ("picture", "website", "nip05"):
        val = profile.get(field)
        if val:
            content[field] = val
    return STATE.sign_event(0, [], json.dumps(content))


def _build_presence_kind10002() -> Dict[str, Any]:
    """Build and sign a kind 10002 relay list event from current relay config."""
    tags: List[List[str]] = [["r", url] for url in STATE.relay_urls]
    return STATE.sign_event(10002, tags, "")


def _build_presence_kind30078(profile: Dict[str, Any]) -> Dict[str, Any]:
    """Build and sign a kind 30078 Lobstr agent profile event."""
    tags: List[List[str]] = [
        ["d", "lobstr-agent-profile"],
        ["t", "lobstr-agent"],
    ]
    protocols = profile.get("protocols", ["lobstr/0"])
    for proto in protocols:
        tags.append(["t", f"protocol:{proto}"])
    for cap in profile.get("capabilities", []):
        tags.append(["t", f"capability:{cap}"])

    content: Dict[str, Any] = {
        "version": "1",
        "name": profile.get("name", ""),
        "about": profile.get("about", ""),
        "capabilities": profile.get("capabilities", []),
        "lobstr": {
            "supported": protocols,
            "dvm_kinds": profile.get("dvm_kinds", []),
        },
    }
    sw = profile.get("software")
    if isinstance(sw, dict):
        content["software"] = sw
    for field in ("input_modalities", "output_modalities"):
        val = profile.get(field)
        if isinstance(val, list):
            content[field] = val
    nip05 = profile.get("nip05")
    if nip05:
        content["contact"] = {"nip05": nip05}
    return STATE.sign_event(30078, tags, json.dumps(content))


def _build_presence_kind31990(profile: Dict[str, Any]) -> Optional[Dict[str, Any]]:
    """Build and sign a kind 31990 handler advertisement. Returns None if no dvm_kinds."""
    dvm_kinds = profile.get("dvm_kinds")
    if not isinstance(dvm_kinds, list) or len(dvm_kinds) == 0:
        return None
    tags: List[List[str]] = [["d", "openclaw-agent-task"]]
    for k in dvm_kinds:
        tags.append(["k", str(k)])
    tags.append(["t", "lobstr-agent"])

    content: Dict[str, Any] = {
        "name": profile.get("name", ""),
        "about": profile.get("about", ""),
    }
    picture = profile.get("picture")
    if picture:
        content["picture"] = picture
    return STATE.sign_event(31990, tags, json.dumps(content))


def api_health() -> tuple[int, Dict[str, Any]]:
    return HTTPStatus.OK, {"ok": True, "version": "lobstrd/0"}


def api_identity_get() -> tuple[int, Dict[str, Any]]:
    return HTTPStatus.OK, {"pubkey": STATE.pubkey, "npub": "npub-not-implemented", "signing": "local"}


def api_relays_get() -> tuple[int, Dict[str, Any]]:
    relays: List[Dict[str, Any]] = []
    if isinstance(STATE.relay_client, RelayPool):
        for url in STATE.relay_urls:
            entry: Dict[str, Any] = {"url": url, "status": "connected", "last_error": None}
            record = STATE.relay_client.failure_history.get(url)
            if record is not None:
                entry["status"] = "degraded"
                entry["last_failure"] = {
                    "reason": record.reason,
                    "ok_prefix": record.ok_prefix,
                    "timestamp": record.timestamp,
                }
            else:
                entry["last_failure"] = None
            relays.append(entry)
    else:
        status = "connected" if STATE.last_relay_error is None else "degraded"
        relays.append({"url": STATE.relay_url, "status": status, "last_error": STATE.last_relay_error})
    return HTTPStatus.OK, {
        "relays": relays,
        "backend": STATE.relay_backend,
        "fanout_min_success": STATE.fanout_min_success,
    }


def api_relays_put(body: Dict[str, Any]) -> tuple[int, Dict[str, Any]]:
    if body is None or not isinstance(body.get("relays"), list) or not body["relays"]:
        return _error(HTTPStatus.BAD_REQUEST, "INVALID_REQUEST", "field 'relays' must be a non-empty array")
    new_urls = body["relays"]
    new_fanout = body.get("fanout_min_success", STATE.fanout_min_success)
    if not isinstance(new_fanout, int) or new_fanout < 1:
        return _error(HTTPStatus.BAD_REQUEST, "INVALID_REQUEST", "fanout_min_success must be a positive integer")
    if new_fanout > len(new_urls):
        return _error(
            HTTPStatus.BAD_REQUEST, "INVALID_REQUEST",
            f"fanout_min_success ({new_fanout}) cannot exceed number of relays ({len(new_urls)})",
        )
    STATE.relay_urls = new_urls
    STATE.relay_url = new_urls[0]
    STATE.fanout_min_success = new_fanout
    STATE.relay_client = make_relay_client(
        STATE.relay_backend, STATE.relay_url,
        relay_urls=new_urls, fanout_min_success=new_fanout,
    )
    STATE.last_relay_error = None
    return api_relays_get()


def api_relays_info() -> tuple[int, Dict[str, Any]]:
    result = fetch_nip11_info(STATE.relay_url)
    payload: Dict[str, Any] = {
        "ok": result["info"] is not None,
        "relay_url": STATE.relay_url,
        "info": result["info"],
        "fetched_at": _now(),
    }
    if result["error"]:
        payload["error"] = result["error"]
    return HTTPStatus.OK, payload


def api_events_publish(body: Dict[str, Any]) -> tuple[int, Dict[str, Any]]:
    event = body.get("event")
    err = _validate_event_draft(event)
    if err:
        return _error(HTTPStatus.BAD_REQUEST, "INVALID_REQUEST", err)
    tags = event.get("tags", [])
    signed = STATE.sign_event(event["kind"], tags, event["content"])
    result = STATE.relay_client.publish(signed)
    if not result.ok:
        return _relay_error_response(signed, result)
    STATE.last_relay_error = None
    STATE.ingest_event(signed)
    return HTTPStatus.OK, {
        "ok": True,
        "accepted": True,
        "event_id": signed["id"],
        "published_to": result.published_to,
        "failed_relays": _serialize_failed_relays(result.failed_relays),
    }


def api_messages_send(body: Dict[str, Any]) -> tuple[int, Dict[str, Any]]:
    msg = body.get("message")
    if not isinstance(msg, dict):
        return _error(HTTPStatus.BAD_REQUEST, "INVALID_REQUEST", "field 'message' must be an object")
    required = ["spec", "type", "created_at", "content_type", "content"]
    missing = [k for k in required if k not in msg]
    if missing:
        return _error(
            HTTPStatus.BAD_REQUEST,
            "INVALID_REQUEST",
            f"missing required message fields: {', '.join(missing)}",
        )
    if msg.get("spec") != "lobstr/0":
        return _error(HTTPStatus.BAD_REQUEST, "INVALID_REQUEST", "field 'message.spec' must be 'lobstr/0'")

    event_hint = body.get("event", {})
    if not isinstance(event_hint, dict):
        event_hint = {}
    draft = {
        "kind": event_hint.get("kind", 1111),
        "tags": event_hint.get("tags", []),
        "content": _json_dumps(msg),
    }
    err = _validate_event_draft(draft)
    if err:
        return _error(HTTPStatus.BAD_REQUEST, "INVALID_REQUEST", err)

    signed = STATE.sign_event(draft["kind"], draft["tags"], draft["content"])
    result = STATE.relay_client.publish(signed)
    if not result.ok:
        return _relay_error_response(signed, result)
    STATE.last_relay_error = None
    STATE.ingest_event(signed)
    return HTTPStatus.OK, {
        "ok": True,
        "accepted": True,
        "event_id": signed["id"],
        "published_to": result.published_to,
        "failed_relays": _serialize_failed_relays(result.failed_relays),
    }


def api_subscriptions_post(body: Dict[str, Any]) -> tuple[int, Dict[str, Any]]:
    sub_id = body.get("id")
    filt = body.get("filter")
    if not isinstance(sub_id, str) or not sub_id:
        return _error(HTTPStatus.BAD_REQUEST, "INVALID_REQUEST", "field 'id' must be a non-empty string")
    if not isinstance(filt, dict):
        return _error(HTTPStatus.BAD_REQUEST, "INVALID_REQUEST", "field 'filter' must be an object")
    STATE.subscriptions[sub_id] = filt
    return HTTPStatus.OK, {"ok": True, "id": sub_id}


def api_messages_poll(body: Dict[str, Any]) -> tuple[int, Dict[str, Any]]:
    sub_id = body.get("subscription_id")
    max_items = body.get("max_items", 100)
    if not isinstance(sub_id, str) or sub_id not in STATE.subscriptions:
        return _error(HTTPStatus.BAD_REQUEST, "INVALID_REQUEST", "unknown subscription_id")
    if not isinstance(max_items, int) or max_items < 1:
        return _error(HTTPStatus.BAD_REQUEST, "INVALID_REQUEST", "field 'max_items' must be a positive integer")

    filt = STATE.subscriptions[sub_id]
    fetch_result = STATE.sync_from_relay(filt)
    matched = [r for r in STATE.events if _event_matches_filter(r, filt)]
    payload: Dict[str, Any] = {"messages": matched[-max_items:]}
    if fetch_result is not None:
        payload["relay_status"] = {
            "queried": fetch_result.queried,
            "timed_out": fetch_result.timed_out,
        }
    return HTTPStatus.OK, payload


def _to_nostr_filter(local_filter: Dict[str, Any]) -> Dict[str, Any]:
    allowed = ["ids", "authors", "kinds", "since", "until", "limit"]
    nostr_filter: Dict[str, Any] = {}
    for k in allowed:
        if k in local_filter:
            nostr_filter[k] = local_filter[k]
    for k, v in local_filter.items():
        if k.startswith("#"):
            nostr_filter[k] = v
    return nostr_filter


def api_events_verify(body: Dict[str, Any]) -> tuple[int, Dict[str, Any]]:
    event = body.get("event")
    err = _validate_signed_event(event)
    if err:
        return _error(HTTPStatus.BAD_REQUEST, "INVALID_REQUEST", err)

    recomputed_id = _compute_event_id(
        event["pubkey"],
        event["created_at"],
        event["kind"],
        event["tags"],
        event["content"],
    )
    if recomputed_id != event["id"]:
        return HTTPStatus.OK, {
            "ok": False,
            "valid_id": False,
            "valid_sig": False,
            "event_id": event["id"],
            "recomputed_id": recomputed_id,
        }

    valid_sig = schnorr_verify(
        bytes.fromhex(event["id"]),
        bytes.fromhex(event["pubkey"]),
        bytes.fromhex(event["sig"]),
    )
    return HTTPStatus.OK, {
        "ok": bool(valid_sig),
        "valid_id": True,
        "valid_sig": bool(valid_sig),
        "event_id": event["id"],
        "recomputed_id": recomputed_id,
    }


# ---------------------------------------------------------------------------
# Presence hydration
# ---------------------------------------------------------------------------


def _hydrate_presence_from_relays(state: LobstrState) -> None:
    """Recover presence profile from relay events.

    Fetches the agent's own presence events and reconstructs profile state.
    Best-effort: relay timeouts or errors are silently ignored.
    """
    try:
        events = state.relay_client.fetch({
            "kinds": [0, 30078, 31990],
            "authors": [state.pubkey],
        })
    except Exception:
        return
    if not events:
        return

    profile: Dict[str, Any] = {}
    max_created_at = 0

    for ev in events:
        created_at = ev.get("created_at", 0)
        if created_at > max_created_at:
            max_created_at = created_at

        kind = ev.get("kind")
        if kind == 0:
            try:
                content = json.loads(ev["content"])
            except (ValueError, TypeError, KeyError):
                continue
            for field in ("name", "about", "picture", "website", "nip05"):
                val = content.get(field)
                if val and isinstance(val, str):
                    profile[field] = val

        elif kind == 30078:
            try:
                content = json.loads(ev["content"])
            except (ValueError, TypeError, KeyError):
                continue
            caps = content.get("capabilities")
            if isinstance(caps, list):
                profile["capabilities"] = caps
            lobstr_section = content.get("lobstr", {})
            if isinstance(lobstr_section, dict):
                supported = lobstr_section.get("supported")
                if isinstance(supported, list):
                    profile["protocols"] = supported
                dvm = lobstr_section.get("dvm_kinds")
                if isinstance(dvm, list):
                    profile["dvm_kinds"] = dvm
            sw = content.get("software")
            if isinstance(sw, dict):
                profile["software"] = sw
            for field in ("input_modalities", "output_modalities"):
                val = content.get(field)
                if isinstance(val, list):
                    profile[field] = val

        elif kind == 31990:
            # Fallback: extract dvm_kinds from k tags if not already set
            if "dvm_kinds" not in profile:
                k_values = []
                for tag in ev.get("tags", []):
                    if isinstance(tag, list) and len(tag) >= 2 and tag[0] == "k":
                        try:
                            k_values.append(int(tag[1]))
                        except (ValueError, TypeError):
                            pass
                if k_values:
                    profile["dvm_kinds"] = k_values

    if profile:
        state.presence_profile = profile
        state.presence_announced = True
        state.presence_last_announced_at = max_created_at if max_created_at > 0 else None


# ---------------------------------------------------------------------------
# Presence API endpoints
# ---------------------------------------------------------------------------


def api_presence_profile_put(body: Optional[Dict[str, Any]]) -> tuple[int, Dict[str, Any]]:
    """PUT /v0/presence/profile — store profile metadata in daemon state."""
    err = _validate_presence_profile(body)
    if err:
        return _error(HTTPStatus.BAD_REQUEST, "INVALID_REQUEST", err)
    assert isinstance(body, dict)  # guaranteed by validation
    profile: Dict[str, Any] = {
        "name": body["name"],
        "about": body["about"],
        "capabilities": body["capabilities"],
    }
    # Optional fields
    for field in ("picture", "website", "nip05", "software",
                  "dvm_kinds", "input_modalities", "output_modalities"):
        val = body.get(field)
        if val is not None:
            profile[field] = val
    profile["protocols"] = body.get("protocols", ["lobstr/0"])
    STATE.presence_profile = profile
    return HTTPStatus.OK, {"ok": True, "profile": profile}


def api_presence_get() -> tuple[int, Dict[str, Any]]:
    """GET /v0/presence — return current presence state."""
    return HTTPStatus.OK, {
        "profile": STATE.presence_profile,
        "announced": STATE.presence_announced,
        "last_announced_at": STATE.presence_last_announced_at,
        "event_ids": STATE.presence_event_ids,
    }


def api_presence_announce(body: Optional[Dict[str, Any]]) -> tuple[int, Dict[str, Any]]:
    """POST /v0/presence/announce — build and publish presence events to relays."""
    if STATE.presence_profile is None:
        return _error(
            HTTPStatus.BAD_REQUEST, "NO_PROFILE",
            "no profile set — call PUT /v0/presence/profile first or ensure relay hydration succeeded",
        )
    profile = STATE.presence_profile

    # Build events in spec order: kind 0, 10002, 30078, 31990
    events_to_publish: List[tuple[str, Dict[str, Any]]] = [
        ("kind_0", _build_presence_kind0(profile)),
        ("kind_10002", _build_presence_kind10002()),
        ("kind_30078", _build_presence_kind30078(profile)),
    ]
    kind31990 = _build_presence_kind31990(profile)
    if kind31990 is not None:
        events_to_publish.append(("kind_31990", kind31990))

    published: Dict[str, Dict[str, Any]] = {}
    all_failed: List[str] = []
    any_success = False
    has_rate_limit = False

    for label, event in events_to_publish:
        result = STATE.relay_client.publish(event)
        published[label] = {
            "event_id": event["id"],
            "published_to": result.published_to,
            "failed_relays": _serialize_failed_relays(result.failed_relays),
        }
        STATE.presence_event_ids[label] = event["id"]
        if result.ok:
            any_success = True
        else:
            for fd in result.failed_relays:
                if fd.url not in all_failed:
                    all_failed.append(fd.url)
                if fd.ok_prefix == "rate-limited":
                    has_rate_limit = True

    if not any_success:
        STATE.last_relay_error = "presence announcement failed: no relays accepted events"
        error_payload: Dict[str, Any] = {
            "error": {
                "code": "RELAY_UNAVAILABLE",
                "message": STATE.last_relay_error,
                "retryable": True,
            },
            "published": published,
            "failed_relays": all_failed,
        }
        if has_rate_limit:
            error_payload["retry_after_seconds"] = 5
        return HTTPStatus.BAD_GATEWAY, error_payload

    STATE.presence_announced = True
    STATE.presence_last_announced_at = _now()
    STATE.last_relay_error = None
    return HTTPStatus.OK, {
        "ok": True,
        "published": published,
        "failed_relays": all_failed,
    }


class LobstrHandler(BaseHTTPRequestHandler):
    server_version = "lobstrd/0"

    def _write_json(self, status: int, payload: Dict[str, Any]) -> None:
        body = _json_dumps(payload).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    _MAX_BODY_SIZE = 1_048_576  # 1 MB

    def _read_json(self) -> Optional[Dict[str, Any]]:
        try:
            length = int(self.headers.get("Content-Length", "0"))
        except ValueError:
            return None
        if length <= 0:
            return {}
        if length > self._MAX_BODY_SIZE:
            return None
        raw = self.rfile.read(length)
        try:
            obj = json.loads(raw.decode("utf-8"))
        except (ValueError, UnicodeDecodeError):
            return None
        return obj if isinstance(obj, dict) else None

    def _auth_ok(self) -> bool:
        if not AUTH_TOKEN:
            return True
        if self.path == "/v0/health":
            return True
        header = self.headers.get("Authorization", "")
        return hmac.compare_digest(header, f"Bearer {AUTH_TOKEN}")

    def _unauthorized(self) -> None:
        self._write_json(
            HTTPStatus.UNAUTHORIZED,
            {
                "error": {
                    "code": "UNAUTHORIZED",
                    "message": "missing or invalid bearer token",
                    "retryable": False,
                }
            },
        )

    def do_GET(self) -> None:  # noqa: N802
        if not self._auth_ok():
            self._unauthorized()
            return

        if self.path == "/v0/health":
            status, payload = api_health()
            self._write_json(status, payload)
            return

        if self.path == "/v0/identity":
            status, payload = api_identity_get()
            self._write_json(status, payload)
            return

        if self.path == "/v0/relays":
            status, payload = api_relays_get()
            self._write_json(status, payload)
            return

        if self.path == "/v0/relays/info":
            status, payload = api_relays_info()
            self._write_json(status, payload)
            return

        if self.path == "/v0/presence":
            status, payload = api_presence_get()
            self._write_json(status, payload)
            return

        self._write_json(HTTPStatus.NOT_FOUND, {"error": {"code": "NOT_FOUND", "message": "unknown route", "retryable": False}})

    def do_PUT(self) -> None:  # noqa: N802
        if not self._auth_ok():
            self._unauthorized()
            return

        body = self._read_json()

        if self.path == "/v0/relays":
            status, payload = api_relays_put(body)
            self._write_json(status, payload)
            return

        if self.path == "/v0/presence/profile":
            status, payload = api_presence_profile_put(body)
            self._write_json(status, payload)
            return

        self._write_json(HTTPStatus.NOT_FOUND, {"error": {"code": "NOT_FOUND", "message": "unknown route", "retryable": False}})

    def do_POST(self) -> None:  # noqa: N802
        if not self._auth_ok():
            self._unauthorized()
            return

        body = self._read_json()
        if body is None:
            self._write_json(HTTPStatus.BAD_REQUEST, {"error": {"code": "INVALID_REQUEST", "message": "invalid JSON body", "retryable": False}})
            return

        if self.path == "/v0/events/publish":
            status, payload = api_events_publish(body)
            self._write_json(status, payload)
            return
        if self.path == "/v0/events/verify":
            status, payload = api_events_verify(body)
            self._write_json(status, payload)
            return
        if self.path == "/v0/messages/send":
            status, payload = api_messages_send(body)
            self._write_json(status, payload)
            return
        if self.path == "/v0/subscriptions":
            status, payload = api_subscriptions_post(body)
            self._write_json(status, payload)
            return
        if self.path == "/v0/messages/poll":
            status, payload = api_messages_poll(body)
            self._write_json(status, payload)
            return
        if self.path == "/v0/presence/announce":
            status, payload = api_presence_announce(body)
            self._write_json(status, payload)
            return

        self._write_json(HTTPStatus.NOT_FOUND, {"error": {"code": "NOT_FOUND", "message": "unknown route", "retryable": False}})


def main() -> None:
    # Hydrate presence from relays on startup (websocket backend + stable identity)
    if (STATE.relay_backend == "websocket"
            and os.environ.get("LOBSTRD_SECRET_KEY", "")
            and _is_hex(os.environ.get("LOBSTRD_SECRET_KEY", ""), 64)):
        _hydrate_presence_from_relays(STATE)
    host = os.environ.get("LOBSTRD_HOST", "127.0.0.1")
    port = int(os.environ.get("LOBSTRD_PORT", "7777"))
    httpd = ThreadingHTTPServer((host, port), LobstrHandler)
    print(f"lobstrd listening on http://{host}:{port}")
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass


if __name__ == "__main__":
    main()
