"""POST /api/workspace-state/{wid}/report — capture a corrupt workspace state
blob for offline inspection. P5 (Q5 modal escalation).

Phase 5 of console-v2-fix-build: closes Law 4 prong-3 (quality-neutrality) for
workspace_state parse failures. The desktop's <SessionRestoreModal> escalates
the silent default-substitution into a user-visible choice; the Report path
lands here, appending the corrupt blob + zod error to a JSONL side-channel
so JT can inspect it offline.

The fallback `workspace_state_user_chose_report` fires server-side (this
module) — telemetry parity with the existing `workspace_state_parse_failure_default`
fallback that fires client-side. Defaults are NOT persisted on this path —
the corrupt blob stays in SQLite until JT manually fixes it.
"""

from __future__ import annotations

from datetime import datetime, timezone
from pathlib import Path

from fastapi import APIRouter, Path as PathParam
from pydantic import BaseModel

from recoil.core.atomic_write import jsonl_append_locked

from recoil.api.fallback_bridge import emit_fallback
from recoil.api.workspace_state import _validate_workspace_id

router = APIRouter()

# Side-channel JSONL file. Per-process append. Lives outside the SQLite store
# so corrupt-blob investigation never fights with the live workspace_state row.
_REPORTS_PATH = Path.home() / ".recoil" / "v2_corrupt_state_reports.jsonl"


class CorruptStateReport(BaseModel):
    """Body shape for POST /api/workspace-state/{wid}/report.

    `payload_json` is the corrupt blob exactly as the desktop received it
    (server doesn't re-parse it — that's the point). `zod_error` is the
    `String(err)` from the zod failure, useful for triage.
    """

    payload_json: str
    zod_error: str


@router.post("/workspace-state/{workspace_id}/report")
def report_corrupt_state(
    workspace_id: str = PathParam(...),
    body: CorruptStateReport = ...,  # type: ignore[assignment]
) -> dict:
    # Plain `def` (not async def): the body does blocking file I/O —
    # open, fcntl.flock, fsync — which would freeze the FastAPI event
    # loop if executed in the async context. FastAPI auto-delegates
    # plain-def routes to a threadpool, keeping the loop responsive.
    # Parity with the companion workspace_state.py routes — UUIDv4 only.
    # Without this, any string lands as a JSONL key in the side-channel
    # file and as the BUS event payload's workspace_id, polluting both
    # surfaces. Surface malformed ids at the boundary.
    _validate_workspace_id(workspace_id)
    record = {
        "ts": datetime.now(timezone.utc).isoformat(),
        "workspace_id": workspace_id,
        "payload_json": body.payload_json,
        "zod_error": body.zod_error,
    }
    # Phase 20 SSOT: fcntl-locked JSONL append + EINVAL-tolerant fsync.
    # Cross-process append durability defensive against future multi-worker
    # uvicorn configs.
    jsonl_append_locked(_REPORTS_PATH, record)
    emit_fallback(
        "workspace_state_user_chose_report",
        scope="api/workspace_state_report",
        payload={"workspace_id": workspace_id},
    )
    return {"ok": True}
