"""
main.py — People Hub Candidate Interview Platform
==================================================
FastAPI app that serves ALL HTML pages + REST API.

Run from PyCharm:   right-click → Run 'main'
Run from terminal:  python main.py
                    uvicorn main:app --reload --port 8000

Candidate portal:   http://localhost:8000
API explorer:       http://localhost:8000/docs
"""
from __future__ import annotations
from contextlib import asynccontextmanager
from pathlib import Path

import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles

from config import settings
from utils.logger import get_logger
from utils import database as db
from routes.auth import router as auth_router
from routes.coding import router as coding_router
from routes.fraud import router as fraud_router
from routes.setup import router as setup_router
from routes.interview import router as interview_router
from routes.peoplehub_external import router as peoplehub_external_router

# Optional Role Play routers — guarded so a missing dependency in a fresh
# environment doesn't take the rest of the app down.
#
# IMPORTANT: when one of these import blocks fails, every endpoint exposed
# by that router 404s on the deployed server, which is opaque to the user
# (they see "Could not start session: HTTP 404" in the browser with no
# clue what's wrong). To make this debuggable on Ubuntu we:
#   1. Log the FULL traceback (filename + line) instead of just the bare
#      exception message, so it shows up in `journalctl` / uvicorn stdout.
#   2. Stash the failure on `_router_load_errors` so the /api/_health/routers
#      endpoint defined below can surface it via HTTP — no shell access
#      to the server required.
import traceback as _tb

_router_load_errors: dict[str, str] = {}

def _try_import_router(module_path: str, attr: str = "router"):
    try:
        mod = __import__(module_path, fromlist=[attr])
        return getattr(mod, attr)
    except Exception as _exc:  # noqa: BLE001
        tb_text = _tb.format_exc()
        _router_load_errors[module_path] = tb_text
        # Print loudly so it's visible in journalctl / uvicorn stdout.
        print(f"\n[main] !! FAILED TO LOAD ROUTER: {module_path}\n{tb_text}", flush=True)
        return None

sessions_router  = _try_import_router("routes.sessions")
ws_router        = _try_import_router("routes.websocket")
scenarios_router = _try_import_router("routes.scenarios")
tavus_router     = _try_import_router("routes.tavus")

log = get_logger(__name__)

# ── Bootstrap paths ───────────────────────────────────────────────────────────
BASE_DIR      = Path(__file__).parent
STATIC_DIR    = BASE_DIR / "static"
STATIC_JS_DIR = STATIC_DIR / "js"
HTML_DIR      = STATIC_DIR / "html"
TEMPLATES_DIR = BASE_DIR / "templates"
DATA_DIR      = BASE_DIR / "data"

# Create all required directories on startup so first-boot never fails
for _d in (STATIC_DIR, STATIC_JS_DIR, HTML_DIR, TEMPLATES_DIR, DATA_DIR):
    _d.mkdir(parents=True, exist_ok=True)

settings.ensure_dirs()


# ── Lifespan: DB pool init / shutdown ─────────────────────────────────────────
@asynccontextmanager
async def lifespan(app: FastAPI):
    log.info("══════════════════════════════════════════════════════════════")
    log.info("  Starting %s v%s", settings.APP_NAME, settings.APP_VERSION)
    log.info("══════════════════════════════════════════════════════════════")

    # Best-effort DB initialization. We DO NOT fail-fast at boot because the
    # platform also runs against the external People Hub APIs, which means a
    # local Postgres outage shouldn't take the whole app down.
    try:
        db.init_pool()
    except Exception as exc:  # noqa: BLE001
        if settings.DB_FAIL_FAST:
            log.critical("Database init failed and DB_FAIL_FAST=true → refusing to start: %s", exc)
            raise
        log.warning("Database init failed (continuing because DB_FAIL_FAST=false): %s", exc)

    # Fire-and-forget Wav2Lip warmup so the first candidate turn doesn't
    # pay the 5-12 s torch/cv2/librosa cold-import cost. Safe no-op when
    # Wav2Lip isn't installed (e.g. local dev).
    try:
        from services import video_engine as _ve
        _ve.warmup_wav2lip_async()
    except Exception as exc:  # noqa: BLE001
        log.debug("video warmup skipped: %s", exc)

    try:
        yield
    finally:
        log.info("Shutting down — closing DB pool")
        try:
            db.close_pool()
        except Exception as exc:  # noqa: BLE001
            log.debug("close_pool error (ignored): %s", exc)


# ── App ───────────────────────────────────────────────────────────────────────
# OpenAPI / Swagger UI are gated behind APP_DEBUG so the production deployment
# doesn't expose schema and request examples to unauthenticated users.
app = FastAPI(
    title=settings.APP_NAME,
    description=(
        "AI-powered candidate interview platform. "
        "Stages: 1=Assessment · 2=RolePlay · 3=Coding · 4=Technical · 5=Human · 6=HR"
    ),
    version=settings.APP_VERSION,
    docs_url="/docs"  if settings.APP_DEBUG else None,
    redoc_url="/redoc" if settings.APP_DEBUG else None,
    openapi_url="/openapi.json" if settings.APP_DEBUG else None,
    lifespan=lifespan,
)

# ── CORS ──────────────────────────────────────────────────────────────────────
# Methods + headers are explicitly listed (not "*") so VAPT scanners don't
# flag the API as accepting arbitrary verbs from arbitrary origins.
app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.ALLOWED_ORIGINS,
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allow_headers=["Authorization", "Content-Type", "X-Requested-With"],
    max_age=600,
)


# ── Security headers ──────────────────────────────────────────────────────────
# Applied to every response. Tailored for VAPT findings:
#   • Frame-busting (clickjacking)
#   • MIME sniffing protection
#   • Referrer leakage control
#   • Powerful-feature lockdown via Permissions-Policy
#   • CSP that allows only this origin + the libraries already used
#     (Google Fonts, jsDelivr/CDNJS for Monaco-equivalent editors).
#
# HSTS is only emitted in non-debug runs because dev usually runs over plain
# HTTP and a sticky HSTS would lock the developer out of localhost.

_CSP_DIRECTIVES = (
    "default-src 'self'; "
    "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; "
    "style-src  'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; "
    "font-src   'self' https://fonts.gstatic.com data:; "
    "img-src    'self' data: blob: https:; "
    # Tavus's Live AI Role Play needs to load media + WebRTC streams
    # from the Daily.co edge and Tavus's CDN. Without these the iframe
    # paints a blank black box (the candidate sees nothing) because
    # the default-src 'self' fallback blocks every cross-origin
    # request the iframe makes.
    "media-src  'self' blob: https://*.daily.co https://*.tavus.io https://*.tavusapi.com; "
    "connect-src 'self' https: wss:; "
    # frame-src controls what URLs we ALLOW IN our own iframes. Tavus
    # conversation URLs live on tavus.daily.co (Tavus is built on
    # Daily.co's WebRTC infrastructure), so we permit any *.daily.co
    # and *.tavus.io subdomain. Tightened to https only.
    "frame-src  'self' https://*.daily.co https://*.tavus.io https://*.tavusapi.com; "
    "child-src  'self' https://*.daily.co https://*.tavus.io https://*.tavusapi.com; "
    # frame-ancestors controls who can embed US. We still forbid that.
    "frame-ancestors 'none'; "
    "base-uri 'self'; "
    "form-action 'self'; "
    "object-src 'none';"
)

_PERMISSIONS_POLICY = (
    # Camera/microphone/display-capture/fullscreen need to be granted to
    # both 'self' AND the Tavus iframe origins so the WebRTC handshake
    # inside the Live AI Role Play works. Without these, getUserMedia
    # inside the iframe is denied even though the user clicked Allow.
    "accelerometer=(), autoplay=(self \"https://*.daily.co\" \"https://*.tavus.io\"), "
    "camera=(self \"https://*.daily.co\" \"https://*.tavus.io\"), "
    "display-capture=(self \"https://*.daily.co\" \"https://*.tavus.io\"), "
    "encrypted-media=(), fullscreen=(self \"https://*.daily.co\" \"https://*.tavus.io\"), "
    "geolocation=(), gyroscope=(), "
    "microphone=(self \"https://*.daily.co\" \"https://*.tavus.io\"), "
    "midi=(), payment=(), picture-in-picture=(self), "
    "publickey-credentials-get=(), sync-xhr=(), usb=(), xr-spatial-tracking=()"
)


@app.middleware("http")
async def _security_headers(request, call_next):
    response = await call_next(request)
    h = response.headers
    h.setdefault("X-Content-Type-Options", "nosniff")
    h.setdefault("X-Frame-Options", "DENY")
    h.setdefault("Referrer-Policy", "strict-origin-when-cross-origin")
    h.setdefault("Permissions-Policy", _PERMISSIONS_POLICY)
    h.setdefault("Cross-Origin-Opener-Policy", "same-origin")
    h.setdefault("Cross-Origin-Resource-Policy", "same-origin")
    h.setdefault("X-Permitted-Cross-Domain-Policies", "none")
    # Don't reveal the server stack to scanners
    if "Server" in h:
        del h["Server"]
    h["X-Powered-By"] = ""

    # Only API responses (JSON) need the strict CSP; HTML pages get a
    # slightly looser policy injected via _CSP_DIRECTIVES so embedded
    # inline handlers in the candidate UI keep working.
    h.setdefault("Content-Security-Policy", _CSP_DIRECTIVES)

    if not settings.APP_DEBUG:
        h.setdefault(
            "Strict-Transport-Security",
            "max-age=31536000; includeSubDomains; preload",
        )
    return response


# ── Generic error handler (no stack traces leak in production) ────────────────
from fastapi import Request
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException


@app.exception_handler(StarletteHTTPException)
async def _http_exc_handler(_: Request, exc: StarletteHTTPException):
    return JSONResponse(
        status_code=exc.status_code,
        content={"ok": False, "error": exc.detail or "Request failed"},
    )


@app.exception_handler(RequestValidationError)
async def _validation_handler(_: Request, exc: RequestValidationError):
    # Don't echo the raw payload back — VAPT flags reflected input.
    return JSONResponse(
        status_code=400,
        content={"ok": False, "error": "Invalid request"},
    )


@app.exception_handler(Exception)
async def _unhandled_handler(_: Request, exc: Exception):
    log.exception("Unhandled error: %s", exc)
    # Production: opaque message. Dev: keep the original behaviour for debugging.
    if settings.APP_DEBUG:
        return JSONResponse(
            status_code=500,
            content={"ok": False, "error": str(exc)},
        )
    return JSONResponse(
        status_code=500,
        content={"ok": False, "error": "An internal error occurred"},
    )

# ── Routers ───────────────────────────────────────────────────────────────────
app.include_router(auth_router)
app.include_router(coding_router)
app.include_router(fraud_router)
app.include_router(setup_router)
app.include_router(interview_router)
app.include_router(peoplehub_external_router)
if sessions_router  is not None: app.include_router(sessions_router)
if ws_router        is not None: app.include_router(ws_router)
if scenarios_router is not None: app.include_router(scenarios_router)
if tavus_router     is not None: app.include_router(tavus_router)


# ── Fraud Report sub-app ──────────────────────────────────────────────────────
# Mounts the standalone Fraud Report dashboard at /fraudreport so operators
# can open it from the same host/port as the candidate portal:
#
#     http://localhost:8000/fraudreport/                  ← dashboard HTML
#     http://localhost:8000/fraudreport/api/fraud/...     ← APIs
#
# The fraud_report package keeps its own FastAPI instance, models, exception
# handlers and storage layer — this mount is the ONLY line of integration.
# It's import-guarded so a missing dependency in a fresh checkout doesn't
# take the main app down (same pattern as the optional role-play routers).
try:
    from fraud_report.fraudreport import app as _fraud_report_app
    app.mount("/fraudreport", _fraud_report_app)
    print("[main] mounted Fraud Report at /fraudreport", flush=True)
except Exception as _exc:  # noqa: BLE001
    _router_load_errors["fraud_report.fraudreport"] = _tb.format_exc()
    print(f"\n[main] !! FAILED TO MOUNT fraud_report: {_exc}", flush=True)


# ── UI feature-flag endpoint ──
# Returns config values the frontend needs to read at runtime — feature
# flags, support contact details, etc. Kept narrow on purpose: only the
# values that are *meant* to be public are exposed. Anything secret
# (API keys, JWT secret, DB creds) must never appear here.
@app.get("/api/avatars")
def _list_avatars():
    """Return the list of avatars available for Custom Video Role
    Play. The frontend's avatar-picker modal calls this to render
    every MP4 in static/assets/avatars/ as a selectable card.

    Response shape:
        {
          "ok":     true,
          "count":  N,
          "male":   M,
          "female": F,
          "items":  [
            { "name": "male_one.mp4", "gender": "male",
              "size_bytes": 1234567,
              "url": "/static/assets/avatars/male_one.mp4",
              "voice": "onyx" },
            ...
          ]
        }

    Discovery is dynamic (services.video_engine.avatar_manifest), so
    dropping a new MP4 into the assets folder makes it appear in the
    picker without any code change.

    Public on purpose — no PII, just filenames that are already
    served as static files at /static/assets/avatars/*.
    """
    try:
        from services import video_engine
        items = video_engine.avatar_manifest()
    except Exception as exc:  # noqa: BLE001
        log.warning("avatar manifest failed: %s", exc)
        items = []
    male   = sum(1 for x in items if x.get("gender") == "male")
    female = sum(1 for x in items if x.get("gender") == "female")
    return {
        "ok":     True,
        "count":  len(items),
        "male":   male,
        "female": female,
        "items":  items,
    }


@app.get("/api/config/ui")
def _ui_config():
    return {
        "show_dev_skill_button": bool(getattr(settings, "SHOW_DEV_SKILL_BUTTON", False)),
        "support_phone":         getattr(settings, "SUPPORT_PHONE", ""),
        "support_email":         getattr(settings, "SUPPORT_EMAIL", ""),
        # When true the Custom Video Role Play flow opens an avatar
        # picker modal listing every MP4 in static/assets/avatars/
        # instead of relying on the seeded-random server-side pick.
        "is_video_roleplay_selection": bool(
            getattr(settings, "IS_VIDEO_ROLEPLAY_SELECTION", False)
        ),
        # When true the role-play page engages its in-session
        # navigation lock (back/refresh/close guard) once the
        # candidate enters the session screen. Set IS_SESSION_LOCK_ENABLED=false
        # in .env to fully disable it for demos / screen-share
        # walkthroughs. Fraud-detection is independent and keeps
        # running either way.
        "is_session_lock_enabled": bool(
            getattr(settings, "IS_SESSION_LOCK_ENABLED", True)
        ),
    }


# ── Diagnostic endpoint ──
# Hit this from the deployed server (e.g. `curl https://<host>/api/_health/routers`)
# to see which optional routers loaded and the full traceback for any that
# failed. This is what the user should reach for first when role-play /
# coding endpoints 404 in production but work locally.
@app.get("/api/_health/routers")
def _routers_health():
    loaded = {
        "auth":               True,
        "coding":             True,
        "fraud":              True,
        "setup":              True,
        "interview":          True,
        "peoplehub_external": True,
        "sessions":           sessions_router  is not None,
        "websocket":          ws_router        is not None,
        "scenarios":          scenarios_router is not None,
        "tavus":              tavus_router     is not None,
    }
    return {
        "loaded":      loaded,
        "load_errors": _router_load_errors,
    }

# ── Static files ──────────────────────────────────────────────────────────────
# Mount order matters in Starlette: more-specific paths must register
# FIRST so they win over the broader /static mount. Therefore we mount
# /static/cache (Wav2Lip output, lives under data/cache) before /static
# (version-controlled assets, lives under static/). Reversing this would
# 404 every generated video — the /static mount would match first and
# look for the file under the wrong directory.
_VIDEO_CACHE_DIR = BASE_DIR / "data" / "cache"
_VIDEO_CACHE_DIR.mkdir(parents=True, exist_ok=True)
app.mount("/static/cache", StaticFiles(directory=str(_VIDEO_CACHE_DIR)), name="generated_cache")
app.mount("/static",       StaticFiles(directory=str(STATIC_DIR)),       name="static")


# Health endpoint for the video pipeline. Hit this from the deployed
# server to verify Wav2Lip is installed, the avatar MP4s are present,
# and ffmpeg is on PATH — no shell access needed.
@app.get("/api/_health/video")
def _video_health():
    try:
        from services import video_engine
        return video_engine.health()
    except Exception as exc:  # noqa: BLE001
        import traceback
        return JSONResponse(
            status_code=500,
            content={
                "ok":        False,
                "error":     str(exc),
                "traceback": traceback.format_exc(),
            },
        )


# Disable browser caching of /static/* during active development so the
# candidate always pulls the latest JS / CSS. Swap to a long max-age + hashed
# filenames once the UI is locked.
@app.middleware("http")
async def _no_cache_static(request, call_next):
    response = await call_next(request)
    if request.url.path.startswith("/static"):
        response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
        response.headers["Pragma"]        = "no-cache"
        response.headers["Expires"]       = "0"
    return response


# ── HTML page helper ──────────────────────────────────────────────────────────

_NO_CACHE_HEADERS = {
    "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
    "Pragma":        "no-cache",
    "Expires":       "0",
}


def _html(filename: str) -> FileResponse | HTMLResponse:
    """Serve an HTML page with strict no-cache headers so candidates never
    hit a stale version after we ship UI changes (browser cache was causing
    the old profile screen to keep rendering after redesigns)."""
    path = HTML_DIR / filename
    if not path.exists():
        log.warning("Missing HTML page: %s", path)
        return HTMLResponse(
            f"<h2>Missing: {filename}</h2>"
            f"<p>Copy the HTML file to <code>static/html/{filename}</code></p>",
            status_code=404,
            headers=_NO_CACHE_HEADERS,
        )
    return FileResponse(path, headers=_NO_CACHE_HEADERS)


# ── Page routes ───────────────────────────────────────────────────────────────

@app.get("/", include_in_schema=False)
def root():
    """Login page — entry point for all candidates."""
    return _html("people_hub_login.html")


@app.get("/login", include_in_schema=False)
def login_page():
    """Alias for /, so api.js logout redirect works."""
    return _html("people_hub_login.html")


@app.get("/setup", include_in_schema=False)
def setup():
    """Interview environment setup page."""
    return _html("people_hub_setup.html")


@app.get("/profile", include_in_schema=False)
def profile_page():
    """Profile screen — reads candidate data from localStorage."""
    return _html("people_hub_profile.html")


@app.get("/no-interview", include_in_schema=False)
def no_interview_page():
    """Empty state shown when the candidate has no active interview stage."""
    return _html("people_hub_no_interview.html")


# ── Stage-based pages (referenced by STAGE_ROUTES) ────────────────────────────

@app.get("/people_hub_assessment", include_in_schema=False)
def stage_assessment():
    return _html("people_hub_assessment.html")


@app.get("/people_hub_role_play", include_in_schema=False)
def stage_role_play():
    # Single canonical source of truth — the typo-named duplicate
    # (people_hub_role_play..html) has been removed from the repo.
    return _html("people_hub_role_play.html")


@app.get("/people_hub_coding_assessment", include_in_schema=False)
def stage_coding():
    return _html("people_hub_coding_assessment.html")


@app.get("/tavus-guide", include_in_schema=False)
async def tavus_guide_page():
    """Serve local Tavus guide if present, else redirect to public docs."""
    guide = TEMPLATES_DIR / "tavus_guide.html"
    if guide.exists():
        return FileResponse(str(guide))
    return RedirectResponse("https://docs.tavus.io")


# ── Health & API info ─────────────────────────────────────────────────────────

@app.get("/health", tags=["health"])
def health():
    """Liveness + readiness probe. Includes DB pool status."""
    return {
        "status":  "ok",
        "service": settings.APP_NAME,
        "version": settings.APP_VERSION,
        "db":      "ready" if db.is_ready() else "down",
    }


@app.get("/api", tags=["health"])
def api_info():
    return JSONResponse({
        "service":  settings.APP_NAME,
        "version":  settings.APP_VERSION,
        "login":    "GET /",
        "docs":     "GET /docs",
        "stages":   settings.STAGE_ROUTES,
        "endpoints": {
            "external_login":     "POST /api/external/login/{access_key}",
            "external_roleplays": "POST /api/external/roleplays/{access_key}",
            "external_coding":    "POST /api/external/coding/{access_key}",
            "captcha_challenge":  "POST /api/auth/captcha/challenge",
            "captcha_verify":     "POST /api/auth/captcha/verify",
            "login":              "POST /api/auth/login",
            "me":                 "GET  /api/auth/me",
            "photo_upload":       "POST /api/setup/photo",
            "setup_complete":     "POST /api/setup/complete",
            "coding_questions":   "GET  /api/coding/questions",
            "run_code":           "POST /api/coding/run",
            "submit_code":        "POST /api/coding/submit",
            "final_submit":       "POST /api/coding/final-submit",
            "log_fraud":          "POST /api/fraud/event",
            "fraud_report":       "GET  /api/fraud/report",
        },
    })


# ── Entry point ───────────────────────────────────────────────────────────────
if __name__ == "__main__":
    log.info(" Login    → http://localhost:%d", settings.APP_PORT)
    log.info(" API Docs → http://localhost:%d/docs", settings.APP_PORT)
    log.info(" Health   → http://localhost:%d/health", settings.APP_PORT)

    uvicorn.run(
        "main:app",
        host=settings.APP_HOST,
        port=settings.APP_PORT,
        reload=settings.APP_DEBUG,
        log_level=settings.LOG_LEVEL.lower(),
    )
