"""Shared utilities for dataset importers.

Functions for generating oracle patches, writing case files, validating
generated cases, and shared filter helpers used across importers.
"""

from __future__ import annotations

import difflib
import json
import re
import subprocess
from pathlib import Path
from typing import Any


def generate_oracle_patch(
    buggy: str,
    fixed: str,
    filename: str = "buggy.py",
) -> str:
    """Generate a unified diff (oracle.patch) from buggy to fixed code.

    Returns a git-apply-compatible unified diff string.
    """
    buggy_lines = buggy.splitlines(keepends=True)
    fixed_lines = fixed.splitlines(keepends=True)

    diff_lines = list(
        difflib.unified_diff(
            buggy_lines,
            fixed_lines,
            fromfile=f"a/{filename}",
            tofile=f"b/{filename}",
        )
    )

    if not diff_lines:
        return ""

    # Prepend git-style header for patch/git apply compatibility.
    header = f"diff --git a/{filename} b/{filename}\n"
    return header + "".join(diff_lines)


def write_case_files(
    case_dir: Path,
    *,
    buggy_code: str,
    test_code: str,
    oracle_patch: str,
    metadata: dict[str, Any],
) -> None:
    """Write all required case files to a directory.

    Creates: buggy.py, test_buggy.py, oracle.patch, case.json.
    """
    case_dir.mkdir(parents=True, exist_ok=True)

    (case_dir / "buggy.py").write_text(buggy_code, encoding="utf-8")
    (case_dir / "test_buggy.py").write_text(test_code, encoding="utf-8")
    (case_dir / "oracle.patch").write_text(oracle_patch, encoding="utf-8")
    (case_dir / "case.json").write_text(
        json.dumps(metadata, indent=2, ensure_ascii=False) + "\n",
        encoding="utf-8",
    )


def validate_generated_case(
    case_dir: Path,
    *,
    schema_only: bool = True,
) -> tuple[bool, str]:
    """Validate a generated case directory against eval contract.

    When schema_only=True, checks file existence and case.json structure only.
    When schema_only=False, also runs baseline + oracle validation (requires pytest).
    """
    required_files = ("buggy.py", "test_buggy.py", "case.json", "oracle.patch")
    for fname in required_files:
        if not (case_dir / fname).exists():
            return False, f"missing required file: {fname}"

    # Validate case.json.
    try:
        meta = json.loads((case_dir / "case.json").read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError) as e:
        return False, f"invalid case.json: {e}"

    if not isinstance(meta, dict):
        return False, "case.json is not a JSON object"

    from evals.run_eval import VALID_CATEGORIES, VALID_DIFFICULTIES, VALID_SNAPSHOT_DEPENDENCIES

    category = meta.get("category")
    if category not in VALID_CATEGORIES:
        return False, f"invalid category: {category}"

    difficulty = meta.get("difficulty")
    if difficulty not in VALID_DIFFICULTIES:
        return False, f"invalid difficulty: {difficulty}"

    snapshot_dependency = meta.get("snapshot_dependency")
    if snapshot_dependency not in VALID_SNAPSHOT_DEPENDENCIES:
        return False, f"invalid snapshot_dependency: {snapshot_dependency}"

    expected_exception = meta.get("expected_exception")
    if not isinstance(expected_exception, str) or not expected_exception:
        return False, "missing or empty expected_exception"

    # Check oracle.patch is non-empty.
    patch_text = (case_dir / "oracle.patch").read_text(encoding="utf-8")
    if not patch_text.strip():
        return False, "oracle.patch is empty"

    if not schema_only:
        # Full validation: baseline must fail, oracle must fix.
        from evals.validate_cases import validate_one_case

        python_args = meta.get("python_args", ["-m", "pytest", "-q"])
        timeout_sec = meta.get("timeout_sec", 120)
        return validate_one_case(
            case_dir, expected_exception, python_args, timeout_sec, "json_compact"
        )

    return True, "schema valid"


def count_patch_lines(patch: str) -> int:
    """Count the number of changed lines (additions + deletions) in a patch."""
    added = 0
    removed = 0
    for line in patch.splitlines():
        if line.startswith("+") and not line.startswith("+++"):
            added += 1
        elif line.startswith("-") and not line.startswith("---"):
            removed += 1
    return added + removed


# ---------------------------------------------------------------------------
# Shared filter helpers (used by multiple importers)
# ---------------------------------------------------------------------------

# Conservative allowlist of stdlib / common imports that don't require
# external packages.  Importers can extend this set.
_STDLIB_IMPORT_PATTERN = re.compile(
    r"^\s*(?:import|from)\s+"
    r"(?!__future__|collections|itertools|functools|math|re|string|typing|"
    r"dataclasses|enum|operator|copy|heapq|bisect|random|sys|os|io|json|"
    r"abc|contextlib|decimal|fractions|statistics|unittest|array|"
    r"typing_extensions|sortedcontainers|textwrap|struct|hashlib|"
    r"pathlib|datetime|time|numbers|types|inspect|warnings)"
    r"(?!\.)",
    re.MULTILINE,
)


def has_external_deps(code: str) -> bool:
    """Check if code imports anything beyond stdlib (conservative allowlist)."""
    return bool(_STDLIB_IMPORT_PATTERN.search(code))


def is_deterministic(code: str) -> bool:
    """Heuristic: no network, no real randomness, no timing."""
    risky = ["requests.", "urllib.", "http.", "socket.", "time.sleep", "random.random"]
    lower = code.lower()
    return not any(r in lower for r in risky)


def ruff_format(code: str) -> str:
    """Format Python code through ruff, returning original on failure."""
    try:
        result = subprocess.run(
            ["ruff", "format", "--stdin-filename=buggy.py"],
            input=code,
            capture_output=True,
            text=True,
            timeout=10,
        )
        if result.returncode == 0 and result.stdout.strip():
            return result.stdout
    except (FileNotFoundError, subprocess.TimeoutExpired):
        pass
    return code
