"""Self-Consistency Voting for LLM-First Debugging.

Implements self-consistency voting to improve reliability of LLM outputs:
1. Generate multiple outputs (hypotheses or fixes) with temperature variation
2. Cluster similar outputs by semantic similarity
3. Vote for the most consistent cluster
4. Return confidence based on cluster size

This approach leverages the observation that correct answers tend to be more
consistent across multiple generations than incorrect ones.
"""

from __future__ import annotations

import asyncio
import difflib
import logging
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple

from autodebug.llm_first.models import (
    AgentHypothesis,
    FailureContext,
    FixHypothesis,
    FixStrategy,
    RootCauseExplanation,
)
from autodebug.prompts.sanitizer import sanitize_code, sanitize_plain

if TYPE_CHECKING:
    pass

logger = logging.getLogger(__name__)


# =============================================================================
# Data Classes
# =============================================================================


@dataclass
class VotedHypothesis:
    """A hypothesis selected through self-consistency voting.

    Attributes:
        hypothesis: The winning hypothesis from voting.
        confidence: Confidence score based on cluster size and agreement.
        alternatives: Other hypotheses that were considered, ranked by votes.
        cluster_size: Number of similar hypotheses in the winning cluster.
        total_generations: Total number of hypotheses generated.
        vote_distribution: Distribution of votes across clusters.
    """
    hypothesis: AgentHypothesis
    confidence: float
    alternatives: List[AgentHypothesis] = field(default_factory=list)
    cluster_size: int = 1
    total_generations: int = 1
    vote_distribution: Dict[str, int] = field(default_factory=dict)

    @property
    def agreement_ratio(self) -> float:
        """Ratio of votes for winning hypothesis to total votes."""
        if self.total_generations == 0:
            return 0.0
        return self.cluster_size / self.total_generations


@dataclass
class VotedFix:
    """A fix selected through self-consistency voting.

    Attributes:
        fix: The winning fix from voting.
        confidence: Confidence score based on cluster size and agreement.
        vote_distribution: Distribution of votes across fix clusters.
        cluster_size: Number of similar fixes in the winning cluster.
        total_generations: Total number of fixes generated.
        alternatives: Other fixes that were considered.
    """
    fix: FixHypothesis
    confidence: float
    vote_distribution: Dict[str, int] = field(default_factory=dict)
    cluster_size: int = 1
    total_generations: int = 1
    alternatives: List[FixHypothesis] = field(default_factory=list)

    @property
    def agreement_ratio(self) -> float:
        """Ratio of votes for winning fix to total votes."""
        if self.total_generations == 0:
            return 0.0
        return self.cluster_size / self.total_generations


@dataclass
class Cluster:
    """A cluster of similar items for voting.

    Attributes:
        representative: The representative item for this cluster.
        members: All items in the cluster.
        centroid_similarity: Average similarity to the centroid.
    """
    representative: Any
    members: List[Any] = field(default_factory=list)
    centroid_similarity: float = 1.0

    @property
    def size(self) -> int:
        """Number of members in the cluster."""
        return len(self.members)


# =============================================================================
# Self-Consistency Voter
# =============================================================================


class SelfConsistencyVoter:
    """Implements self-consistency voting for LLM outputs.

    Self-consistency is based on the observation that for complex reasoning
    tasks, correct answers tend to be more consistent across multiple
    generations than incorrect ones. By generating multiple outputs with
    temperature variation and voting on the most consistent cluster, we can
    improve reliability.

    Example:
        >>> voter = SelfConsistencyVoter(instructor_client, num_samples=5)
        >>> voted = await voter.vote_on_hypotheses(context)
        >>> print(f"Winning hypothesis: {voted.hypothesis.summary}")
        >>> print(f"Confidence: {voted.confidence:.0%}")
    """

    def __init__(
        self,
        instructor_client: Optional[Any] = None,
        num_samples: int = 5,
        similarity_threshold: float = 0.7,
        temperature_range: Tuple[float, float] = (0.3, 0.9),
        max_clusters: int = 5,
    ):
        """Initialize self-consistency voter.

        Args:
            instructor_client: LLM client for generating outputs.
            num_samples: Number of samples to generate for voting.
            similarity_threshold: Threshold for clustering similar outputs.
            temperature_range: Range of temperatures for diversity.
            max_clusters: Maximum number of clusters to form.
        """
        self.instructor_client = instructor_client
        self.num_samples = num_samples
        self.similarity_threshold = similarity_threshold
        self.temperature_range = temperature_range
        self.max_clusters = max_clusters

    async def vote_on_hypotheses(
        self,
        context: FailureContext,
        root_cause: Optional[RootCauseExplanation] = None,
        custom_prompt: Optional[str] = None,
    ) -> VotedHypothesis:
        """Generate multiple hypotheses and vote on most consistent.

        Args:
            context: Failure context for hypothesis generation.
            root_cause: Optional root cause explanation for context.
            custom_prompt: Optional custom prompt override.

        Returns:
            VotedHypothesis with winning hypothesis and confidence.

        Raises:
            ValueError: If no hypotheses could be generated.
        """
        if not self.instructor_client:
            raise ValueError("instructor_client is required for hypothesis generation")

        # Generate multiple hypotheses with varying temperatures
        hypotheses = await self._generate_hypotheses_parallel(
            context=context,
            root_cause=root_cause,
            custom_prompt=custom_prompt,
        )

        if not hypotheses:
            raise ValueError("Failed to generate any hypotheses")

        # Cluster by similarity
        clusters = self._cluster_by_similarity(
            items=hypotheses,
            similarity_fn=self._hypothesis_similarity,
        )

        if not clusters:
            # Fallback: return first hypothesis with low confidence
            return VotedHypothesis(
                hypothesis=hypotheses[0],
                confidence=0.3,
                alternatives=hypotheses[1:],
                cluster_size=1,
                total_generations=len(hypotheses),
            )

        # Sort clusters by votes, then cohesion, then representative confidence.
        clusters.sort(
            key=lambda c: (
                c.size,
                c.centroid_similarity,
                getattr(c.representative, "confidence", 0.0),
            ),
            reverse=True,
        )

        # Winner is the largest cluster
        winner_cluster = clusters[0]
        winner = winner_cluster.representative

        # Calculate confidence based on cluster size and agreement
        confidence = self._calculate_hypothesis_confidence(
            cluster=winner_cluster,
            total_samples=len(hypotheses),
            all_clusters=clusters,
        )

        # Build vote distribution
        vote_distribution = {}
        for i, cluster in enumerate(clusters):
            label = cluster.representative.summary[:50] if hasattr(cluster.representative, 'summary') else f"cluster_{i}"
            vote_distribution[label] = cluster.size

        # Collect alternatives from other clusters
        alternatives = []
        for cluster in clusters[1:]:
            alternatives.append(cluster.representative)

        return VotedHypothesis(
            hypothesis=winner,
            confidence=confidence,
            alternatives=alternatives,
            cluster_size=winner_cluster.size,
            total_generations=len(hypotheses),
            vote_distribution=vote_distribution,
        )

    async def vote_on_fixes(
        self,
        context: FailureContext,
        root_cause: RootCauseExplanation,
        strategy: FixStrategy = FixStrategy.CONSERVATIVE,
        custom_prompt: Optional[str] = None,
    ) -> VotedFix:
        """Generate multiple fixes and vote on most consistent.

        Args:
            context: Failure context for fix generation.
            root_cause: Root cause explanation to guide fixes.
            strategy: Fix strategy to use for generation.
            custom_prompt: Optional custom prompt override.

        Returns:
            VotedFix with winning fix and confidence.

        Raises:
            ValueError: If no fixes could be generated.
        """
        if not self.instructor_client:
            raise ValueError("instructor_client is required for fix generation")

        # Generate multiple fixes with varying temperatures
        fixes = await self._generate_fixes_parallel(
            context=context,
            root_cause=root_cause,
            strategy=strategy,
            custom_prompt=custom_prompt,
        )

        if not fixes:
            raise ValueError("Failed to generate any fixes")

        # Cluster fixes by diff similarity
        clusters = self._cluster_fixes(fixes)

        if not clusters:
            # Fallback: return first fix with low confidence
            return VotedFix(
                fix=fixes[0],
                confidence=0.3,
                vote_distribution={"single": 1},
                cluster_size=1,
                total_generations=len(fixes),
                alternatives=fixes[1:],
            )

        # Sort clusters by votes, then cohesion, then representative confidence.
        clusters.sort(
            key=lambda c: (
                c.size,
                c.centroid_similarity,
                getattr(c.representative, "confidence", 0.0),
            ),
            reverse=True,
        )

        # Winner is the largest cluster
        winner_cluster = clusters[0]
        winner = winner_cluster.representative

        # Calculate confidence
        confidence = self._calculate_fix_confidence(
            cluster=winner_cluster,
            total_samples=len(fixes),
            all_clusters=clusters,
        )

        # Build vote distribution
        vote_distribution = {}
        for i, cluster in enumerate(clusters):
            label = f"approach_{i+1}"
            if hasattr(cluster.representative, 'description'):
                label = cluster.representative.description[:30]
            vote_distribution[label] = cluster.size

        # Collect alternatives
        alternatives = []
        for cluster in clusters[1:]:
            alternatives.append(cluster.representative)

        return VotedFix(
            fix=winner,
            confidence=confidence,
            vote_distribution=vote_distribution,
            cluster_size=winner_cluster.size,
            total_generations=len(fixes),
            alternatives=alternatives,
        )

    def _cluster_by_similarity(
        self,
        items: List[Any],
        similarity_fn: Callable[[Any, Any], float],
    ) -> List[Cluster]:
        """Cluster items by semantic similarity using greedy clustering.

        Uses a greedy agglomerative approach where each item is assigned
        to the most similar existing cluster, or starts a new cluster if
        no cluster is similar enough.

        Args:
            items: Items to cluster.
            similarity_fn: Function to compute similarity between items.

        Returns:
            List of clusters, each containing similar items.
        """
        if not items:
            return []

        clusters: List[Cluster] = []

        for item in items:
            # Find the most similar cluster
            best_cluster = None
            best_similarity = 0.0

            for cluster in clusters:
                similarity = similarity_fn(item, cluster.representative)
                if similarity > best_similarity:
                    best_similarity = similarity
                    best_cluster = cluster

            # Add to existing cluster or create new one
            if best_cluster and best_similarity >= self.similarity_threshold:
                best_cluster.members.append(item)
                # Update centroid similarity (running average)
                n = len(best_cluster.members)
                best_cluster.centroid_similarity = (
                    (best_cluster.centroid_similarity * (n - 1) + best_similarity) / n
                )
            else:
                # Create new cluster
                if len(clusters) < self.max_clusters:
                    clusters.append(Cluster(
                        representative=item,
                        members=[item],
                        centroid_similarity=1.0,
                    ))
                else:
                    # Force assignment to nearest cluster
                    if best_cluster:
                        best_cluster.members.append(item)

        return clusters

    def _cluster_fixes(
        self,
        fixes: List[FixHypothesis],
    ) -> List[Cluster]:
        """Cluster fixes by diff similarity.

        Uses diff-based similarity to group fixes that make similar
        code changes, even if worded differently.

        Args:
            fixes: Fixes to cluster.

        Returns:
            List of clusters containing similar fixes.
        """
        return self._cluster_by_similarity(
            items=fixes,
            similarity_fn=self._fix_similarity,
        )

    def _hypothesis_similarity(
        self,
        h1: AgentHypothesis,
        h2: AgentHypothesis,
    ) -> float:
        """Compute similarity between two hypotheses.

        Considers:
        - Category match (strong signal)
        - Summary text similarity
        - Evidence overlap

        Args:
            h1: First hypothesis.
            h2: Second hypothesis.

        Returns:
            Similarity score between 0 and 1.
        """
        score = 0.0

        # Category match is a strong signal (40% weight)
        if h1.category == h2.category:
            score += 0.4

        # Summary similarity (40% weight)
        summary_sim = self._text_similarity(h1.summary, h2.summary)
        score += 0.4 * summary_sim

        # Evidence overlap (20% weight)
        if h1.evidence and h2.evidence:
            evidence_sim = self._list_similarity(h1.evidence, h2.evidence)
            score += 0.2 * evidence_sim
        else:
            # If no evidence, give full weight to other signals
            score += 0.1

        return min(1.0, score)

    def _fix_similarity(
        self,
        f1: FixHypothesis,
        f2: FixHypothesis,
    ) -> float:
        """Compute similarity between two fixes.

        Focuses on diff similarity since that's the actual change.

        Args:
            f1: First fix.
            f2: Second fix.

        Returns:
            Similarity score between 0 and 1.
        """
        # Diff similarity is the primary signal (70% weight)
        diff_sim = self._text_similarity(f1.diff, f2.diff)

        # Description similarity (20% weight)
        desc_sim = self._text_similarity(f1.description, f2.description)

        # Strategy match (10% weight)
        strategy_match = 1.0 if f1.strategy == f2.strategy else 0.0

        return 0.7 * diff_sim + 0.2 * desc_sim + 0.1 * strategy_match

    def _text_similarity(
        self,
        text1: str,
        text2: str,
    ) -> float:
        """Compute text similarity using SequenceMatcher with keyword overlap boost.

        Uses SequenceMatcher as a fast baseline and supplements it with
        keyword-overlap scoring to partially capture semantic similarity
        (e.g., "race condition in connection pool" vs "connection pool
        concurrency issue") without requiring an embedding model.

        Args:
            text1: First text.
            text2: Second text.

        Returns:
            Similarity ratio between 0 and 1.
        """
        if not text1 or not text2:
            return 0.0

        # Normalize whitespace
        text1_norm = " ".join(text1.split())
        text2_norm = " ".join(text2.split())

        # Baseline: character-level SequenceMatcher
        seq_ratio = difflib.SequenceMatcher(None, text1_norm, text2_norm).ratio()

        # Keyword overlap (Jaccard on lowercased words, stopwords removed)
        _stopwords = {
            "the", "a", "an", "is", "are", "was", "were", "be", "been",
            "being", "in", "on", "at", "to", "for", "of", "with", "by",
            "from", "and", "or", "but", "not", "this", "that", "it",
        }
        words1 = {w.lower() for w in text1_norm.split() if w.lower() not in _stopwords and len(w) > 2}
        words2 = {w.lower() for w in text2_norm.split() if w.lower() not in _stopwords and len(w) > 2}

        if words1 and words2:
            keyword_overlap = len(words1 & words2) / len(words1 | words2)
        else:
            keyword_overlap = 0.0

        # Blend: 60% SequenceMatcher + 40% keyword overlap
        return 0.6 * seq_ratio + 0.4 * keyword_overlap

    def _list_similarity(
        self,
        list1: List[str],
        list2: List[str],
    ) -> float:
        """Compute similarity between two lists of strings.

        Uses Jaccard similarity on the text content.

        Args:
            list1: First list.
            list2: Second list.

        Returns:
            Similarity score between 0 and 1.
        """
        if not list1 or not list2:
            return 0.0

        # Normalize and create sets
        set1 = set(" ".join(s.lower().split()) for s in list1)
        set2 = set(" ".join(s.lower().split()) for s in list2)

        # Jaccard similarity
        intersection = len(set1 & set2)
        union = len(set1 | set2)

        if union == 0:
            return 0.0

        return intersection / union

    def _calculate_hypothesis_confidence(
        self,
        cluster: Cluster,
        total_samples: int,
        all_clusters: List[Cluster],
    ) -> float:
        """Calculate confidence score for hypothesis voting result.

        Confidence is based on:
        - Cluster size relative to total samples
        - Gap between winning cluster and runner-up
        - Average similarity within winning cluster

        Args:
            cluster: Winning cluster.
            total_samples: Total number of samples generated.
            all_clusters: All clusters for comparison.

        Returns:
            Confidence score between 0 and 1.
        """
        if total_samples == 0:
            return 0.0

        # Base confidence from cluster size ratio
        size_ratio = cluster.size / total_samples

        # Boost for clear winner (gap to second place)
        gap_boost = 0.0
        if len(all_clusters) > 1:
            runner_up_size = all_clusters[1].size
            gap = (cluster.size - runner_up_size) / total_samples
            gap_boost = min(0.2, gap * 0.5)
        else:
            # Single cluster = unanimous
            gap_boost = 0.15

        # Factor in cluster cohesion
        cohesion = cluster.centroid_similarity

        # Combined confidence
        confidence = (size_ratio * 0.6) + (gap_boost) + (cohesion * 0.2)

        # Clamp to [0.1, 0.95] - never fully certain or uncertain
        return max(0.1, min(0.95, confidence))

    def _calculate_fix_confidence(
        self,
        cluster: Cluster,
        total_samples: int,
        all_clusters: List[Cluster],
    ) -> float:
        """Calculate confidence score for fix voting result.

        Similar to hypothesis confidence but with more weight on
        cluster cohesion since code changes need to be precise.

        Args:
            cluster: Winning cluster.
            total_samples: Total number of samples generated.
            all_clusters: All clusters for comparison.

        Returns:
            Confidence score between 0 and 1.
        """
        if total_samples == 0:
            return 0.0

        # Base confidence from cluster size ratio
        size_ratio = cluster.size / total_samples

        # Gap to runner-up
        gap_boost = 0.0
        if len(all_clusters) > 1:
            runner_up_size = all_clusters[1].size
            gap = (cluster.size - runner_up_size) / total_samples
            gap_boost = min(0.15, gap * 0.4)
        else:
            gap_boost = 0.1

        # Higher weight on cohesion for fixes (code precision matters)
        cohesion = cluster.centroid_similarity

        # Combined confidence
        confidence = (size_ratio * 0.5) + (gap_boost) + (cohesion * 0.3)

        # Also factor in the fix's own confidence if available
        if hasattr(cluster.representative, 'confidence'):
            confidence = confidence * 0.8 + cluster.representative.confidence * 0.2

        return max(0.1, min(0.95, confidence))

    async def _generate_hypotheses_parallel(
        self,
        context: FailureContext,
        root_cause: Optional[RootCauseExplanation] = None,
        custom_prompt: Optional[str] = None,
    ) -> List[AgentHypothesis]:
        """Generate multiple hypotheses in parallel with temperature variation.

        Args:
            context: Failure context.
            root_cause: Optional root cause for context.
            custom_prompt: Optional custom prompt.

        Returns:
            List of generated hypotheses.
        """
        # Calculate temperatures for each sample
        temps = self._get_temperature_schedule(self.num_samples)

        # Build base prompt
        prompt = custom_prompt or self._build_hypothesis_prompt(context, root_cause)

        # Generate in parallel
        tasks = [
            self._generate_single_hypothesis(prompt, temp)
            for temp in temps
        ]

        results = await asyncio.gather(*tasks, return_exceptions=True)

        # Collect successful generations
        hypotheses = []
        for result in results:
            if isinstance(result, Exception):
                logger.warning(f"Hypothesis generation failed: {result}")
                continue
            if result:
                hypotheses.append(result)

        return hypotheses

    async def _generate_fixes_parallel(
        self,
        context: FailureContext,
        root_cause: RootCauseExplanation,
        strategy: FixStrategy,
        custom_prompt: Optional[str] = None,
    ) -> List[FixHypothesis]:
        """Generate multiple fixes in parallel with temperature variation.

        Args:
            context: Failure context.
            root_cause: Root cause explanation.
            strategy: Fix strategy to use.
            custom_prompt: Optional custom prompt.

        Returns:
            List of generated fixes.
        """
        # Calculate temperatures for each sample
        temps = self._get_temperature_schedule(self.num_samples)

        # Build base prompt
        prompt = custom_prompt or self._build_fix_prompt(context, root_cause, strategy)

        # Generate in parallel
        tasks = [
            self._generate_single_fix(prompt, temp, strategy)
            for temp in temps
        ]

        results = await asyncio.gather(*tasks, return_exceptions=True)

        # Collect successful generations
        fixes = []
        for result in results:
            if isinstance(result, Exception):
                logger.warning(f"Fix generation failed: {result}")
                continue
            if result:
                fixes.append(result)

        return fixes

    async def _generate_single_hypothesis(
        self,
        prompt: str,
        temperature: float,
    ) -> Optional[AgentHypothesis]:
        """Generate a single hypothesis with specific temperature.

        Args:
            prompt: Prompt for generation.
            temperature: Temperature for sampling.

        Returns:
            Generated hypothesis or None on failure.
        """
        try:
            response = await self.instructor_client.generate(
                prompt,
                response_model=AgentHypothesis,
                temperature=temperature,
            )
            return response
        except Exception as e:
            logger.error(f"Single hypothesis generation failed: {e}")
            return None

    async def _generate_single_fix(
        self,
        prompt: str,
        temperature: float,
        strategy: FixStrategy,
    ) -> Optional[FixHypothesis]:
        """Generate a single fix with specific temperature.

        Args:
            prompt: Prompt for generation.
            temperature: Temperature for sampling.
            strategy: Fix strategy for metadata.

        Returns:
            Generated fix or None on failure.
        """
        try:
            # Use a simpler response model for generation
            import uuid

            from autodebug.llm_first.models import FixResponse

            response = await self.instructor_client.generate(
                prompt,
                response_model=FixResponse,
                temperature=temperature,
            )

            return FixHypothesis(
                id=str(uuid.uuid4()),
                strategy=strategy,
                diff=response.diff,
                description=response.description,
                confidence=response.confidence,
            )
        except Exception as e:
            logger.error(f"Single fix generation failed: {e}")
            return None

    def _get_temperature_schedule(self, num_samples: int) -> List[float]:
        """Get temperature schedule for samples.

        Distributes temperatures across the configured range to ensure
        diversity in generations.

        Args:
            num_samples: Number of samples to generate.

        Returns:
            List of temperatures for each sample.
        """
        if num_samples == 1:
            return [(self.temperature_range[0] + self.temperature_range[1]) / 2]

        min_temp, max_temp = self.temperature_range
        step = (max_temp - min_temp) / (num_samples - 1)

        return [min_temp + (i * step) for i in range(num_samples)]

    def _build_hypothesis_prompt(
        self,
        context: FailureContext,
        root_cause: Optional[RootCauseExplanation] = None,
    ) -> str:
        """Build prompt for hypothesis generation.

        Args:
            context: Failure context.
            root_cause: Optional root cause for context.

        Returns:
            Prompt string.
        """
        root_cause_section = ""
        if root_cause:
            root_cause_section = f"""
Previous analysis suggests:
- Category: {root_cause.category.value}
- Summary: {root_cause.summary}

Consider whether you agree with this analysis or have a different hypothesis.
"""

        return f"""
Analyze this test failure and generate a hypothesis about the root cause.

Failure Details:
- Test: {context.test_name}
- File: {context.test_file}
- Error: {sanitize_plain(context.error_type)}: {sanitize_plain(context.error_message)}

Stack Trace:
{sanitize_plain(context.stack_trace[:1500]) if context.stack_trace else 'Not available'}

Code Context:
{sanitize_code(context.code_snippet[:1500]) if context.code_snippet else 'Not available'}

{root_cause_section}

Generate a hypothesis with:
- agent_name: A descriptive name for your analysis approach
- category: One of: timing, ordering, resource, state, network, concurrency, unknown
- summary: Brief summary of the root cause (1-2 sentences)
- detailed_explanation: Full explanation of why this is happening
- evidence: List of specific evidence from the failure data
- confidence: Your confidence level (0-1)
- testable_predictions: How this hypothesis could be verified
- fix_direction: General direction for fixing this issue
"""

    def _build_fix_prompt(
        self,
        context: FailureContext,
        root_cause: RootCauseExplanation,
        strategy: FixStrategy,
    ) -> str:
        """Build prompt for fix generation.

        Args:
            context: Failure context.
            root_cause: Root cause explanation.
            strategy: Fix strategy.

        Returns:
            Prompt string.
        """
        strategy_guidance = {
            FixStrategy.CONSERVATIVE: "Make the MINIMAL change needed. Don't refactor.",
            FixStrategy.RAG_BASED: "Apply patterns from similar successful fixes.",
            FixStrategy.FRAMEWORK_SPECIFIC: "Use framework-specific best practices.",
            FixStrategy.CREATIVE: "Consider broader architectural improvements.",
            FixStrategy.ENSEMBLE: "Combine the best elements from multiple approaches.",
        }

        guidance = strategy_guidance.get(
            strategy,
            "Fix the issue appropriately."
        )

        return f"""
Generate a fix for this test failure.

Failure:
- Error: {sanitize_plain(context.error_type)}: {sanitize_plain(context.error_message)}
- File: {context.test_file}
- Function: {context.function_name or 'unknown'}

Root Cause:
- Category: {root_cause.category.value}
- Summary: {sanitize_plain(root_cause.summary)}

Code Context:
{sanitize_code(context.code_snippet[:2000]) if context.code_snippet else 'Not available'}

Strategy: {strategy.value}
Guidance: {guidance}

Before generating the fix, think step by step:
1. What is the exact line or expression causing the issue?
2. What minimal change would fix the root cause?
3. Are there any side effects of this change?

Example of a good response:
- diff:
  --- a/tests/test_api.py
  +++ b/tests/test_api.py
  @@ -28,7 +28,7 @@
  -    assert response.json() == expected
  +    assert response.json()["items"] == sorted(expected["items"], key=lambda x: x["id"])
- description: The API returns items in non-deterministic order. Sorting before comparison eliminates order dependency.
- confidence: 0.85

Return:
- diff: The fix as a unified diff
- description: Explanation of what the fix does and why
- confidence: Your confidence this fix resolves the issue (0.0-1.0)
"""
