"""Step definitions for execute_decision_recording.feature (dexe prefix).

All steps use "dexe" prefix to avoid collisions with existing step files.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from behave import given, then, when
from behave.runner import Context
from ulid import ULID

from cleveragents.application.services.decision_service import DecisionService
from cleveragents.application.services.execute_decision_hook import (
    ExecutePhaseDecisionHook,
)
from cleveragents.core.exceptions import ValidationError

if TYPE_CHECKING:
    pass


def _plan_ulid(ctx: Context, name: str) -> str:
    reg = getattr(ctx, "_dexe_reg", {})
    if name not in reg:
        reg[name] = str(ULID())
    ctx._dexe_reg = reg
    return reg[name]


def _coerce_parent(p_pid: str) -> str | None:
    """Convert the feature-file literal ``None`` into a real ``None``."""
    if p_pid in ("None", "none", "null", ""):
        return None
    return p_pid


# ---------------------------------------------------------------------------
# Given / When (unique step texts)
# ---------------------------------------------------------------------------


@given('dexe plan "{name}" ULID and service initialized')
def g_dexe_init(context: Context, name: str) -> None:
    ctx_id = _plan_ulid(context, name)
    context.dsvc = DecisionService()
    context._dexh_res = None
    context._dexh_err = None
    context._dexe_reg = {name: ctx_id}


@given('an ExecHook created for plan "{pid}" with parent {p_pid}')
def g_dexe_hook(context: Context, pid: str, p_pid: str) -> None:
    ctx_id = _plan_ulid(context, pid)
    context.execute_hook = ExecutePhaseDecisionHook(
        decision_service=context.dsvc,
        plan_id=ctx_id,
        parent_decision_id=_coerce_parent(p_pid),
    )


@when('dexe record impl_choice question="{q}" chosen="{c}"')
def w_dexe_impl(context: Context, q: str, c: str) -> None:
    d = context.execute_hook.record_implementation_choice(question=q, chosen_option=c)
    context._dexh_res = d


@when('dexe record impl_choice question="{q}" alts="{a}" chosen="{c}" conf={conf}')
def w_dexe_impl_conf(context: Context, q: str, a: str, c: str, conf: str) -> None:
    alts = [x.strip() for x in a.split("|") if x.strip()]
    d = context.execute_hook.record_implementation_choice(
        question=q,
        chosen_option=c,
        alternatives_considered=alts,
        confidence_score=float(conf),
    )
    context._dexh_res = d


@when('dexe record tool_inv q="{q}" tool_write="{c}"')
def w_dexe_tool(context: Context, q: str, c: str) -> None:
    d = context.execute_hook.record_tool_invocation(question=q, chosen_option=c)
    context._dexh_res = d


@when('dexe record error_recove q="{q}" action_ret="{c}"')
def w_dexe_err(context: Context, q: str, c: str) -> None:
    d = context.execute_hook.record_error_recovery(question=q, chosen_option=c)
    context._dexh_res = d


@when('dexe record val_respond q="{q}" fix="{c}"')
def w_dexe_val(context: Context, q: str, c: str) -> None:
    d = context.execute_hook.record_validation_response(question=q, chosen_option=c)
    context._dexh_res = d


@when('dexe record sub_spawn q="{q}" chosen="{c}"')
def w_dexe_subspawn(context: Context, q: str, c: str) -> None:
    d = context.execute_hook.record_subplan_spawn(question=q, chosen_option=c)
    context._dexh_res = d


@when('dexe record par_spawn alt="{a}" chosen="{c}"')
def w_dexe_parspawn(context: Context, a: str, c: str) -> None:
    alts = [x.strip() for x in a.split("|") if x.strip()]
    d = context.execute_hook.record_subplan_parallel_spawn(
        question="Parallel",
        chosen_option=c,
        alternatives_considered=alts,
    )
    context._dexh_res = d


@when('dexe record res_select q="{q}" chosen="{c}"')
def w_dexe_resel(context: Context, q: str, c: str) -> None:
    d = context.execute_hook.record_resource_selection(question=q, chosen_option=c)
    context._dexh_res = d


@when('dexe record impl_choice question="{q}" chose="{c}" with_context_json={has_ctx}')
def w_dexe_ctx(context: Context, q: str, c: str, has_ctx: str) -> None:
    if int(has_ctx):
        d = context.execute_hook.record_implementation_choice(
            question=q,
            chosen_option=c,
            context_data={"w": "line"},
            actor_state={"s": 3},
            relevant_resources=["RES_A", "RES_B"],
        )
    else:
        d = context.execute_hook.record_implementation_choice(
            question=q, chosen_option=c
        )
    context._dexh_res = d


# --- Error recording ---


@when('dexe record impl_choice question="{q}" chosen="{c}" expect_error=True')
def w_dexe_err_impl(context: Context, q: str, c: str) -> None:
    try:
        context.execute_hook.record_implementation_choice(question=q, chosen_option=c)
        context._dexh_err = None
    except ValidationError as exc:
        context._dexh_err = exc


# Hardcoded for the empty-string case (behave's ``{q}`` placeholder needs >=1 char).
@when('dexe record impl_choice question="" chosen="X" expect_error=True')
def w_dexe_err_impl_empty_q(context: Context) -> None:
    try:
        context.execute_hook.record_implementation_choice(
            question="", chosen_option="X"
        )
        context._dexh_err = None
    except ValidationError as exc:
        context._dexh_err = exc


@when('dexe record tool_inv q="{q}" tool_write="{c}" expect_error=True')
def w_dexe_err_tool(context: Context, q: str, c: str) -> None:
    try:
        context.execute_hook.record_tool_invocation(question=q, chosen_option=c)
        context._dexh_err = None
    except ValidationError as exc:
        context._dexh_err = exc


# Hardcoded for the empty-string case (behave's ``{c}`` placeholder needs >=1 char).
@when('dexe record tool_inv q="Q" tool_write="" expect_error=True')
def w_dexe_err_tool_empty_c(context: Context) -> None:
    try:
        context.execute_hook.record_tool_invocation(question="Q", chosen_option="")
        context._dexh_err = None
    except ValidationError as exc:
        context._dexh_err = exc


@when("dexe construct hook with empty plan_id")
def w_dexe_ctor_empty(context: Context) -> None:
    try:
        ExecutePhaseDecisionHook(decision_service=context.dsvc, plan_id="")
        context._dexh_err = None
    except ValidationError as exc:
        context._dexh_err = exc


@when("dexe construct hook with whitespace plan_id")
def w_dexe_ctor_ws(context: Context) -> None:
    try:
        ExecutePhaseDecisionHook(decision_service=context.dsvc, plan_id="   ")
        context._dexh_err = None
    except ValidationError as exc:
        context._dexh_err = exc


# ---------------------------------------------------------------------------
# Then (unique step texts)
# ---------------------------------------------------------------------------


@then('dexe decision recorded successfully type="{dtype}"')
def t_dexe_type(context: Context, dtype: str) -> None:
    assert context._dexh_res is not None
    actual = context._dexh_res.decision_type
    # Compare against either the enum-value or the str() form
    actual_str = getattr(actual, "value", str(actual))
    assert actual_str == dtype or str(actual) == dtype, (
        f"Expected {dtype!r}, got {actual!r}"
    )


@then('dexe decision recorded successfully phase="{phase}"')
def t_dexe_phase(context: Context, phase: str) -> None:
    assert context._dexh_res is not None
    # Phase is set at record_time via plan_phase kwarg
    assert phase == "execute"


@then("dexe decision recorded successfully")
def t_dexe_success(context: Context) -> None:
    assert context._dexh_res is not None


@then("dexe decision recorded successfully alternatives_count={n}")
def t_dexe_alts(context: Context, n: str) -> None:
    assert context._dexh_res is not None
    assert len(context._dexh_res.alternatives_considered) == int(n)


@then("dexe decision recorded successfully confidence_score={conf}")
def t_dexe_conf(context: Context, conf: str) -> None:
    assert context._dexh_res is not None
    assert context._dexh_res.confidence_score == float(conf)


@then("dexe decision recorded successfully snapshot_hash_not_empty=True")
def t_dexe_hash(context: Context) -> None:
    assert context._dexh_res is not None
    snap = context._dexh_res.context_snapshot
    assert snap.hot_context_hash != "", "hot_context_hash must not be empty"


@then('dexe validation error raised mentions="{text}"')
def t_dexe_err(context: Context, text: str) -> None:
    assert context._dexh_err is not None
    assert isinstance(context._dexh_err, ValidationError), (
        f"Expected ValidationError, got {type(context._dexh_err).__name__}"
    )
    assert text in str(context._dexh_err), f"'{text}' not found: {context._dexh_err!s}"
