"""Step definitions for revert-mode re-execution from decision point.

Covers checkpoint restoration, actor state recovery, phase transition
signalling, user intervention decision creation, and subtree selectivity.
"""

from __future__ import annotations

import re
from typing import Any
from unittest.mock import MagicMock

from behave import given, then, when  # type: ignore[import-untyped]
from behave.runner import Context  # type: ignore[import-untyped]

from cleveragents.application.services.checkpoint_service import CheckpointService
from cleveragents.application.services.correction_service import CorrectionService
from cleveragents.domain.models.core.checkpoint import Checkpoint, CheckpointMetadata
from cleveragents.domain.models.core.correction import (
    CorrectionMode,
    CorrectionResult,
    CorrectionStatus,
)
from cleveragents.domain.models.core.decision import (
    ContextSnapshot,
    Decision,
    DecisionType,
)
from cleveragents.domain.models.core.plan import ULID_PATTERN

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


_ULID_RE = re.compile(ULID_PATTERN)


def _ensure_ulid(value: str) -> str:
    """Return ``value`` when already ULID-like, otherwise generate one."""
    if _ULID_RE.match(value):
        return value
    from ulid import ULID

    return str(ULID())


def _make_decision(
    decision_id: str,
    plan_id: str,
    actor_state_ref: str = "",
) -> Decision:
    """Create a minimal Decision for testing actor state extraction."""
    from ulid import ULID

    return Decision(
        decision_id=str(ULID()),
        plan_id=str(ULID()),
        sequence_number=0,
        decision_type=DecisionType.STRATEGY_CHOICE,
        question="Test question?",
        chosen_option="Option A",
        context_snapshot=ContextSnapshot(actor_state_ref=actor_state_ref),
    )


def _make_checkpoint(
    plan_id: str,
    decision_id: str,
    sandbox_ref: str,
) -> Checkpoint:
    """Create a Checkpoint aligned to a decision."""
    from ulid import ULID

    return Checkpoint(
        checkpoint_id=str(ULID()),
        plan_id=_ensure_ulid(plan_id),
        sandbox_ref=sandbox_ref,
        decision_id=decision_id,
        checkpoint_type="pre_write",
        metadata=CheckpointMetadata(reason="decision-aligned"),
    )


# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------


@given("revexec a correction service with a mock checkpoint service")
def step_given_correction_service_with_checkpoint(context: Context) -> None:
    mock_cp_svc = MagicMock(spec=CheckpointService)
    mock_cp_svc.list_checkpoints.return_value = []
    mock_cp_svc.rollback_to_checkpoint.return_value = MagicMock(
        restored_files_count=3,
        changed_paths=["a.py", "b.py", "c.py"],
        from_checkpoint_id="cp1",
    )
    context.revexec_checkpoint_service = mock_cp_svc
    context.revexec_service = CorrectionService(
        checkpoint_service=mock_cp_svc,
    )
    context.revexec_decisions = {}
    context.revexec_tree = {}


@given("revexec a correction service without checkpoint service")
def step_given_correction_service_no_checkpoint(context: Context) -> None:
    context.revexec_service = CorrectionService()
    context.revexec_decisions = {}
    context.revexec_tree = {}


@given("revexec a correction service with a failing checkpoint list")
def step_given_correction_service_failing_list(context: Context) -> None:
    mock_cp_svc = MagicMock(spec=CheckpointService)
    mock_cp_svc.list_checkpoints.side_effect = RuntimeError("list failed")
    context.revexec_checkpoint_service = mock_cp_svc
    context.revexec_service = CorrectionService(
        checkpoint_service=mock_cp_svc,
    )
    context.revexec_decisions = {}
    context.revexec_tree = {}


@given("revexec a correction service with a failing checkpoint rollback")
def step_given_correction_service_failing_rollback(context: Context) -> None:
    mock_cp_svc = MagicMock(spec=CheckpointService)
    cp = _make_checkpoint("P1", "D1", "ref-fail")
    mock_cp_svc.list_checkpoints.return_value = [cp]
    mock_cp_svc.rollback_to_checkpoint.side_effect = RuntimeError("rollback failed")
    context.revexec_checkpoint_service = mock_cp_svc
    context.revexec_service = CorrectionService(
        checkpoint_service=mock_cp_svc,
    )
    context.revexec_decisions = {}
    context.revexec_tree = {}


@given(
    'revexec a checkpoint exists for plan "{plan_id}" decision "{decision_id}" with ref "{ref}"'
)
def step_given_checkpoint_exists(
    context: Context, plan_id: str, decision_id: str, ref: str
) -> None:
    cp = _make_checkpoint(plan_id, decision_id, ref)
    context.revexec_checkpoint_service.list_checkpoints.return_value = [cp]


@given('revexec no checkpoint exists for plan "{plan_id}" decision "{decision_id}"')
def step_given_no_checkpoint(context: Context, plan_id: str, decision_id: str) -> None:
    context.revexec_checkpoint_service.list_checkpoints.return_value = []


@given('revexec a decision "{decision_id}" with actor_state_ref "{ref}"')
def step_given_decision_with_actor_state(
    context: Context, decision_id: str, ref: str
) -> None:
    decision = _make_decision(decision_id, "P1", actor_state_ref=ref)
    context.revexec_decisions[decision_id] = decision


@given(
    'revexec a correction request targeting decision "{decision_id}" in plan "{plan_id}" for revert'
)
def step_given_correction_request(
    context: Context, decision_id: str, plan_id: str
) -> None:
    svc: CorrectionService = context.revexec_service
    context.revexec_request = svc.request_correction(
        plan_id,
        decision_id,
        CorrectionMode.REVERT,
    )


@given(
    'revexec a correction request targeting decision "{decision_id}" in plan "{plan_id}" for revert with decisions'
)
def step_given_correction_request_with_decisions(
    context: Context, decision_id: str, plan_id: str
) -> None:
    svc: CorrectionService = context.revexec_service
    context.revexec_request = svc.request_correction(
        plan_id,
        decision_id,
        CorrectionMode.REVERT,
    )


@given(
    'revexec a correction request targeting decision "{decision_id}" in plan "{plan_id}" for revert with guidance "{guidance}"'
)
def step_given_correction_request_guidance(
    context: Context, decision_id: str, plan_id: str, guidance: str
) -> None:
    svc: CorrectionService = context.revexec_service
    context.revexec_request = svc.request_correction(
        plan_id,
        decision_id,
        CorrectionMode.REVERT,
        guidance=guidance,
    )


@given('revexec a decision tree where "{parent}" has children "{children}"')
def step_given_decision_tree_simple(
    context: Context, parent: str, children: str
) -> None:
    child_list = [c.strip() for c in children.split(",")]
    context.revexec_tree[parent] = child_list


@given(
    'revexec a two-level tree "{parent}" children "{children}" then "{parent2}" children "{children2}"'
)
def step_given_decision_tree_two_level(
    context: Context, parent: str, children: str, parent2: str, children2: str
) -> None:
    context.revexec_tree[parent] = [c.strip() for c in children.split(",")]
    context.revexec_tree[parent2] = [c.strip() for c in children2.split(",")]


# ---------------------------------------------------------------------------
# When steps
# ---------------------------------------------------------------------------


@when("revexec I execute the revert correction")
def step_when_execute_revert(context: Context) -> None:
    svc: CorrectionService = context.revexec_service
    tree: dict[str, list[str]] = getattr(context, "revexec_tree", {})
    context.revexec_result = svc.execute_revert(
        context.revexec_request.correction_id,
        decision_tree=tree if tree else None,
    )


@when("revexec I execute the revert correction with decisions")
def step_when_execute_revert_with_decisions(context: Context) -> None:
    svc: CorrectionService = context.revexec_service
    context.revexec_result = svc.execute_revert(
        context.revexec_request.correction_id,
        decisions=context.revexec_decisions,
    )


@when("revexec I execute the revert correction with decisions and tree")
def step_when_execute_revert_full(context: Context) -> None:
    svc: CorrectionService = context.revexec_service
    context.revexec_result = svc.execute_revert(
        context.revexec_request.correction_id,
        decision_tree=context.revexec_tree,
        decisions=context.revexec_decisions,
    )


@when("revexec I execute the revert correction with tree")
def step_when_execute_revert_with_tree(context: Context) -> None:
    svc: CorrectionService = context.revexec_service
    context.revexec_result = svc.execute_revert(
        context.revexec_request.correction_id,
        decision_tree=context.revexec_tree,
    )


@when("revexec I dispatch execute_correction with decisions")
def step_when_dispatch_with_decisions(context: Context) -> None:
    svc: CorrectionService = context.revexec_service
    context.revexec_result = svc.execute_correction(
        context.revexec_request.correction_id,
        decisions=context.revexec_decisions,
    )


@when("revexec I create a CorrectionResult with only correction_id and status")
def step_when_create_default_result(context: Context) -> None:
    context.revexec_result = CorrectionResult(
        correction_id="test-corr-1",
        status=CorrectionStatus.APPLIED,
    )


# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------


@then("revexec the result checkpoint_restored should be {expected}")
def step_then_checkpoint_restored(context: Context, expected: str) -> None:
    result: CorrectionResult = context.revexec_result
    expected_bool = expected.lower() == "true"
    assert result.checkpoint_restored == expected_bool, (
        f"Expected checkpoint_restored={expected_bool}, "
        f"got {result.checkpoint_restored}"
    )


@then('revexec the result status should be "{expected}"')
def step_then_result_status(context: Context, expected: str) -> None:
    result: CorrectionResult = context.revexec_result
    assert str(result.status) == expected, (
        f"Expected status={expected}, got {result.status}"
    )


@then('revexec the result actor_state_ref should be "{expected}"')
def step_then_actor_state_ref(context: Context, expected: str) -> None:
    result: CorrectionResult = context.revexec_result
    assert result.actor_state_ref == expected, (
        f"Expected actor_state_ref={expected!r}, got {result.actor_state_ref!r}"
    )


@then('revexec the result actor_state_ref should be ""')
def step_then_actor_state_ref_empty(context: Context) -> None:
    result: CorrectionResult = context.revexec_result
    assert result.actor_state_ref == "", (
        f"Expected actor_state_ref to be empty, got {result.actor_state_ref!r}"
    )


@then('revexec the result phase_transition_target should be "{expected}"')
def step_then_phase_transition(context: Context, expected: str) -> None:
    result: CorrectionResult = context.revexec_result
    assert result.phase_transition_target == expected, (
        f"Expected phase_transition_target={expected!r}, "
        f"got {result.phase_transition_target!r}"
    )


@then('revexec the result phase_transition_target should be ""')
def step_then_phase_transition_empty(context: Context) -> None:
    result: CorrectionResult = context.revexec_result
    assert result.phase_transition_target == "", (
        "Expected phase_transition_target to be empty, "
        f"got {result.phase_transition_target!r}"
    )


@then("revexec the result user_intervention_decision_id should not be empty")
def step_then_intervention_not_empty(context: Context) -> None:
    result: CorrectionResult = context.revexec_result
    assert result.user_intervention_decision_id, (
        "Expected non-empty user_intervention_decision_id"
    )


@then('revexec the result user_intervention_decision_id should be ""')
def step_then_intervention_empty(context: Context) -> None:
    result: CorrectionResult = context.revexec_result
    assert result.user_intervention_decision_id == "", (
        f"Expected empty user_intervention_decision_id, "
        f"got {result.user_intervention_decision_id!r}"
    )


@then('revexec the reverted decisions should contain "{decision_id}"')
def step_then_reverted_contains(context: Context, decision_id: str) -> None:
    result: CorrectionResult = context.revexec_result
    assert decision_id in result.reverted_decisions, (
        f"Expected {decision_id} in reverted_decisions, got {result.reverted_decisions}"
    )


@then('revexec the reverted decisions should not contain "{decision_id}"')
def step_then_reverted_not_contains(context: Context, decision_id: str) -> None:
    result: CorrectionResult = context.revexec_result
    assert decision_id not in result.reverted_decisions, (
        f"Expected {decision_id} NOT in reverted_decisions, "
        f"got {result.reverted_decisions}"
    )


@then("revexec the attempt details should contain phase_transition_target")
def step_then_attempt_has_phase(context: Context) -> None:
    svc: CorrectionService = context.revexec_service
    attempts = svc.list_attempts(context.revexec_request.correction_id)
    assert len(attempts) > 0, "Expected at least one attempt"
    details: dict[str, Any] = attempts[0].details
    assert "phase_transition_target" in details, (
        f"Expected phase_transition_target in attempt details, "
        f"got keys: {list(details.keys())}"
    )
