"""
cortex/hippo/tools/describe.py — hippo_describe tool implementation.

Full parity with tools/lcm-describe-tool.ts (234 lines).
Issue: #806, #1118
"""

from __future__ import annotations

import math
from typing import Any, Optional

from cortex.hippo.tools.common import json_result
from cortex.hippo.tools.conversation_scope import resolve_conversation_scope


# ═══════════════════════════════════════════════════════════════════════════
# Schema
# ═══════════════════════════════════════════════════════════════════════════

HIPPO_DESCRIBE_TOOL = {
    "name": "hippo_describe",
    "description": (
        "Look up metadata and content for an LCM item by ID. "
        "Use this to inspect summaries (sum_xxx) or stored files (file_xxx) "
        "from compacted conversation history. "
        "Returns summary content, lineage, token counts, and file exploration results."
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "id": {
                "type": "string",
                "description": "The LCM ID to look up. Use sum_xxx for summaries, file_xxx for files.",
            },
            "conversationId": {
                "type": "integer",
                "description": (
                    "Conversation ID to scope describe lookups to. "
                    "If omitted, uses the current session conversation."
                ),
            },
            "allConversations": {
                "type": "boolean",
                "default": False,
                "description": (
                    "Set true to explicitly allow lookups across all conversations. "
                    "Ignored when conversationId is provided."
                ),
            },
            "tokenCap": {
                "type": "integer",
                "minimum": 1,
                "description": "Optional budget cap used for subtree manifest budget-fit annotations.",
            },
        },
        "required": ["id"],
    },
}


# ═══════════════════════════════════════════════════════════════════════════
# Helpers
# ═══════════════════════════════════════════════════════════════════════════


def _normalize_token_cap(value: Any) -> Optional[int]:
    """Normalize a requested token cap to a positive integer or None."""
    if not isinstance(value, (int, float)) or not math.isfinite(value):
        return None
    return max(1, int(value))


def _format_iso(value: Optional[str], _timezone: Optional[str] = None) -> str:
    """Format an ISO timestamp for display. Returns '-' for missing values."""
    if not value:
        return "-"
    return str(value)


# ═══════════════════════════════════════════════════════════════════════════
# Execute
# ═══════════════════════════════════════════════════════════════════════════


async def execute_describe(
    params: dict[str, Any],
    retrieval,
    *,
    session_id: Optional[str] = None,
    owner_id: str = "default",
    conversation_store=None,
    timezone: Optional[str] = None,
    default_max_expand_tokens: int = 50_000,
) -> dict[str, Any]:
    """Execute the hippo_describe tool.

    Args:
        params: Tool parameters (id, conversationId, allConversations, tokenCap).
        retrieval: RetrievalEngine instance.
        session_id: Current session ID for conversation scope resolution.
        owner_id: Owner ID for multi-tenant isolation.
        conversation_store: Optional conversation store for session-based scope resolution.
        timezone: Optional timezone string for timestamp formatting.
        default_max_expand_tokens: Default token cap when none specified.

    Returns:
        Tool result dict with content and details.
    """
    item_id = str(params.get("id", "")).strip()
    if not item_id:
        return json_result({"error": "id parameter is required."})

    # Resolve conversation scope
    scope = await resolve_conversation_scope(
        params=params,
        session_id=session_id,
        owner_id=owner_id,
        conversation_store=conversation_store,
    )

    if not scope.all_conversations and scope.conversation_id is None:
        return json_result({
            "error": (
                "No LCM conversation found for this session. "
                "Provide conversationId or set allConversations=true."
            ),
        })

    # Look up item
    result = await retrieval.describe(
        id=item_id,
        owner_id=owner_id,
        conversation_id=scope.conversation_id,
        all_conversations=scope.all_conversations,
    )

    if result is None:
        return json_result({
            "error": f"Not found: {item_id}",
            "hint": "Check the ID format (sum_xxx for summaries, file_xxx for files).",
        })

    # Conversation scope check
    if scope.conversation_id is not None:
        item_conv_id = None
        if result.type == "summary" and result.summary:
            item_conv_id = result.summary.get("conversation_id")
        elif result.type == "file" and result.file:
            item_conv_id = result.file.get("conversation_id")
        if item_conv_id is not None and item_conv_id != scope.conversation_id:
            return json_result({
                "error": f"Not found in conversation {scope.conversation_id}: {item_id}",
                "hint": "Use allConversations=true for cross-conversation lookup.",
            })

    # ── Summary rendering ──────────────────────────────────────────
    if result.type == "summary" and result.summary:
        s = result.summary
        requested_cap = _normalize_token_cap(params.get("tokenCap"))
        resolved_token_cap = requested_cap if requested_cap is not None else max(1, default_max_expand_tokens)
        budget_source = "request" if requested_cap is not None else "config_default"

        # Build subtree manifest with budget annotations
        subtree = s.get("subtree", [])
        manifest_nodes = []
        for node in subtree:
            tok = node.get("token_count", 0)
            desc_tok = node.get("descendant_token_count", 0)
            src_tok = node.get("source_message_token_count", 0)
            summaries_only_cost = max(0, tok + desc_tok)
            with_messages_cost = max(0, summaries_only_cost + src_tok)
            manifest_nodes.append({
                "summaryId": node.get("summary_id", ""),
                "depthFromRoot": node.get("depth_from_root", 0),
                "depth": node.get("depth", 0),
                "kind": node.get("kind", ""),
                "tokenCount": tok,
                "descendantCount": node.get("descendant_count", 0),
                "descendantTokenCount": desc_tok,
                "sourceMessageTokenCount": src_tok,
                "childCount": node.get("child_count", 0),
                "earliestAt": node.get("earliest_at"),
                "latestAt": node.get("latest_at"),
                "costs": {
                    "summariesOnly": summaries_only_cost,
                    "withMessages": with_messages_cost,
                },
                "budgetFit": {
                    "summariesOnly": summaries_only_cost <= resolved_token_cap,
                    "withMessages": with_messages_cost <= resolved_token_cap,
                },
            })

        # Render text output
        lines: list[str] = []
        lines.append(f"LCM_SUMMARY {item_id}")

        parent_ids = s.get("parent_ids", [])
        child_ids = s.get("child_ids", [])
        earliest = _format_iso(s.get("earliest_at"), timezone)
        latest = _format_iso(s.get("latest_at"), timezone)

        lines.append(
            f"meta conv={s.get('conversation_id')} kind={s.get('kind')} "
            f"depth={s.get('depth')} tok={s.get('token_count')} "
            f"descTok={s.get('descendant_token_count', 0)} "
            f"srcTok={s.get('source_message_token_count', 0)} "
            f"desc={s.get('descendant_count', 0)} "
            f"range={earliest}..{latest} "
            f"budgetCap={resolved_token_cap}"
        )

        if parent_ids:
            lines.append(f"parents {' '.join(parent_ids)}")
        if child_ids:
            lines.append(f"children {' '.join(child_ids)}")

        lines.append("manifest")
        for node in manifest_nodes:
            e_at = _format_iso(node.get("earliestAt"), timezone)
            l_at = _format_iso(node.get("latestAt"), timezone)
            s_fit = "in" if node["budgetFit"]["summariesOnly"] else "over"
            m_fit = "in" if node["budgetFit"]["withMessages"] else "over"
            lines.append(
                f"d{node['depthFromRoot']} {node['summaryId']} "
                f"k={node['kind']} tok={node['tokenCount']} "
                f"descTok={node['descendantTokenCount']} "
                f"srcTok={node['sourceMessageTokenCount']} "
                f"desc={node['descendantCount']} child={node['childCount']} "
                f"range={e_at}..{l_at} "
                f"cost[s={node['costs']['summariesOnly']},m={node['costs']['withMessages']}] "
                f"budget[s={s_fit},m={m_fit}]"
            )

        lines.append("content")
        lines.append(s.get("content", ""))

        return {
            "content": [{"type": "text", "text": "\n".join(lines)}],
            "details": {
                "id": result.id,
                "type": result.type,
                "summary": s,
                "manifest": {
                    "tokenCap": resolved_token_cap,
                    "budgetSource": budget_source,
                    "nodes": manifest_nodes,
                },
            },
        }

    # ── File rendering ─────────────────────────────────────────────
    if result.type == "file" and result.file:
        f = result.file
        lines = []
        lines.append(f"## LCM File: {item_id}")
        lines.append("")
        lines.append(f"**Conversation:** {f.get('conversation_id')}")
        lines.append(f"**Name:** {f.get('file_name') or '(no name)'}")
        lines.append(f"**Type:** {f.get('mime_type') or 'unknown'}")
        byte_size = f.get("byte_size")
        if byte_size is not None:
            lines.append(f"**Size:** {byte_size:,} bytes")
        lines.append(f"**Created:** {_format_iso(f.get('created_at'), timezone)}")

        exploration = f.get("exploration_summary")
        if exploration:
            lines.append("")
            lines.append("## Exploration Summary")
            lines.append("")
            lines.append(exploration)
        else:
            lines.append("")
            lines.append("*No exploration summary available.*")

        return {
            "content": [{"type": "text", "text": "\n".join(lines)}],
            "details": {
                "id": result.id,
                "type": result.type,
                "file": f,
            },
        }

    # Fallback
    return json_result({
        "id": result.id,
        "type": result.type,
        "summary": result.summary,
        "file": result.file,
    })
