"""Per-project click history. ~/.recoil/click-history.jsonl is the canonical log; _RING is a bounded cache."""
from __future__ import annotations

import json
import threading
from collections import deque
from pathlib import Path
from typing import Any, Deque, Literal

from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel

from recoil.api.adapters._ids import validate_project_id
from recoil.api.chat_sessions import _now_iso

router = APIRouter()

SCHEMA_VERSION = 1
_MAX_PER_PROJECT = 50
_DEFAULT_N = 10
_HISTORY_PATH = Path.home() / ".recoil" / "click-history.jsonl"
_RING: dict[str, Deque[dict[str, Any]]] = {}
_RING_LOCK = threading.Lock()

ClickKind = Literal["shot", "take", "pass", "file"]


class _ClickCreate(BaseModel):
    project_id: str
    id: str
    kind: ClickKind


def _validated_project(project_id: str) -> str:
    try:
        validate_project_id(project_id)
    except ValueError as exc:
        raise HTTPException(
            status_code=400, detail=f"invalid project: {project_id!r}"
        ) from exc
    return project_id


def _get_ring(project_id: str) -> Deque[dict[str, Any]]:
    ring = _RING.get(project_id)
    if ring is None:
        ring = deque(maxlen=_MAX_PER_PROJECT)
        _RING[project_id] = ring
    return ring


@router.post("/clicks")
def post_click(body: _ClickCreate) -> dict[str, Any]:
    project_id = _validated_project(body.project_id)
    record = {
        "schema_version": SCHEMA_VERSION,
        "project_id": project_id,
        "id": body.id,
        "kind": body.kind,
        "ts": _now_iso(),
    }
    _HISTORY_PATH.parent.mkdir(parents=True, exist_ok=True)
    line = json.dumps(record, sort_keys=True) + "\n"
    # Disk write + ring update under one lock so concurrent POSTs land in
    # the same order on disk and in cache. The line itself is <PIPE_BUF
    # so O_APPEND is atomic; the lock just sequences A→B vs B→A.
    with _RING_LOCK:
        with open(_HISTORY_PATH, "a", encoding="utf-8") as f:
            f.write(line)
        _get_ring(project_id).append(record)
    return {"ok": True}


def _tail_records(project_id: str, max_bytes: int = 256 * 1024) -> list[dict[str, Any]]:
    """Read the last ``max_bytes`` of the JSONL log and filter to project."""
    if not _HISTORY_PATH.is_file():
        return []
    try:
        size = _HISTORY_PATH.stat().st_size
        with _HISTORY_PATH.open("rb") as f:
            if size > max_bytes:
                f.seek(size - max_bytes)
                # Drop the partial first line — only complete records.
                f.readline()
            raw = f.read().decode("utf-8", errors="replace")
    except OSError:
        return []
    out: list[dict[str, Any]] = []
    for ln in raw.splitlines():
        if not ln.strip():
            continue
        try:
            rec = json.loads(ln)
        except json.JSONDecodeError:
            continue
        if isinstance(rec, dict) and rec.get("project_id") == project_id:
            out.append(rec)
    return out[-_MAX_PER_PROJECT:]


@router.get("/clicks")
def get_clicks(
    project_id: str = Query(...),
    n: int = Query(_DEFAULT_N, ge=1, le=_MAX_PER_PROJECT),
) -> list[dict[str, Any]]:
    project_id = _validated_project(project_id)
    with _RING_LOCK:
        ring = _RING.get(project_id)
        cached = list(ring) if ring is not None else None
    if cached is None:
        cached = _tail_records(project_id)
        with _RING_LOCK:
            ring = _get_ring(project_id)
            if not ring:
                ring.extend(cached)
            cached = list(ring)
    cached.reverse()
    return cached[:n]


__all__ = ["router", "SCHEMA_VERSION"]
