"""
cortex/hippo/tools/conversation_scope.py — Conversation scope resolution for LCM tools.

Port of tools/lcm-conversation-scope.ts from lossless-claw.
Issues: #1118, #1120
"""

from __future__ import annotations

import math
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Awaitable, Callable, Optional


def parse_iso_timestamp_param(
    params: dict[str, Any],
    key: str,
) -> Optional[datetime]:
    """Parse an ISO-8601 timestamp tool parameter into a datetime.

    Returns None if the param is missing or not a string.
    Raises ValueError when the value is not a parseable timestamp.
    """
    raw = params.get(key)
    if not isinstance(raw, str):
        return None
    value = raw.strip()
    if not value:
        return None
    try:
        # Handle Z suffix for UTC
        normalized = value.replace("Z", "+00:00")
        return datetime.fromisoformat(normalized)
    except (ValueError, TypeError) as exc:
        raise ValueError(f"{key} must be a valid ISO timestamp.") from exc


@dataclass
class LcmConversationScope:
    """Resolved conversation scope for LCM tool calls."""

    conversation_id: Optional[int] = None
    all_conversations: bool = False


# Backward compat alias
ConversationScope = LcmConversationScope


async def resolve_conversation_scope(
    engine: Any = None,
    params: Optional[dict[str, Any]] = None,
    *,
    session_id: Optional[str] = None,
    session_key: Optional[str] = None,
    owner_id: str = "default",
    conversation_store: Any = None,
    resolve_session_id_from_key: Optional[Callable[[str], Awaitable[Optional[str]]]] = None,
) -> LcmConversationScope:
    """Resolve LCM conversation scope for tool calls.

    Supports two call signatures for backward compat:
    - resolve_conversation_scope(engine, params, ...)  (new — engine first)
    - resolve_conversation_scope(params=params, session_id=..., conversation_store=...)  (legacy)

    Priority:
    1. Explicit conversationId parameter
    2. allConversations=True (cross-conversation mode)
    3. Current session's LCM conversation (via session_id or session_key)
    4. Fallback: no scope
    """
    # Detect call style: if first arg is a dict, it's the legacy params-first style
    if isinstance(engine, dict):
        p = engine
        engine = None
    elif params is not None:
        p = params
        # Extract store/owner from engine if not explicitly provided
        if engine is not None:
            if conversation_store is None and hasattr(engine, "conversation_store"):
                conversation_store = engine.conversation_store
            if owner_id == "default" and hasattr(engine, "owner_id"):
                owner_id = engine.owner_id
    else:
        p = {}

    # 1. Explicit conversationId
    conv_id = p.get("conversationId")
    if isinstance(conv_id, (int, float)) and math.isfinite(conv_id):
        return LcmConversationScope(
            conversation_id=int(conv_id),
            all_conversations=False,
        )

    # 2. allConversations
    if p.get("allConversations") is True:
        return LcmConversationScope(
            conversation_id=None,
            all_conversations=True,
        )

    # 3. Resolve from session
    normalized_session_id = session_id.strip() if session_id else None
    if not normalized_session_id and session_key and resolve_session_id_from_key:
        normalized_session_id = await resolve_session_id_from_key(session_key.strip())

    if not normalized_session_id:
        return LcmConversationScope(conversation_id=None, all_conversations=False)

    # Look up conversation by session ID
    if conversation_store is not None:
        try:
            # Try get_or_create (preferred) or get_conversation_by_session_id
            if hasattr(conversation_store, "get_or_create_conversation"):
                conversation = await conversation_store.get_or_create_conversation(
                    owner_id=owner_id,
                    session_id=normalized_session_id,
                )
            elif hasattr(conversation_store, "get_conversation_by_session_id"):
                conversation = await conversation_store.get_conversation_by_session_id(
                    normalized_session_id,
                )
            else:
                conversation = None

            if conversation:
                return LcmConversationScope(
                    conversation_id=conversation.conversation_id,
                    all_conversations=False,
                )
        except Exception:
            pass

    return LcmConversationScope(conversation_id=None, all_conversations=False)
