"""Workspace-state routes.

Three routes per SYNTHESIS Locked Decision #6:
  POST /api/workspace-state/{workspace_id}    upsert
  GET  /api/workspace-state/{workspace_id}    read (404 on miss)
  DELETE /api/workspace-state/{workspace_id}  remove

The server stores opaque JSON. It does NOT parse `ColumnLayout`, `TabState`,
etc. Those types are TypeScript+zod owned (SYNTHESIS LOCKED #8).

200-LOC budget per SYNTHESIS Risk #1. Resist scope creep.
"""
from __future__ import annotations

import json
import re
from datetime import datetime, timezone

from fastapi import APIRouter, HTTPException, Path, status

from recoil.api.db import get_conn
from recoil.api.schemas.workspace import (
    ENVELOPE_SCHEMA_VERSION,
    WorkspaceStateRead,
    WorkspaceStateWrite,
)

router = APIRouter()

# UUIDv4 pattern. Tighter than blanket "any string" — surfaces malformed
# workspace ids at the boundary instead of letting them poison the table.
WORKSPACE_ID_RE = re.compile(
    r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
)


class UnknownSchemaVersionError(Exception):
    """Per ADR-0010 — raised on payload_json schema version mismatch.

    Not raised from these routes (server doesn't parse payload_json) — clients
    parse and raise. Defined here so the contract docs reference one location.
    """


def _validate_workspace_id(workspace_id: str) -> None:
    if not WORKSPACE_ID_RE.match(workspace_id):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"workspace_id must be a UUIDv4; got {workspace_id!r}",
        )


@router.get(
    "/workspace-state/{workspace_id}",
    response_model=WorkspaceStateRead,
)
def read_workspace_state(workspace_id: str = Path(...)) -> WorkspaceStateRead:
    _validate_workspace_id(workspace_id)
    with get_conn() as conn:
        row = conn.execute(
            "SELECT workspace_id, schema_version, payload_json, updated_at "
            "FROM workspace_state WHERE workspace_id = ?",
            (workspace_id,),
        ).fetchone()
    if row is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"no workspace_state for {workspace_id}",
        )
    wid, sv, payload, updated = row
    if sv != ENVELOPE_SCHEMA_VERSION:
        # Loud failure per Law 4 — never best-effort parse a future envelope.
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"envelope schema_version {sv} != server {ENVELOPE_SCHEMA_VERSION}",
        )
    return WorkspaceStateRead(
        workspace_id=wid,
        schema_version=sv,
        payload_json=payload,
        updated_at=updated,
    )


@router.post(
    "/workspace-state/{workspace_id}",
    response_model=WorkspaceStateRead,
    status_code=status.HTTP_200_OK,
)
def write_workspace_state(
    workspace_id: str = Path(...),
    body: WorkspaceStateWrite = ...,  # type: ignore[assignment]
) -> WorkspaceStateRead:
    _validate_workspace_id(workspace_id)
    if body.schema_version != ENVELOPE_SCHEMA_VERSION:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=(
                f"envelope schema_version {body.schema_version} "
                f"!= server {ENVELOPE_SCHEMA_VERSION}"
            ),
        )
    # Validate payload_json is well-formed JSON (server doesn't care about
    # SHAPE — that's the UI's concern — but a malformed string is a contract
    # violation we surface immediately).
    try:
        json.loads(body.payload_json)
    except json.JSONDecodeError as e:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"payload_json is not valid JSON: {e}",
        ) from e

    now = datetime.now(timezone.utc).isoformat()
    with get_conn() as conn:
        conn.execute(
            """
            INSERT INTO workspace_state(workspace_id, schema_version, payload_json, updated_at)
            VALUES (?, ?, ?, ?)
            ON CONFLICT(workspace_id) DO UPDATE
              SET schema_version=excluded.schema_version,
                  payload_json=excluded.payload_json,
                  updated_at=excluded.updated_at
            """,
            (workspace_id, body.schema_version, body.payload_json, now),
        )
        conn.commit()
    return WorkspaceStateRead(
        workspace_id=workspace_id,
        schema_version=body.schema_version,
        payload_json=body.payload_json,
        updated_at=now,
    )


@router.delete(
    "/workspace-state/{workspace_id}",
    status_code=status.HTTP_204_NO_CONTENT,
)
def delete_workspace_state(workspace_id: str = Path(...)) -> None:
    _validate_workspace_id(workspace_id)
    with get_conn() as conn:
        cursor = conn.execute(
            "DELETE FROM workspace_state WHERE workspace_id = ?",
            (workspace_id,),
        )
        conn.commit()
        if cursor.rowcount == 0:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail=f"no workspace_state for {workspace_id}",
            )
