"""Step definitions for features/actor_hierarchy.feature."""

from __future__ import annotations

import tempfile
from pathlib import Path
from typing import Any

import yaml
from behave import given, then, when
from behave.runner import Context

from cleveragents.actor.schema import ActorConfigSchema, NodeType
from cleveragents.core.exceptions import ValidationError


def _base_graph_yaml(
    nodes: list[dict[str, Any]],
    edges: list[dict[str, Any]] | None = None,
    entry_node: str = "agent_node",
    exit_nodes: list[str] | None = None,
    extra: dict[str, Any] | None = None,
) -> dict[str, Any]:
    if edges is None:
        ids = [n["id"] for n in nodes]
        edges = []
        for i in range(len(ids) - 1):
            edges.append({"from_node": ids[i], "to_node": ids[i + 1]})
    data: dict[str, Any] = {
        "name": "local/hierarchy-test",
        "type": "graph",
        "description": "Test hierarchical actor",
        "model": "gpt-4",
        "route": {
            "nodes": nodes,
            "edges": edges,
            "entry_node": entry_node,
            "exit_nodes": exit_nodes or [nodes[-1]["id"]],
        },
    }
    if extra:
        data.update(extra)
    return data


def _simple_agent_node(node_id: str = "agent_node", **overrides: Any) -> dict[str, Any]:
    base: dict[str, Any] = {
        "id": node_id,
        "type": "agent",
        "name": f"Agent {node_id}",
        "description": f"Agent node {node_id}",
        "config": {"model": "gpt-4"},
    }
    base.update(overrides)
    return base


def _get_node(config: ActorConfigSchema, node_id: str) -> Any:
    if config.route is None:
        return None
    for node in config.route.nodes:
        if node.id == node_id:
            return node
    return None


# ---------------------------------------------------------------
# LSP Binding on NodeDefinition
# ---------------------------------------------------------------


@given("a hierarchy actor YAML with node lsp_binding")
def step_hierarchy_yaml_node_lsp(context: Context) -> None:
    node = _simple_agent_node(
        "code_writer",
        lsp_binding={
            "server": "local/pyright",
            "languages": ["python"],
        },
    )
    context.hierarchy_yaml = _base_graph_yaml([node], entry_node="code_writer")


@given("a hierarchy actor YAML with auto lsp_binding")
def step_hierarchy_yaml_auto_lsp(context: Context) -> None:
    node = _simple_agent_node(
        "implementer",
        lsp_binding={"auto": True},
    )
    context.hierarchy_yaml = _base_graph_yaml([node], entry_node="implementer")


@given("a hierarchy actor YAML with no lsp_binding")
def step_hierarchy_yaml_no_lsp(context: Context) -> None:
    node = _simple_agent_node("agent_node")
    context.hierarchy_yaml = _base_graph_yaml([node])


@given('a hierarchy actor YAML with lsp_binding server "{server}"')
def step_hierarchy_yaml_lsp_bad_server(context: Context, server: str) -> None:
    node = _simple_agent_node(
        "code_writer",
        lsp_binding={"server": server, "languages": ["python"]},
    )
    context.hierarchy_yaml = _base_graph_yaml([node], entry_node="code_writer")


# ---------------------------------------------------------------
# Tool-Source References on NodeDefinition
# ---------------------------------------------------------------


@given("a hierarchy actor YAML with node tool_sources skills")
def step_hierarchy_yaml_tool_sources_skills(context: Context) -> None:
    node = _simple_agent_node(
        "executor",
        tool_sources=[{"type": "skill", "name": "local/file-ops"}],
    )
    context.hierarchy_yaml = _base_graph_yaml([node], entry_node="executor")


@given("a hierarchy actor YAML with node tool_sources mcp")
def step_hierarchy_yaml_tool_sources_mcp(context: Context) -> None:
    node = _simple_agent_node(
        "executor",
        tool_sources=[{"type": "mcp", "name": "local/filesystem"}],
    )
    context.hierarchy_yaml = _base_graph_yaml([node], entry_node="executor")


@given("a hierarchy actor YAML with node tool_sources builtin")
def step_hierarchy_yaml_tool_sources_builtin(context: Context) -> None:
    node = _simple_agent_node(
        "executor",
        tool_sources=[
            {"type": "builtin", "group": "file_operations"},
        ],
    )
    context.hierarchy_yaml = _base_graph_yaml([node], entry_node="executor")


@given("a hierarchy actor YAML with mixed node tool_sources")
def step_hierarchy_yaml_tool_sources_mixed(context: Context) -> None:
    node = _simple_agent_node(
        "executor",
        tool_sources=[
            {"type": "skill", "name": "local/file-ops"},
            {"type": "mcp", "name": "local/filesystem"},
            {"type": "builtin", "group": "git_operations"},
        ],
    )
    context.hierarchy_yaml = _base_graph_yaml([node], entry_node="executor")


@given("a hierarchy actor YAML with invalid tool_sources type")
def step_hierarchy_yaml_tool_sources_invalid(context: Context) -> None:
    node = _simple_agent_node(
        "executor",
        tool_sources=[{"type": "invalid_type", "name": "x/y"}],
    )
    context.hierarchy_yaml = _base_graph_yaml([node], entry_node="executor")


# ---------------------------------------------------------------
# Subgraph Node Enhancements
# ---------------------------------------------------------------


@given("a hierarchy actor YAML with subgraph actor_ref")
def step_hierarchy_yaml_subgraph_ref(context: Context) -> None:
    planner = _simple_agent_node("planner")
    review: dict[str, Any] = {
        "id": "review",
        "type": "subgraph",
        "name": "Review",
        "description": "Code review subgraph",
        "actor_ref": "local/code-reviewer",
    }
    context.hierarchy_yaml = _base_graph_yaml(
        [planner, review],
        edges=[{"from_node": "planner", "to_node": "review"}],
        entry_node="planner",
        exit_nodes=["review"],
    )


@given('a hierarchy actor YAML with subgraph actor_ref "{bad_ref}"')
def step_hierarchy_yaml_subgraph_bad_ref(context: Context, bad_ref: str) -> None:
    planner = _simple_agent_node("planner")
    review: dict[str, Any] = {
        "id": "review",
        "type": "subgraph",
        "name": "Review",
        "description": "Code review subgraph",
        "actor_ref": bad_ref,
    }
    context.hierarchy_yaml = _base_graph_yaml(
        [planner, review],
        edges=[{"from_node": "planner", "to_node": "review"}],
        entry_node="planner",
        exit_nodes=["review"],
    )


# ---------------------------------------------------------------
# Skills Field on ActorConfigSchema
# ---------------------------------------------------------------


@given("a hierarchy actor YAML with skills list")
def step_hierarchy_yaml_skills(context: Context) -> None:
    node = _simple_agent_node("agent_node")
    context.hierarchy_yaml = _base_graph_yaml(
        [node],
        extra={"skills": ["local/file-ops", "local/git-ops"]},
    )


@given("a hierarchy actor YAML with empty skills")
def step_hierarchy_yaml_empty_skills(context: Context) -> None:
    node = _simple_agent_node("agent_node")
    context.hierarchy_yaml = _base_graph_yaml([node], extra={"skills": []})


# ---------------------------------------------------------------
# LSP Fields on ActorConfigSchema (top-level)
# ---------------------------------------------------------------


@given("a hierarchy actor YAML with lsp list")
def step_hierarchy_yaml_lsp_list(context: Context) -> None:
    node = _simple_agent_node("agent_node")
    context.hierarchy_yaml = _base_graph_yaml(
        [node],
        extra={"lsp": ["local/pyright", "local/clangd"]},
    )


@given("a hierarchy actor YAML with lsp auto")
def step_hierarchy_yaml_lsp_auto(context: Context) -> None:
    node = _simple_agent_node("agent_node")
    context.hierarchy_yaml = _base_graph_yaml([node], extra={"lsp": {"auto": True}})


@given("a hierarchy actor YAML with lsp_capabilities")
def step_hierarchy_yaml_lsp_caps(context: Context) -> None:
    node = _simple_agent_node("agent_node")
    context.hierarchy_yaml = _base_graph_yaml(
        [node],
        extra={
            "lsp": ["local/pyright"],
            "lsp_capabilities": ["diagnostics", "hover"],
        },
    )


@given("a hierarchy actor YAML with lsp_capabilities all")
def step_hierarchy_yaml_lsp_caps_all(context: Context) -> None:
    node = _simple_agent_node("agent_node")
    context.hierarchy_yaml = _base_graph_yaml(
        [node],
        extra={
            "lsp": ["local/pyright"],
            "lsp_capabilities": "all",
        },
    )


# ---------------------------------------------------------------
# Precise Validation Error Messages
# ---------------------------------------------------------------


@given('a hierarchy actor YAML with bad entry_node "{bad_node}"')
def step_hierarchy_yaml_bad_entry(context: Context, bad_node: str) -> None:
    node = _simple_agent_node("agent_node")
    data = _base_graph_yaml([node])
    data["route"]["entry_node"] = bad_node
    context.hierarchy_yaml = data


@given("a hierarchy actor YAML with duplicate node ids")
def step_hierarchy_yaml_dup_ids(context: Context) -> None:
    n1 = _simple_agent_node("dup_node")
    n2 = _simple_agent_node("dup_node")
    n2["name"] = "Second Dup"
    context.hierarchy_yaml = _base_graph_yaml([n1, n2], entry_node="dup_node")


@given("a hierarchy actor YAML with cycle")
def step_hierarchy_yaml_cycle(context: Context) -> None:
    n1 = _simple_agent_node("a")
    n2 = _simple_agent_node("b")
    context.hierarchy_yaml = _base_graph_yaml(
        [n1, n2],
        edges=[
            {"from_node": "a", "to_node": "b"},
            {"from_node": "b", "to_node": "a"},
        ],
        entry_node="a",
        exit_nodes=["b"],
    )


@given('a hierarchy actor YAML with bad edge from_node "{bad_node}"')
def step_hierarchy_yaml_bad_edge(context: Context, bad_node: str) -> None:
    n1 = _simple_agent_node("real_node")
    context.hierarchy_yaml = _base_graph_yaml(
        [n1],
        edges=[{"from_node": bad_node, "to_node": "real_node"}],
        entry_node="real_node",
    )


# ---------------------------------------------------------------
# When Steps
# ---------------------------------------------------------------


@when("I parse the hierarchy actor YAML")
def step_parse_hierarchy_yaml(context: Context) -> None:
    context.hierarchy_error = None
    try:
        context.hierarchy_config = ActorConfigSchema.model_validate(
            context.hierarchy_yaml
        )
    except Exception as exc:
        context.hierarchy_error = str(exc)
        context.hierarchy_config = None


@when("I parse the hierarchy actor YAML expecting failure")
def step_parse_hierarchy_yaml_fail(context: Context) -> None:
    context.hierarchy_error = None
    try:
        context.hierarchy_config = ActorConfigSchema.model_validate(
            context.hierarchy_yaml
        )
        context.hierarchy_error = None
    except Exception as exc:
        context.hierarchy_error = str(exc)
        context.hierarchy_config = None


# ---------------------------------------------------------------
# Then Steps - Validation
# ---------------------------------------------------------------


@then("the hierarchy actor should be valid")
def step_hierarchy_valid(context: Context) -> None:
    assert context.hierarchy_config is not None, (
        f"Expected valid config, got error: {context.hierarchy_error}"
    )


@then('the hierarchy graph node "{node_id}" should have lsp_binding')
def step_hierarchy_node_has_lsp(context: Context, node_id: str) -> None:
    node = _get_node(context.hierarchy_config, node_id)
    assert node is not None, f"Node '{node_id}' not found"
    assert node.lsp_binding is not None, f"Node '{node_id}' has no lsp_binding"


@then('the hierarchy node "{node_id}" lsp_binding server should be "{server}"')
def step_hierarchy_lsp_server(context: Context, node_id: str, server: str) -> None:
    node = _get_node(context.hierarchy_config, node_id)
    assert node is not None
    assert node.lsp_binding is not None
    assert node.lsp_binding.server == server


@then('the hierarchy node "{node_id}" lsp_binding languages should contain "{lang}"')
def step_hierarchy_lsp_lang(context: Context, node_id: str, lang: str) -> None:
    node = _get_node(context.hierarchy_config, node_id)
    assert node is not None
    assert node.lsp_binding is not None
    assert lang in node.lsp_binding.languages


@then('the hierarchy node "{node_id}" lsp_binding auto should be true')
def step_hierarchy_lsp_auto(context: Context, node_id: str) -> None:
    node = _get_node(context.hierarchy_config, node_id)
    assert node is not None
    assert node.lsp_binding is not None
    assert node.lsp_binding.auto is True


@then('the hierarchy graph node "{node_id}" should not have lsp_binding')
def step_hierarchy_node_no_lsp(context: Context, node_id: str) -> None:
    node = _get_node(context.hierarchy_config, node_id)
    assert node is not None, f"Node '{node_id}' not found"
    assert node.lsp_binding is None


@then('the hierarchy graph node "{node_id}" should have tool_sources')
def step_hierarchy_node_has_tool_sources(context: Context, node_id: str) -> None:
    node = _get_node(context.hierarchy_config, node_id)
    assert node is not None, f"Node '{node_id}' not found"
    assert node.tool_sources, f"Node '{node_id}' has no tool_sources"


@then('the hierarchy node "{node_id}" tool_sources should have {count:d} items')
def step_hierarchy_tool_sources_count(
    context: Context, node_id: str, count: int
) -> None:
    node = _get_node(context.hierarchy_config, node_id)
    assert node is not None
    assert len(node.tool_sources) == count


@then(
    'the hierarchy node "{node_id}" tool_sources item {idx:d} '
    'type should be "{ts_type}"'
)
def step_hierarchy_tool_source_type(
    context: Context, node_id: str, idx: int, ts_type: str
) -> None:
    node = _get_node(context.hierarchy_config, node_id)
    assert node is not None
    assert node.tool_sources[idx].type == ts_type


@then(
    'the hierarchy node "{node_id}" tool_sources item {idx:d} '
    'name should be "{ts_name}"'
)
def step_hierarchy_tool_source_name(
    context: Context, node_id: str, idx: int, ts_name: str
) -> None:
    node = _get_node(context.hierarchy_config, node_id)
    assert node is not None
    assert node.tool_sources[idx].name == ts_name


@then(
    'the hierarchy node "{node_id}" tool_sources item {idx:d} '
    'group should be "{ts_group}"'
)
def step_hierarchy_tool_source_group(
    context: Context, node_id: str, idx: int, ts_group: str
) -> None:
    node = _get_node(context.hierarchy_config, node_id)
    assert node is not None
    assert node.tool_sources[idx].group == ts_group


@then('the hierarchy graph node "{node_id}" should not have tool_sources')
def step_hierarchy_node_no_tool_sources(context: Context, node_id: str) -> None:
    node = _get_node(context.hierarchy_config, node_id)
    assert node is not None
    assert not node.tool_sources


@then('the hierarchy graph node "{node_id}" should have type "{ntype}"')
def step_hierarchy_node_type(context: Context, node_id: str, ntype: str) -> None:
    node = _get_node(context.hierarchy_config, node_id)
    assert node is not None
    assert node.type == NodeType(ntype)


@then('the hierarchy node "{node_id}" actor_ref should be "{ref}"')
def step_hierarchy_node_actor_ref(context: Context, node_id: str, ref: str) -> None:
    node = _get_node(context.hierarchy_config, node_id)
    assert node is not None
    assert node.actor_ref == ref


@then("the hierarchy actor should have {count:d} skills")
def step_hierarchy_actor_skills_count(context: Context, count: int) -> None:
    assert len(context.hierarchy_config.skills) == count


@then("the hierarchy actor lsp should have {count:d} items")
def step_hierarchy_actor_lsp_count(context: Context, count: int) -> None:
    assert isinstance(context.hierarchy_config.lsp, list)
    assert len(context.hierarchy_config.lsp) == count


@then("the hierarchy actor lsp_auto should be true")
def step_hierarchy_actor_lsp_auto(context: Context) -> None:
    assert isinstance(context.hierarchy_config.lsp, dict)
    assert context.hierarchy_config.lsp.get("auto") is True


@then('the hierarchy actor lsp_capabilities should contain "{cap}"')
def step_hierarchy_actor_lsp_caps_contain(context: Context, cap: str) -> None:
    assert isinstance(context.hierarchy_config.lsp_capabilities, list)
    assert cap in context.hierarchy_config.lsp_capabilities


@then('the hierarchy actor lsp_capabilities should be "{value}"')
def step_hierarchy_actor_lsp_caps_str(context: Context, value: str) -> None:
    assert context.hierarchy_config.lsp_capabilities == value


@then('the hierarchy parse error should mention "{text}"')
def step_hierarchy_error_mention(context: Context, text: str) -> None:
    assert context.hierarchy_error is not None, "Expected error but none occurred"
    assert text.lower() in context.hierarchy_error.lower(), (
        f"Error '{context.hierarchy_error}' does not mention '{text}'"
    )


# ---------------------------------------------------------------
# Loader Integration
# ---------------------------------------------------------------


def _hierarchy_tmpdir() -> Path:
    return Path(tempfile.mkdtemp(prefix="hierarchy_actor_"))


@given("a hierarchy temp dir with a hierarchical actor YAML file")
def step_hierarchy_loader_setup(context: Context) -> None:
    from cleveragents.actor.loader import ActorLoader

    tmpdir = _hierarchy_tmpdir()
    actor_data: dict[str, Any] = {
        "name": "local/hierarchy-demo",
        "type": "graph",
        "description": "Hierarchical demo",
        "model": "gpt-4",
        "skills": ["local/file-ops"],
        "lsp": ["local/pyright"],
        "lsp_capabilities": ["diagnostics"],
        "route": {
            "nodes": [
                {
                    "id": "planner",
                    "type": "agent",
                    "name": "Planner",
                    "description": "Plans tasks",
                    "config": {"model": "gpt-4"},
                    "lsp_binding": {
                        "server": "local/pyright",
                        "languages": ["python"],
                    },
                    "tool_sources": [
                        {"type": "skill", "name": "local/file-ops"},
                    ],
                },
            ],
            "edges": [],
            "entry_node": "planner",
            "exit_nodes": ["planner"],
        },
    }
    yaml_path = tmpdir / "hierarchy_demo.yaml"
    with yaml_path.open("w") as f:
        yaml.safe_dump(actor_data, f)
    context.hierarchy_tmpdir = tmpdir
    context.hierarchy_loader_cls = ActorLoader


@given("a hierarchy temp dir with invalid lsp_binding actor YAML")
def step_hierarchy_loader_invalid(context: Context) -> None:
    from cleveragents.actor.loader import ActorLoader

    tmpdir = _hierarchy_tmpdir()
    actor_data: dict[str, Any] = {
        "name": "local/bad-lsp",
        "type": "graph",
        "description": "Bad LSP binding",
        "model": "gpt-4",
        "route": {
            "nodes": [
                {
                    "id": "code_node",
                    "type": "agent",
                    "name": "Coder",
                    "description": "Codes stuff",
                    "config": {},
                    "lsp_binding": {
                        "server": "no-namespace",
                        "languages": ["python"],
                    },
                },
            ],
            "edges": [],
            "entry_node": "code_node",
            "exit_nodes": ["code_node"],
        },
    }
    yaml_path = tmpdir / "bad_lsp.yaml"
    with yaml_path.open("w") as f:
        yaml.safe_dump(actor_data, f)
    context.hierarchy_tmpdir = tmpdir
    context.hierarchy_loader_cls = ActorLoader


@when("I create a hierarchy loader and run discovery")
def step_hierarchy_loader_discover(context: Context) -> None:
    loader = context.hierarchy_loader_cls(search_roots=[context.hierarchy_tmpdir])
    context.hierarchy_loaded_actors = loader.discover()
    context.hierarchy_loader_error = None


@when("I create a hierarchy loader and run discovery expecting error")
def step_hierarchy_loader_discover_error(context: Context) -> None:
    loader = context.hierarchy_loader_cls(search_roots=[context.hierarchy_tmpdir])
    try:
        context.hierarchy_loaded_actors = loader.discover()
        context.hierarchy_loader_error = None
    except (ValidationError, Exception) as exc:
        context.hierarchy_loaded_actors = []
        context.hierarchy_loader_error = str(exc)


@then("the hierarchy loader should find {count:d} actors")
def step_hierarchy_loader_count(context: Context, count: int) -> None:
    assert len(context.hierarchy_loaded_actors) == count, (
        f"Expected {count} actors, got {len(context.hierarchy_loaded_actors)}"
    )


@then("the hierarchy loaded actor should have skills")
def step_hierarchy_loaded_has_skills(context: Context) -> None:
    actor = context.hierarchy_loaded_actors[0]
    assert actor.skills, "Loaded actor should have skills"


@then("the hierarchy loaded actor should have lsp")
def step_hierarchy_loaded_has_lsp(context: Context) -> None:
    actor = context.hierarchy_loaded_actors[0]
    assert actor.lsp is not None, "Loaded actor should have lsp"


@then('the hierarchy loader error should mention "{text}"')
def step_hierarchy_loader_error_mention(context: Context, text: str) -> None:
    assert context.hierarchy_loader_error is not None, (
        "Expected loader error but none occurred"
    )
    assert text.lower() in context.hierarchy_loader_error.lower(), (
        f"Error '{context.hierarchy_loader_error}' does not mention '{text}'"
    )
