"""Step definitions for the Tool Runtime Core feature."""

import json
import threading
from typing import Any

from behave import given, then, use_step_matcher, when

from cleveragents.domain.models.core.tool import ToolCapability
from cleveragents.tool.registry import ToolRegistry
from cleveragents.tool.runner import ToolRunner
from cleveragents.tool.runtime import ToolError, ToolResult, ToolSpec

__all__: list[str] = []


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _echo_handler(inputs: dict[str, Any]) -> dict[str, Any]:
    return dict(inputs)


def _adder_handler(inputs: dict[str, Any]) -> dict[str, Any]:
    return {"sum": inputs.get("a", 0) + inputs.get("b", 0)}


def _failing_handler(inputs: dict[str, Any]) -> dict[str, Any]:
    raise RuntimeError("handler exploded")


def _non_serialisable_handler(inputs: dict[str, Any]) -> dict[str, Any]:
    return {"bad": object()}


def _scalar_handler(inputs: dict[str, Any]) -> int:
    return 42


# ---------------------------------------------------------------------------
# Givens
# ---------------------------------------------------------------------------


@given("a tool registry")
def step_given_tool_registry(context: Any) -> None:
    context.registry = ToolRegistry()


@given('a tool spec named "{name}" with a handler')
def step_given_tool_spec_with_handler(context: Any, name: str) -> None:
    context.tool_spec = ToolSpec(
        name=name,
        description=f"Test tool {name}",
        handler=_echo_handler,
    )


use_step_matcher("re")


@given(r'a registered tool spec named "(?P<name>[^"]+)"')
def step_given_registered_tool_spec(context: Any, name: str) -> None:
    spec = ToolSpec(
        name=name,
        description=f"Test tool {name}",
        handler=_echo_handler,
    )
    context.registry.register(spec)


@given(r'a registered tool spec named "(?P<name>[^"]+)" typed as "(?P<tt>[^"]+)"')
def step_given_registered_tool_spec_with_type(context: Any, name: str, tt: str) -> None:
    spec = ToolSpec(
        name=name,
        description=f"Test tool {name}",
        handler=_echo_handler,
        tool_type=tt,
    )
    context.registry.register(spec)


use_step_matcher("parse")


@given('a registered tool spec named "{name}" with an adder handler')
def step_given_registered_adder(context: Any, name: str) -> None:
    spec = ToolSpec(
        name=name,
        description=f"Adder tool {name}",
        handler=_adder_handler,
    )
    context.registry.register(spec)


@given('a registered tool spec named "{name}" with a failing handler')
def step_given_registered_failing(context: Any, name: str) -> None:
    spec = ToolSpec(
        name=name,
        description=f"Failing tool {name}",
        handler=_failing_handler,
    )
    context.registry.register(spec)


@given('a registered tool spec named "{name}" with a non-serialisable handler')
def step_given_registered_non_serialisable(context: Any, name: str) -> None:
    spec = ToolSpec(
        name=name,
        description=f"Bad output tool {name}",
        handler=_non_serialisable_handler,
    )
    context.registry.register(spec)


@given('a registered tool spec named "{name}" with a scalar handler')
def step_given_registered_scalar(context: Any, name: str) -> None:
    spec = ToolSpec(
        name=name,
        description=f"Scalar tool {name}",
        handler=_scalar_handler,
    )
    context.registry.register(spec)


@given('a tool spec named "{name}" with read_only capability')
def step_given_spec_read_only(context: Any, name: str) -> None:
    context.tool_spec = ToolSpec(
        name=name,
        description=f"Read-only tool {name}",
        capabilities=ToolCapability(read_only=True),
        handler=_echo_handler,
    )


@given('a tool spec named "{name}" with writes capability')
def step_given_spec_writes(context: Any, name: str) -> None:
    context.tool_spec = ToolSpec(
        name=name,
        description=f"Writer tool {name}",
        capabilities=ToolCapability(writes=True),
        handler=_echo_handler,
    )


@given("a tool runner for the registry")
def step_given_tool_runner(context: Any) -> None:
    context.runner = ToolRunner(context.registry)
    context.lifecycle_events = []


@given("an alternate registry with {count:d} tools")
def step_given_alternate_registry(context: Any, count: int) -> None:
    context.alt_registry = ToolRegistry()
    for i in range(count):
        spec = ToolSpec(
            name=f"alt/tool-{i}",
            description=f"Alt tool {i}",
            handler=_echo_handler,
        )
        context.alt_registry.register(spec)


# ---------------------------------------------------------------------------
# Whens
# ---------------------------------------------------------------------------


@when("I register the tool spec")
def step_when_register(context: Any) -> None:
    context.registry.register(context.tool_spec)


@when('I get the tool by name "{name}"')
def step_when_get_tool(context: Any, name: str) -> None:
    context.retrieved_spec = context.registry.get(name)


@when("I list all tools")
def step_when_list_all(context: Any) -> None:
    context.tool_list = context.registry.list_tools()


@when('I list tools with namespace "{ns}"')
def step_when_list_namespace(context: Any, ns: str) -> None:
    context.tool_list = context.registry.list_tools(namespace=ns)


@when('I list tools with tool_type "{tt}"')
def step_when_list_tool_type(context: Any, tt: str) -> None:
    context.tool_list = context.registry.list_tools(tool_type=tt)


@when('I remove the tool "{name}"')
def step_when_remove(context: Any, name: str) -> None:
    context.removal_result = context.registry.remove(name)


@when('I try to register a duplicate tool "{name}"')
def step_when_duplicate_register(context: Any, name: str) -> None:
    dup_spec = ToolSpec(
        name=name,
        description=f"Duplicate {name}",
        handler=_echo_handler,
    )
    try:
        context.registry.register(dup_spec)
        context.tool_error = None
    except ToolError as exc:
        context.tool_error = exc


@when("I discover tools from the runner")
def step_when_discover(context: Any) -> None:
    context.discovered = context.runner.discover()
    context.lifecycle_events.append("discover")


@when('I activate tool "{name}"')
def step_when_activate(context: Any, name: str) -> None:
    context.activated_spec = context.runner.activate(name)
    if hasattr(context, "lifecycle_events"):
        context.lifecycle_events.append("activate")


@when('I execute tool "{name}" with inputs {inputs_json}')
def step_when_execute(context: Any, name: str, inputs_json: str) -> None:
    inputs = json.loads(inputs_json)
    context.tool_result = context.runner.execute(name, inputs)
    if hasattr(context, "lifecycle_events"):
        context.lifecycle_events.append("execute")


@when('I deactivate tool "{name}"')
def step_when_deactivate(context: Any, name: str) -> None:
    context.deactivation_result = context.runner.deactivate(name)
    if hasattr(context, "lifecycle_events"):
        context.lifecycle_events.append("deactivate")


@when("I concurrently register {count:d} tools")
def step_when_concurrent_register(context: Any, count: int) -> None:
    errors: list[Exception] = []
    barrier = threading.Barrier(count)

    def _register(idx: int) -> None:
        try:
            barrier.wait(timeout=5)
            spec = ToolSpec(
                name=f"conc/tool-{idx}",
                description=f"Concurrent tool {idx}",
                handler=_echo_handler,
            )
            context.registry.register(spec)
        except Exception as exc:
            errors.append(exc)

    threads = [threading.Thread(target=_register, args=(i,)) for i in range(count)]
    for t in threads:
        t.start()
    for t in threads:
        t.join(timeout=10)

    context.concurrent_errors = errors


@when('I execute tool "{name}" with non-serialisable inputs')
def step_when_execute_bad_inputs(context: Any, name: str) -> None:
    bad_inputs: dict[str, Any] = {"bad": object()}
    context.tool_result = context.runner.execute(name, bad_inputs)


@when(
    'I create a ToolError with name "{name}" type "{err_type}" and details "{details}"'
)
def step_when_create_tool_error(
    context: Any, name: str, err_type: str, details: str
) -> None:
    context.tool_error_obj = ToolError(
        tool_name=name,
        error_type=err_type,
        details=details,
    )


@when("I create a ToolResult with success True and metadata")
def step_when_create_result_success(context: Any) -> None:
    context.tool_result_obj = ToolResult(
        success=True,
        output={"value": 42},
        metadata={"trace_id": "abc123"},
        duration_ms=1.5,
    )


@when('I create a ToolResult with success False and error "{error}"')
def step_when_create_result_failure(context: Any, error: str) -> None:
    context.tool_result_obj = ToolResult(
        success=False,
        output={},
        error=error,
        duration_ms=0.0,
    )


@when('I try to activate a missing tool "{name}"')
def step_when_activate_missing(context: Any, name: str) -> None:
    try:
        context.runner.activate(name)
        context.tool_error = None
    except ToolError as exc:
        context.tool_error = exc


@when('I try to execute a missing tool "{name}" with inputs {inputs_json}')
def step_when_execute_missing(context: Any, name: str, inputs_json: str) -> None:
    try:
        inputs = json.loads(inputs_json)
        context.runner.execute(name, inputs)
        context.tool_error = None
    except ToolError as exc:
        context.tool_error = exc


@when("I discover tools from the alternate registry")
def step_when_discover_alt(context: Any) -> None:
    context.discovered = context.runner.discover(registry=context.alt_registry)


# ---------------------------------------------------------------------------
# Thens
# ---------------------------------------------------------------------------


@then("the tool should be in the registry")
def step_then_tool_in_registry(context: Any) -> None:
    found = context.registry.get(context.tool_spec.name)
    assert found is not None, "Tool not found in registry"
    assert found.name == context.tool_spec.name


@then('the retrieved tool spec name should be "{name}"')
def step_then_retrieved_name(context: Any, name: str) -> None:
    assert context.retrieved_spec is not None
    assert context.retrieved_spec.name == name


@then("the retrieved tool spec should be None")
def step_then_retrieved_none(context: Any) -> None:
    assert context.retrieved_spec is None


@then("the tool list should contain {count:d} tools")
def step_then_list_count(context: Any, count: int) -> None:
    assert len(context.tool_list) == count, (
        f"Expected {count} tools, got {len(context.tool_list)}"
    )


@then("the removal should succeed")
def step_then_removal_success(context: Any) -> None:
    assert context.removal_result is True


@then("the removal should fail")
def step_then_removal_fail(context: Any) -> None:
    assert context.removal_result is False


@then('getting "{name}" should return None')
def step_then_get_returns_none(context: Any, name: str) -> None:
    assert context.registry.get(name) is None


@then('a ToolError should be raised with type "{err_type}"')
def step_then_tool_error_raised(context: Any, err_type: str) -> None:
    assert context.tool_error is not None, "Expected ToolError but none was raised"
    assert context.tool_error.error_type == err_type


@then("the tool spec capabilities read_only should be True")
def step_then_cap_read_only(context: Any) -> None:
    assert context.tool_spec.capabilities.read_only is True


@then("the tool spec capabilities writes should be False")
def step_then_cap_no_writes(context: Any) -> None:
    assert context.tool_spec.capabilities.writes is False


@then("the tool spec capabilities writes should be True")
def step_then_cap_writes(context: Any) -> None:
    assert context.tool_spec.capabilities.writes is True


@then("the lifecycle should complete in order")
def step_then_lifecycle_order(context: Any) -> None:
    expected = ["discover", "activate", "execute", "deactivate"]
    assert context.lifecycle_events == expected, (
        f"Expected {expected}, got {context.lifecycle_events}"
    )


@then("the tool result should be successful")
def step_then_result_success(context: Any) -> None:
    assert context.tool_result.success is True


@then("the tool result should not be successful")
def step_then_result_failure(context: Any) -> None:
    assert context.tool_result.success is False


@then('the tool result output should contain key "{key}"')
def step_then_result_has_key(context: Any, key: str) -> None:
    assert key in context.tool_result.output, (
        f"Key '{key}' not in output: {context.tool_result.output}"
    )


@then("the tool result duration_ms should be non-negative")
def step_then_duration_non_negative(context: Any) -> None:
    assert context.tool_result.duration_ms >= 0.0


@then("the tool result error should not be empty")
def step_then_result_error_present(context: Any) -> None:
    assert context.tool_result.error is not None
    assert len(context.tool_result.error) > 0


@then('the tool result error should mention "{text}"')
def step_then_result_error_mentions(context: Any, text: str) -> None:
    assert context.tool_result.error is not None
    assert text in context.tool_result.error, (
        f"Expected '{text}' in error: {context.tool_result.error}"
    )


@then("all {count:d} tools should be in the registry")
def step_then_all_concurrent_registered(context: Any, count: int) -> None:
    assert len(context.concurrent_errors) == 0, (
        f"Errors during concurrent registration: {context.concurrent_errors}"
    )
    tools = context.registry.list_tools()
    assert len(tools) == count, f"Expected {count} tools, got {len(tools)}"


@then('the ToolError tool_name should be "{name}"')
def step_then_error_tool_name(context: Any, name: str) -> None:
    assert context.tool_error_obj.tool_name == name


@then('the ToolError error_type should be "{err_type}"')
def step_then_error_type(context: Any, err_type: str) -> None:
    assert context.tool_error_obj.error_type == err_type


@then('the ToolError details should be "{details}"')
def step_then_error_details(context: Any, details: str) -> None:
    assert context.tool_error_obj.details == details


@then('the ToolError message should contain "{text}"')
def step_then_error_message_contains(context: Any, text: str) -> None:
    assert text in str(context.tool_error_obj)


@then("the ToolResult success should be True")
def step_then_result_obj_success_true(context: Any) -> None:
    assert context.tool_result_obj.success is True


@then("the ToolResult success should be False")
def step_then_result_obj_success_false(context: Any) -> None:
    assert context.tool_result_obj.success is False


@then("the ToolResult metadata should not be empty")
def step_then_result_metadata_not_empty(context: Any) -> None:
    assert len(context.tool_result_obj.metadata) > 0


@then('the ToolResult error should be "{error}"')
def step_then_result_obj_error(context: Any, error: str) -> None:
    assert context.tool_result_obj.error == error


@then("the deactivation should return False")
def step_then_deactivation_false(context: Any) -> None:
    assert context.deactivation_result is False


@then("the discovered list should contain {count:d} tools")
def step_then_discovered_count(context: Any, count: int) -> None:
    assert len(context.discovered) == count, (
        f"Expected {count} discovered, got {len(context.discovered)}"
    )
