"""
cortex/capture/episode_adapter.py — Episode event creation and logging.

Extracted from cortex.api.plugin (remember() pipeline) to keep the orchestrator
lean. Provides helpers to normalize a conversation into a durable episode_event
payload and returns the new event's ID for downstream provenance stamping.

Used by: CortexPlugin.remember()
"""
from __future__ import annotations

import json
import logging
from collections import Counter
from typing import Any, Dict, List, Optional, Tuple

logger = logging.getLogger(__name__)


def _extract_text_content(content: Any) -> str:
    """Flatten OpenClaw/OpenAI-style content blocks into plain text."""
    if isinstance(content, str):
        return content
    if not isinstance(content, list):
        return ""

    parts: List[str] = []
    for block in content:
        if not isinstance(block, dict):
            continue
        text = block.get("text")
        if isinstance(text, str) and text.strip():
            parts.append(text.strip())
            continue
        # Some tool/result blocks may nest text in content/output fields.
        nested = block.get("content") or block.get("output")
        if isinstance(nested, str) and nested.strip():
            parts.append(nested.strip())
    return "\n".join(parts)


def _normalize_message(msg: Any) -> Dict[str, Any]:
    """Return a compact, JSON-safe message representation for episode storage."""
    if not isinstance(msg, dict):
        return {"role": "unknown", "content": str(msg)}

    role = str(msg.get("role") or msg.get("actor") or "unknown")
    content = _extract_text_content(msg.get("content"))
    normalized: Dict[str, Any] = {
        "role": role,
        "content": content,
    }

    for src_key, dst_key in (
        ("tool_name", "tool_name"),
        ("toolName", "tool_name"),
        ("tool_call_id", "tool_call_id"),
        ("toolCallId", "tool_call_id"),
        ("tool_trace_ref", "tool_trace_ref"),
        ("source_uri", "source_uri"),
        ("sourceUri", "source_uri"),
        ("outcome", "outcome"),
        ("status", "status"),
    ):
        value = msg.get(src_key)
        if isinstance(value, str) and value:
            normalized[dst_key] = value

    return normalized


def _build_episode_payload(conversation: List[Any], sid: str) -> Tuple[str, str, Optional[str], Optional[str], str]:
    """Build raw_text/metadata for an episode_event from a conversation."""
    normalized_messages = [_normalize_message(msg) for msg in conversation]
    role_counts = Counter(m.get("role", "unknown") for m in normalized_messages)

    tool_trace_ref = next(
        (m.get("tool_trace_ref") or m.get("tool_call_id") for m in normalized_messages
         if m.get("tool_trace_ref") or m.get("tool_call_id")),
        None,
    )
    source_uri = next(
        (m.get("source_uri") for m in normalized_messages if m.get("source_uri")),
        None,
    )

    has_tool_calls = any(m.get("role") == "tool" or m.get("tool_name") for m in normalized_messages)
    has_tool_results = any(m.get("outcome") or m.get("status") for m in normalized_messages)

    metadata = {
        "session_id": sid,
        "message_count": len(normalized_messages),
        "role_counts": dict(role_counts),
        "roles": sorted(role_counts.keys()),
        "has_tool_calls": has_tool_calls,
        "has_tool_results": has_tool_results,
    }

    if tool_trace_ref:
        metadata["tool_trace_ref"] = tool_trace_ref
    if source_uri:
        metadata["source_uri"] = source_uri

    # Single actor only when the conversation is homogeneous; otherwise mark mixed.
    non_empty_roles = [r for r in role_counts.keys() if r and r != "unknown"]
    actor_id = non_empty_roles[0] if len(non_empty_roles) == 1 else "conversation"

    return (
        json.dumps(normalized_messages, ensure_ascii=False),
        actor_id,
        tool_trace_ref,
        source_uri,
        json.dumps(metadata, ensure_ascii=False),
    )


async def log_episode_event(
    storage: object,
    session_id_pk: "Optional[str]",
    conversation: list,
    owner_id: str,
    sid: str,
) -> Optional[str]:
    """Create an episode_event row for *conversation* and return its ID.

    Architecture invariant: "evidence first, always."
    This is best-effort — failure never blocks extraction.

    Args:
        storage:        StorageAdapter instance.
        session_id_pk:  The session_record.id (PK) to associate the episode with.
                        If None, falls back to *sid* (session_id string).
        conversation:   Raw conversation list (will be normalized and JSON-encoded).
        owner_id:       Memory owner namespace.
        sid:            Human-readable session ID for logging.

    Returns:
        Episode event ID string on success, or None on failure.
    """
    try:
        raw_text, actor_id, tool_trace_ref, source_uri, metadata_json = _build_episode_payload(
            conversation,
            sid,
        )
        episode = await storage.log_episode(  # type: ignore[attr-defined]
            session_id=session_id_pk or sid,
            actor_id=actor_id,
            kind="conversation",
            raw_text=raw_text,
            tool_trace_ref=tool_trace_ref,
            source_uri=source_uri,
            metadata_json=metadata_json,
            owner_id=owner_id,
        )
        episode_id: str = episode.id
        logger.debug(
            "Logged episode event id=%s for session %s",
            episode_id,
            sid,
        )
        return episode_id
    except Exception as exc:
        logger.warning(
            "Episode logging failed (non-fatal) — provenance will be missing "
            "for session %s: %s",
            sid,
            exc,
        )
        return None
