"""Retell LLM (single/multi-prompt) JSON importer."""

import json
from pathlib import Path
from typing import Any

from pydantic import BaseModel
from pydantic import ConfigDict

from voicetest.importers.base import ImporterInfo
from voicetest.models.agent import AgentGraph
from voicetest.models.agent import AgentNode
from voicetest.models.agent import NodeType
from voicetest.models.agent import ToolDefinition
from voicetest.models.agent import Transition
from voicetest.models.agent import TransitionCondition


_LLM_KEYS = {
    "llm_id",
    "model",
    "general_prompt",
    "begin_message",
    "general_tools",
    "states",
    "retellLlmData",
}


def _extract_agent_envelope(config: dict[str, Any]) -> dict[str, Any]:
    """Extract agent-level fields (voice_id, language, etc.) from the raw config.

    These live alongside the LLM data at the top level of a Retell agent
    export and are needed when re-wrapping for the Retell UI import.
    """
    return {k: v for k, v in config.items() if k not in _LLM_KEYS}


class RetellLLMTool(BaseModel):
    """Retell LLM tool definition."""

    model_config = ConfigDict(extra="ignore")

    type: str
    name: str
    description: str = ""
    url: str | None = None
    parameters: dict[str, Any] | None = None
    transfer_destination: dict[str, Any] | None = None
    transfer_option: dict[str, Any] | None = None


class RetellLLMEdge(BaseModel):
    """Retell LLM state edge definition."""

    model_config = ConfigDict(extra="ignore")

    destination_state_name: str
    description: str = ""
    parameters: dict[str, Any] | None = None


class RetellLLMState(BaseModel):
    """Retell LLM state definition."""

    model_config = ConfigDict(extra="ignore")

    name: str
    state_prompt: str = ""
    edges: list[RetellLLMEdge] = []
    tools: list[RetellLLMTool] = []


class RetellLLMConfig(BaseModel):
    """Retell LLM configuration."""

    model_config = ConfigDict(extra="ignore")

    llm_id: str | None = None
    model: str = "gpt-4o"
    general_prompt: str = ""
    begin_message: str | None = None
    general_tools: list[RetellLLMTool] = []
    states: list[RetellLLMState] = []


class RetellLLMImporter:
    """Import Retell LLM (single/multi-prompt) JSON."""

    @property
    def source_type(self) -> str:
        return "retell-llm"

    def get_info(self) -> ImporterInfo:
        return ImporterInfo(
            source_type="retell-llm",
            description="Import Retell LLM (single/multi-prompt) JSON exports",
            file_patterns=["*.json"],
        )

    def _unwrap_config(self, config: dict) -> dict:
        """Unwrap retellLlmData if present (dashboard export format).

        Supports both:
        - Top-level format (official API): {"general_prompt": ..., "states": ...}
        - Wrapped format (dashboard): {"retellLlmData": {"general_prompt": ..., "states": ...}}

        Raises ValueError if LLM fields exist at both levels (ambiguous).
        """
        llm_fields = {"general_prompt", "llm_id", "states"}
        has_wrapper = "retellLlmData" in config
        has_top_level = bool(llm_fields & set(config.keys()))

        if has_wrapper and has_top_level:
            raise ValueError(
                "Ambiguous config: LLM fields found at both top level and inside retellLlmData"
            )

        if has_wrapper:
            return config["retellLlmData"]
        return config

    def can_import(self, path_or_config: str | Path | dict) -> bool:
        """Detect Retell LLM format by checking for characteristic fields."""
        try:
            config = self._load_config(path_or_config)
            config = self._unwrap_config(config)
            has_general_prompt = "general_prompt" in config
            has_llm_id = "llm_id" in config
            has_states = "states" in config
            # Distinguish from Conversation Flow (which has start_node_id and nodes)
            is_not_flow = "start_node_id" not in config and "nodes" not in config
            return (has_general_prompt or has_llm_id or has_states) and is_not_flow
        except Exception:
            return False

    def import_agent(self, path_or_config: str | Path | dict) -> AgentGraph:
        """Convert Retell LLM JSON to AgentGraph."""
        full_config = self._load_config(path_or_config)
        agent_envelope = _extract_agent_envelope(full_config)
        raw_config = self._unwrap_config(full_config)
        llm_config = RetellLLMConfig.model_validate(raw_config)

        nodes: dict[str, AgentNode] = {}

        if llm_config.states:
            # Multi-state: each state becomes a node
            for i, state in enumerate(llm_config.states):
                transitions = [self._convert_edge(edge) for edge in state.edges]
                tools = [self._convert_tool(t) for t in state.tools]
                # Add general tools to each state
                tools.extend([self._convert_tool(t) for t in llm_config.general_tools])

                nodes[state.name] = AgentNode(
                    id=state.name,
                    state_prompt=state.state_prompt,  # State-specific only, NOT merged
                    transitions=transitions,
                    tools=tools or [],
                    metadata={"state_index": i},
                )

            entry_node_id = llm_config.states[0].name
        else:
            # Single-prompt: create one node (general_prompt becomes state_prompt)
            tools = [self._convert_tool(t) for t in llm_config.general_tools]
            nodes["main"] = AgentNode(
                id="main",
                state_prompt=llm_config.general_prompt,
                tools=tools or [],
                transitions=[],
            )
            entry_node_id = "main"

        # Convert end_call/transfer_call tools into synthetic nodes + edges
        nodes = self._synthesize_terminal_nodes(nodes)

        source_metadata: dict[str, Any] = {
            "llm_id": llm_config.llm_id,
            "model": llm_config.model,
            "begin_message": llm_config.begin_message,
            "general_prompt": llm_config.general_prompt,  # Stored separately
        }
        if agent_envelope:
            source_metadata["agent_envelope"] = agent_envelope

        return AgentGraph(
            nodes=nodes,
            entry_node_id=entry_node_id,
            source_type="retell-llm",
            source_metadata=source_metadata,
            default_model=llm_config.model,
        )

    def _synthesize_terminal_nodes(self, nodes: dict[str, AgentNode]) -> dict[str, AgentNode]:
        """Convert end_call/transfer_call tools into synthetic nodes and edges.

        For each end_call or transfer_call tool found on any node:
        1. Create a synthetic node (no prompt) with the tool attached
        2. Add an llm_prompt edge from the node to the synthetic node
        3. Strip the tool from the original node
        """
        # Collect unique terminal tools across all nodes: tool_name -> ToolDefinition
        terminal_tools: dict[str, ToolDefinition] = {}
        for node in nodes.values():
            for tool in node.tools:
                if tool.type in ("end_call", "transfer_call"):
                    terminal_tools[tool.name] = tool

        if not terminal_tools:
            return nodes

        # Create synthetic nodes
        synthetic: dict[str, AgentNode] = {}
        for tool_name, tool_def in terminal_tools.items():
            node_type = NodeType.END if tool_def.type == "end_call" else NodeType.TRANSFER
            synthetic[tool_name] = AgentNode(
                id=tool_name,
                state_prompt="",
                node_type=node_type,
                tools=[tool_def],
                transitions=[],
            )

        # Add edges and strip terminal tools from conversation nodes
        updated: dict[str, AgentNode] = {}
        for node_id, node in nodes.items():
            node_terminal = [t for t in node.tools if t.type in ("end_call", "transfer_call")]
            if not node_terminal:
                updated[node_id] = node
                continue

            new_transitions = list(node.transitions)
            for tool in node_terminal:
                new_transitions.append(
                    Transition(
                        target_node_id=tool.name,
                        condition=TransitionCondition(
                            type="llm_prompt",
                            value=tool.description or f"Invoke {tool.name}",
                        ),
                        description=tool.description,
                    )
                )

            remaining_tools = [t for t in node.tools if t.type not in ("end_call", "transfer_call")]
            updated[node_id] = node.model_copy(
                update={
                    "tools": remaining_tools,
                    "transitions": new_transitions,
                }
            )

        updated.update(synthetic)
        return updated

    def _load_config(self, path_or_config: str | Path | dict) -> dict[str, Any]:
        """Load config from path or return dict directly."""
        if isinstance(path_or_config, dict):
            return path_or_config
        path = Path(path_or_config)
        return json.loads(path.read_text())

    def _convert_edge(self, edge: RetellLLMEdge) -> Transition:
        """Convert Retell LLM edge to Transition."""
        return Transition(
            target_node_id=edge.destination_state_name,
            condition=TransitionCondition(
                type="llm_prompt",
                value=edge.description,
            ),
            description=edge.description,
        )

    def _convert_tool(self, tool: RetellLLMTool) -> ToolDefinition:
        """Convert Retell LLM tool to ToolDefinition.

        Retell's LLM format declares built-in actions (end_call, transfer_call)
        as type=custom. This method resolves the actual type from the tool name.
        Transfer tools carry transfer_destination and transfer_option in metadata
        so the CF exporter can emit proper transfer_call nodes.
        """
        tool_type = _resolve_tool_type(tool.name, tool.type)
        metadata: dict[str, Any] = {}
        if tool.transfer_destination:
            metadata["transfer_destination"] = tool.transfer_destination
        if tool.transfer_option:
            metadata["transfer_option"] = tool.transfer_option
        return ToolDefinition(
            name=tool.name,
            description=tool.description,
            parameters=tool.parameters or {},
            type=tool_type,
            metadata=metadata,
        )


def _resolve_tool_type(name: str, declared_type: str) -> str:
    """Resolve the effective tool type from its name.

    In Retell's LLM format, built-in actions like end_call and transfer_call
    are declared as type=custom. This function returns the correct type based
    on well-known tool name patterns.
    """
    if name == "end_call":
        return "end_call"
    if name.startswith("transfer_call"):
        return "transfer_call"
    return declared_type
