"""Deterministic sandbox coding task workbench for Gate 3."""

from __future__ import annotations

import re
from dataclasses import dataclass, field, replace
from pathlib import Path
from uuid import uuid4

from .bytecode import CognitiveOp, Instruction
from .skills import Skill, SkillGraph
from .tools import SandboxToolRunner
from .trace import Episode, ToolCallTrace, TraceRecorder


GATE3_FAMILIES = [
    "missing_or_wrong_import",
    "wrong_function_name_or_symbol",
    "bad_default_config_value",
    "simple_data_shape_mismatch",
]

ORACLE_LEAK_KEYS = {
    "actual_symbol",
    "bad_limit",
    "defect_kind",
    "expected_file_name",
    "expected_limit",
    "expected_repair",
    "expected_shape",
    "expected_symbol",
    "family",
    "good_content",
    "helper_symbol",
    "hidden_verifier",
    "new_text",
    "nonmatching_for",
    "old_text",
    "target_file",
    "task_family",
    "variant",
    "verifier_commands",
}


def oracle_leak_keys(value: object, prefix: str = "") -> list[str]:
    leaks: list[str] = []
    if isinstance(value, dict):
        for key, item in value.items():
            key_text = str(key)
            path = f"{prefix}.{key_text}" if prefix else key_text
            if key_text.lower() in ORACLE_LEAK_KEYS:
                leaks.append(path)
            leaks.extend(oracle_leak_keys(item, path))
    elif isinstance(value, list):
        for index, item in enumerate(value):
            leaks.extend(oracle_leak_keys(item, f"{prefix}[{index}]"))
    return leaks


def assert_no_oracle_leak(value: object) -> None:
    leaks = oracle_leak_keys(value)
    if leaks:
        raise ValueError(f"agent-visible payload contains evaluator oracle fields: {', '.join(leaks)}")


@dataclass(slots=True)
class TaskVariant:
    family: str
    variant_id: str
    split: str
    defect_kind: str
    package: str
    target_file: str
    old_text: str
    new_text: str
    good_content: str
    verifier_commands: list[list[str]]
    negative: bool = False
    metadata: dict[str, str] = field(default_factory=dict)

    def to_dict(self) -> dict[str, object]:
        return {
            "family": self.family,
            "variant_id": self.variant_id,
            "split": self.split,
            "defect_kind": self.defect_kind,
            "package": self.package,
            "target_file": self.target_file,
            "old_text": self.old_text,
            "new_text": self.new_text,
            "good_content": self.good_content,
            "verifier_commands": self.verifier_commands,
            "negative": self.negative,
            "metadata": self.metadata,
        }

    @classmethod
    def from_dict(cls, data: dict[str, object]) -> "TaskVariant":
        return cls(
            family=str(data["family"]),
            variant_id=str(data["variant_id"]),
            split=str(data["split"]),
            defect_kind=str(data["defect_kind"]),
            package=str(data["package"]),
            target_file=str(data["target_file"]),
            old_text=str(data.get("old_text", "")),
            new_text=str(data.get("new_text", "")),
            good_content=str(data.get("good_content", "")),
            verifier_commands=[list(item) for item in data.get("verifier_commands", [])],
            negative=bool(data.get("negative", False)),
            metadata=dict(data.get("metadata") or {}),
        )


@dataclass(slots=True)
class GeneratedTask:
    root: Path
    variant: TaskVariant

    def to_dict(self) -> dict[str, object]:
        return {"root": str(self.root), "variant": self.variant.to_dict()}

    def agent_view(self) -> "AgentTaskView":
        view = AgentTaskView(
            sandbox_path=self.root,
            description="A small local Python project is failing verification. Repair it using only the allowed sandbox tools.",
            visible_task_id="sandbox-task",
        )
        view.assert_no_oracle_leak()
        return view


@dataclass(slots=True)
class AgentTaskView:
    sandbox_path: Path
    description: str
    visible_task_id: str
    allowed_tools: list[str] = field(
        default_factory=lambda: [
            "list_files",
            "read_file",
            "write_file",
            "patch_file",
            "python -m compileall src tests",
            "python -m pytest -q",
        ]
    )
    visible_context: dict[str, str] = field(default_factory=dict)

    def to_dict(self) -> dict[str, object]:
        return {
            "sandbox_path": str(self.sandbox_path),
            "description": self.description,
            "visible_task_id": self.visible_task_id,
            "allowed_tools": self.allowed_tools,
            "visible_context": self.visible_context,
        }

    @classmethod
    def from_dict(cls, data: dict[str, object]) -> "AgentTaskView":
        return cls(
            sandbox_path=Path(str(data["sandbox_path"])),
            description=str(data["description"]),
            visible_task_id=str(data["visible_task_id"]),
            allowed_tools=[str(item) for item in data.get("allowed_tools", [])],
            visible_context={str(key): str(value) for key, value in dict(data.get("visible_context") or {}).items()},
        )

    def assert_no_oracle_leak(self) -> None:
        assert_no_oracle_leak(self.to_dict())


@dataclass(slots=True)
class VisibleProjectObservation:
    files: list[str]
    read_files: dict[str, str]
    compileall: dict[str, object]
    pytest: dict[str, object]
    symptoms: list[str] = field(default_factory=list)

    @property
    def text(self) -> str:
        return "\n".join(
            [
                *self.files,
                *self.read_files.values(),
                str(self.compileall.get("stdout_tail", "")),
                str(self.compileall.get("stderr_tail", "")),
                str(self.pytest.get("stdout_tail", "")),
                str(self.pytest.get("stderr_tail", "")),
            ]
        )

    @property
    def pytest_passed(self) -> bool:
        return bool(self.pytest.get("success", False))

    @property
    def compileall_passed(self) -> bool:
        return bool(self.compileall.get("success", False))

    def to_dict(self) -> dict[str, object]:
        payload = {
            "files": self.files,
            "read_files": self.read_files,
            "compileall": self.compileall,
            "pytest": self.pytest,
            "symptoms": self.symptoms,
        }
        assert_no_oracle_leak(payload)
        return payload

    @classmethod
    def from_dict(cls, data: dict[str, object]) -> "VisibleProjectObservation":
        return cls(
            files=[str(item) for item in data.get("files", [])],
            read_files={str(key): str(value) for key, value in dict(data.get("read_files") or {}).items()},
            compileall=dict(data.get("compileall") or {}),
            pytest=dict(data.get("pytest") or {}),
            symptoms=[str(item) for item in data.get("symptoms", [])],
        )


@dataclass(slots=True)
class RepairResult:
    task: GeneratedTask
    episode: Episode
    success: bool
    step_count: int
    tool_calls: list[ToolCallTrace]
    reused_skill: bool = False
    fallback_used: bool = False
    triggered: bool = False
    quarantined: bool = False

    def to_dict(self) -> dict[str, object]:
        return {
            "task": self.task.to_dict(),
            "episode": self.episode.to_dict(),
            "success": self.success,
            "step_count": self.step_count,
            "tool_calls": [call.to_dict() for call in self.tool_calls],
            "reused_skill": self.reused_skill,
            "fallback_used": self.fallback_used,
            "triggered": self.triggered,
            "quarantined": self.quarantined,
        }


class SandboxWorkbench:
    def __init__(self, state_dir: str | Path, run_id: str | None = None, timeout_seconds: float = 120.0) -> None:
        self.state_dir = Path(state_dir)
        self.run_id = run_id or uuid4().hex
        self.root = self.state_dir / "runs" / self.run_id
        self.root.mkdir(parents=True, exist_ok=True)
        self.timeout_seconds = timeout_seconds
        self._generated_count = 0

    def variants_for_family(self, family: str) -> list[TaskVariant]:
        variants = []
        for split, count in [("train", 3), ("heldout", 3)]:
            for index in range(1, count + 1):
                variants.append(self.make_variant(family, f"{split}_{index}", split, index))
        variants.append(self.make_negative_variant(family))
        return variants

    def gate3b_variants_for_family(self, family: str) -> list[TaskVariant]:
        variants: list[TaskVariant] = []
        for split, count in [("train", 3), ("heldout", 3), ("adversarial", 1)]:
            for index in range(1, count + 1):
                variants.append(_gate3b_variant(family, f"{split}_{index}", split, index))
        variants.append(_gate3b_false_trigger_variant(family))
        return variants

    def generate_task(self, variant: TaskVariant, phase: str) -> GeneratedTask:
        task_root = self.root / phase / variant.family / variant.variant_id
        return self._write_generated_task(task_root, variant)

    def generate_visible_task(self, variant: TaskVariant, phase: str) -> GeneratedTask:
        self._generated_count += 1
        task_root = self.root / "visible" / f"case_{self._generated_count:04d}"
        return self._write_generated_task(task_root, variant)

    def _write_generated_task(self, task_root: Path, variant: TaskVariant) -> GeneratedTask:
        src_dir = task_root / "src" / variant.package
        tests_dir = task_root / "tests"
        src_dir.mkdir(parents=True, exist_ok=True)
        tests_dir.mkdir(parents=True, exist_ok=True)
        (src_dir / "__init__.py").write_text("", encoding="utf-8")

        files = _project_files(variant)
        for relative_path, content in files.items():
            target = task_root / relative_path
            target.parent.mkdir(parents=True, exist_ok=True)
            target.write_text(content, encoding="utf-8")

        return GeneratedTask(root=task_root, variant=variant)

    def run_initial_verifier(self, task: GeneratedTask) -> tuple[bool, list[ToolCallTrace]]:
        runner = SandboxToolRunner(task.root, timeout_seconds=self.timeout_seconds)
        compile_result = runner.run_compileall()
        pytest_result = runner.run_pytest()
        return compile_result.success and pytest_result.success, list(runner.calls)

    def run_baseline_repair(self, task: GeneratedTask, store_metadata: dict[str, object] | None = None) -> RepairResult:
        runner = SandboxToolRunner(task.root, timeout_seconds=self.timeout_seconds)
        recorder = TraceRecorder(_task_label(task.variant))

        self._record_simple(recorder, CognitiveOp.PARSE_INPUT, "Parsed grounded workbench task.", task)
        self._record_simple(recorder, CognitiveOp.CLASSIFY_TASK, f"Classified family {task.variant.family}.", task)
        list_result = runner.list_files(".")
        self._record_tool(recorder, CognitiveOp.CALL_TOOL, list_result.trace, task)
        read_result = runner.read_file(task.variant.target_file)
        self._record_tool(recorder, CognitiveOp.CALL_TOOL, read_result.trace, task)
        patch_result = runner.patch_file(task.variant.target_file, task.variant.old_text, task.variant.new_text)
        if not patch_result.success:
            patch_result = runner.write_file(task.variant.target_file, task.variant.good_content)
        self._record_tool(recorder, CognitiveOp.PATCH, patch_result.trace, task)
        success = self._record_verifier(recorder, runner, task)
        self._record_answer(recorder, task, success, reused_skill=False, fallback_used=False)
        return self._to_result(task, recorder, runner, success, reused_skill=False, fallback_used=False, metadata=store_metadata)

    def observe_visible_project(self, view: AgentTaskView, runner: SandboxToolRunner) -> VisibleProjectObservation:
        view.assert_no_oracle_leak()
        list_result = runner.list_files(".")
        files = list_result.files or []
        read_files: dict[str, str] = {}
        for relative_path in files:
            if _is_visible_python_file(relative_path):
                read_result = runner.read_file(relative_path)
                if read_result.success and read_result.content is not None:
                    read_files[relative_path] = read_result.content
        compile_result = runner.run_compileall()
        pytest_result = runner.run_pytest()
        observation = VisibleProjectObservation(
            files=files,
            read_files=read_files,
            compileall=compile_result.trace.to_dict(),
            pytest=pytest_result.trace.to_dict(),
            symptoms=_visible_symptoms(compile_result.trace.to_dict(), pytest_result.trace.to_dict()),
        )
        observation.to_dict()
        return observation

    def run_visible_baseline_repair(self, task: GeneratedTask) -> RepairResult:
        runner = SandboxToolRunner(task.root, timeout_seconds=self.timeout_seconds)
        recorder = TraceRecorder("repair sandbox python project")
        view = task.agent_view()
        self._record_visible_simple(recorder, CognitiveOp.PARSE_INPUT, "Parsed visible sandbox task.", view)
        observation = self.observe_visible_project(view, runner)
        self._record_visible_observation(recorder, observation, runner.calls, task)
        self._record_visible_simple(recorder, CognitiveOp.CLASSIFY_TASK, "Classified visible verifier symptoms.", view)
        self._record_visible_simple(recorder, CognitiveOp.CREATE_HYPOTHESIS, "Formed a visible source-repair hypothesis.", view)
        action = _infer_visible_repair(observation)
        patch_success = False
        if action is not None:
            patch_result = runner.patch_file(action.path, action.old_text, action.new_text)
            patch_success = patch_result.success
            self._record_visible_tool(recorder, CognitiveOp.PATCH, patch_result.trace, task, {"strategy": action.strategy})
        else:
            self._record_visible_simple(
                recorder,
                CognitiveOp.PATCH,
                "No visible safe source patch was inferred.",
                view,
                {"patch_inferred": False},
            )
        success = patch_success and self._record_visible_verifier(recorder, runner, task)
        self._record_visible_answer(recorder, task, success, reused_skill=False, fallback_used=False)
        return self._to_visible_result(task, recorder, runner, success, reused_skill=False, fallback_used=False)

    def run_skill_repair(
        self,
        task: GeneratedTask,
        skill: Skill,
        skill_graph: SkillGraph | None = None,
        allow_fallback: bool = False,
    ) -> RepairResult:
        runner = SandboxToolRunner(task.root, timeout_seconds=self.timeout_seconds)
        recorder = TraceRecorder(_task_label(task.variant))
        triggered = _skill_matches_variant(skill, task.variant)

        self._record_simple(recorder, CognitiveOp.PARSE_INPUT, "Parsed grounded workbench task.", task)
        self._record_simple(
            recorder,
            CognitiveOp.RETRIEVE_SKILL,
            "Promoted skill matched task preconditions." if triggered else "Promoted skill did not match task preconditions.",
            task,
            {"skill_name": skill.name, "triggered": triggered},
        )

        if not triggered:
            success = task.variant.negative
            self._record_answer(recorder, task, success, reused_skill=False, fallback_used=False)
            return self._to_result(
                task,
                recorder,
                runner,
                success,
                reused_skill=False,
                fallback_used=False,
                triggered=False,
            )

        read_result = runner.read_file(task.variant.target_file)
        self._record_tool(recorder, CognitiveOp.CALL_TOOL, read_result.trace, task)
        old_text = _slot_value(skill, "old_text", task.variant, task.variant.old_text)
        new_text = _slot_value(skill, "new_text", task.variant, task.variant.new_text)
        patch_result = runner.patch_file(task.variant.target_file, old_text, new_text)
        self._record_tool(recorder, CognitiveOp.PATCH, patch_result.trace, task)
        success = patch_result.success and self._record_verifier(recorder, runner, task)

        fallback_used = False
        if not success and allow_fallback:
            fallback_used = True
            if skill_graph is not None:
                skill_graph.record_result(skill.name, False)
            fallback_write = runner.write_file(task.variant.target_file, task.variant.good_content)
            self._record_tool(recorder, CognitiveOp.PATCH, fallback_write.trace, task, {"fallback": True})
            success = self._record_verifier(recorder, runner, task)
        elif skill_graph is not None:
            skill_graph.record_result(skill.name, success)

        quarantined = bool(skill_graph and skill_graph.get(skill.name) and skill_graph.get(skill.name).status == "quarantined")
        self._record_answer(recorder, task, success, reused_skill=True, fallback_used=fallback_used)
        return self._to_result(
            task,
            recorder,
            runner,
            success,
            reused_skill=True,
            fallback_used=fallback_used,
            triggered=True,
            quarantined=quarantined,
        )

    def run_visible_skill_repair(
        self,
        task: GeneratedTask,
        skill: Skill,
        skill_graph: SkillGraph | None = None,
        allow_fallback: bool = False,
        expected_match: bool | None = None,
    ) -> RepairResult:
        runner = SandboxToolRunner(task.root, timeout_seconds=self.timeout_seconds)
        recorder = TraceRecorder("repair sandbox python project with promoted skill")
        view = task.agent_view()

        self._record_visible_simple(recorder, CognitiveOp.PARSE_INPUT, "Parsed visible sandbox task.", view)
        observation = self.observe_visible_project(view, runner)
        self._record_visible_observation(recorder, observation, runner.calls, task)
        skill_input = {
            "agent_task_view": view.to_dict(),
            "visible_observation": observation.to_dict(),
            "skill_name": skill.name,
            "skill_preconditions": skill.preconditions,
        }
        assert_no_oracle_leak(skill_input)
        triggered = _visible_skill_matches(skill, observation)
        self._record_visible_simple(
            recorder,
            CognitiveOp.RETRIEVE_SKILL,
            "Promoted skill matched visible preconditions." if triggered else "Promoted skill did not match visible preconditions.",
            view,
            {
                "skill_name": skill.name,
                "triggered": triggered,
                "skill_execution_input": skill_input,
            },
        )

        if not triggered:
            success = bool(task.variant.negative)
            self._record_visible_answer(recorder, task, success, reused_skill=False, fallback_used=False)
            return self._to_visible_result(
                task,
                recorder,
                runner,
                success,
                reused_skill=False,
                fallback_used=False,
                triggered=False,
            )

        if skill_graph is not None and skill.name in skill_graph.skills:
            skill_graph.record_trigger(skill.name, true_positive=bool(expected_match))

        action = _infer_visible_repair(observation, preferred_strategy=str(skill.metadata.get("repair_strategy", "")))
        patch_success = False
        if action is not None:
            patch_result = runner.patch_file(action.path, action.old_text, action.new_text)
            patch_success = patch_result.success
            self._record_visible_tool(recorder, CognitiveOp.PATCH, patch_result.trace, task, {"strategy": action.strategy})
        else:
            self._record_visible_simple(
                recorder,
                CognitiveOp.PATCH,
                "Promoted skill did not infer an executable visible patch.",
                view,
                {"patch_inferred": False},
            )
        success = patch_success and self._record_visible_verifier(recorder, runner, task)

        fallback_used = False
        if not success and allow_fallback:
            fallback_used = True
            if skill_graph is not None and skill.name in skill_graph.skills:
                skill_graph.record_result(skill.name, False, fallback_used=True)
            fallback_observation = self.observe_visible_project(view, runner)
            self._record_visible_observation(recorder, fallback_observation, runner.calls, task, fallback=True)
            fallback_action = _infer_visible_repair(fallback_observation)
            fallback_patch_success = False
            if fallback_action is not None:
                fallback_patch = runner.patch_file(fallback_action.path, fallback_action.old_text, fallback_action.new_text)
                fallback_patch_success = fallback_patch.success
                self._record_visible_tool(
                    recorder,
                    CognitiveOp.PATCH,
                    fallback_patch.trace,
                    task,
                    {"fallback": True, "strategy": fallback_action.strategy},
                )
            success = fallback_patch_success and self._record_visible_verifier(recorder, runner, task)
        elif skill_graph is not None and skill.name in skill_graph.skills:
            skill_graph.record_result(skill.name, success)

        quarantined = bool(skill_graph and skill_graph.get(skill.name) and skill_graph.get(skill.name).status == "quarantined")
        self._record_visible_answer(recorder, task, success, reused_skill=True, fallback_used=fallback_used)
        return self._to_visible_result(
            task,
            recorder,
            runner,
            success,
            reused_skill=True,
            fallback_used=fallback_used,
            triggered=True,
            quarantined=quarantined,
        )

    def make_variant(self, family: str, variant_id: str, split: str, index: int) -> TaskVariant:
        return _make_variant(family, variant_id, split, index)

    def make_negative_variant(self, family: str) -> TaskVariant:
        variant = _make_variant("missing_or_wrong_import", "negative_1", "negative", 99)
        good_content = "def render_label(value):\n    return f'label:{value}'\n"
        return replace(
            variant,
            family=f"negative_for_{family}",
            defect_kind="none",
            negative=True,
            metadata={"nonmatching_for": family},
            old_text="",
            new_text="",
            good_content=good_content,
        )

    def _record_simple(
        self,
        recorder: TraceRecorder,
        op: CognitiveOp,
        summary: str,
        task: GeneratedTask,
        data: dict[str, object] | None = None,
    ) -> None:
        recorder.record(
            Instruction(op),
            summary,
            True,
            data={**_trace_context(task), **(data or {})},
        )

    def _record_visible_simple(
        self,
        recorder: TraceRecorder,
        op: CognitiveOp,
        summary: str,
        view: AgentTaskView,
        data: dict[str, object] | None = None,
    ) -> None:
        payload = {"agent_task_view": view.to_dict(), **(data or {})}
        assert_no_oracle_leak(payload)
        recorder.record(Instruction(op), summary, True, data=payload)

    def _record_visible_observation(
        self,
        recorder: TraceRecorder,
        observation: VisibleProjectObservation,
        calls: list[ToolCallTrace],
        task: GeneratedTask,
        fallback: bool = False,
    ) -> None:
        data = {
            "visible_observation": observation.to_dict(),
            "tool_calls": [call.to_dict() for call in calls],
            "fallback": fallback,
        }
        recorder.record(
            Instruction(CognitiveOp.CALL_TOOL),
            "Observed sandbox project through allowed tools.",
            True,
            data=data,
        )

    def _record_visible_tool(
        self,
        recorder: TraceRecorder,
        op: CognitiveOp,
        tool_call: ToolCallTrace,
        task: GeneratedTask,
        extra: dict[str, object] | None = None,
    ) -> None:
        payload = {"tool_call": tool_call.to_dict(), **(extra or {})}
        recorder.record(Instruction(op), tool_call.summary, tool_call.success, data=payload)

    def _record_tool(
        self,
        recorder: TraceRecorder,
        op: CognitiveOp,
        tool_call: ToolCallTrace,
        task: GeneratedTask,
        extra: dict[str, object] | None = None,
    ) -> None:
        recorder.record(
            Instruction(op),
            tool_call.summary,
            tool_call.success,
            data={**_trace_context(task), "tool_call": tool_call.to_dict(), **(extra or {})},
        )

    def _record_verifier(self, recorder: TraceRecorder, runner: SandboxToolRunner, task: GeneratedTask) -> bool:
        compile_result = runner.run_compileall()
        pytest_result = runner.run_pytest()
        success = compile_result.success and pytest_result.success
        recorder.record(
            Instruction(CognitiveOp.VERIFY),
            "Verifier passed." if success else "Verifier failed.",
            success,
            data={
                **_trace_context(task),
                "tool_calls": [compile_result.trace.to_dict(), pytest_result.trace.to_dict()],
                "verifier_outputs": {
                    "compileall": compile_result.trace.to_dict(),
                    "pytest": pytest_result.trace.to_dict(),
                },
            },
        )
        return success

    def _record_visible_verifier(self, recorder: TraceRecorder, runner: SandboxToolRunner, task: GeneratedTask) -> bool:
        compile_result = runner.run_compileall()
        pytest_result = runner.run_pytest()
        success = compile_result.success and pytest_result.success
        payload = {
            "tool_calls": [compile_result.trace.to_dict(), pytest_result.trace.to_dict()],
            "verifier_outputs": {
                "compileall": compile_result.trace.to_dict(),
                "pytest": pytest_result.trace.to_dict(),
            },
        }
        assert_no_oracle_leak(payload)
        recorder.record(
            Instruction(CognitiveOp.VERIFY),
            "Verifier passed." if success else "Verifier failed.",
            success,
            data=payload,
        )
        return success

    def _record_answer(
        self,
        recorder: TraceRecorder,
        task: GeneratedTask,
        success: bool,
        reused_skill: bool,
        fallback_used: bool,
    ) -> None:
        recorder.record(
            Instruction(CognitiveOp.ANSWER),
            "Grounded workbench task solved." if success else "Grounded workbench task failed.",
            success,
            data={
                **_trace_context(task),
                "final_status": "success" if success else "failure",
                "promoted_skill_reused": reused_skill,
                "fallback_used": fallback_used,
            },
        )

    def _record_visible_answer(
        self,
        recorder: TraceRecorder,
        task: GeneratedTask,
        success: bool,
        reused_skill: bool,
        fallback_used: bool,
    ) -> None:
        recorder.record(
            Instruction(CognitiveOp.ANSWER),
            "Grounded visible workbench task solved." if success else "Grounded visible workbench task failed.",
            success,
            data={
                "final_status": "success" if success else "failure",
                "promoted_skill_reused": reused_skill,
                "fallback_used": fallback_used,
            },
        )

    def _to_result(
        self,
        task: GeneratedTask,
        recorder: TraceRecorder,
        runner: SandboxToolRunner,
        success: bool,
        reused_skill: bool,
        fallback_used: bool,
        triggered: bool = False,
        quarantined: bool = False,
        metadata: dict[str, object] | None = None,
    ) -> RepairResult:
        episode = recorder.to_episode(
            outcome="success" if success else "failure",
            score=1.0 if success else 0.0,
            tags=["gate3", task.variant.family, task.variant.split],
            metadata={
                **_trace_context(task),
                "gate": "gate3",
                "tool_calls": [call.to_dict() for call in runner.calls],
                "files_inspected": [
                    str(call.args.get("path"))
                    for call in runner.calls
                    if call.operation in {"list_files", "read_file"}
                ],
                "patches_applied": [
                    call.args
                    for call in runner.calls
                    if call.operation in {"patch_file", "write_file"} and call.success
                ],
                "verifier_outputs": [
                    call.to_dict()
                    for call in runner.calls
                    if call.operation == "run_command"
                ],
                "final_status": "success" if success else "failure",
                "step_count": len(recorder.steps),
                "promoted_skill_reused": reused_skill,
                "fallback_used": fallback_used,
                "skill_triggered": triggered,
                "skill_quarantined": quarantined,
                **(metadata or {}),
            },
        )
        return RepairResult(
            task=task,
            episode=episode,
            success=success,
            step_count=len(episode.steps),
            tool_calls=list(runner.calls),
            reused_skill=reused_skill,
            fallback_used=fallback_used,
            triggered=triggered,
            quarantined=quarantined,
        )

    def _to_visible_result(
        self,
        task: GeneratedTask,
        recorder: TraceRecorder,
        runner: SandboxToolRunner,
        success: bool,
        reused_skill: bool,
        fallback_used: bool,
        triggered: bool = False,
        quarantined: bool = False,
    ) -> RepairResult:
        episode = recorder.to_episode(
            outcome="success" if success else "failure",
            score=1.0 if success else 0.0,
            tags=["gate3b", task.variant.split],
            metadata={
                "gate": "gate3b",
                "tool_calls": [call.to_dict() for call in runner.calls],
                "files_inspected": [
                    str(call.args.get("path"))
                    for call in runner.calls
                    if call.operation in {"list_files", "read_file"}
                ],
                "patches_applied": [
                    call.args
                    for call in runner.calls
                    if call.operation in {"patch_file", "write_file"} and call.success
                ],
                "verifier_outputs": [
                    call.to_dict()
                    for call in runner.calls
                    if call.operation == "run_command"
                ],
                "final_status": "success" if success else "failure",
                "step_count": len(recorder.steps),
                "promoted_skill_reused": reused_skill,
                "fallback_used": fallback_used,
                "skill_triggered": triggered,
                "skill_quarantined": quarantined,
                "evaluator_metadata": {
                    "task_family": task.variant.family,
                    "variant_id": task.variant.variant_id,
                    "split": task.variant.split,
                    "defect_kind": task.variant.defect_kind,
                    "negative": task.variant.negative,
                },
            },
        )
        return RepairResult(
            task=task,
            episode=episode,
            success=success,
            step_count=len(episode.steps),
            tool_calls=list(runner.calls),
            reused_skill=reused_skill,
            fallback_used=fallback_used,
            triggered=triggered,
            quarantined=quarantined,
        )


def default_gate3_families() -> list[str]:
    return list(GATE3_FAMILIES)


@dataclass(slots=True)
class _VisibleRepairAction:
    path: str
    old_text: str
    new_text: str
    strategy: str


def _task_label(variant: TaskVariant) -> str:
    return f"fix {variant.family} {variant.variant_id}"


def _trace_context(task: GeneratedTask) -> dict[str, object]:
    return {
        "task_family": task.variant.family,
        "variant_id": task.variant.variant_id,
        "split": task.variant.split,
        "defect_kind": task.variant.defect_kind,
        "sandbox_root": str(task.root),
        **task.variant.metadata,
    }


def _skill_matches_variant(skill: Skill, variant: TaskVariant) -> bool:
    family = skill.preconditions.get("family") or skill.family
    defect_kind = skill.preconditions.get("defect_kind")
    if family is not None and str(family) != variant.family:
        return False
    if defect_kind is not None and str(defect_kind) != variant.defect_kind:
        return False
    return skill.status in {"candidate", "promoted"} and not variant.negative


def _slot_value(skill: Skill, slot_name: str, variant: TaskVariant, default: str) -> str:
    value = skill.parameter_slots.get(slot_name)
    if value is None or value in {slot_name, f"variant.{slot_name}"}:
        return default
    return str(value)


def _is_visible_python_file(relative_path: str) -> bool:
    return relative_path.endswith(".py") and (relative_path.startswith("src/") or relative_path.startswith("tests/"))


def _visible_symptoms(compileall: dict[str, object], pytest: dict[str, object]) -> list[str]:
    text = "\n".join(
        [
            str(compileall.get("summary", "")),
            str(compileall.get("stdout_tail", "")),
            str(compileall.get("stderr_tail", "")),
            str(pytest.get("summary", "")),
            str(pytest.get("stdout_tail", "")),
            str(pytest.get("stderr_tail", "")),
        ]
    )
    symptoms: list[str] = []
    for marker in ["ImportError", "ModuleNotFoundError", "NameError", "AssertionError", "FAILED"]:
        if marker in text:
            symptoms.append(marker)
    return symptoms


def _visible_skill_matches(skill: Skill, observation: VisibleProjectObservation) -> bool:
    if skill.status not in {"candidate", "promoted"}:
        return False
    if skill.status in {"quarantined", "deprecated"}:
        return False
    preconditions = skill.preconditions or {}
    visible_text = observation.text
    for required in preconditions.get("contains_all", []):
        if str(required) not in visible_text:
            return False
    for forbidden in preconditions.get("contains_none", []):
        if str(forbidden) in visible_text:
            return False
    strategy = str(skill.metadata.get("repair_strategy", ""))
    return _infer_visible_repair(observation, preferred_strategy=strategy) is not None


def _infer_visible_repair(
    observation: VisibleProjectObservation,
    preferred_strategy: str = "",
) -> _VisibleRepairAction | None:
    strategies = [preferred_strategy] if preferred_strategy else []
    strategies.extend(["relative_import_symbol", "default_config_value", "shape_mismatch", "absolute_import_layout"])
    seen: set[str] = set()
    for strategy in strategies:
        if not strategy or strategy in seen:
            continue
        seen.add(strategy)
        if strategy == "relative_import_symbol":
            action = _infer_relative_import_symbol_fix(observation)
        elif strategy == "default_config_value":
            action = _infer_default_config_fix(observation)
        elif strategy == "shape_mismatch":
            action = _infer_shape_fix(observation)
        elif strategy == "absolute_import_layout":
            action = _infer_absolute_import_layout_fix(observation)
        elif strategy == "no_op_failure":
            action = _infer_noop_failure(observation)
        else:
            action = None
        if action is not None:
            return action
    return None


def _infer_relative_import_symbol_fix(observation: VisibleProjectObservation) -> _VisibleRepairAction | None:
    for path, content in _source_files(observation).items():
        source_dir = str(Path(path).parent)
        for match in re.finditer(r"from \.([A-Za-z_][A-Za-z0-9_]*) import ([A-Za-z0-9_, ]+)", content):
            module = match.group(1)
            module_path = f"{source_dir}/{module}.py"
            module_content = observation.read_files.get(module_path)
            if module_content is None:
                continue
            definitions = _defined_functions(module_content)
            imported_symbols = [item.strip() for item in match.group(2).split(",") if item.strip()]
            for symbol in imported_symbols:
                if symbol in definitions:
                    continue
                replacement = _best_symbol(symbol, definitions)
                if replacement:
                    return _VisibleRepairAction(path=path, old_text=symbol, new_text=replacement, strategy="relative_import_symbol")
    return None


def _infer_absolute_import_layout_fix(observation: VisibleProjectObservation) -> _VisibleRepairAction | None:
    for path, content in _source_files(observation).items():
        source_dir = str(Path(path).parent)
        for match in re.finditer(r"from ([A-Za-z_][A-Za-z0-9_]*) import ([A-Za-z_][A-Za-z0-9_]*)", content):
            module = match.group(1)
            if match.group(0).startswith("from ."):
                continue
            if f"{source_dir}/{module}.py" in observation.read_files:
                old_text = match.group(0)
                new_text = f"from .{module} import {match.group(2)}"
                return _VisibleRepairAction(path=path, old_text=old_text, new_text=new_text, strategy="absolute_import_layout")
    return None


def _infer_default_config_fix(observation: VisibleProjectObservation) -> _VisibleRepairAction | None:
    expected_values = _expected_ints_from_tests(observation)
    if len(expected_values) != 1:
        return None
    expected = next(iter(expected_values))
    for path, content in _source_files(observation).items():
        match = re.search(r"^DEFAULT_LIMIT\s*=\s*(\d+)\s*$", content, flags=re.MULTILINE)
        if match and int(match.group(1)) != expected:
            return _VisibleRepairAction(
                path=path,
                old_text=match.group(0),
                new_text=f"DEFAULT_LIMIT = {expected}",
                strategy="default_config_value",
            )
    return None


def _infer_shape_fix(observation: VisibleProjectObservation) -> _VisibleRepairAction | None:
    tests_text = "\n".join(_test_files(observation).values())
    if "'name'" not in tests_text or "'score'" not in tests_text:
        return None
    for path, content in _source_files(observation).items():
        if "return (name, score)" in content:
            return _VisibleRepairAction(
                path=path,
                old_text="    return (name, score)\n",
                new_text='    return {"name": name, "score": score}\n',
                strategy="shape_mismatch",
            )
    return None


def _infer_noop_failure(observation: VisibleProjectObservation) -> _VisibleRepairAction | None:
    for path in _source_files(observation):
        return _VisibleRepairAction(path=path, old_text="__not_present__", new_text="__still_wrong__", strategy="no_op_failure")
    return None


def _source_files(observation: VisibleProjectObservation) -> dict[str, str]:
    return {
        path: content
        for path, content in observation.read_files.items()
        if path.startswith("src/") and path.endswith(".py")
    }


def _test_files(observation: VisibleProjectObservation) -> dict[str, str]:
    return {
        path: content
        for path, content in observation.read_files.items()
        if path.startswith("tests/") and path.endswith(".py")
    }


def _defined_functions(content: str) -> list[str]:
    return re.findall(r"^def\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(", content, flags=re.MULTILINE)


def _best_symbol(symbol: str, definitions: list[str]) -> str | None:
    if not definitions:
        return None
    if symbol.startswith("missing_") and symbol.removeprefix("missing_") in definitions:
        return symbol.removeprefix("missing_")
    suffix_matches = [definition for definition in definitions if symbol.endswith(definition) or definition.endswith(symbol)]
    if suffix_matches:
        return sorted(suffix_matches, key=len, reverse=True)[0]
    symbol_tokens = set(symbol.split("_"))
    ranked = sorted(
        definitions,
        key=lambda definition: (len(symbol_tokens & set(definition.split("_"))), len(definition)),
        reverse=True,
    )
    return ranked[0] if symbol_tokens & set(ranked[0].split("_")) else None


def _expected_ints_from_tests(observation: VisibleProjectObservation) -> set[int]:
    expected: set[int] = set()
    for content in _test_files(observation).values():
        for match in re.finditer(r"assert\s+[A-Za-z_][A-Za-z0-9_]*\(\)\s*==\s*(\d+)", content):
            expected.add(int(match.group(1)))
    return expected


def _gate3b_variant(family: str, variant_id: str, split: str, index: int) -> TaskVariant:
    variant = _make_variant(family, variant_id, split, index + (20 if split == "adversarial" else 0))
    slot = {"train": index, "heldout": index + 10, "adversarial": index + 20}.get(split, index + 30)
    package = f"app_{slot:02d}"
    module = _gate3b_module_name(family, split)
    target_file = f"src/{package}/{module}.py"
    return replace(
        variant,
        package=package,
        target_file=target_file,
        metadata={
            **variant.metadata,
            "layout_profile": "flat" if split == "train" else "alternate",
            "task_wording": f"local repair case {slot}",
        },
    )


def _gate3b_module_name(family: str, split: str) -> str:
    if split == "train":
        return "main" if family != "bad_default_config_value" else "config"
    names = {
        "missing_or_wrong_import": "labels_view",
        "wrong_function_name_or_symbol": "pipeline",
        "bad_default_config_value": "settings",
        "simple_data_shape_mismatch": "records",
    }
    if split == "adversarial":
        adversarial = {
            "missing_or_wrong_import": "rendering",
            "wrong_function_name_or_symbol": "cleanup_flow",
            "bad_default_config_value": "limits",
            "simple_data_shape_mismatch": "payloads",
        }
        return adversarial[family]
    return names[family]


def _gate3b_false_trigger_variant(family: str) -> TaskVariant:
    package = "app_31"
    target_file = "src/app_31/main.py"
    return TaskVariant(
        family=family,
        variant_id="negative_1",
        split="negative",
        defect_kind="false_trigger_scenario",
        package=package,
        target_file=target_file,
        old_text="",
        new_text="",
        good_content="",
        verifier_commands=[["python", "-m", "compileall", "src", "tests"], ["python", "-m", "pytest", "-q"]],
        negative=True,
        metadata={"gate3b_false_trigger": _false_trigger_scenario_for_family(family)},
    )


def _false_trigger_scenario_for_family(family: str) -> str:
    return {
        "missing_or_wrong_import": "package_layout_import_error",
        "wrong_function_name_or_symbol": "wrong_symbol_inside_test",
        "bad_default_config_value": "conflicting_config_expectations",
        "simple_data_shape_mismatch": "test_data_shape_mismatch",
    }[family]


def _make_variant(family: str, variant_id: str, split: str, index: int) -> TaskVariant:
    safe_suffix = variant_id.replace("-", "_")
    package = f"pkg_{family}_{safe_suffix}".replace("__", "_")
    verifier_commands = [["python", "-m", "compileall", "src", "tests"], ["python", "-m", "pytest", "-q"]]

    if family == "missing_or_wrong_import":
        symbol = f"format_label_{index}"
        bad = f"missing_{symbol}"
        target_file = f"src/{package}/main.py"
        good_content = (
            f"from .helpers import {symbol}\n\n"
            "def render_label(value):\n"
            f"    return {symbol}(value)\n"
        )
        return TaskVariant(
            family=family,
            variant_id=variant_id,
            split=split,
            defect_kind="wrong_import_symbol",
            package=package,
            target_file=target_file,
            old_text=bad,
            new_text=symbol,
            good_content=good_content,
            verifier_commands=verifier_commands,
            metadata={"helper_symbol": symbol},
        )

    if family == "wrong_function_name_or_symbol":
        actual = f"normalize_{index}"
        wrong = f"norm_{index}"
        target_file = f"src/{package}/main.py"
        good_content = (
            f"from .ops import transform, {actual}\n\n"
            "def clean(value):\n"
            f"    return transform(value, {actual})\n"
        )
        return TaskVariant(
            family=family,
            variant_id=variant_id,
            split=split,
            defect_kind="wrong_symbol_reference",
            package=package,
            target_file=target_file,
            old_text=wrong,
            new_text=actual,
            good_content=good_content,
            verifier_commands=verifier_commands,
            metadata={"actual_symbol": actual},
        )

    if family == "bad_default_config_value":
        good_limit = 5 + index
        bad_limit = good_limit - 2
        target_file = f"src/{package}/config.py"
        good_content = f"DEFAULT_LIMIT = {good_limit}\n"
        return TaskVariant(
            family=family,
            variant_id=variant_id,
            split=split,
            defect_kind="bad_default_value",
            package=package,
            target_file=target_file,
            old_text=f"DEFAULT_LIMIT = {bad_limit}",
            new_text=f"DEFAULT_LIMIT = {good_limit}",
            good_content=good_content,
            verifier_commands=verifier_commands,
            metadata={"expected_limit": str(good_limit), "bad_limit": str(bad_limit)},
        )

    if family == "simple_data_shape_mismatch":
        target_file = f"src/{package}/main.py"
        old = "    return (name, score)\n"
        new = "    return {\"name\": name, \"score\": score}\n"
        good_content = "def build_record(name, score):\n" + new
        return TaskVariant(
            family=family,
            variant_id=variant_id,
            split=split,
            defect_kind="shape_mismatch",
            package=package,
            target_file=target_file,
            old_text=old,
            new_text=new,
            good_content=good_content,
            verifier_commands=verifier_commands,
            metadata={"expected_shape": "dict"},
        )

    raise ValueError(f"unknown Gate 3 task family: {family}")


def _project_files(variant: TaskVariant) -> dict[str, str]:
    package_dir = f"src/{variant.package}"
    tests_dir = "tests"
    target_module = Path(variant.target_file).stem

    false_trigger = variant.metadata.get("gate3b_false_trigger")
    if false_trigger:
        return _false_trigger_project_files(variant, str(false_trigger))

    if variant.negative:
        return {
            f"{package_dir}/__init__.py": "",
            variant.target_file: variant.good_content,
            f"{tests_dir}/test_behavior.py": (
                f"from {variant.package}.main import render_label\n\n"
                "def test_negative_project_is_already_valid():\n"
                "    assert render_label('ok') == 'label:ok'\n"
            ),
        }

    if variant.family == "missing_or_wrong_import":
        symbol = variant.metadata["helper_symbol"]
        bad = variant.old_text
        return {
            f"{package_dir}/__init__.py": "",
            f"{package_dir}/helpers.py": f"def {symbol}(value):\n    return f'label:{{value}}'\n",
            variant.target_file: variant.good_content.replace(symbol, bad),
            f"{tests_dir}/test_behavior.py": (
                f"from {variant.package}.{target_module} import render_label\n\n"
                "def test_render_label():\n"
                "    assert render_label('alpha') == 'label:alpha'\n"
            ),
        }

    if variant.family == "wrong_function_name_or_symbol":
        actual = variant.metadata["actual_symbol"]
        wrong = variant.old_text
        ops = (
            f"def {actual}(value):\n"
            "    return value.strip().lower()\n\n"
            "def transform(value, callback):\n"
            "    return callback(value)\n"
        )
        return {
            f"{package_dir}/__init__.py": "",
            f"{package_dir}/ops.py": ops,
            variant.target_file: variant.good_content.replace(actual, wrong),
            f"{tests_dir}/test_behavior.py": (
                f"from {variant.package}.{target_module} import clean\n\n"
                "def test_clean():\n"
                "    assert clean('  MIXED  ') == 'mixed'\n"
            ),
        }

    if variant.family == "bad_default_config_value":
        config_module = Path(variant.target_file).stem
        return {
            f"{package_dir}/__init__.py": "",
            variant.target_file: variant.good_content.replace(variant.new_text, variant.old_text),
            f"{package_dir}/main.py": f"from .{config_module} import DEFAULT_LIMIT\n\ndef limit():\n    return DEFAULT_LIMIT\n",
            f"{tests_dir}/test_behavior.py": (
                f"from {variant.package}.main import limit\n\n"
                "def test_default_limit():\n"
                f"    assert limit() == {variant.metadata['expected_limit']}\n"
            ),
        }

    if variant.family == "simple_data_shape_mismatch":
        return {
            f"{package_dir}/__init__.py": "",
            variant.target_file: variant.good_content.replace(variant.new_text, variant.old_text),
            f"{tests_dir}/test_behavior.py": (
                f"from {variant.package}.{target_module} import build_record\n\n"
                "def test_build_record_shape():\n"
                "    assert build_record('ra', 7) == {'name': 'ra', 'score': 7}\n"
            ),
        }

    raise ValueError(f"unknown Gate 3 task family: {variant.family}")


def _false_trigger_project_files(variant: TaskVariant, scenario: str) -> dict[str, str]:
    package_dir = f"src/{variant.package}"
    tests_dir = "tests"
    if scenario == "package_layout_import_error":
        return {
            f"{package_dir}/__init__.py": "",
            f"{package_dir}/helpers.py": "def format_label(value):\n    return f'label:{value}'\n",
            f"{package_dir}/main.py": "from helpers import format_label\n\ndef render_label(value):\n    return format_label(value)\n",
            f"{tests_dir}/test_behavior.py": (
                f"from {variant.package}.main import render_label\n\n"
                "def test_render_label():\n"
                "    assert render_label('alpha') == 'label:alpha'\n"
            ),
        }
    if scenario == "wrong_symbol_inside_test":
        return {
            f"{package_dir}/__init__.py": "",
            f"{package_dir}/main.py": "def clean(value):\n    return value.strip().lower()\n",
            f"{tests_dir}/test_behavior.py": (
                f"from {variant.package}.main import cleen\n\n"
                "def test_clean():\n"
                "    assert cleen('  MIXED  ') == 'mixed'\n"
            ),
        }
    if scenario == "conflicting_config_expectations":
        return {
            f"{package_dir}/__init__.py": "",
            f"{package_dir}/config.py": "DEFAULT_LIMIT = 7\n",
            f"{package_dir}/main.py": "from .config import DEFAULT_LIMIT\n\ndef limit():\n    return DEFAULT_LIMIT\n",
            f"{tests_dir}/test_behavior.py": (
                f"from {variant.package}.main import limit\n\n"
                "def test_default_limit_one():\n"
                "    assert limit() == 7\n\n"
                "def test_default_limit_two():\n"
                "    assert limit() == 9\n"
            ),
        }
    if scenario == "test_data_shape_mismatch":
        return {
            f"{package_dir}/__init__.py": "",
            f"{package_dir}/main.py": "def build_record(name, score):\n    return {\"name\": name, \"score\": score}\n",
            f"{tests_dir}/test_behavior.py": (
                f"from {variant.package}.main import build_record\n\n"
                "def test_build_record_shape():\n"
                "    assert build_record('ra', 7) == ('ra', 7)\n"
            ),
        }
    raise ValueError(f"unknown Gate 3B false-trigger scenario: {scenario}")
