"""ConDefects dataset importer for llmdebug framework.

Source: GitHub ``appmlk/ConDefects``.
1,625 Python cases from AtCoder (post-Oct 2021), specifically designed to
avoid LLM training data leakage.

contamination_risk = "low" — highest value for honest benchmarking since
results are least likely to be inflated by memorization.
"""

from __future__ import annotations

import shutil
from pathlib import Path
from typing import Any

from evals.category_mapping import infer_snapshot_dependency, map_condefects_type
from evals.importers.base import (
    count_patch_lines,
    generate_oracle_patch,
    validate_generated_case,
    write_case_files,
)

# ---------------------------------------------------------------------------
# Filter criteria
# ---------------------------------------------------------------------------

MAX_FIX_LINES = 15
MAX_CODE_LINES = 200


# ---------------------------------------------------------------------------
# Test wrapping
# ---------------------------------------------------------------------------


def _generate_test(
    buggy_code: str,
    test_pairs: list[tuple[str, str]],
) -> str:
    """Generate test_buggy.py that runs buggy.py as a script with stdin/stdout.

    Uses multiple test input/output pairs for stronger validation.
    """
    lines = ["import subprocess", "import sys", "", ""]
    for i, (test_input, expected_output) in enumerate(test_pairs):
        lines.append(f"def test_condefects_{i}():")
        lines.append(f"    proc = subprocess.run(")
        lines.append(f"        [sys.executable, \"buggy.py\"],")
        lines.append(f"        input={test_input!r},")
        lines.append(f"        capture_output=True,")
        lines.append(f"        text=True,")
        lines.append(f"        timeout=10,")
        lines.append(f"    )")
        lines.append(f"    expected = {expected_output!r}")
        lines.append(f"    actual = proc.stdout.strip()")
        lines.append(f"    assert actual == expected.strip(), (")
        lines.append(f"        f\"Expected {{expected.strip()!r}}, got {{actual!r}}\"")
        lines.append(f"    )")
        lines.append("")
        lines.append("")
    return "\n".join(lines).rstrip() + "\n"


def _infer_exception(verdict: str) -> str:
    """Infer expected exception from verdict type."""
    verdict_lower = verdict.lower()
    if "runtime" in verdict_lower:
        return "RuntimeError"
    return "AssertionError"


def _classify_bug_from_patch(buggy_code: str, fixed_code: str) -> str:
    """Heuristic: classify bug type from patch content differences."""
    import difflib

    buggy_lines = buggy_code.splitlines()
    fixed_lines = fixed_code.splitlines()
    diff = list(difflib.unified_diff(buggy_lines, fixed_lines, lineterm=""))

    removed = [l[1:] for l in diff if l.startswith("-") and not l.startswith("---")]
    added = [l[1:] for l in diff if l.startswith("+") and not l.startswith("+++")]
    changed = " ".join(removed + added).lower()

    # Check for comparison/boundary changes
    if any(op in changed for op in ["<=", ">=", "< ", "> ", "==", "!="]):
        return "logic_error"
    # Check for off-by-one patterns
    if any(p in changed for p in ["+ 1", "- 1", "+1", "-1", "range("]):
        return "logic_error"
    # Check for type-related changes
    if any(p in changed for p in ["int(", "str(", "float(", "list(", "type("]):
        return "type_coercion"
    # Default
    return "logic_error"


def _estimate_difficulty(fix_lines: int, code_lines: int) -> str:
    """Estimate difficulty from fix size and code size."""
    if fix_lines <= 2 and code_lines <= 30:
        return "easy"
    if fix_lines <= 4 and code_lines <= 80:
        return "medium"
    return "hard"


# ---------------------------------------------------------------------------
# Main import function
# ---------------------------------------------------------------------------


def import_condefects(
    output_dir: Path,
    *,
    repo_path: Path | None = None,
    max_cases: int = 0,
    validate: bool = True,
    dry_run: bool = False,
) -> list[dict[str, Any]]:
    """Import cases from ConDefects repository.

    The ConDefects repo must be cloned locally (not on HuggingFace).
    Each problem has buggy/fixed Python files with test inputs/outputs.

    Args:
        output_dir: Directory to write cases to.
        repo_path: Path to cloned ConDefects repository.
        max_cases: Maximum number of cases to import.
        validate: Whether to schema-validate each case.
        dry_run: If True, preview cases without writing files.

    Returns list of imported case metadata dicts.
    """
    if repo_path is None:
        raise ValueError(
            "ConDefects requires a local clone. "
            "Clone with: git clone https://github.com/appmlk/ConDefects.git"
        )

    python_dir = repo_path / "Code" / "atcoder" / "Python"
    if not python_dir.exists():
        raise FileNotFoundError(f"ConDefects Python directory not found: {python_dir}")

    imported: list[dict[str, Any]] = []
    skipped = 0

    for problem_dir in sorted(python_dir.iterdir()):
        if max_cases > 0 and len(imported) >= max_cases:
            break
        if not problem_dir.is_dir():
            continue

        buggy_dir = problem_dir / "buggy"
        fixed_dir = (
            problem_dir / "fixed" if (problem_dir / "fixed").exists()
            else problem_dir / "correct"
        )
        test_dir = (
            problem_dir / "tests" if (problem_dir / "tests").exists()
            else problem_dir / "test"
        )

        if not buggy_dir.exists():
            continue

        for buggy_file in sorted(buggy_dir.glob("*.py")):
            if max_cases > 0 and len(imported) >= max_cases:
                break

            submission_id = buggy_file.stem
            fixed_file = fixed_dir / buggy_file.name if fixed_dir.exists() else None

            if fixed_file is None or not fixed_file.exists():
                skipped += 1
                continue

            buggy_code = buggy_file.read_text(encoding="utf-8", errors="replace")
            fixed_code = fixed_file.read_text(encoding="utf-8", errors="replace")

            if not buggy_code.strip() or not fixed_code.strip():
                skipped += 1
                continue

            oracle_patch = generate_oracle_patch(buggy_code, fixed_code)
            if not oracle_patch.strip():
                skipped += 1
                continue

            fix_lines = count_patch_lines(oracle_patch)
            if fix_lines > MAX_FIX_LINES:
                skipped += 1
                continue

            if len(buggy_code.splitlines()) > MAX_CODE_LINES:
                skipped += 1
                continue

            # Collect all test input/output pairs.
            test_pairs: list[tuple[str, str]] = []
            if test_dir.exists():
                in_files = sorted(test_dir.glob("*.in")) or sorted(test_dir.glob("*input*"))
                out_files = sorted(test_dir.glob("*.out")) or sorted(test_dir.glob("*output*"))
                # Match input/output files by name
                out_map = {f.stem: f for f in out_files}
                for in_f in in_files:
                    out_f = out_map.get(in_f.stem)
                    if out_f and out_f.exists():
                        inp = in_f.read_text(encoding="utf-8", errors="replace")
                        out = out_f.read_text(encoding="utf-8", errors="replace")
                        if inp.strip() and out.strip():
                            test_pairs.append((inp, out))
                    if len(test_pairs) >= 5:  # Cap at 5 test pairs
                        break

            if not test_pairs:
                skipped += 1
                continue

            # Classify bug type from patch content.
            category = _classify_bug_from_patch(buggy_code, fixed_code)
            snapshot_dep = infer_snapshot_dependency(category)
            expected_exception = _infer_exception("wrong answer")
            case_id = f"cd_{problem_dir.name}_{submission_id}"
            difficulty = _estimate_difficulty(fix_lines, len(buggy_code.splitlines()))

            test_code = _generate_test(buggy_code, test_pairs)

            metadata = {
                "description": f"ConDefects {problem_dir.name}/{submission_id}",
                "category": category,
                "difficulty": difficulty,
                "snapshot_dependency": snapshot_dep,
                "expected_exception": expected_exception,
                "tags": ["condefects", "atcoder"],
                "python_args": ["-m", "pytest", "-q"],
                "timeout_sec": 30,
                "bug_source": "condefects",
                "contamination_risk": "low",
                "expected_fix_lines": fix_lines,
                "source_id": f"condefects_{problem_dir.name}_{submission_id}",
            }

            case_info = {"case_id": case_id, "metadata": metadata, "fix_lines": fix_lines}

            if dry_run:
                imported.append(case_info)
                continue

            case_dir = output_dir / case_id
            write_case_files(
                case_dir,
                buggy_code=buggy_code,
                test_code=test_code,
                oracle_patch=oracle_patch,
                metadata=metadata,
            )

            if validate:
                ok, msg = validate_generated_case(case_dir, schema_only=True)
                if not ok:
                    print(f"  [skip] {case_id}: validation failed: {msg}")
                    skipped += 1
                    shutil.rmtree(case_dir, ignore_errors=True)
                    continue

            imported.append(case_info)

    return imported
