# version: v2
"""Pulse Room REST endpoints.

Wire into data_server.py:
    from pulse_room.routes import create_pulse_router
    app.include_router(create_pulse_router())

Endpoints:
    GET  /api/pulse/state       — full state (scanner, watchlist, snapshot, meta)
    POST /api/pulse/calculate   — rebuild snapshot (sync; 409 if in-flight)
    PUT  /api/pulse/watchlist   — replace watchlist
    PUT  /api/pulse/scanner     — set scanner enabled flag
"""

import asyncio
import json
import logging
from pathlib import Path
from typing import Any

from fastapi import APIRouter, HTTPException
from pydantic import BaseModel

from . import builder, state
# Concurrency: only one Calculate at a time. Lock lives in state.py so
# sync_hook.py shares the same instance and the two paths can't race.
from .state import calc_lock as _calc_lock


log = logging.getLogger("mk.pulse_room.routes")


# ---------- request models ----------

class WatchlistAssetIn(BaseModel):
    symbol: str
    families: list[str]


class WatchlistIn(BaseModel):
    assets: list[WatchlistAssetIn]


class ScannerIn(BaseModel):
    enabled: bool


# ---------- helpers ----------

def _read_snapshot() -> tuple[dict | None, dict]:
    """Return (snapshot, meta). Snapshot may be None if not yet built."""
    path = state._pulse_dir() / "pulse_snapshot.json"
    if not path.exists():
        return None, {"exists": False}
    try:
        with open(path, "r", encoding="utf-8") as f:
            snap = json.load(f)
    except (OSError, json.JSONDecodeError):
        return None, {"exists": False}
    meta = {
        "exists": True,
        "computed_at": snap.get("computed_at"),
        "source_sync_at": snap.get("source_sync_at"),
        "watchlist_hash": snap.get("watchlist_hash"),
        "asset_count": len(snap.get("assets", {})),
        "size_bytes": path.stat().st_size,
    }
    return snap, meta


def _build_state_response() -> dict:
    snap, meta = _read_snapshot()
    return {
        "scanner_enabled": state.load_scanner().get("enabled", False),
        "watchlist": state.load_watchlist(),
        "snapshot_meta": meta,
        "snapshot": snap,
    }


# ---------- router ----------

def create_pulse_router() -> APIRouter:
    router = APIRouter(prefix="/api/pulse", tags=["pulse"])

    @router.get("/state")
    async def get_state():
        return _build_state_response()

    @router.put("/scanner")
    async def put_scanner(body: ScannerIn):
        state.save_scanner({"enabled": body.enabled})
        return _build_state_response()

    @router.put("/watchlist")
    async def put_watchlist(body: WatchlistIn):
        wl = {"assets": [a.model_dump() for a in body.assets]}
        state.save_watchlist(wl)
        return _build_state_response()

    @router.post("/calculate")
    async def post_calculate():
        wl = state.load_watchlist()
        if not wl.get("assets"):
            raise HTTPException(400, detail="Watchlist is empty")

        if not _calc_lock.acquire(blocking=False):
            raise HTTPException(409, detail="A calculate is already in progress")
        try:
            try:
                # build_snapshot does sync file I/O; offload to a worker
                # thread so we don't block the FastAPI event loop (which
                # would freeze /api/ticks, /ws/prices, etc. for 0.5–3 s).
                await asyncio.to_thread(builder.build_snapshot, wl, write=True)
            except Exception as e:
                log.exception("Calculate failed")
                raise HTTPException(500, detail=f"Calculate failed: {e}") from e
        finally:
            _calc_lock.release()

        return _build_state_response()

    return router
