"""
routers/sessions.py — session management and PDF report download.
"""
from __future__ import annotations
import uuid
from datetime import datetime, timezone
from typing import Any

from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import Response

from models.schemas import Scenario, ScenarioCreate, SessionReport, SessionState
from services import store
from services.auth import get_current_user, decode_token
from services.openai_service import generate_report_analysis
# NOTE: `services.report_pdf` is imported lazily inside the PDF download
# endpoint below. ReportLab is a heavyweight optional dependency, and a
# missing/broken install would otherwise blow up *this* module at import
# time — which silently knocks out every /api/sessions/* route (including
# /start-external used by the role-play screen, producing the opaque
# "Could not start session: HTTP 404" the candidate sees).
from utils.logger import get_logger

log = get_logger(__name__)
router = APIRouter(prefix="/api/sessions", tags=["sessions"])


@router.post("/start")
async def start_session(body: dict, current_user=Depends(get_current_user)):
    scenario_id   = body.get("scenario_id", "").strip()
    language_code = (body.get("language_code") or "en").strip() or "en"
    if not scenario_id:
        raise HTTPException(422, detail="scenario_id is required")
    scenario = await store.get_scenario(scenario_id)
    if not scenario:
        raise HTTPException(404, detail="Scenario not found")
    session_id = str(uuid.uuid4())[:12]
    session = SessionState(
        session_id=session_id,
        scenario_id=scenario_id,
        language_code=language_code,
        started_at=datetime.now(timezone.utc).isoformat(),
    )
    await store.save_session(session)
    return {"session_id": session_id}


# ── External-scenario bridge (new external login flow) ───────────────────────
# The browser receives role-play scenarios from the external People Hub API
# and sends the entire scenario object inline when the user picks one. We
# persist it to the local store so the existing WebSocket implementation
# (which loads scenario + session from store) keeps working unchanged.

def _coerce_scenario(raw: dict, access_key: str) -> Scenario:
    """Accept arbitrary external API shape (snake_case OR camelCase), fill in
    safe defaults, and validate."""

    def g(*keys, default=""):
        """Pick the first non-empty value from any of the given keys."""
        for k in keys:
            v = raw.get(k)
            if v not in (None, ""):
                return v
        return default

    # Synthetic id keyed off access_key + raw id, to keep entries stable
    raw_id  = str(g("id", default=uuid.uuid4().hex[:12]))
    syn_id  = f"ak-{access_key[:8]}-{raw_id}"[:48]

    return Scenario(
        id=syn_id,
        status="live",
        completions=int(g("completions", default=0) or 0),
        avg_score=float(g("avg_score", "avgScore", default=0) or 0),
        created_at=datetime.now(timezone.utc).isoformat(),
        updated_at=datetime.now(timezone.utc).isoformat(),
        title          = str(g("title", "name", default="Untitled scenario"))[:80],
        description    = str(g("description", "summary", "desc", default="Practise this conversation."))[:300],
        category       = str(g("category", "cat", "industry", default="cs")),
        cat_label      = str(g("cat_label", "catLabel", "category_label", "categoryLabel",
                               "category", default="General")),
        context        = str(g("context", "scene", "background", "description", default="Have a realistic conversation.")),
        learner_role   = str(g("learner_role", "learnerRole", "user_role", "userRole",
                               default="Candidate"))[:60],
        learner_emoji  = str(g("learner_emoji", "learnerEmoji", "user_emoji", "userEmoji",
                               default="🧑‍💼")),
        ai_character   = str(g("ai_character", "aiCharacter", "character", "partner", "npc",
                               default="AI Character"))[:60],
        ai_emoji       = str(g("ai_emoji", "aiEmoji", "character_emoji", "characterEmoji",
                               default="🤖")),
        ai_personality = str(g("ai_personality", "aiPersonality", "personality", default="")),
        difficulty     = str(g("difficulty", "level", default="Intermediate")),
        turns          = max(4, min(int(g("turns", "max_turns", "maxTurns", default=8) or 8), 25)),
        scoring        = str(g("scoring", "scoring_mode", "scoringMode", default="Guided with Feedback")),
        time_limit     = str(g("time_limit", "timeLimit", "duration", "time", default="10 min")),
        language_code  = str(g("language_code", "languageCode", "lang_code", "langCode",
                               default="en")),
        language_name  = str(g("language_name", "languageName", "lang_name", "langName",
                               default="English")),
        language_flag  = str(g("language_flag", "languageFlag", "lang_flag", "langFlag", "flag",
                               default="🇬🇧")),
        industry       = str(g("industry", "industryName", default="")),
        assign_group   = str(g("assign_group", "assignGroup", default="All users")),
        # Voice is always ON — the candidate UI expects audio playback. The
        # external admin panel sometimes returns voiceOn=false as a default,
        # which would silence the AI. We override that here so role plays
        # always have voice. (chat_on / report_on still respect the API.)
        voice_on       = True,
        chat_on        = bool(g("chat_on", "chatOn", "enable_chat", "enableChat", default=True)),
        report_on      = bool(g("report_on", "reportOn", "enable_report", "enableReport", default=True)),
        featured       = bool(g("featured", "is_featured", "isFeatured", default=False)),
    )


@router.post("/start-external")
async def start_session_external(body: dict[str, Any]):
    """
    Start a session for a candidate authenticated via the external People
    Hub login (no JWT). Body shape:

        {
          "access_key":   "<the candidate's access key>",
          "scenario":     { ...full external scenario object... },
          "language_code": "en"   (optional)
        }
    """
    access_key = (body or {}).get("access_key", "").strip()
    raw_sc     = (body or {}).get("scenario") or {}
    lang_code  = ((body or {}).get("language_code") or "en").strip() or "en"
    # Candidate display name — used by the WS greeting ("Hey <name>,
    # shall we start the interview?"). Optional; if missing the greeting
    # falls back to a generic "there".
    cand_name  = ((body or {}).get("candidate_name") or "").strip()
    # Optional explicit avatar selection from the picker modal — only
    # honored when IS_VIDEO_ROLEPLAY_SELECTION is enabled in .env and
    # the candidate completed the picker. Stored on the SessionState
    # so _ensure_avatar() in the WS route picks it up.
    selected_avatar = ((body or {}).get("selected_avatar") or "").strip()

    if not access_key:
        raise HTTPException(400, detail="access_key is required")
    if not isinstance(raw_sc, dict) or not raw_sc:
        raise HTTPException(422, detail="scenario object is required")

    # Coerce to a valid Scenario; reject only if it's truly unusable
    try:
        scenario = _coerce_scenario(raw_sc, access_key)
    except Exception as exc:  # noqa: BLE001
        log.warning("[sessions] coerce scenario failed: %s", exc)
        raise HTTPException(422, detail=f"Invalid scenario: {exc}")

    # Persist (upsert) into local store under its synthetic id
    try:
        await store.upsert_scenario(scenario)
    except Exception as exc:  # noqa: BLE001
        log.warning("[sessions] persist external scenario failed: %s", exc)

    session_id = str(uuid.uuid4())[:12]

    # Defensive validation of selected_avatar — only accept filenames
    # that actually exist on disk, so a tampered body can't make the
    # WS open a random file or sneak in a path traversal.
    cleaned_avatar = ""
    if selected_avatar:
        try:
            from services import video_engine
            allowed = set(video_engine.list_available_avatars())
            if selected_avatar in allowed:
                cleaned_avatar = selected_avatar
            else:
                log.warning(
                    "[sessions] rejected selected_avatar=%s (not in asset list)",
                    selected_avatar,
                )
        except Exception as exc:  # noqa: BLE001
            log.warning("[sessions] avatar validation failed: %s", exc)

    session = SessionState(
        session_id     = session_id,
        scenario_id    = scenario.id,
        language_code  = lang_code,
        started_at     = datetime.now(timezone.utc).isoformat(),
        candidate_name = cand_name or None,
        selected_avatar = cleaned_avatar or None,
    )
    await store.save_session(session)

    log.info("[sessions] external start  ak=%s  scenario=%s  session=%s",
             access_key[:6] + "…", scenario.id, session_id)

    return {
        "session_id":  session_id,
        "scenario_id": scenario.id,
        "ws_token":    "ak:" + access_key,   # WS endpoint accepts ak:<access_key>
    }


@router.post("/{session_id}/end-external")
async def end_session_external(session_id: str, body: dict[str, Any]):
    """End a session that was started via /start-external. No JWT required —
    the session_id itself serves as the proof of having started a session.

    The full end-session request is logged as an "API SPEC SAMPLE" so the
    backend developer can use the exact shape when designing the upstream
    role-play results endpoint, and is mirrored to
    data/by_access_key/{access_key}/end_session.json.
    """
    body = body or {}
    session = await store.get_session(session_id)
    if not session:
        raise HTTPException(404, "Session not found")
    scenario = await store.get_scenario(session.scenario_id)
    if not scenario:
        raise HTTPException(404, "Scenario not found")

    duration   = int(body.get("duration_seconds") or 0)
    mode       = (body.get("mode") or "chat").strip()
    access_key = str(body.get("access_key") or "").strip()
    end_reason = (body.get("end_reason") or "completed").strip()

    # Lazy import to avoid a hard module-load coupling between sessions
    # and the external proxy, and so the helpers stay defined in one place.
    from routes.peoplehub_external import (
        _log_outgoing_request, _audit_dir, _ACCESS_KEY_RE,
    )
    import json

    spec_payload = {
        "access_key":      access_key or "<not-provided>",
        "session_id":      session_id,
        "scenario_id":     scenario.id,
        "scenario_title":  scenario.title,
        "duration_seconds": duration,
        "mode":            mode,
        "end_reason":      end_reason,
        "turns_completed": session.turn,
        "ended_at":        datetime.now(timezone.utc).isoformat(),
    }
    _log_outgoing_request("end_session", access_key or "anonymous", spec_payload)

    analysis = await generate_report_analysis(session.messages, scenario, duration)

    report = SessionReport(
        session_id      = session_id,
        scenario_title  = scenario.title,
        learner_role    = scenario.learner_role,
        ai_character    = scenario.ai_character,
        difficulty      = scenario.difficulty,
        language        = f"{scenario.language_flag} {scenario.language_name}",
        mode            = mode,
        turns_completed = session.turn,
        duration_seconds= duration,
        overall_score   = float(analysis.get("overall_score") or 0),
        verdict         = analysis.get("verdict", "needs_improvement"),
        dimensions      = analysis.get("dimensions", []),
        feedback        = analysis.get("feedback", []),
        sentiment_timeline = analysis.get("sentiment_timeline", []),
        transcript      = session.messages,
        generated_at    = datetime.now(timezone.utc).isoformat(),
    )

    # Mirror against access_key only when one was supplied and looks valid.
    if access_key and _ACCESS_KEY_RE.fullmatch(access_key):
        try:
            path = _audit_dir(access_key) / "end_session.json"
            existing: list = []
            if path.exists():
                try:
                    existing = json.loads(path.read_text(encoding="utf-8"))
                    if not isinstance(existing, list):
                        existing = [existing]
                except Exception:  # noqa: BLE001
                    existing = []
            existing.append({
                "request":  spec_payload,
                "response": report.model_dump(),
                "saved_at": datetime.now(timezone.utc).isoformat(),
            })
            # ensure_ascii=False keeps Hindi / Arabic / Devanagari etc.
            # readable in the mirror file instead of \uXXXX escapes.
            path.write_text(
                json.dumps(existing, indent=2, default=str, ensure_ascii=False),
                encoding="utf-8",
            )
            log.info("[end_session] mirrored → %s/end_session.json (%d total)",
                     access_key, len(existing))
        except Exception as exc:  # noqa: BLE001
            log.warning("[end_session] mirror failed (ignored): %s", exc)

    return report


@router.get("/{session_id}")
async def get_session(session_id: str, _=Depends(get_current_user)):
    s = await store.get_session(session_id)
    if not s:
        raise HTTPException(404, "Session not found")
    return s


@router.post("/{session_id}/end")
async def end_session(session_id: str, body: dict, _=Depends(get_current_user)):
    session = await store.get_session(session_id)
    if not session:
        raise HTTPException(404, "Session not found")
    scenario = await store.get_scenario(session.scenario_id)
    if not scenario:
        raise HTTPException(404, "Scenario not found")
    duration = int(body.get("duration_seconds") or 0)
    mode     = (body.get("mode") or "chat").strip()
    analysis = await generate_report_analysis(session.messages, scenario, duration)
    report = SessionReport(
        session_id=session_id,
        scenario_title=scenario.title,
        learner_role=scenario.learner_role,
        ai_character=scenario.ai_character,
        difficulty=scenario.difficulty,
        language=f"{scenario.language_flag} {scenario.language_name}",
        mode=mode,
        turns_completed=session.turn,
        duration_seconds=duration,
        overall_score=float(analysis.get("overall_score") or 0),
        verdict=analysis.get("verdict", "needs_improvement"),
        dimensions=analysis.get("dimensions", []),
        feedback=analysis.get("feedback", []),
        sentiment_timeline=analysis.get("sentiment_timeline", []),
        transcript=session.messages,
        generated_at=datetime.now(timezone.utc).isoformat(),
    )
    await store.save_session(SessionState(
        **session.model_dump(exclude={"ended_at"}),
        ended_at=datetime.now(timezone.utc).isoformat(),
    ))
    await store.patch_scenario(session.scenario_id, {
        "completions": (scenario.completions or 0) + 1,
    })
    return report


@router.get("/{session_id}/report/pdf")
async def download_report(
    session_id: str,
    token: str = Query(default=""),
):
    """
    Stream a PDF report. Auth via ?token= query param only —
    window.open() cannot send Authorization headers so we skip Depends()
    and validate the token manually from the query string.
    """
    if not token:
        raise HTTPException(status_code=401, detail="Authentication required — token missing")
    try:
        decode_token(token)
    except ValueError:
        raise HTTPException(status_code=401, detail="Invalid or expired token")

    session = await store.get_session(session_id)
    if not session:
        raise HTTPException(404, "Session not found")
    scenario = await store.get_scenario(session.scenario_id)
    if not scenario:
        raise HTTPException(404, "Scenario not found")

    analysis = await generate_report_analysis(session.messages, scenario, 0)
    report = SessionReport(
        session_id=session_id,
        scenario_title=scenario.title,
        learner_role=scenario.learner_role,
        ai_character=scenario.ai_character,
        difficulty=scenario.difficulty,
        language=f"{scenario.language_flag} {scenario.language_name}",
        mode="session",
        turns_completed=session.turn,
        duration_seconds=0,
        overall_score=float(analysis.get("overall_score") or 0),
        verdict=analysis.get("verdict", "needs_improvement"),
        dimensions=analysis.get("dimensions", []),
        feedback=analysis.get("feedback", []),
        sentiment_timeline=analysis.get("sentiment_timeline", []),
        transcript=session.messages,
        generated_at=datetime.now(timezone.utc).isoformat(),
    )
    # Lazy import — keeps the rest of /api/sessions/* working even if
    # ReportLab isn't installed in this environment.
    try:
        from services.report_pdf import generate_pdf
    except Exception as exc:  # noqa: BLE001
        log.exception("[sessions] report_pdf import failed")
        raise HTTPException(
            500,
            detail=f"PDF generator unavailable: {exc}. Install reportlab on the server.",
        )
    pdf_bytes = generate_pdf(report)
    return Response(
        content=pdf_bytes,
        media_type="application/pdf",
        headers={"Content-Disposition": f'attachment; filename="report_{session_id}.pdf"'},
    )