"""Multi-Hypothesis Fix Generation (Phase 4).

Generates multiple diverse fix hypotheses in parallel using different strategies:
1. Conservative (minimal change)
2. RAG-based (from similar fixes)
3. Framework-specific patterns
4. Creative (higher temperature)
5. Ensemble (combine elements)
6. Template-based (pattern matching with fix templates)

All fixes are scored and ranked by quality.

New features (optional, enabled via parameters):
- Test minimization: Reduce test to essential elements before generating fixes
- Self-consistency voting: Generate multiple fixes per strategy, vote for most consistent
- Fix templates/synthesis: Try template-based fixes first (faster, more reliable)
- Speculative execution: Race top fixes in parallel for faster verification
- Execution grounding: Include divergence point and locals in fix generation prompts
"""

from __future__ import annotations

import asyncio
import difflib
import logging
import uuid
from typing import TYPE_CHECKING, Any, Dict, List, Optional

from autodebug.llm_first.execution_grounding import ExecutionGrounder, GroundedEvidence
from autodebug.llm_first.models import (
    FailureContext,
    FixHypothesis,
    FixQualityScores,
    FixResponse,
    FixStrategy,
    RootCauseExplanation,
    RootCauseScore,
)
from autodebug.llm_first.speculative import RaceResult, SpeculativeFixer
from autodebug.llm_first.synthesis import FixSynthesizer

# Import new components
from autodebug.llm_first.test_minimizer import MinimizedTest, TestMinimizer
from autodebug.llm_first.token_budget import TokenBudget
from autodebug.llm_first.voting import SelfConsistencyVoter, VotedFix
from autodebug.prompts.sanitizer import sanitize_code, sanitize_plain

if TYPE_CHECKING:
    from autodebug.llm_first.hybrid_search import HybridFixSearch

logger = logging.getLogger(__name__)


class MultiHypothesisFixer:
    """Generate multiple diverse fix hypotheses in parallel.

    Supports new optional features:
    - Test minimization: Reduce test to essential elements
    - Self-consistency voting: Vote for most consistent fix
    - Template-based fixes: Try fast template matching first
    - Speculative execution: Race fixes in parallel
    - Execution grounding: Include divergence/locals in prompts
    """

    def __init__(
        self,
        instructor_client=None,
        fix_search: Optional[HybridFixSearch] = None,
        num_hypotheses: int = 5,
        # New optional parameters
        use_minimization: bool = False,
        use_voting: bool = False,
        voting_samples: int = 3,
        verify_speculatively: bool = False,
        speculative_max_candidates: int = 3,
        use_templates: bool = True,
        use_execution_grounding: bool = True,
        token_budget: Optional[TokenBudget] = None,
        strategy_timeout_seconds: float = 90.0,
    ):
        """Initialize multi-hypothesis fixer.

        Args:
            instructor_client: LLM client for fix generation.
            fix_search: Search client for finding similar fixes.
            num_hypotheses: Target number of hypotheses.
            use_minimization: Whether to minimize tests before generating fixes.
            use_voting: Whether to use self-consistency voting for fixes.
            voting_samples: Number of samples for voting (if use_voting=True).
            verify_speculatively: Whether to race top fixes in parallel.
            speculative_max_candidates: Max candidates for speculative verification.
            use_templates: Whether to try template-based fixes first.
            use_execution_grounding: Whether to include divergence/locals in prompts.
        """
        self.instructor_client = instructor_client
        self.fix_search = fix_search
        self.num_hypotheses = num_hypotheses

        # New feature flags
        self.use_minimization = use_minimization
        self.use_voting = use_voting
        self.voting_samples = voting_samples
        self.verify_speculatively = verify_speculatively
        self.speculative_max_candidates = speculative_max_candidates
        self.use_templates = use_templates
        self.use_execution_grounding = use_execution_grounding
        self.token_budget = token_budget
        self.strategy_timeout_seconds = strategy_timeout_seconds

        # Initialize optional components lazily
        self._minimizer: Optional[TestMinimizer] = None
        self._voter: Optional[SelfConsistencyVoter] = None
        self._synthesizer: Optional[FixSynthesizer] = None
        self._speculative_fixer: Optional[SpeculativeFixer] = None
        self._grounder: Optional[ExecutionGrounder] = None

        # Cache for minimized test
        self._minimized_test: Optional[MinimizedTest] = None

        # Cache for grounded evidence
        self._grounded_evidence: Optional[GroundedEvidence] = None

    def _record_budget(self, purpose: str, prompt: str, response: Any) -> None:
        """Record token usage in the budget if available."""
        if self.token_budget is None:
            return
        # Estimate tokens from prompt/response lengths when raw usage unavailable
        input_tokens = len(prompt) // 4
        output_tokens = 50
        if hasattr(response, 'diff'):
            output_tokens = max(len(response.diff) // 4, 50)
        elif hasattr(response, 'reasoning') and response.reasoning:
            output_tokens = max(len(response.reasoning) // 4, 50)
        self.token_budget.record_usage(
            purpose=purpose,
            model="claude-sonnet-4-20250514",
            input_tokens=input_tokens,
            output_tokens=output_tokens,
        )

    @property
    def minimizer(self) -> TestMinimizer:
        """Lazy initialization of test minimizer."""
        if self._minimizer is None:
            self._minimizer = TestMinimizer()
        return self._minimizer

    @property
    def voter(self) -> SelfConsistencyVoter:
        """Lazy initialization of self-consistency voter."""
        if self._voter is None:
            self._voter = SelfConsistencyVoter(
                instructor_client=self.instructor_client,
                num_samples=self.voting_samples,
            )
        return self._voter

    @property
    def synthesizer(self) -> FixSynthesizer:
        """Lazy initialization of fix synthesizer."""
        if self._synthesizer is None:
            self._synthesizer = FixSynthesizer()
        return self._synthesizer

    @property
    def speculative_fixer(self) -> SpeculativeFixer:
        """Lazy initialization of speculative fixer."""
        if self._speculative_fixer is None:
            self._speculative_fixer = SpeculativeFixer(
                max_candidates=self.speculative_max_candidates,
            )
        return self._speculative_fixer

    @property
    def grounder(self) -> ExecutionGrounder:
        """Lazy initialization of execution grounder."""
        if self._grounder is None:
            self._grounder = ExecutionGrounder()
        return self._grounder

    async def generate_fixes(
        self,
        root_cause: RootCauseExplanation,
        context: FailureContext,
        passing_run_events: Optional[List[Dict[str, Any]]] = None,
        failing_run_events: Optional[List[Dict[str, Any]]] = None,
    ) -> List[FixHypothesis]:
        """Generate multiple diverse fix hypotheses.

        Args:
            root_cause: Root cause explanation.
            context: Failure context.
            passing_run_events: Events from a passing run (for execution grounding).
            failing_run_events: Events from a failing run (for execution grounding).

        Returns:
            List of fix hypotheses ranked by quality.
        """
        # Step 1: Minimize test if enabled
        effective_context = context
        if self.use_minimization and context.code_snippet:
            try:
                minimized = await self._minimize_test(context)
                if minimized:
                    self._minimized_test = minimized
                    # Create a new context with minimized test code
                    effective_context = context.model_copy()
                    effective_context.code_snippet = minimized.essential_code
                    logger.info(
                        "Minimized test: %d -> %d lines (%.0f%% reduction)",
                        minimized.original_lines,
                        minimized.minimized_lines,
                        minimized.reduction_ratio * 100,
                    )
            except Exception as e:
                logger.warning(f"Test minimization failed: {e}")

        # Step 2: Ground with execution traces if enabled
        if self.use_execution_grounding:
            try:
                self._grounded_evidence = self.grounder.ground_with_traces(
                    failure_context=context,
                    passing_run_events=passing_run_events,
                    failing_run_events=failing_run_events,
                )
                # Update context with grounded evidence
                if self._grounded_evidence.divergence:
                    effective_context.divergence_point = {
                        "index": self._grounded_evidence.divergence.index,
                        "type": self._grounded_evidence.divergence.divergence_type.value,
                        "description": self._grounded_evidence.divergence.description,
                    }
                if self._grounded_evidence.relevant_locals:
                    effective_context.locals_snapshot = self._grounded_evidence.relevant_locals
                if self._grounded_evidence.io_summary:
                    effective_context.transcript_summary = self._grounded_evidence.io_summary
                logger.debug(
                    "Grounded context with confidence boost: %.2f",
                    self._grounded_evidence.confidence_boost,
                )
            except Exception as e:
                logger.warning(f"Execution grounding failed: {e}")

        # Step 3: Try template-based fixes first (faster, more reliable)
        fixes: List[FixHypothesis] = []
        if self.use_templates:
            template_fixes = await self._generate_template_based(root_cause, effective_context)
            if template_fixes:
                fixes.extend(template_fixes)
                logger.info(f"Generated {len(template_fixes)} template-based fix(es)")

        # Step 4: Generate fixes with different strategies in parallel
        tasks = [
            self._generate_conservative(root_cause, effective_context),
            self._generate_framework_specific(root_cause, effective_context),
            self._generate_creative(root_cause, effective_context),
        ]

        # Add RAG-based if search is available
        if self.fix_search:
            tasks.append(self._generate_from_similar(root_cause, effective_context))

        # Run all strategies (with voting if enabled)
        if self.use_voting and self.instructor_client:
            results = await self._generate_with_voting(tasks, root_cause, effective_context)
        else:
            timed = [self._run_strategy_with_timeout(task) for task in tasks]
            results = await asyncio.gather(*timed, return_exceptions=True)

        # Collect valid fixes
        for result in results:
            if isinstance(result, Exception):
                logger.error(f"Fix generation failed: {result}")
                continue
            if isinstance(result, VotedFix):
                # Extract fix from voted result
                fixes.append(result.fix)
            elif result and isinstance(result, FixHypothesis):
                fixes.append(result)
            elif isinstance(result, list):
                fixes.extend(result)

        if not fixes:
            return []

        # Generate ensemble fix from best candidates
        if len(fixes) >= 2:
            ensemble = await self._generate_ensemble(fixes[:3], root_cause, effective_context)
            if ensemble:
                fixes.append(ensemble)

        # Deduplicate and enforce diversity
        unique_fixes = self._deduplicate(fixes)
        unique_fixes = self._ensure_diversity(unique_fixes)

        # Score and rank (with new scoring factors)
        scored_fixes = await self._score_fixes(unique_fixes, effective_context)

        # Step 5: Speculative verification if enabled
        if self.verify_speculatively and scored_fixes:
            try:
                verified_fixes = await self._verify_speculatively(scored_fixes, context)
                if verified_fixes:
                    return verified_fixes[:self.num_hypotheses]
            except Exception as e:
                logger.warning(f"Speculative verification failed: {e}")

        return scored_fixes[:self.num_hypotheses]

    async def _minimize_test(
        self,
        context: FailureContext,
    ) -> Optional[MinimizedTest]:
        """Minimize test to essential elements.

        Args:
            context: Failure context with test code.

        Returns:
            MinimizedTest or None if minimization fails.
        """
        if not context.code_snippet:
            return None

        # Create a simple failure predicate that always returns True
        # In production, this would run the actual test
        def failure_predicate(code: str) -> bool:
            # This is a simplified predicate - in practice, we'd run the test
            # For now, we check if the code contains the error-related assertions
            return True

        try:
            return self.minimizer.minimize_failing_test(
                test_code=context.code_snippet,
                failure_predicate=failure_predicate,
                test_name=context.function_name,
            )
        except Exception as e:
            logger.warning(f"Test minimization failed: {e}")
            return None

    async def _generate_with_voting(
        self,
        base_tasks: List,
        root_cause: RootCauseExplanation,
        context: FailureContext,
    ) -> List:
        """Generate fixes with self-consistency voting.

        Args:
            base_tasks: Base generation tasks.
            root_cause: Root cause explanation.
            context: Failure context.

        Returns:
            List of results (VotedFix or FixHypothesis).
        """
        results = []

        # For each strategy, generate multiple and vote
        strategies = [
            (FixStrategy.CONSERVATIVE, self._generate_conservative),
            (FixStrategy.FRAMEWORK_SPECIFIC, self._generate_framework_specific),
            (FixStrategy.CREATIVE, self._generate_creative),
        ]

        for strategy, _ in strategies:
            try:
                voted = await self.voter.vote_on_fixes(
                    context=context,
                    root_cause=root_cause,
                    strategy=strategy,
                )
                results.append(voted)
            except Exception as e:
                logger.warning(f"Voting failed for {strategy.value}: {e}")
                # Fall back to single generation
                fallback = await self._generate_conservative(root_cause, context)
                if fallback:
                    results.append(fallback)

        return results

    async def _generate_template_based(
        self,
        root_cause: RootCauseExplanation,
        context: FailureContext,
    ) -> List[FixHypothesis]:
        """Generate fixes using templates (6th strategy).

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

        Returns:
            List of template-based fix hypotheses.
        """
        fixes = []

        # Find matching templates
        templates = self.synthesizer.match_error_to_template(
            error_type=context.error_type,
            error_message=context.error_message,
        )

        if not templates:
            logger.debug("No matching templates found for error")
            return fixes

        # Try to apply each matching template
        for template in templates[:3]:  # Limit to top 3 templates
            try:
                fixed_code = self.synthesizer.synthesize_from_template(
                    template=template,
                    context=context,
                )

                if fixed_code and fixed_code != (context.surrounding_code or context.code_snippet):
                    # Create a fix hypothesis from the template
                    template_confidence = self.synthesizer.adjusted_confidence(
                        template.confidence
                    )
                    fix = FixHypothesis(
                        id=str(uuid.uuid4()),
                        strategy=FixStrategy.TEMPLATE_BASED,
                        diff=self._create_diff(context.code_snippet or "", fixed_code),
                        description=f"Template-based fix: {template.description}",
                        confidence=template_confidence,
                    )
                    # Mark as template-based in the quality score
                    fix.quality_score = template_confidence
                    fixes.append(fix)

                    logger.debug(
                        "Applied template '%s' with confidence %.2f",
                        template.name,
                        template_confidence,
                    )

            except Exception as e:
                logger.warning(f"Template {template.name} application failed: {e}")

        # Try simple template composition for complementary patterns.
        # Example: ordering fix + missing await in async path.
        if len(templates) >= 2 and context.code_snippet:
            primary = templates[0]
            secondary = templates[1]
            try:
                first_pass = self.synthesizer.synthesize_from_template(primary, context)
                composed_context = context.model_copy()
                composed_context.code_snippet = first_pass
                composed_code = self.synthesizer.synthesize_from_template(secondary, composed_context)
                if composed_code and composed_code != context.code_snippet:
                    composed_conf = min(
                        0.95,
                        self.synthesizer.adjusted_confidence(primary.confidence) * 0.9,
                    )
                    fixes.append(
                        FixHypothesis(
                            id=str(uuid.uuid4()),
                            strategy=FixStrategy.TEMPLATE_BASED,
                            diff=self._create_diff(context.code_snippet, composed_code),
                            description=(
                                f"Template-composed fix: {primary.name} + {secondary.name}"
                            ),
                            confidence=composed_conf,
                            quality_score=composed_conf,
                        )
                    )
            except Exception as e:
                logger.debug("Template composition skipped: %s", e)

        return fixes

    async def _run_strategy_with_timeout(self, coro: Any) -> Any:
        """Run a generation strategy with a timeout."""
        try:
            return await asyncio.wait_for(coro, timeout=self.strategy_timeout_seconds)
        except asyncio.TimeoutError:
            return RuntimeError(
                f"Strategy timed out after {self.strategy_timeout_seconds:.0f}s"
            )

    async def _verify_speculatively(
        self,
        fixes: List[FixHypothesis],
        context: FailureContext,
    ) -> List[FixHypothesis]:
        """Verify fixes speculatively by racing them in parallel.

        Args:
            fixes: Fixes to verify.
            context: Failure context.

        Returns:
            Verified fixes, with winner first.
        """
        top_candidates = fixes[:self.speculative_max_candidates]

        logger.info(
            "Racing %d fix candidates speculatively",
            len(top_candidates),
        )

        race_result: RaceResult = await self.speculative_fixer.apply_and_race(
            fixes=top_candidates,
            context=context,
        )

        if race_result.has_winner and race_result.winner:
            # Winner found - put it first
            race_result.winner.verified_deterministic = True
            if race_result.winner_result:
                race_result.winner.quality_score = min(
                    1.0,
                    race_result.winner.quality_score + 0.2,  # Boost for verification
                )

            # Return winner followed by others
            remaining = [f for f in fixes if f.id != race_result.winner.id]
            return [race_result.winner] + remaining

        # No winner - return original order but update scores based on results
        for fix in fixes:
            result = race_result.all_results.get(fix.id)
            if result and result.total_runs > 0:
                pass_rate = result.passed_runs / result.total_runs
                # Adjust quality score based on verification
                fix.quality_score = fix.quality_score * 0.7 + pass_rate * 0.3

        return fixes

    def _create_diff(self, original: str, modified: str) -> str:
        """Create a unified diff from original to modified code.

        Args:
            original: Original code.
            modified: Modified code.

        Returns:
            Unified diff string.
        """
        import difflib

        original_lines = original.splitlines(keepends=True)
        modified_lines = modified.splitlines(keepends=True)

        diff = difflib.unified_diff(
            original_lines,
            modified_lines,
            fromfile="original",
            tofile="fixed",
        )

        return "".join(diff)

    async def _generate_conservative(
        self,
        root_cause: RootCauseExplanation,
        context: FailureContext,
    ) -> Optional[FixHypothesis]:
        """Generate conservative minimal-change fix.

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

        Returns:
            Conservative fix hypothesis.
        """
        if not self.instructor_client:
            return None

        # Build execution grounding section if available
        grounding_section = self._build_grounding_section(context)

        prompt = f"""
Generate a CONSERVATIVE fix for this failure. Make the MINIMAL change needed.

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

Root cause: {sanitize_plain(root_cause.summary)}
Category: {root_cause.category.value}
{grounding_section}
Code context:
{sanitize_code(context.code_snippet[:2000]) if context.code_snippet else 'Not available'}

Requirements:
1. Change as few lines as possible
2. Don't refactor surrounding code
3. Don't add unnecessary error handling
4. Focus on fixing the specific issue

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 (not just suppress the symptom)?
3. Are there any side effects of this change?

Example of a good response:
- diff:
  --- a/tests/test_users.py
  +++ b/tests/test_users.py
  @@ -42,7 +42,7 @@
  -    assert users == expected_users
  +    assert sorted(users, key=lambda u: u["id"]) == sorted(expected_users, key=lambda u: u["id"])
- description: The test compared two lists that could be returned in any order from the database. Sorting both sides by id makes the assertion order-independent.
- confidence: 0.9
- reasoning: The SQL query has no ORDER BY clause, so row order is non-deterministic. Sorting both lists by a stable key fixes the flakiness.

Return:
- diff: The minimal diff to fix the issue
- description: Brief explanation of the fix
- confidence: How confident you are (0.0-1.0)
- reasoning: Your step-by-step reasoning about the root cause and fix
"""

        try:
            response = await self.instructor_client.generate(
                prompt,
                response_model=FixResponse,
            )
            self._record_budget("conservative_fix", prompt, response)

            return FixHypothesis(
                id=str(uuid.uuid4()),
                strategy=FixStrategy.CONSERVATIVE,
                diff=response.diff,
                description=response.description,
                confidence=response.confidence,
            )

        except Exception as e:
            logger.error(f"Conservative fix generation failed: {e}")
            return None

    def _build_grounding_section(self, context: FailureContext) -> str:
        """Build execution grounding section for prompts.

        Args:
            context: Failure context with optional grounded data.

        Returns:
            Grounding section string for prompt.
        """
        sections = []

        # Add divergence point if available
        if context.divergence_point:
            div = context.divergence_point
            sections.append(f"""
Divergence Point (where passing and failing runs differ):
- Type: {div.get('type', 'unknown')}
- Description: {sanitize_plain(str(div.get('description', 'No description')))}
- Event Index: {div.get('index', 'unknown')}""")

        # Add local variables if available
        if context.locals_snapshot:
            locals_data = context.locals_snapshot
            if isinstance(locals_data, dict):
                frame_info = locals_data.get("frame", {})
                variables = locals_data.get("variables", {})
                suspicious = locals_data.get("suspicious", [])

                if frame_info or variables:
                    sections.append(f"""
Local Variables at Failure:
- Function: {frame_info.get('function', 'unknown')}
- Line: {frame_info.get('line', 'unknown')}""")

                    if suspicious:
                        sections.append(f"- Suspicious variables: {', '.join(str(s) for s in suspicious)}")

                    # Add first few variables
                    var_items = list(variables.items())[:5]
                    if var_items:
                        sections.append("- Values:")
                        for name, value in var_items:
                            sections.append(f"  {name} = {sanitize_plain(str(value)[:100])}")

        # Add I/O transcript summary if available
        if context.transcript_summary:
            sections.append(f"""
I/O Transcript Summary:
{sanitize_plain(context.transcript_summary[:500])}""")

        return "\n".join(sections)

    async def _generate_from_similar(
        self,
        root_cause: RootCauseExplanation,
        context: FailureContext,
    ) -> Optional[FixHypothesis]:
        """Generate fix based on similar historical fixes.

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

        Returns:
            RAG-based fix hypothesis.
        """
        if not self.fix_search or not self.instructor_client:
            return None

        # Retrieve similar fixes
        try:
            similar = await self.fix_search.search_from_context(context, top_k=3)

            if not similar:
                return None

            # Format similar fixes for prompt
            similar_text = "\n\n".join([
                f"[{i+1}] Error: {fix.error_type}\n"
                f"Category: {fix.category.value}\n"
                f"Description: {fix.description}\n"
                f"Success rate: {fix.verification_pass_rate:.0%}\n"
                f"Diff:\n{fix.diff[:500]}"
                for i, fix in enumerate(similar)
            ])

            # Build execution grounding section
            grounding_section = self._build_grounding_section(context)

            prompt = f"""
Generate a fix based on similar past fixes that worked.

Current failure:
- Error: {sanitize_plain(context.error_type)}: {sanitize_plain(context.error_message)}
- Root cause: {sanitize_plain(root_cause.summary)}
- Category: {root_cause.category.value}
- File: {context.test_file}
{grounding_section}
Similar fixes that worked:
{sanitize_plain(similar_text)}

Adapt the most relevant fix pattern to this specific case.

Before generating the fix, think step by step:
1. Which historical fix is most relevant and why?
2. What adaptations are needed for this specific failure?
3. Does the adapted fix address the root cause completely?

Return:
- diff: The adapted fix
- description: How you adapted the pattern
- confidence: How confident you are (0.0-1.0)
- reasoning: Step-by-step analysis of how you adapted the historical pattern
"""

            response = await self.instructor_client.generate(
                prompt,
                response_model=FixResponse,
            )
            self._record_budget("rag_fix", prompt, response)

            return FixHypothesis(
                id=str(uuid.uuid4()),
                strategy=FixStrategy.RAG_BASED,
                diff=response.diff,
                description=response.description,
                confidence=response.confidence,
                based_on=[fix.fix_id for fix in similar],
            )

        except Exception as e:
            logger.error(f"RAG-based fix generation failed: {e}")
            return None

    async def _generate_framework_specific(
        self,
        root_cause: RootCauseExplanation,
        context: FailureContext,
    ) -> Optional[FixHypothesis]:
        """Generate fix using framework-specific patterns.

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

        Returns:
            Framework-specific fix hypothesis.
        """
        if not self.instructor_client:
            return None

        # Detect framework from context
        framework = context.framework or self._detect_framework(context)

        framework_hints = self._get_framework_hints(framework, root_cause.category)

        # Build execution grounding section
        grounding_section = self._build_grounding_section(context)

        prompt = f"""
Generate a fix using {framework} best practices.

Failure:
- Error: {sanitize_plain(context.error_type)}: {sanitize_plain(context.error_message)}
- Root cause: {sanitize_plain(root_cause.summary)}
- Category: {root_cause.category.value}
{grounding_section}
{framework} patterns for {root_cause.category.value} issues:
{framework_hints}

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

Apply the most appropriate pattern to fix this issue.

Before generating the fix, think step by step:
1. Which framework pattern best addresses this category of issue?
2. How should the pattern be adapted to this specific code?
3. Does the pattern fully address the root cause?

Return:
- diff: The fix using framework patterns
- description: Which pattern you applied and why
- confidence: How confident you are (0.0-1.0)
- reasoning: Step-by-step analysis of the pattern selection and application
"""

        try:
            response = await self.instructor_client.generate(
                prompt,
                response_model=FixResponse,
            )
            self._record_budget("framework_fix", prompt, response)

            return FixHypothesis(
                id=str(uuid.uuid4()),
                strategy=FixStrategy.FRAMEWORK_SPECIFIC,
                diff=response.diff,
                description=response.description,
                confidence=response.confidence,
            )

        except Exception as e:
            logger.error(f"Framework-specific fix generation failed: {e}")
            return None

    async def _generate_creative(
        self,
        root_cause: RootCauseExplanation,
        context: FailureContext,
    ) -> Optional[FixHypothesis]:
        """Generate creative fix with higher temperature.

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

        Returns:
            Creative fix hypothesis.
        """
        if not self.instructor_client:
            return None

        # Build execution grounding section
        grounding_section = self._build_grounding_section(context)

        prompt = f"""
Generate a creative, robust fix for this failure.

Failure:
- Error: {sanitize_plain(context.error_type)}: {sanitize_plain(context.error_message)}
- Root cause: {sanitize_plain(root_cause.summary)}
- Category: {root_cause.category.value}
{grounding_section}
Code:
{sanitize_code(context.code_snippet[:2000]) if context.code_snippet else 'Not available'}

Think outside the box. Consider:
1. Is there a better architectural approach?
2. Can we prevent this class of bugs entirely?
3. Are there defensive coding patterns we could apply?

Don't be afraid to suggest more significant refactoring if it makes the code more robust.

Before generating the fix, reason step by step:
1. What is the underlying design issue that makes this code fragile?
2. What pattern would prevent this entire class of failures?
3. Does the fix introduce any new risks?

Return:
- diff: Your creative fix
- description: Your reasoning and approach
- confidence: How confident you are (0.0-1.0)
- reasoning: Step-by-step analysis of why this approach is more robust
"""

        try:
            # Use higher temperature for creativity
            response = await self.instructor_client.generate(
                prompt,
                response_model=FixResponse,
                temperature=0.8,
            )
            self._record_budget("creative_fix", prompt, response)

            return FixHypothesis(
                id=str(uuid.uuid4()),
                strategy=FixStrategy.CREATIVE,
                diff=response.diff,
                description=response.description,
                confidence=response.confidence,
            )

        except Exception as e:
            logger.error(f"Creative fix generation failed: {e}")
            return None

    async def _generate_ensemble(
        self,
        candidates: List[FixHypothesis],
        root_cause: RootCauseExplanation,
        context: FailureContext,
    ) -> Optional[FixHypothesis]:
        """Generate ensemble fix combining best elements.

        Args:
            candidates: Candidate fixes to combine.
            root_cause: Root cause explanation.
            context: Failure context.

        Returns:
            Ensemble fix hypothesis.
        """
        if not self.instructor_client or len(candidates) < 2:
            return None

        candidates_text = "\n\n".join([
            f"[{i+1}] {fix.strategy.value}:\n"
            f"Description: {fix.description}\n"
            f"Confidence: {fix.confidence:.0%}\n"
            f"Diff:\n{fix.diff[:400]}"
            for i, fix in enumerate(candidates)
        ])

        prompt = f"""
Combine the best elements from these fix candidates.

Failure:
- Error: {sanitize_plain(context.error_type)}: {sanitize_plain(context.error_message)}
- Root cause: {sanitize_plain(root_cause.summary)}

Fix candidates:
{candidates_text}

Create a combined fix that:
1. Takes the best approach from each candidate
2. Avoids weaknesses of individual fixes
3. Is complete and coherent

Return:
- diff: The combined fix
- description: How you combined the approaches
- confidence: How confident you are (0-1)
"""

        try:
            response = await self.instructor_client.generate(
                prompt,
                response_model=FixResponse,
            )
            self._record_budget("ensemble_fix", prompt, response)

            return FixHypothesis(
                id=str(uuid.uuid4()),
                strategy=FixStrategy.ENSEMBLE,
                diff=response.diff,
                description=response.description,
                confidence=response.confidence,
                based_on=[fix.id for fix in candidates],
            )

        except Exception as e:
            logger.error(f"Ensemble fix generation failed: {e}")
            return None

    def _deduplicate(
        self,
        fixes: List[FixHypothesis],
    ) -> List[FixHypothesis]:
        """Remove duplicate fixes based on diff similarity.

        Args:
            fixes: List of fix hypotheses.

        Returns:
            Deduplicated list.
        """
        seen_diffs = set()
        unique = []

        for fix in fixes:
            # Normalize diff for comparison
            normalized = fix.diff.strip().replace(" ", "").replace("\n", "")

            if normalized not in seen_diffs:
                seen_diffs.add(normalized)
                unique.append(fix)

        return unique

    def _ensure_diversity(
        self,
        fixes: List[FixHypothesis],
        similarity_threshold: float = 0.8,
    ) -> List[FixHypothesis]:
        """Reject hypotheses that are too similar to existing ones.

        Uses SequenceMatcher to compare diffs and ensures each fix is
        at least 20% different from all previously accepted fixes.

        Args:
            fixes: Deduplicated fix hypotheses.
            similarity_threshold: Max similarity allowed (0-1). Default 0.8.

        Returns:
            Diverse list of fixes.
        """
        if len(fixes) <= 1:
            return fixes

        diverse: List[FixHypothesis] = []
        for fix in fixes:
            if not diverse:
                diverse.append(fix)
                continue

            # Check similarity against all accepted fixes
            max_sim = max(
                self._diff_similarity(fix.diff, d.diff)
                for d in diverse
            )
            if max_sim < similarity_threshold:
                diverse.append(fix)
            else:
                logger.debug(
                    "Rejected fix from %s: %.0f%% similar to existing fix",
                    fix.strategy.value if fix.strategy else "unknown",
                    max_sim * 100,
                )

        return diverse

    @staticmethod
    def _diff_similarity(diff_a: str, diff_b: str) -> float:
        """Compute similarity ratio between two diffs.

        Args:
            diff_a: First diff string.
            diff_b: Second diff string.

        Returns:
            Similarity ratio 0-1.
        """
        if not diff_a or not diff_b:
            return 0.0
        return difflib.SequenceMatcher(None, diff_a.strip(), diff_b.strip()).ratio()

    async def _score_fixes(
        self,
        fixes: List[FixHypothesis],
        context: FailureContext,
    ) -> List[FixHypothesis]:
        """Score fixes using quality scorer with new factors.

        Scoring now considers:
        - Traditional quality metrics (syntax, imports, root cause)
        - Template match confidence (for template-based fixes)
        - Voting confidence (if voting was used)
        - Speculative verification results (if available)

        Args:
            fixes: Fixes to score.
            context: Failure context.

        Returns:
            Fixes with quality scores, sorted.
        """
        scorer = FixQualityScorer(
            self.instructor_client,
            use_execution_grounding=self.use_execution_grounding,
            grounded_evidence=self._grounded_evidence,
        )

        for fix in fixes:
            try:
                # Get base score from scorer
                base_score = await scorer.score(fix, context)

                # Apply additional scoring factors
                final_score = base_score

                # Boost for template-based fixes (already have confidence from template)
                if "Template-based" in fix.description:
                    # Template fixes get a slight boost for reliability
                    final_score = min(1.0, final_score * 1.05)

                # Boost if fix addresses grounded divergence
                if self._grounded_evidence and self._grounded_evidence.divergence:
                    if self._fix_addresses_divergence(fix, self._grounded_evidence):
                        final_score = min(1.0, final_score + 0.1)

                fix.quality_score = final_score

            except Exception as e:
                logger.error(f"Scoring failed for fix {fix.id}: {e}")
                fix.quality_score = fix.confidence * 0.5  # Fallback

        # Sort by quality score
        fixes.sort(key=lambda f: f.quality_score, reverse=True)
        return fixes

    def _fix_addresses_divergence(
        self,
        fix: FixHypothesis,
        evidence: GroundedEvidence,
    ) -> bool:
        """Check if a fix appears to address the divergence point.

        Args:
            fix: Fix hypothesis.
            evidence: Grounded evidence with divergence info.

        Returns:
            True if fix seems to address the divergence.
        """
        if not evidence.divergence:
            return False

        diff_lower = fix.diff.lower()
        desc_lower = fix.description.lower()

        # Check for divergence-related keywords in the fix
        divergence_type = evidence.divergence.divergence_type.value.lower()

        # Simple keyword matching
        type_keywords = {
            "http_status": ["status", "response", "http"],
            "http_body": ["body", "response", "data", "json"],
            "sql_result": ["query", "sql", "result", "rows"],
            "sql_rowcount": ["count", "rows", "query"],
            "redis_value": ["cache", "redis", "value"],
            "event_count": ["count", "events"],
            "event_order": ["order", "sorted", "sequence"],
        }

        keywords = type_keywords.get(divergence_type, [])
        for keyword in keywords:
            if keyword in diff_lower or keyword in desc_lower:
                return True

        return False

    def _detect_framework(self, context: FailureContext) -> str:
        """Detect testing framework from context.

        Args:
            context: Failure context.

        Returns:
            Framework name.
        """
        code = (context.code_snippet or "") + (context.stack_trace or "")

        if "pytest" in code:
            return "pytest"
        if "unittest" in code:
            return "unittest"
        if "django" in code.lower():
            return "django"
        if "flask" in code.lower():
            return "flask"
        if "fastapi" in code.lower():
            return "fastapi"

        return "pytest"  # Default

    def _get_framework_hints(
        self,
        framework: str,
        category,
    ) -> str:
        """Get framework-specific hints for fix generation.

        Args:
            framework: Framework name.
            category: Flakiness category.

        Returns:
            Framework hints.
        """
        from autodebug.llm_first.models import FlakinessCategory

        hints = {
            "pytest": {
                FlakinessCategory.TIMING: """
- Use pytest-asyncio with proper await patterns
- Use pytest-timeout for time limits
- Use freezegun to mock time
- Use async fixtures with scope management
""",
                FlakinessCategory.STATE: """
- Use function-scoped fixtures for isolation
- Use pytest.mark.usefixtures for cleanup
- Use tmp_path fixture for temp files
- Use monkeypatch for environment isolation
""",
                FlakinessCategory.ORDERING: """
- Add ORDER BY to SQL queries
- Use sorted() when comparing unordered collections
- Use pytest-randomly to expose order dependencies
- Don't rely on dict/set ordering
""",
            },
            "unittest": {
                FlakinessCategory.TIMING: "Use asyncio.run() with timeout",
                FlakinessCategory.STATE: "Use setUp/tearDown for proper cleanup",
                FlakinessCategory.ORDERING: "Sort results before comparison",
            },
        }

        framework_hints = hints.get(framework, hints["pytest"])
        category_hint = framework_hints.get(category, "Apply general best practices")

        return category_hint


class FixQualityScorer:
    """Score fix quality using multiple signals.

    Enhanced to consider:
    - Template match confidence
    - Voting confidence
    - Speculative verification results
    - Execution grounding alignment
    """

    def __init__(
        self,
        instructor_client=None,
        use_execution_grounding: bool = False,
        grounded_evidence: Optional[GroundedEvidence] = None,
    ):
        """Initialize quality scorer.

        Args:
            instructor_client: LLM client for semantic checks.
            use_execution_grounding: Whether to use execution grounding in scoring.
            grounded_evidence: Pre-computed grounded evidence.
        """
        self.instructor_client = instructor_client
        self.use_execution_grounding = use_execution_grounding
        self.grounded_evidence = grounded_evidence

    async def score(
        self,
        fix: FixHypothesis,
        context: FailureContext,
    ) -> float:
        """Score fix quality using multiple signals.

        Args:
            fix: Fix to score.
            context: Failure context.

        Returns:
            Quality score (0-1).
        """
        scores = FixQualityScores()

        # Syntactic checks (fast, no LLM)
        scores.syntax_valid = self._check_syntax(fix)
        scores.imports_valid = self._check_imports(fix)

        # Semantic checks (need LLM)
        if self.instructor_client:
            scores.addresses_root_cause = await self._check_root_cause(fix, context)
            scores.minimal_change = self._estimate_minimality(fix)
            scores.style_consistent = 0.7  # Would need more context

        # New: Check pattern confidence for template-based fixes
        scores.pattern_confidence = self._check_pattern_confidence(fix)

        # New: Check alignment with grounded evidence
        grounding_score = 0.0
        if self.use_execution_grounding and self.grounded_evidence:
            grounding_score = self._score_grounding_alignment(fix, context)

        # Compute weighted total with new factors
        weights = {
            "syntax_valid": 0.12,
            "imports_valid": 0.05,
            "addresses_root_cause": 0.35,
            "minimal_change": 0.13,
            "style_consistent": 0.05,
            "pattern_confidence": 0.10,  # New: template match confidence
        }

        # Also factor in the fix's own confidence
        weighted_sum = sum(
            getattr(scores, k, 0.0) * v
            for k, v in weights.items()
        )

        # Add grounding alignment (new factor)
        weighted_sum += grounding_score * 0.10

        # Combine with fix confidence
        scores.total = weighted_sum * 0.7 + fix.confidence * 0.3

        return scores.total

    def _check_pattern_confidence(self, fix: FixHypothesis) -> float:
        """Check if fix is from a high-confidence pattern/template.

        Args:
            fix: Fix to check.

        Returns:
            Pattern confidence score (0-1).
        """
        # Template-based fixes already have confidence from template
        if fix.strategy == FixStrategy.TEMPLATE_BASED:
            # The fix's confidence was set from the template's confidence
            return fix.confidence

        # RAG-based fixes get a moderate boost if they have based_on references
        if fix.strategy == FixStrategy.RAG_BASED and fix.based_on:
            return 0.7

        # Default: no pattern confidence
        return 0.5

    def _score_grounding_alignment(
        self,
        fix: FixHypothesis,
        context: FailureContext,
    ) -> float:
        """Score how well fix aligns with grounded execution evidence.

        Args:
            fix: Fix to score.
            context: Failure context.

        Returns:
            Grounding alignment score (0-1).
        """
        if not self.grounded_evidence:
            return 0.5

        score = 0.5  # Base score

        # Check if fix addresses divergence point
        if self.grounded_evidence.divergence:
            divergence = self.grounded_evidence.divergence
            diff_lower = fix.diff.lower()

            # Check for divergence-type specific patterns
            type_patterns = {
                "http_status": ["status", "response", "error", "retry"],
                "http_body": ["body", "json", "data", "response"],
                "sql_result": ["order by", "sorted", "sort", "query"],
                "sql_rowcount": ["count", "rows", "limit"],
                "event_order": ["sorted", "order", "sort"],
            }

            div_type = divergence.divergence_type.value.lower().replace("_diff", "")
            patterns = type_patterns.get(div_type, [])

            for pattern in patterns:
                if pattern in diff_lower:
                    score += 0.15
                    break

        # Check if fix references suspicious variables
        if self.grounded_evidence.relevant_locals:
            locals_data = self.grounded_evidence.relevant_locals
            suspicious = locals_data.get("suspicious", [])
            if suspicious:
                diff_lower = fix.diff.lower()
                for var in suspicious:
                    if isinstance(var, str) and var.lower() in diff_lower:
                        score += 0.1
                        break

        return min(1.0, score)

    def _check_syntax(self, fix: FixHypothesis) -> float:
        """Quick syntax check on the diff.

        Args:
            fix: Fix to check.

        Returns:
            Score (0-1).
        """
        diff = fix.diff

        # Basic checks
        if not diff.strip():
            return 0.0

        # Extract only added lines from the diff for bracket checking
        # (raw diff has both + and - lines, so bracket balance is meaningless)
        added_lines = '\n'.join(
            line[1:] for line in diff.splitlines()
            if line.startswith('+') and not line.startswith('+++')
        )
        if not added_lines.strip():
            return 0.8  # No added lines to check

        # Try AST parse of added lines for syntax validation
        try:
            import ast as _ast
            import textwrap as _tw
            _ast.parse(_tw.dedent(added_lines))
            return 1.0  # Valid Python syntax
        except SyntaxError:
            # Added lines may be partial (not a complete module) — check brackets
            issues = 0
            if added_lines.count("(") != added_lines.count(")"):
                issues += 1
            if added_lines.count("[") != added_lines.count("]"):
                issues += 1
            if added_lines.count("{") != added_lines.count("}"):
                issues += 1
            return max(0.0, 0.8 - (issues * 0.2))

    def _check_imports(self, fix: FixHypothesis) -> float:
        """Check if required imports are present.

        Args:
            fix: Fix to check.

        Returns:
            Score (0-1).
        """
        # This would need more context in practice
        diff = fix.diff

        # Check for common missing imports
        patterns = [
            ("async def", "import asyncio"),
            ("await ", "async def"),
            ("time.sleep", "import time"),
            ("sorted(", ""),  # built-in, no import needed
        ]

        score = 1.0
        for pattern, required in patterns:
            if pattern in diff and required and required not in diff:
                score -= 0.1

        return max(0.0, score)

    async def _check_root_cause(
        self,
        fix: FixHypothesis,
        context: FailureContext,
    ) -> float:
        """LLM evaluates if fix addresses root cause vs symptoms.

        Args:
            fix: Fix to check.
            context: Failure context.

        Returns:
            Score (0-1).
        """
        if not self.instructor_client or not context.root_cause:
            return 0.5

        prompt = f"""
Evaluate if this fix addresses the root cause or just suppresses symptoms.

Root cause: {context.root_cause.summary}
Category: {context.root_cause.category.value}

Proposed fix:
```diff
{fix.diff[:1500]}
```

Red flags (symptom suppression):
- Bare except clauses
- Catching and ignoring errors
- Adding retries without fixing underlying issue
- Disabling assertions
- Adding try/except around the failure

Green flags (root cause fix):
- Addresses the specific condition that caused failure
- Adds proper validation at the source
- Fixes the logic error directly
- Adds proper synchronization for timing issues
- Adds ORDER BY for ordering issues

Score 0-1 where 1 = definitely addresses root cause.
"""

        try:
            response = await self.instructor_client.generate(
                prompt,
                response_model=RootCauseScore,
            )
            return response.score

        except Exception as e:
            logger.error(f"Root cause check failed: {e}")
            return 0.5

    def _estimate_minimality(self, fix: FixHypothesis) -> float:
        """Estimate how minimal the fix is.

        Args:
            fix: Fix to check.

        Returns:
            Score (0-1) where 1 is very minimal.
        """
        diff = fix.diff

        # Count changed lines (unified diff format)
        lines = diff.split("\n")
        add_lines = sum(1 for l in lines if l.startswith("+") and not l.startswith("+++"))
        remove_lines = sum(1 for l in lines if l.startswith("-") and not l.startswith("---"))
        total_changes = add_lines + remove_lines

        if total_changes == 0:
            return 0.0  # Empty fix
        if total_changes <= 3:
            return 1.0  # Very minimal
        if total_changes <= 10:
            return 0.8
        if total_changes <= 20:
            return 0.6
        if total_changes <= 50:
            return 0.4

        return 0.2  # Large change
