"""VP deep-learn API — /v1/virtual-personas/{id}/deep-learn/*。"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field

from niannian.chat.session import SessionNotFoundError, TurnInProgressError
from niannian.chat.stream import format_sse_event
from niannian.http.routes.virtual_persona import _require_vp_write
from niannian.soul.search_providers import build_search_provider_from_yaml
from niannian.soul.web_learning import WebLearningService
from niannian.virtual_persona.deep_learn import (
    DeepLearnError,
    DeepLearnSearchUnavailable,
    complete_deep_learn,
    resolve_deep_learn_mock_default,
    run_all_deep_learn,
    run_deep_learn,
    start_deep_learn,
)
from niannian.virtual_persona.canon_lookup import CanonLookupError, approve_canon_candidates, run_canon_lookup
from niannian.virtual_persona.store import VirtualPersonaStore
from niannian.virtual_persona.trial import (
    TrialChatForbiddenError,
    TrialSessionError,
    assert_trial_chat_allowed,
    ensure_trial_session,
)

if TYPE_CHECKING:
    from niannian.client import NianNian


class DeepLearnRunBody(BaseModel):
    task_id: str = Field(..., min_length=1)
    mock: bool | None = None


class DeepLearnRunAllBody(BaseModel):
    mock: bool | None = None


class TrialChatBody(BaseModel):
    text: str = ""
    session_id: str | None = None
    stream: bool = True
    show_trace: bool = True


class CanonLookupBody(BaseModel):
    query: str = Field(..., min_length=1)
    sources: list[str] = Field(default_factory=list)
    mock: bool = True


class CanonLookupApproveBody(BaseModel):
    candidate_ids: list[str] = Field(..., min_length=1)


def _load_web_learning_yaml(nn: NianNian) -> dict:
    svc = WebLearningService(nn._subject_store, nn._config, home=nn._home)
    return svc.load_yaml_config()


def _web_learning_yaml(nn: NianNian, *, mock: bool = False) -> dict:
    yaml_cfg = _load_web_learning_yaml(nn)
    if mock:
        yaml_cfg = {
            **yaml_cfg,
            "mock_search": {**dict(yaml_cfg.get("mock_search") or {}), "enabled": True},
        }
    return yaml_cfg


def _resolve_deep_learn_mock(nn: NianNian, requested: bool | None) -> bool:
    if requested is not None:
        return requested
    return resolve_deep_learn_mock_default(_load_web_learning_yaml(nn))


def create_vp_deep_learn_router(nn: NianNian) -> APIRouter:
    router = APIRouter(prefix="/v1/virtual-personas", tags=["virtual-persona"])
    pipeline = nn.chat

    def _resolve_vp(request: Request, vp_id: str) -> tuple[str, VirtualPersonaStore]:
        user_id, _role = _require_vp_write(request, nn)
        store = VirtualPersonaStore(nn._home, user_id)
        try:
            store.get(vp_id)
        except FileNotFoundError as exc:
            raise HTTPException(
                status_code=404,
                detail={"code": "vp_not_found", "message": str(exc)},
            ) from exc
        except ValueError as exc:
            raise HTTPException(
                status_code=400,
                detail={"code": "invalid_vp_id", "message": str(exc)},
            ) from exc
        return user_id, store

    @router.post("/{vp_id}/trial/chat")
    def trial_chat(request: Request, vp_id: str, body: TrialChatBody):
        user_id, store = _resolve_vp(request, vp_id)
        try:
            assert_trial_chat_allowed(store, vp_id)
        except TrialChatForbiddenError as exc:
            raise HTTPException(
                status_code=403,
                detail={"code": "trial_chat_forbidden", "message": str(exc)},
            ) from exc
        try:
            session = ensure_trial_session(
                pipeline,
                vp_id=vp_id,
                user_id=user_id,
                session_id=body.session_id,
            )
        except SessionNotFoundError:
            raise HTTPException(status_code=404, detail={"code": "session_not_found"})
        except TrialSessionError as exc:
            raise HTTPException(
                status_code=400,
                detail={"code": "invalid_trial_session", "message": str(exc)},
            ) from exc

        if body.stream:
            return StreamingResponse(
                _sse_trial_chat_generator(pipeline, session.id, body),
                media_type="text/event-stream",
                headers={
                    "Cache-Control": "no-cache",
                    "Connection": "keep-alive",
                    "X-Accel-Buffering": "no",
                },
            )

        try:
            result = pipeline.run_turn(
                session.id,
                body.text,
                stream=False,
                show_trace=body.show_trace,
            )
        except TurnInProgressError:
            raise HTTPException(status_code=409, detail={"code": "turn_in_progress"})
        except Exception as exc:
            raise HTTPException(
                status_code=500,
                detail={"code": "turn_failed", "message": str(exc) or "trial chat failed"},
            )
        payload = result.to_dict()
        payload["session_id"] = session.id
        payload["trial"] = True
        payload["equipped_vp_id"] = vp_id
        return payload

    @router.post("/{vp_id}/deep-learn/start")
    def deep_learn_start(request: Request, vp_id: str) -> dict[str, Any]:
        user_id, store = _resolve_vp(request, vp_id)
        try:
            return start_deep_learn(store, vp_id)
        except DeepLearnError as exc:
            raise HTTPException(
                status_code=400,
                detail={"code": "deep_learn_error", "message": str(exc)},
            ) from exc

    @router.post("/{vp_id}/deep-learn/run")
    def deep_learn_run(request: Request, vp_id: str, body: DeepLearnRunBody) -> dict[str, Any]:
        _user_id, store = _resolve_vp(request, vp_id)
        mock = _resolve_deep_learn_mock(nn, body.mock)
        yaml_cfg = _web_learning_yaml(nn, mock=mock)
        search = build_search_provider_from_yaml(yaml_cfg)
        svc = WebLearningService(nn._subject_store, nn._config, home=nn._home)
        allowlist = svc.allowlist_patterns()
        try:
            return run_deep_learn(
                store,
                vp_id,
                task_id=body.task_id,
                search=search,
                allowlist=allowlist,
                mock=mock,
            )
        except DeepLearnSearchUnavailable as exc:
            raise HTTPException(
                status_code=400,
                detail={"code": "search_unavailable", "message": str(exc)},
            ) from exc
        except DeepLearnError as exc:
            raise HTTPException(
                status_code=400,
                detail={"code": "deep_learn_error", "message": str(exc)},
            ) from exc

    @router.post("/{vp_id}/deep-learn/run-all")
    def deep_learn_run_all(request: Request, vp_id: str, body: DeepLearnRunAllBody) -> dict[str, Any]:
        _user_id, store = _resolve_vp(request, vp_id)
        mock = _resolve_deep_learn_mock(nn, body.mock)
        yaml_cfg = _web_learning_yaml(nn, mock=mock)
        search = build_search_provider_from_yaml(yaml_cfg)
        svc = WebLearningService(nn._subject_store, nn._config, home=nn._home)
        allowlist = svc.allowlist_patterns()
        try:
            return run_all_deep_learn(
                store,
                vp_id,
                search=search,
                allowlist=allowlist,
                mock=mock,
            )
        except DeepLearnSearchUnavailable as exc:
            raise HTTPException(
                status_code=400,
                detail={"code": "search_unavailable", "message": str(exc)},
            ) from exc
        except DeepLearnError as exc:
            raise HTTPException(
                status_code=400,
                detail={"code": "deep_learn_error", "message": str(exc)},
            ) from exc

    @router.post("/{vp_id}/deep-learn/complete")
    def deep_learn_complete(request: Request, vp_id: str) -> dict[str, Any]:
        _user_id, store = _resolve_vp(request, vp_id)
        try:
            return complete_deep_learn(store, vp_id)
        except DeepLearnError as exc:
            raise HTTPException(
                status_code=400,
                detail={"code": "deep_learn_error", "message": str(exc)},
            ) from exc

    @router.post("/{vp_id}/canon-lookup")
    def canon_lookup(request: Request, vp_id: str, body: CanonLookupBody) -> dict[str, Any]:
        _user_id, store = _resolve_vp(request, vp_id)
        yaml_cfg = _web_learning_yaml(nn, mock=body.mock)
        search = build_search_provider_from_yaml(yaml_cfg)
        svc = WebLearningService(nn._subject_store, nn._config, home=nn._home)
        allowlist = svc.allowlist_patterns()
        try:
            return run_canon_lookup(
                store,
                vp_id,
                query=body.query,
                sources=body.sources,
                search=search,
                allowlist=allowlist,
                mock=body.mock,
            )
        except CanonLookupError as exc:
            raise HTTPException(
                status_code=400,
                detail={"code": "canon_lookup_error", "message": str(exc)},
            ) from exc

    @router.post("/{vp_id}/canon-lookup/approve")
    def canon_lookup_approve(
        request: Request,
        vp_id: str,
        body: CanonLookupApproveBody,
    ) -> dict[str, Any]:
        _user_id, store = _resolve_vp(request, vp_id)
        try:
            return approve_canon_candidates(store, vp_id, candidate_ids=body.candidate_ids)
        except CanonLookupError as exc:
            raise HTTPException(
                status_code=400,
                detail={"code": "canon_lookup_error", "message": str(exc)},
            ) from exc

    return router


def _sse_trial_chat_generator(pipeline, session_id: str, payload: TrialChatBody):
    session = pipeline.sessions.require(session_id)
    yield format_sse_event(
        "session",
        {
            "session_id": session_id,
            "trial": True,
            "equipped_vp_id": session.meta.get("equipped_vp_id"),
            "project": session.project,
        },
    )
    try:
        events = pipeline.run_turn(
            session_id,
            payload.text,
            stream=True,
            show_trace=payload.show_trace,
        )
        for event, data in events:
            if event == "_return":
                continue
            yield format_sse_event(event, data)
    except TurnInProgressError:
        yield format_sse_event(
            "error",
            {"code": "turn_in_progress", "message": "turn in progress", "recoverable": True},
        )
        yield format_sse_event("done", {"turn_id": "", "ingested": False, "trace": []})
    except SessionNotFoundError:
        yield format_sse_event(
            "error",
            {"code": "session_not_found", "message": "session not found", "recoverable": False},
        )
        yield format_sse_event("done", {"turn_id": "", "ingested": False, "trace": []})
    except Exception as exc:
        yield format_sse_event(
            "error",
            {"code": "turn_failed", "message": str(exc) or "trial chat failed", "recoverable": True},
        )
        yield format_sse_event("done", {"turn_id": "", "ingested": False, "trace": []})
