"""
Plan-to-Workflow Integration Adapter.

Bridge module that transforms abstract execution `Plans` into concrete
`WorkflowDefinitions`. This allows high-level plans generated by LLMs
to be executed by the framework's robust workflow engine.
"""

from __future__ import annotations

from core.observability.logging import get_logger
from typing import Dict, Optional

from core.planning.planner import Plan, PlanStep
from core.workflows.builder import (
    NodeType,
    WorkflowDefinition,
    WorkflowEdge,
    WorkflowNode,
)

logger = get_logger(__name__)

# -----------------------------------------------------------------------
# Action → NodeType mapping
# -----------------------------------------------------------------------

ACTION_MAP: Dict[str, NodeType] = {
    "analyze": NodeType.AGENT,
    "execute": NodeType.AGENT,
    "validate": NodeType.AGENT,
    "transform": NodeType.TRANSFORM,
    "check": NodeType.CONDITION,
    "condition": NodeType.CONDITION,
    "tool": NodeType.TOOL,
    "human": NodeType.HUMAN,
    "parallel": NodeType.PARALLEL,
}


def _resolve_node_type(step: PlanStep) -> NodeType:
    """Resolve the workflow :class:`NodeType` for a plan step."""
    return ACTION_MAP.get(step.action.lower().strip(), NodeType.AGENT)


# -----------------------------------------------------------------------
# Public API
# -----------------------------------------------------------------------


def plan_to_workflow(
    plan: Plan,
    *,
    name: Optional[str] = None,
    default_agent_id: str = "planner-agent",
) -> WorkflowDefinition:
    """Convert a :class:`Plan` into an executable :class:`WorkflowDefinition`.

    The converter:

    1. Creates a ``START`` node.
    2. Maps each :class:`PlanStep` to a :class:`WorkflowNode` (type
       determined by :data:`ACTION_MAP`).
    3. Wires edges respecting the step dependency graph.  Steps with no
       dependencies are connected to the ``START`` node.  When multiple
       independent steps exist at the same level they can later be wrapped
       in a ``PARALLEL`` node (not done automatically to keep the graph
       simple).
    4. Steps with no successors are connected to the ``END`` node.

    Args:
        plan: The source execution plan.
        name: Optional workflow name (defaults to the plan goal).
        default_agent_id: Agent ID assigned to ``AGENT`` nodes when the
            step parameters don't specify one.

    Returns:
        A validated :class:`WorkflowDefinition`.
    """
    wf = WorkflowDefinition(name=name or plan.goal)
    wf.metadata["source"] = "plan_adapter"
    if plan.metadata:
        wf.metadata["plan"] = plan.metadata

    # --- START node ---
    start = WorkflowNode(id="start", type=NodeType.START, label="Start")
    wf.add_node(start)

    # --- Map steps → nodes ---
    step_to_node: Dict[str, str] = {}
    edge_counter = 0

    for step in plan.steps:
        node_type = _resolve_node_type(step)
        node_id = f"step_{step.id}"
        step_to_node[step.id] = node_id

        agent_id = (
            step.parameters.get("agent_id", default_agent_id)
            if node_type == NodeType.AGENT
            else None
        )
        tool_id = step.parameters.get("tool_id") if node_type == NodeType.TOOL else None
        condition_expr = (
            step.parameters.get("expression")
            if node_type == NodeType.CONDITION
            else None
        )

        node = WorkflowNode(
            id=node_id,
            type=node_type,
            label=step.description,
            config={"plan_step_id": step.id, "action": step.action, **step.parameters},
            agent_id=agent_id,
            tool_id=tool_id,
            condition_expression=condition_expr,
        )
        wf.add_node(node)

    # --- Wire edges ---
    nodes_with_incoming: set[str] = set()
    start_connected: set[str] = set()  # prevent duplicate START→node edges

    for step in plan.steps:
        node_id = step_to_node[step.id]
        if not step.dependencies:
            # No deps → connect from START
            edge_counter += 1
            wf.add_edge(
                WorkflowEdge(
                    id=f"e{edge_counter}", source_id="start", target_id=node_id
                )
            )
            start_connected.add(node_id)
            nodes_with_incoming.add(node_id)
        else:
            has_valid_dep = False
            for dep_id in step.dependencies:
                dep_node = step_to_node.get(dep_id)
                if dep_node:
                    edge_counter += 1
                    wf.add_edge(
                        WorkflowEdge(
                            id=f"e{edge_counter}", source_id=dep_node, target_id=node_id
                        )
                    )
                    nodes_with_incoming.add(node_id)
                    has_valid_dep = True
                else:
                    logger.warning(f"Unknown dependency '{dep_id}' in step '{step.id}'")
            # If no valid deps resolved, connect to START (once)
            if not has_valid_dep and node_id not in start_connected:
                edge_counter += 1
                wf.add_edge(
                    WorkflowEdge(
                        id=f"e{edge_counter}", source_id="start", target_id=node_id
                    )
                )
                start_connected.add(node_id)
                nodes_with_incoming.add(node_id)

    # --- END node ---
    end = WorkflowNode(id="end", type=NodeType.END, label="End")
    wf.add_node(end)

    # Connect leaf nodes (no outgoing edges) → END
    all_sources = {e.source_id for e in wf.edges}
    for step in plan.steps:
        node_id = step_to_node[step.id]
        if node_id not in all_sources:
            edge_counter += 1
            wf.add_edge(
                WorkflowEdge(id=f"e{edge_counter}", source_id=node_id, target_id="end")
            )

    errors = wf.validate()
    if errors:
        logger.warning(f"Generated workflow has validation issues: {errors}")

    return wf
