"""HTTP API：只读聊天历史 + 可选全功能 Runtime（多 connection，与 Host 同款 invoke）。"""

from __future__ import annotations

import os
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Any, AsyncGenerator

from aicd.api_runtime import RuntimeConnectionManager
from aicd.config import AgentConfig, load_config
from aicd.db.store import TaskStore
from aicd.home import init_home
from aicd.agent.chat_serialize import VISION_IMAGE_TOKENS_PER_IMAGE


def create_aicd_serve_app(
    *,
    layout: dict[str, Path],
    home: Path,
    api_key: str | None,
    runtime_api: bool = False,
    max_connections: int = 16,
    require_invoke_nonce: bool = False,
    skip_setup: bool = True,
    no_plugins: bool = False,
    plugin_policy: str | None = None,
) -> Any:
    from fastapi import Body, Depends, FastAPI, Header, HTTPException, Query

    db_path = layout["db"]

    @asynccontextmanager
    async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
        store = TaskStore(db_path)
        conn = await store.connect()
        app.state.store = store
        app.state.conn = conn
        cfg_path = layout["config"]
        try:
            app.state.app_config = load_config(cfg_path) if cfg_path.is_file() else None
        except Exception:  # noqa: BLE001
            app.state.app_config = None
        app.state.runtime_manager = None
        if runtime_api:
            app.state.runtime_manager = RuntimeConnectionManager(
                home=home,
                max_connections=max_connections,
                skip_setup=skip_setup,
                setup_tty_relaxed=True,
                no_plugins=no_plugins,
                plugin_policy=plugin_policy,
            )
        yield
        mgr: RuntimeConnectionManager | None = app.state.runtime_manager
        if mgr is not None:
            await mgr.shutdown_all()
        await conn.close()

    app = FastAPI(title="Aicd HTTP API", version="2.0", lifespan=lifespan)

    async def optional_auth(
        x_api_key: str | None = Header(default=None, alias="X-API-Key"),
        authorization: str | None = Header(default=None),
    ) -> None:
        expected = (api_key or os.environ.get("AICD_API_KEY") or "").strip()
        if not expected:
            return
        got = (x_api_key or "").strip()
        if not got and authorization:
            auth = authorization.strip()
            if auth.lower().startswith("bearer "):
                got = auth[7:].strip()
        if got != expected:
            raise HTTPException(status_code=401, detail="invalid or missing API key")

    def _msg_row(r: Any) -> dict[str, Any]:
        return {
            "id": r.id,
            "session_id": r.session_id,
            "role": r.role,
            "content": r.content,
            "tool_calls_json": r.tool_calls_json,
            "created_at": r.created_at,
            "tool_call_id": r.tool_call_id,
            "tool_name": r.tool_name,
        }

    def _sum_row(r: Any) -> dict[str, Any]:
        return {
            "id": r.id,
            "session_id": r.session_id,
            "content": r.content,
            "covers_up_to_message_id": r.covers_up_to_message_id,
            "created_at": r.created_at,
        }

    @app.get("/health")
    async def health() -> dict[str, Any]:
        out: dict[str, Any] = {"ok": True, "runtime_api": runtime_api}
        mgr: RuntimeConnectionManager | None = app.state.runtime_manager
        if mgr is not None:
            out["connections"] = mgr.connection_count
        return out

    @app.get("/v1/sessions/{session_id}/overview")
    async def session_overview(
        session_id: str,
        _: None = Depends(optional_auth),
    ) -> dict[str, Any]:
        """会话规模 + 与 TUI 一致的上下文预算说明；便于外部追溯用户/助手/tool 行（见 messages）。"""
        store: TaskStore = app.state.store
        conn = app.state.conn
        cfg = getattr(app.state, "app_config", None)
        ag = cfg.agent if cfg is not None else AgentConfig()
        msg_n = await store.count_chat_messages(conn, session_id)
        sum_n = await store.count_chat_summaries(conn, session_id)
        ctx_payload: dict[str, Any] = {
            "context_budget_tokens": ag.context_budget_tokens,
            "chat_history_max_messages": ag.chat_history_max_messages,
            "vision_image_tokens_per_image_for_budget": VISION_IMAGE_TOKENS_PER_IMAGE,
            "hydration_note": (
                "Tail up to chat_history_max_messages, then trim by estimated tokens "
                f"(text ~4 chars/token; each vision image_url counts as "
                f"{VISION_IMAGE_TOKENS_PER_IMAGE} tokens)."
            ),
        }
        mgr_live: RuntimeConnectionManager | None = getattr(
            app.state, "runtime_manager", None
        )
        if mgr_live is not None:
            rctx = mgr_live.find_runtime_for_chat_session(session_id)
            if rctx is not None:
                try:
                    live = rctx.llm.model_context_meta()
                    ctx_payload["runtime_connected"] = True
                    ctx_payload["context_budget_tokens_effective"] = live.get(
                        "context_budget_tokens_effective"
                    )
                    ctx_payload["model_context_window_tokens"] = live.get(
                        "model_context_window_tokens"
                    )
                    ctx_payload["model_context_window_main_merged"] = live.get(
                        "model_context_window_main_merged"
                    )
                    ctx_payload["model_context_window_vision_merged"] = live.get(
                        "model_context_window_vision_merged"
                    )
                    ctx_payload["model_context_probe_source"] = live.get(
                        "model_context_probe_source"
                    )
                    ctx_payload["completion_reserve_tokens"] = live.get(
                        "completion_reserve_tokens"
                    )
                except Exception:  # noqa: BLE001
                    ctx_payload["runtime_connected"] = True
                    ctx_payload["runtime_model_context_error"] = True
        return {
            "session_id": session_id,
            "chat_messages_total": msg_n,
            "chat_summaries_total": sum_n,
            "context": ctx_payload,
            "trace": {
                "messages_url": f"/v1/sessions/{session_id}/messages?limit=&offset=",
                "summaries_url": f"/v1/sessions/{session_id}/summaries",
                "row_roles": "user | assistant | tool (tool rows carry tool_call_id, tool_name)",
            },
        }

    @app.get("/v1/sessions/{session_id}/messages")
    async def list_messages(
        session_id: str,
        limit: int = Query(default=200, ge=1, le=2000),
        offset: int = Query(default=0, ge=0),
        _: None = Depends(optional_auth),
    ) -> dict[str, Any]:
        store: TaskStore = app.state.store
        conn = app.state.conn
        rows = await store.list_chat_messages(
            conn, session_id, limit=limit, offset=offset
        )
        total = await store.count_chat_messages(conn, session_id)
        return {
            "session_id": session_id,
            "total": total,
            "offset": offset,
            "limit": limit,
            "messages": [_msg_row(r) for r in rows],
        }

    @app.get("/v1/sessions/{session_id}/summaries")
    async def list_summaries(
        session_id: str,
        limit: int = Query(default=50, ge=1, le=500),
        _: None = Depends(optional_auth),
    ) -> dict[str, Any]:
        store: TaskStore = app.state.store
        conn = app.state.conn
        rows = await store.list_chat_summaries(conn, session_id, limit=limit)
        return {
            "session_id": session_id,
            "summaries": [_sum_row(r) for r in rows],
        }

    if runtime_api:

        @app.post("/v1/connections")
        async def create_connection(
            body: dict[str, Any] = Body(default_factory=dict),
            _: None = Depends(optional_auth),
        ) -> dict[str, Any]:
            mgr: RuntimeConnectionManager = app.state.runtime_manager
            chat_sid = body.get("chat_session_id")
            cs = str(chat_sid).strip() if chat_sid not in (None, "") else None
            np = body.get("no_plugins")
            np_b: bool | None = bool(np) if isinstance(np, bool) else None
            pol_raw = body.get("plugin_policy")
            pol = str(pol_raw).strip() if isinstance(pol_raw, str) and pol_raw.strip() else None
            try:
                cid, sid = await mgr.create(
                    chat_session_id=cs,
                    no_plugins=np_b,
                    plugin_policy=pol,
                )
            except RuntimeError as e:
                raise HTTPException(status_code=503, detail=str(e)) from e
            return {"connection_id": cid, "chat_session_id": sid}

        @app.delete("/v1/connections/{connection_id}")
        async def delete_connection(
            connection_id: str,
            _: None = Depends(optional_auth),
        ) -> dict[str, Any]:
            mgr: RuntimeConnectionManager = app.state.runtime_manager
            gone = await mgr.destroy(connection_id)
            if not gone:
                raise HTTPException(status_code=404, detail="unknown connection_id")
            return {"ok": True}

        @app.post("/v1/connections/{connection_id}/invoke")
        async def invoke_on_connection(
            connection_id: str,
            body: dict[str, Any] = Body(default_factory=dict),
            _: None = Depends(optional_auth),
        ) -> dict[str, Any]:
            mgr: RuntimeConnectionManager = app.state.runtime_manager
            env_nonce = os.environ.get("AICD_HTTP_REQUIRE_INVOKE_NONCE") == "1"
            return await mgr.invoke(
                connection_id,
                body,
                require_nonce=require_invoke_nonce or env_nonce,
            )

    return app


def create_chat_read_api(*, db_path: Path, api_key: str | None) -> Any:
    """向后兼容：等同仅开启只读路由的 ``create_aicd_serve_app``。"""
    home = db_path.parent.parent
    root, layout, _cfg = init_home(home)
    return create_aicd_serve_app(
        layout=layout,
        home=root,
        api_key=api_key,
        runtime_api=False,
    )


async def run_serve(
    *,
    home: Path | None,
    host: str,
    port: int,
    api_key: str | None,
    runtime_api: bool = False,
    max_connections: int = 16,
    require_invoke_nonce: bool = False,
    skip_setup: bool = True,
    no_plugins: bool = False,
    plugin_policy: str | None = None,
) -> None:
    import uvicorn

    root, layout, _cfg = init_home(home)
    app = create_aicd_serve_app(
        layout=layout,
        home=root,
        api_key=api_key,
        runtime_api=runtime_api,
        max_connections=max_connections,
        require_invoke_nonce=require_invoke_nonce,
        skip_setup=skip_setup,
        no_plugins=no_plugins,
        plugin_policy=plugin_policy,
    )
    config = uvicorn.Config(app, host=host, port=port, log_level="info")
    await uvicorn.Server(config).serve()
