"""
cortex/hippo/router.py — FastAPI HTTP router for Hippo engine operations.

Exposes Hippo engine lifecycle and tool operations as REST endpoints
for external integration (gateway, dashboard, etc.).

Issue: #1121
"""

from __future__ import annotations

import logging
from typing import Any, Dict, List, Optional

from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field

from cortex.hippo import __version__ as hippo_version
from cortex.hippo.config import HippoConfig
from cortex.hippo.tools.common import json_result
from cortex.hippo.tools.conversation_scope import LcmConversationScope

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Request / Response models
# ---------------------------------------------------------------------------


class BootstrapRequest(BaseModel):
    session_id: str
    session_file: Optional[str] = None
    session_key: Optional[str] = None


class BootstrapResponse(BaseModel):
    bootstrapped: bool
    message_count: int
    conversation_id: int


class IngestRequest(BaseModel):
    conversation_id: int
    role: str
    content: Any  # str or list of content blocks
    metadata: Optional[str] = None
    session_key: Optional[str] = None


class IngestResponse(BaseModel):
    message_id: int
    conversation_id: int
    seq: int
    token_count: int


class AfterTurnRequest(BaseModel):
    session_id: str  # used to resolve conversation
    messages: List[Dict[str, Any]]
    pre_prompt_count: int = 0
    token_budget: Optional[int] = None
    is_heartbeat: bool = False
    session_key: Optional[str] = None


class AfterTurnResponse(BaseModel):
    ok: bool = True


class AssembleRequest(BaseModel):
    conversation_id: int
    budget_tokens: Optional[int] = None


class AssembleResponse(BaseModel):
    items: List[Dict[str, Any]] = Field(default_factory=list)
    messages: List[Dict[str, Any]] = Field(default_factory=list)
    total_tokens: int = 0
    truncated: bool = False
    system_prompt_addition: Optional[str] = None


class CompactRequest(BaseModel):
    conversation_id: int
    mode: str = "leaf"
    token_budget: Optional[int] = None


class CompactResponse(BaseModel):
    summaries_created: int
    messages_compacted: int
    tokens_before: int
    tokens_after: int
    rounds: int


class InfoResponse(BaseModel):
    version: str
    config: Dict[str, Any]
    feature_flags: Dict[str, Any]


# -- Tool request models ---------------------------------------------------


class GrepRequest(BaseModel):
    pattern: str
    mode: str = "regex"
    scope: str = "both"
    limit: int = 50
    since: Optional[str] = None
    before: Optional[str] = None
    conversation_id: Optional[int] = Field(None, alias="conversationId")
    all_conversations: bool = Field(False, alias="allConversations")

    model_config = {"populate_by_name": True}


class DescribeRequest(BaseModel):
    id: str
    conversation_id: Optional[int] = Field(None, alias="conversationId")
    all_conversations: bool = Field(False, alias="allConversations")
    token_cap: Optional[int] = Field(None, alias="tokenCap")

    model_config = {"populate_by_name": True}


class ExpandRequest(BaseModel):
    summary_ids: Optional[List[str]] = Field(None, alias="summaryIds")
    query: Optional[str] = None
    max_depth: int = Field(3, alias="maxDepth")
    token_cap: Optional[int] = Field(None, alias="tokenCap")
    include_messages: bool = Field(False, alias="includeMessages")
    conversation_id: Optional[int] = Field(None, alias="conversationId")
    all_conversations: bool = Field(False, alias="allConversations")

    model_config = {"populate_by_name": True}


class ExpandQueryRequest(BaseModel):
    prompt: str
    query: Optional[str] = None
    summary_ids: Optional[List[str]] = Field(None, alias="summaryIds")
    max_tokens: int = Field(2000, alias="maxTokens")
    token_cap: Optional[int] = Field(None, alias="tokenCap")
    conversation_id: Optional[int] = Field(None, alias="conversationId")
    all_conversations: bool = Field(False, alias="allConversations")

    model_config = {"populate_by_name": True}


class ToolResponse(BaseModel):
    """Generic wrapper for tool results."""
    result: Dict[str, Any]


# ---------------------------------------------------------------------------
# Router factory
# ---------------------------------------------------------------------------


def create_hippo_router(
    engine,
    assembler,
    conversation_store,
    summary_store,
    retrieval,
    config: HippoConfig,
    *,
    owner_id: str = "default",
) -> APIRouter:
    """Create a FastAPI APIRouter exposing Hippo engine operations.

    Parameters
    ----------
    engine : HippoEngine
        Initialized engine instance.
    assembler : ContextAssembler
        Context assembler.
    conversation_store : ConversationStore
        For message CRUD.
    summary_store : SummaryStore
        For summary operations.
    retrieval : RetrievalEngine
        For tool operations (grep, describe, expand).
    config : HippoConfig
        Hippo configuration.
    owner_id : str
        Default owner ID for multi-tenant isolation.
    """

    router = APIRouter(prefix="/hippo", tags=["hippo"])

    # ── Engine lifecycle endpoints ─────────────────────────────────────

    @router.post("/bootstrap", response_model=BootstrapResponse)
    async def bootstrap(req: BootstrapRequest):
        """Bootstrap a conversation from a session file."""
        try:
            result = await engine.bootstrap(
                session_id=req.session_id,
                session_file=req.session_file or "",
                session_key=req.session_key,
            )
            return BootstrapResponse(
                bootstrapped=result["bootstrapped"],
                message_count=result["message_count"],
                conversation_id=result["conversation_id"],
            )
        except RuntimeError as exc:
            raise HTTPException(status_code=400, detail=str(exc))
        except Exception as exc:
            logger.exception("bootstrap failed")
            raise HTTPException(status_code=500, detail=str(exc))

    @router.post("/ingest", response_model=IngestResponse)
    async def ingest(req: IngestRequest):
        """Ingest a single message into a conversation."""
        try:
            msg = await conversation_store.create_message(
                conversation_id=req.conversation_id,
                owner_id=owner_id,
                role=req.role,
                content=req.content,
                metadata=req.metadata,
            )
            # Write context item so assembler can find this message
            await summary_store.append_context_item(
                conversation_id=req.conversation_id,
                item_type="message",
                message_id=msg.message_id,
            )
            return IngestResponse(
                message_id=msg.message_id,
                conversation_id=msg.conversation_id,
                seq=msg.seq,
                token_count=msg.token_count,
            )
        except Exception as exc:
            logger.exception("ingest failed")
            raise HTTPException(status_code=500, detail=str(exc))

    @router.post("/after-turn", response_model=AfterTurnResponse)
    async def after_turn(req: AfterTurnRequest):
        """Post-turn hook: ingest new messages + optionally trigger compaction."""
        try:
            # Resolve conversation from session_id (with optional session_key)
            conv = await conversation_store.get_or_create_conversation(
                owner_id, req.session_id, session_key=req.session_key
            )
            await engine.after_turn(
                conversation_id=conv.conversation_id,
                messages=req.messages,
                pre_prompt_count=req.pre_prompt_count,
                token_budget=req.token_budget,
                is_heartbeat=req.is_heartbeat,
                session_key=req.session_key,
            )
            return AfterTurnResponse(ok=True)
        except RuntimeError as exc:
            raise HTTPException(status_code=400, detail=str(exc))
        except Exception as exc:
            logger.exception("after_turn failed")
            raise HTTPException(status_code=500, detail=str(exc))

    @router.post("/assemble", response_model=AssembleResponse)
    async def assemble(req: AssembleRequest):
        """Assemble context for a conversation."""
        try:
            result = await assembler.assemble(
                conversation_id=req.conversation_id,
                owner_id=owner_id,
                token_budget=req.budget_tokens or config.max_tokens,
            )
            return AssembleResponse(
                items=[item.model_dump() if hasattr(item, "model_dump") else dict(item) for item in result.items],
                messages=result.messages,
                total_tokens=result.total_tokens,
                truncated=result.truncated,
                system_prompt_addition=result.system_prompt_addition,
            )
        except Exception as exc:
            logger.exception("assemble failed")
            raise HTTPException(status_code=500, detail=str(exc))

    @router.post("/compact", response_model=CompactResponse)
    async def compact(req: CompactRequest):
        """Trigger compaction on a conversation."""
        try:
            result = await engine.compact(
                conversation_id=req.conversation_id,
                mode=req.mode,
                token_budget=req.token_budget,
            )
            return CompactResponse(
                summaries_created=result.summaries_created,
                messages_compacted=result.messages_compacted,
                tokens_before=result.tokens_before,
                tokens_after=result.tokens_after,
                rounds=result.rounds,
            )
        except RuntimeError as exc:
            raise HTTPException(status_code=400, detail=str(exc))
        except Exception as exc:
            logger.exception("compact failed")
            raise HTTPException(status_code=500, detail=str(exc))

    @router.get("/info", response_model=InfoResponse)
    async def info():
        """Return version, config summary, and feature flags."""
        return InfoResponse(
            version=hippo_version,
            config={
                "enabled": config.enabled,
                "max_tokens": config.max_tokens,
                "context_threshold": config.context_threshold,
                "fresh_tail_count": config.fresh_tail_count,
                "db_path": config.db_path,
            },
            feature_flags={
                "compaction": config.enabled,
                "wal_mode": config.enable_wal,
            },
        )

    # ── Tool endpoints ─────────────────────────────────────────────────

    @router.post("/tools/grep", response_model=ToolResponse)
    async def tools_grep(req: GrepRequest):
        """Execute grep search across conversation history."""
        from cortex.hippo.tools.grep import execute_grep

        scope = LcmConversationScope(
            conversation_id=req.conversation_id,
            all_conversations=req.all_conversations,
        )
        params = {
            "pattern": req.pattern,
            "mode": req.mode,
            "scope": req.scope,
            "limit": req.limit,
        }
        if req.since:
            params["since"] = req.since
        if req.before:
            params["before"] = req.before

        try:
            result = await execute_grep(retrieval, params, scope)
            return ToolResponse(result=result)
        except Exception as exc:
            logger.exception("tools/grep failed")
            raise HTTPException(status_code=500, detail=str(exc))

    @router.post("/tools/describe", response_model=ToolResponse)
    async def tools_describe(req: DescribeRequest):
        """Describe an LCM item (summary or file) by ID."""
        from cortex.hippo.tools.describe import execute_describe

        params: Dict[str, Any] = {"id": req.id}
        if req.conversation_id is not None:
            params["conversationId"] = req.conversation_id
        if req.all_conversations:
            params["allConversations"] = True
        if req.token_cap is not None:
            params["tokenCap"] = req.token_cap

        try:
            result = await execute_describe(
                params=params,
                retrieval=retrieval,
                owner_id=owner_id,
                conversation_store=conversation_store,
            )
            return ToolResponse(result=result)
        except Exception as exc:
            logger.exception("tools/describe failed")
            raise HTTPException(status_code=500, detail=str(exc))

    @router.post("/tools/expand", response_model=ToolResponse)
    async def tools_expand(req: ExpandRequest):
        """Expand compacted conversation summaries."""
        from cortex.hippo.tools.expand import execute_expand

        params: Dict[str, Any] = {}
        if req.summary_ids:
            params["summaryIds"] = req.summary_ids
        if req.query:
            params["query"] = req.query
        params["maxDepth"] = req.max_depth
        if req.token_cap is not None:
            params["tokenCap"] = req.token_cap
        params["includeMessages"] = req.include_messages
        if req.conversation_id is not None:
            params["conversationId"] = req.conversation_id
        if req.all_conversations:
            params["allConversations"] = True

        try:
            result = await execute_expand(
                params=params,
                retrieval=retrieval,
                owner_id=owner_id,
                conversation_store=conversation_store,
            )
            return ToolResponse(result=result)
        except Exception as exc:
            logger.exception("tools/expand failed")
            raise HTTPException(status_code=500, detail=str(exc))

    @router.post("/tools/expand-query", response_model=ToolResponse)
    async def tools_expand_query(req: ExpandQueryRequest):
        """Answer a question using delegated expansion + LLM.

        Graceful degradation: when no synthesize_fn or gateway_delegator is
        available (router context), runs the grep→expand pipeline locally and
        returns the expanded context as the answer instead of a hard error.
        """
        from cortex.hippo.tools.expand_query import execute_expand_query

        params: Dict[str, Any] = {"prompt": req.prompt}
        if req.query:
            params["query"] = req.query
        if req.summary_ids:
            params["summaryIds"] = req.summary_ids
        params["maxTokens"] = req.max_tokens
        if req.token_cap is not None:
            params["tokenCap"] = req.token_cap
        if req.conversation_id is not None:
            params["conversationId"] = req.conversation_id
        if req.all_conversations:
            params["allConversations"] = True

        try:
            result = await execute_expand_query(
                params=params,
                retrieval=retrieval,
                owner_id=owner_id,
                conversation_store=conversation_store,
                # synthesize_fn=None → _local_synthesize returns raw expanded context
                # gateway_delegator=None → always uses local path
                synthesize_fn=None,
                gateway_delegator=None,
                max_expand_tokens=config.max_tokens,
            )
            return ToolResponse(result=result)
        except Exception as exc:
            logger.exception("tools/expand-query failed")
            raise HTTPException(status_code=500, detail=str(exc))

    return router
