# api/routes/files.py
"""File serving endpoints — output/*, refs/*, client review, mobile assets."""

import json
from datetime import datetime as _dt_cls
from pathlib import Path

from fastapi import APIRouter, Body, Depends, Query
from fastapi.responses import FileResponse, JSONResponse

from .. import state
from ..deps import get_project, get_paths, get_store, _paths_for_project, get_project_aspect_ratio
from ..state import PROJECT_ROOT, EDITORS_DIR

from recoil.core.paths import ProjectPaths

router = APIRouter(tags=["files"])


# ── Output file serving ───────────────────────────────────────────

@router.get("/output/{file_path:path}")
def serve_output(file_path: str, paths: dict = Depends(get_paths)):
    """Serve files from project output directory with path traversal protection."""
    target = (paths["output_dir"] / file_path).resolve()
    if not target.is_relative_to(paths["output_dir"].resolve()):
        return JSONResponse({"error": "Access denied"}, status_code=403)
    if not target.exists():
        return JSONResponse({"error": "Not found"}, status_code=404)
    return FileResponse(str(target))


@router.get("/refs/{file_path:path}")
def serve_refs(file_path: str, paths: dict = Depends(get_paths)):
    """Serve reference files with path traversal protection."""
    target = (paths["refs_dir"] / file_path).resolve()
    if not target.is_relative_to(paths["refs_dir"].resolve()):
        return JSONResponse({"error": "Access denied"}, status_code=403)
    if not target.exists():
        return JSONResponse({"error": "Not found"}, status_code=404)
    return FileResponse(str(target))


# ── Client Review Routes ──────────────────────────────────────────

@router.get("/review/{token}")
def serve_client_review(token: str):
    """Serve client review SPA after validating token."""
    if not token or ".." in token:
        return JSONResponse({"error": "Invalid token"}, status_code=400)
    # Find project by review_token in project_config.json
    projects_root = _paths_for_project(state.default_project)["project_dir"].parent
    found_project = None
    for pdir in projects_root.iterdir():
        if not pdir.is_dir():
            continue
        cfg_path = pdir / "project_config.json"
        if cfg_path.exists():
            try:
                cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
                if not isinstance(cfg, dict):
                    continue
                if cfg.get("review_token") == token:
                    found_project = pdir.name
                    break
            except (json.JSONDecodeError, OSError):
                continue
    if not found_project:
        return JSONResponse({"error": "Invalid review link"}, status_code=404)
    # Serve the client review HTML
    html_path = EDITORS_DIR / "client_review.html"
    if html_path.exists():
        return FileResponse(str(html_path))
    return JSONResponse({"error": "Client review page not found"}, status_code=404)


@router.get("/api/client/review/{token}")
def client_review_data(token: str):
    """Client review data (sanitized) — returns shots in client_review/video_complete status."""
    if not token or ".." in token:
        return JSONResponse({"error": "Invalid token"}, status_code=400)
    projects_root = _paths_for_project(state.default_project)["project_dir"].parent
    found_project = None
    for pdir in projects_root.iterdir():
        if not pdir.is_dir():
            continue
        cfg_path = pdir / "project_config.json"
        if cfg_path.exists():
            try:
                cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
                if not isinstance(cfg, dict):
                    continue
                if cfg.get("review_token") == token:
                    found_project = pdir.name
                    break
            except (json.JSONDecodeError, OSError):
                continue
    if not found_project:
        return JSONResponse({"error": "Invalid review link"}, status_code=404)

    store = get_store(found_project)
    if store is None:
        return JSONResponse({"error": "Project data unavailable"}, status_code=503)

    ar = get_project_aspect_ratio(found_project)
    css_ar = ar.replace(":", " / ") if ":" in ar else ar

    client_shots = []
    all_shots = store.get_all_shots() if hasattr(store, "get_all_shots") else []
    for shot in all_shots:
        if isinstance(shot, dict) and shot.get("status") in ("client_review", "video_complete"):
            gate = shot.get("gate_results") or {}
            video_url = None
            hero = gate.get("hero_frame") or shot.get("output_path")
            if hero and str(hero).endswith((".mp4", ".webm")):
                video_url = f"/output/{hero}?project={found_project}" if not str(hero).startswith("/") else f"{hero}?project={found_project}"
            if not video_url:
                for take in reversed(shot.get("takes") or []):
                    tp = take.get("output_path", "")
                    if tp and tp.endswith((".mp4", ".webm")):
                        video_url = f"/output/{tp}?project={found_project}" if not tp.startswith("/") else f"{tp}?project={found_project}"
                        break
            if video_url:
                client_shots.append({
                    "id": shot.get("shot_id", shot.get("id", "")),
                    "video_url": video_url,
                    "description": shot.get("description", ""),
                    "episode": shot.get("episode", ""),
                    "order": shot.get("order", 0),
                    "feedback": shot.get("client_feedback", None),
                })

    client_shots.sort(key=lambda s: (s.get("episode") or "", s.get("order") or 0))

    return {
        "project_name": found_project.replace("-", " ").replace("_", " ").title(),
        "aspect_ratio": ar,
        "aspect_ratio_css": css_ar,
        "shots": client_shots,
        "total": len(client_shots),
    }


@router.post("/api/client/feedback/{shot_id}")
def client_feedback(shot_id: str, body: dict = Body(default={})):
    """Submit client feedback on a shot."""
    if not shot_id:
        return JSONResponse({"error": "Missing shot_id"}, status_code=400)
    token = body.get("token")
    if not token:
        return JSONResponse({"error": "Missing token"}, status_code=401)
    # Find project by token
    projects_root = _paths_for_project(state.default_project)["project_dir"].parent
    found_project = None
    for pdir in projects_root.iterdir():
        if not pdir.is_dir():
            continue
        cfg_path = pdir / "project_config.json"
        if cfg_path.exists():
            try:
                cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
                if not isinstance(cfg, dict):
                    continue
                if cfg.get("review_token") == token:
                    found_project = pdir.name
                    break
            except (json.JSONDecodeError, OSError):
                continue
    if not found_project:
        return JSONResponse({"error": "Invalid token"}, status_code=403)

    store = get_store(found_project)
    if store is None:
        return JSONResponse({"error": "Project data unavailable"}, status_code=503)

    status = body.get("status")
    notes = body.get("notes", "")
    if status not in ("approved", "revision"):
        return JSONResponse({"error": "status must be 'approved' or 'revision'"}, status_code=400)

    shot = store.get_shot(shot_id)
    if shot is None:
        return JSONResponse({"error": f"Shot not found: {shot_id}"}, status_code=404)

    store.update_shot(shot_id, client_feedback={
        "status": status,
        "notes": notes,
        "submitted_at": _dt_cls.now().isoformat(),
    })

    return {"ok": True, "shot_id": shot_id}


# ── Mobile file serving ───────────────────────────────────────────

@router.get("/m/{file_path:path}")
def serve_mobile_assets(file_path: str):
    """Serve mobile console static assets."""
    fpath = EDITORS_DIR / "mobile" / file_path
    if file_path == "sw.js":
        # Service workers must never be cached
        if fpath.exists():
            from fastapi.responses import Response
            content = fpath.read_bytes()
            return Response(
                content=content,
                media_type="application/javascript",
                headers={
                    "Cache-Control": "no-cache, no-store, must-revalidate",
                    "Service-Worker-Allowed": "/",
                },
            )
        return JSONResponse({"error": "sw.js not found"}, status_code=404)
    if not fpath.exists():
        return JSONResponse({"error": "Not found"}, status_code=404)
    # Path traversal protection
    target = fpath.resolve()
    mobile_root = (EDITORS_DIR / "mobile").resolve()
    if not target.is_relative_to(mobile_root):
        return JSONResponse({"error": "Access denied"}, status_code=403)
    return FileResponse(str(target))


# ── Location & Casting State Endpoints ─────────────────────────

@router.get("/api/files/location-ids")
def get_location_ids(
    project: str = Depends(get_project),
    paths: dict = Depends(get_paths),
):
    """List all available location ref directories."""
    refs_dir = paths["project_dir"] / "output" / "refs" / "locations"
    location_ids = []
    if refs_dir.is_dir():
        for d in sorted(refs_dir.iterdir()):
            if d.is_dir() and not d.name.startswith('.'):
                location_ids.append(d.name)
    return JSONResponse({"location_ids": location_ids})


@router.get("/api/files/casting-state")
def get_casting_state(
    project: str = Depends(get_project),
    paths: dict = Depends(get_paths),
):
    """Return casting_state.json for the Console character picker."""
    casting_path = ProjectPaths.from_root(paths["project_dir"]).casting_state_path
    if not casting_path.exists():
        return JSONResponse({"characters": {}, "locations": {}})
    try:
        data = json.loads(casting_path.read_text(encoding="utf-8"))
        return JSONResponse(data)
    except (json.JSONDecodeError, IOError):
        return JSONResponse({"characters": {}, "locations": {}})
