"""Event validation and processing API."""

import json
import logging
import os
import threading
import time
from typing import Annotated, Any

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

from python_sidecar.capture.accessibility import start_event_capture

logger = logging.getLogger(__name__)

try:
    from python_sidecar.capture.filesystem import start_fs_watcher
except ImportError as import_error:
    start_fs_watcher = None  # type: ignore[assignment]
    FS_WATCHER_IMPORT_ERROR = import_error
else:
    FS_WATCHER_IMPORT_ERROR = None

router = APIRouter()


class CommandEvent(BaseModel):
    type: str = Field(default="command", pattern="^command$")
    timestamp: int = Field(ge=0, default=0)
    text: str = ""
    cwd: str = ""


class AnnotationEvent(BaseModel):
    type: str = Field(default="annotation", pattern="^annotation$")
    timestamp: int = Field(ge=0, default=0)
    text: str = ""
    position: dict[str, int] = Field(default_factory=dict)


class MarkerEvent(BaseModel):
    type: str = Field(default="marker", pattern="^marker$")
    timestamp: int = Field(ge=0, default=0)
    label: str = ""


class MouseEvent(BaseModel):
    type: str = Field(default="click", pattern="^click$")
    timestamp_ms: int = 0
    x: int = 0
    y: int = 0
    button: str = "left"


class KeyboardEvent(BaseModel):
    type: str = Field(default="keypress", pattern="^keypress$")
    timestamp_ms: int = 0
    key: str = ""
    modifiers: list[str] = Field(default_factory=list)


class FileEvent(BaseModel):
    type: str = Field(default="file_change", pattern="^file_change$")
    timestamp_ms: int = 0
    path: str = ""
    action: str = ""


Event = Annotated[
    CommandEvent | AnnotationEvent | MarkerEvent | MouseEvent | KeyboardEvent | FileEvent,
    Field(discriminator="type"),
]


class StartCaptureRequest(BaseModel):
    session_dir: str
    project_path: str | None = None
    recording_start_ms: int | None = None


active_watchers = []
event_file = None
recording_start_ms = 0
sidecar_event_count = 0
event_write_lock = threading.Lock()


def _normalize_ts_ms(event_data: dict[str, Any]) -> dict[str, Any]:
    """Normalize timestamps into ts_ms relative to recording_start_ms."""

    normalized = dict(event_data)
    raw_ts = (
        normalized.get("timestamp_ms")
        or normalized.get("ts_ms")
        or normalized.get("timestamp")
        or 0
    )
    try:
        raw_ts = int(raw_ts)
    except (TypeError, ValueError):
        raw_ts = 0

    if recording_start_ms > 0 and raw_ts >= recording_start_ms:
        normalized["ts_ms"] = max(0, raw_ts - recording_start_ms)
    elif raw_ts > 1_000_000_000 and recording_start_ms > 0:
        # Guard against clock anomalies that still produce large epoch-like timestamps.
        normalized["ts_ms"] = max(0, raw_ts - recording_start_ms)
    else:
        normalized["ts_ms"] = raw_ts
    normalized.pop("timestamp", None)
    return normalized


def append_event(event_data: dict[str, Any]) -> None:
    """Append a single event line into the active events.ndjson file."""

    global sidecar_event_count
    if event_file is None:
        raise RuntimeError("capture_not_active")

    normalized = _normalize_ts_ms(event_data)
    with event_write_lock:
        if event_file is None:
            raise RuntimeError("capture_not_active")
        event_file.write(json.dumps(normalized) + "\n")
        event_file.flush()
        sidecar_event_count += 1


class AppendEventsRequest(BaseModel):
    # Accept either a single event or a batch.
    event: dict[str, Any] | None = None
    events: list[dict[str, Any]] | None = None


@router.post("/start")
async def start_capture(request: StartCaptureRequest):
    global event_file, recording_start_ms, sidecar_event_count
    os.makedirs(request.session_dir, exist_ok=True)
    event_file_path = os.path.join(request.session_dir, "events.ndjson")
    event_file = open(event_file_path, "a")
    recording_start_ms = request.recording_start_ms or int(time.time() * 1000)
    sidecar_event_count = 0

    def log_event(event_data):
        try:
            append_event(event_data)
        except Exception:
            # Avoid crashing capture threads on serialization errors.
            logger.debug("Failed to append event: %s", event_data, exc_info=True)

    # Start A11y capture
    limited_reasons: list[str] = []

    event_watcher = start_event_capture(log_event)
    if event_watcher is not None:
        active_watchers.append(event_watcher)
        if hasattr(event_watcher, "is_active") and not event_watcher.is_active():
            limited_reasons.append("accessibility_event_tap_unavailable")
    else:
        limited_reasons.append("accessibility_event_tap_unavailable")

    # Start FS watcher if path provided
    if request.project_path:
        if start_fs_watcher is None:
            logger.warning(
                "Filesystem watcher unavailable: optional dependency missing (%s)",
                FS_WATCHER_IMPORT_ERROR,
            )
            limited_reasons.append("filesystem_watcher_unavailable")
        else:
            fs_watcher = start_fs_watcher(request.project_path, log_event)
            if fs_watcher is not None:
                active_watchers.append(fs_watcher)
            else:
                limited_reasons.append("filesystem_watcher_unavailable")

    return {
        "status": "started",
        "file": event_file_path,
        "limited": bool(limited_reasons),
        "reasons": limited_reasons,
    }


@router.get("/state")
async def capture_state() -> dict[str, Any]:
    return {
        "active": event_file is not None,
        "event_count": sidecar_event_count,
        "recording_start_ms": recording_start_ms,
    }


@router.post("/append")
async def append_events(request: AppendEventsRequest) -> dict[str, Any]:
    if event_file is None:
        raise HTTPException(status_code=409, detail="Capture is not active")

    payloads: list[dict[str, Any]] = []
    if request.event is not None:
        payloads.append(request.event)
    if request.events is not None:
        payloads.extend(request.events)

    if not payloads:
        raise HTTPException(status_code=400, detail="No events provided")

    appended = 0
    for payload in payloads:
        try:
            event_type = payload.get("type")
            if event_type not in {"dom_action", "dom_navigation"}:
                continue
            append_event(payload)
            appended += 1
        except Exception:
            # Ignore malformed events, keep ingestion resilient.
            logger.debug("Failed to append event: %s", payload, exc_info=True)
            continue

    return {"status": "ok", "appended": appended}


@router.post("/stop")
async def stop_capture():
    global event_file, recording_start_ms, sidecar_event_count
    for watcher in active_watchers:
        if hasattr(watcher, "stop"):
            watcher.stop()
        elif hasattr(watcher, "join"):
            # In a real app we might need more careful cleanup
            pass
    active_watchers.clear()

    if event_file:
        event_file.close()
        event_file = None
    recorded = sidecar_event_count
    sidecar_event_count = 0
    recording_start_ms = 0

    return {"status": "stopped", "event_count": recorded}


@router.get("/health")
async def events_health() -> dict[str, str]:
    return {"status": "ok", "module": "events"}
