"""Transcript minimization using delta debugging.

This module implements ddmin (delta debugging) to reduce transcript size
while preserving the failure. Smaller transcripts are easier to debug
and use less storage.
"""

from __future__ import annotations

import json
import subprocess
import tempfile
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Set


def ddmin(
    transcript: List[Dict[str, Any]],
    test_fn: Callable[[List[Dict[str, Any]]], bool],
    granularity: int = 2,
) -> List[Dict[str, Any]]:
    """Delta debugging minimization algorithm.

    Finds a minimal subset of the transcript that still reproduces the failure.

    Args:
        transcript: List of transcript records to minimize
        test_fn: Function that returns True if failure still reproduces
        granularity: Initial number of subsets to divide into

    Returns:
        Minimal transcript that still reproduces the failure
    """
    n = granularity

    while len(transcript) > 1:
        subset_size = max(1, len(transcript) // n)
        subsets = []

        # Divide into n subsets
        for i in range(0, len(transcript), subset_size):
            subsets.append(transcript[i : i + subset_size])

        found_smaller = False

        # Try removing each subset (test the complement)
        for i in range(len(subsets)):
            complement = []
            for j, subset in enumerate(subsets):
                if j != i:
                    complement.extend(subset)

            if complement and test_fn(complement):
                transcript = complement
                n = max(2, n - 1)
                found_smaller = True
                break

        if not found_smaller:
            # Try each subset individually
            for subset in subsets:
                if len(subset) < len(transcript) and test_fn(subset):
                    transcript = subset
                    n = 2
                    found_smaller = True
                    break

        if not found_smaller:
            if n >= len(transcript):
                break
            n = min(n * 2, len(transcript))

    return transcript


def hierarchical_ddmin(
    transcript: List[Dict[str, Any]],
    test_fn: Callable[[List[Dict[str, Any]]], bool],
) -> List[Dict[str, Any]]:
    """Hierarchical delta debugging - minimize by type, then within types.

    This is more efficient than plain ddmin because it first removes
    entire categories of records (HTTP, SQL, etc.) before fine-grained
    minimization.

    Args:
        transcript: List of transcript records
        test_fn: Function that returns True if failure reproduces

    Returns:
        Minimal transcript
    """
    # Phase 1: Minimize HTTP records (usually largest)
    http_records = [r for r in transcript if r.get("type") == "HTTP"]
    other_records = [r for r in transcript if r.get("type") != "HTTP"]

    if http_records:
        minimal_http = ddmin(
            http_records, lambda subset: test_fn(subset + other_records)
        )
    else:
        minimal_http = []

    # Phase 2: Minimize SQL records
    current = minimal_http + other_records
    sql_records = [r for r in current if r.get("type") == "SQL"]
    other_records = [r for r in current if r.get("type") != "SQL"]

    if sql_records:
        minimal_sql = ddmin(
            sql_records, lambda subset: test_fn(minimal_http + subset + other_records)
        )
    else:
        minimal_sql = []

    # Phase 3: Minimize Redis/MongoDB/AWS records
    current = minimal_http + minimal_sql + other_records
    dep_types = {"REDIS", "MONGODB", "AWS", "GRPC"}
    dep_records = [r for r in current if r.get("type") in dep_types]
    other_records = [r for r in current if r.get("type") not in dep_types]

    if dep_records:
        minimal_deps = ddmin(
            dep_records,
            lambda subset: test_fn(minimal_http + minimal_sql + subset + other_records),
        )
    else:
        minimal_deps = []

    # Phase 4: Minimize time/random (usually small impact)
    current = minimal_http + minimal_sql + minimal_deps + other_records
    time_types = {"TIME", "RNG", "ASYNC_SLEEP"}
    time_records = [r for r in current if r.get("type") in time_types]
    other_records = [r for r in current if r.get("type") not in time_types]

    if time_records:
        minimal_time = ddmin(
            time_records,
            lambda subset: test_fn(
                minimal_http + minimal_sql + minimal_deps + subset + other_records
            ),
        )
    else:
        minimal_time = []

    # Combine all minimal sets
    result = minimal_http + minimal_sql + minimal_deps + minimal_time + other_records

    # Sort by sequence number to maintain order
    result.sort(key=lambda r: r.get("seq", 0))

    return result


def slice_by_causality(
    transcript: List[Dict[str, Any]],
    failure_seq: int,
    window_size: int = 20,
) -> List[Dict[str, Any]]:
    """Slice transcript to records causally related to failure.

    Simple heuristic: keep records within window_size of failure,
    plus any records that share keys/URLs with those records.

    Args:
        transcript: Full transcript
        failure_seq: Sequence number of failure
        window_size: Number of records before failure to keep

    Returns:
        Sliced transcript with likely relevant records
    """
    # Start with records in the window
    relevant_seqs: Set[int] = set()

    for record in transcript:
        seq = record.get("seq", 0)
        if failure_seq - window_size <= seq <= failure_seq:
            relevant_seqs.add(seq)

    # Expand to include related records
    urls_seen: Set[str] = set()
    keys_seen: Set[str] = set()
    tables_seen: Set[str] = set()

    # Collect identifiers from relevant records
    for record in transcript:
        if record.get("seq") not in relevant_seqs:
            continue

        if record.get("type") == "HTTP":
            url = record.get("url_canonical", "")
            if url:
                urls_seen.add(url.split("?")[0])  # Base URL without query

        if record.get("type") == "SQL":
            # Extract table names from query
            query = record.get("query", "")
            for word in query.upper().split():
                if word in ("FROM", "INTO", "UPDATE", "JOIN"):
                    idx = query.upper().split().index(word)
                    parts = query.split()
                    if idx + 1 < len(parts):
                        tables_seen.add(parts[idx + 1].strip("();,"))

        if record.get("type") == "REDIS":
            key = record.get("key")
            if key:
                keys_seen.add(key.split(":")[0])  # Key prefix

        if record.get("type") == "MONGODB":
            collection = record.get("collection")
            if collection:
                tables_seen.add(collection)

    # Add earlier records that touch the same resources
    for record in transcript:
        seq = record.get("seq", 0)
        if seq >= failure_seq - window_size:
            continue  # Already included

        if record.get("type") == "HTTP":
            url = record.get("url_canonical", "")
            if url and url.split("?")[0] in urls_seen:
                relevant_seqs.add(seq)

        if record.get("type") == "SQL":
            query = record.get("query", "")
            for table in tables_seen:
                if table.upper() in query.upper():
                    relevant_seqs.add(seq)
                    break

        if record.get("type") == "REDIS":
            key = record.get("key")
            if key and key.split(":")[0] in keys_seen:
                relevant_seqs.add(seq)

        if record.get("type") == "MONGODB":
            collection = record.get("collection")
            if collection and collection in tables_seen:
                relevant_seqs.add(seq)

    return [r for r in transcript if r.get("seq") in relevant_seqs]


def create_test_harness_runner(
    harness_path: Path,
    original_exception: Optional[Dict[str, Any]] = None,
) -> Callable[[List[Dict[str, Any]]], bool]:
    """Create a test function that runs the harness with a given transcript.

    Args:
        harness_path: Path to the repro harness directory
        original_exception: Expected exception to match

    Returns:
        Function that tests if a transcript reproduces the failure
    """

    def test_fn(transcript: List[Dict[str, Any]]) -> bool:
        """Test if transcript reproduces the failure."""
        # Write transcript to temp file
        with tempfile.NamedTemporaryFile(
            mode="w", suffix=".jsonl", delete=False
        ) as f:
            for record in transcript:
                f.write(json.dumps(record, sort_keys=True) + "\n")
            transcript_path = Path(f.name)

        try:
            # Build docker command
            cmd = [
                "docker",
                "run",
                "--rm",
                "--network=none",
                "-v",
                f"{transcript_path}:/repro/transcripts/merged.jsonl:ro",
                str(harness_path),
            ]

            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=60,
            )

            # Check if failure reproduced
            if result.returncode == 0:
                return False  # Test passed, failure not reproduced

            # If we have an expected exception, verify it matches
            if original_exception:
                # Parse result output for exception info
                try:
                    output = json.loads(result.stdout)
                    if output.get("exc_type") != original_exception.get("exc_type"):
                        return False
                except (json.JSONDecodeError, KeyError):
                    pass

            return True  # Failure reproduced

        except subprocess.TimeoutExpired:
            return False
        except Exception:
            return False
        finally:
            transcript_path.unlink(missing_ok=True)

    return test_fn


def minimize_transcript(
    transcript: List[Dict[str, Any]],
    harness_path: Path,
    original_exception: Optional[Dict[str, Any]] = None,
    method: str = "hierarchical",
) -> Dict[str, Any]:
    """Minimize a transcript while preserving the failure.

    Args:
        transcript: Original transcript records
        harness_path: Path to repro harness
        original_exception: Expected exception to match
        method: Minimization method ("ddmin", "hierarchical", "causal")

    Returns:
        Dict with original_count, minimal_count, reduction_pct, minimal_transcript
    """
    original_count = len(transcript)

    # Create test function
    test_fn = create_test_harness_runner(harness_path, original_exception)

    # First verify the original reproduces
    if not test_fn(transcript):
        return {
            "error": "Original transcript does not reproduce failure",
            "original_count": original_count,
            "minimal_count": original_count,
            "reduction_pct": 0.0,
            "minimal_transcript": transcript,
        }

    # Apply minimization
    if method == "ddmin":
        minimal = ddmin(transcript, test_fn)
    elif method == "hierarchical":
        minimal = hierarchical_ddmin(transcript, test_fn)
    elif method == "causal":
        # Find failure sequence
        failure_seq = max(r.get("seq", 0) for r in transcript)
        minimal = slice_by_causality(transcript, failure_seq)
        # Then apply ddmin on the slice
        if test_fn(minimal):
            minimal = ddmin(minimal, test_fn)
        else:
            # Causal slice too aggressive, fall back
            minimal = hierarchical_ddmin(transcript, test_fn)
    else:
        minimal = transcript

    minimal_count = len(minimal)
    reduction_pct = (1 - minimal_count / original_count) * 100 if original_count > 0 else 0

    return {
        "original_count": original_count,
        "minimal_count": minimal_count,
        "reduction_pct": round(reduction_pct, 1),
        "minimal_transcript": minimal,
    }


def format_minimization_report(result: Dict[str, Any]) -> str:
    """Format minimization result as a human-readable report."""
    lines = [
        "",
        "Transcript Minimization Report",
        "==============================",
        "",
        f"Original records: {result['original_count']}",
        f"Minimal records:  {result['minimal_count']}",
        f"Reduction:        {result['reduction_pct']}%",
        "",
    ]

    if result.get("error"):
        lines.append(f"Error: {result['error']}")

    # Breakdown by type
    if "minimal_transcript" in result:
        by_type: Dict[str, int] = {}
        for record in result["minimal_transcript"]:
            rtype = record.get("type", "UNKNOWN")
            by_type[rtype] = by_type.get(rtype, 0) + 1

        lines.append("Minimal transcript breakdown:")
        for rtype, count in sorted(by_type.items()):
            lines.append(f"  {rtype}: {count}")

    return "\n".join(lines)
