"""Multi-stage repair pipeline for generating high-quality fixes.

Orchestrates multiple stages: Generate → Validate → Verify → Test → Rank
to ensure only high-quality, verified fixes reach users.
"""

from __future__ import annotations

import logging
import time
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Protocol

if TYPE_CHECKING:
    from autodebug.pipeline.fix_ranker import FixRanker
    from autodebug.pipeline.fix_tester import FixTester
    from autodebug.validation import FixValidator, RootCauseVerifier

logger = logging.getLogger(__name__)


class PipelineStage(str, Enum):
    """Stages in the repair pipeline."""

    GENERATE = "generate"
    VALIDATE = "validate"
    VERIFY = "verify"
    TEST = "test"
    RANK = "rank"


@dataclass
class GeneratedFix:
    """A generated fix candidate."""

    unified_diff: str
    explanation: str
    strategy: str = "direct"
    temperature: float = 0.2
    metadata: Dict[str, Any] = field(default_factory=dict)

    def __str__(self) -> str:
        return f"Fix(strategy={self.strategy}, {len(self.unified_diff)} chars)"

    @property
    def diff_lines(self) -> int:
        """Count the number of changed lines."""
        count = 0
        for line in self.unified_diff.split("\n"):
            if line.startswith("+") and not line.startswith("+++"):
                count += 1
            elif line.startswith("-") and not line.startswith("---"):
                count += 1
        return count


@dataclass
class FailureContext:
    """Context about a failure to fix."""

    source_code: str
    file_path: str
    exception_type: str
    exception_message: str
    line_number: int
    function_name: Optional[str] = None
    stated_root_cause: str = ""
    test_command: Optional[str] = None
    stack_trace: str = ""
    metadata: Dict[str, Any] = field(default_factory=dict)

    def __str__(self) -> str:
        return f"FailureContext({self.exception_type} at {self.file_path}:{self.line_number})"


@dataclass
class PipelineResult:
    """Result from a pipeline stage."""

    stage: PipelineStage
    passed: bool
    fix: GeneratedFix
    details: Dict[str, Any] = field(default_factory=dict)
    duration_ms: float = 0.0

    def __str__(self) -> str:
        status = "PASSED" if self.passed else "FAILED"
        return f"{self.stage.value}: {status} ({self.duration_ms:.1f}ms)"


@dataclass
class PipelineFix:
    """A fix that has been processed through the pipeline."""

    fix: GeneratedFix
    results: List[PipelineResult] = field(default_factory=list)
    final_score: float = 0.0
    passed_all_stages: bool = False

    def __str__(self) -> str:
        stages_passed = sum(1 for r in self.results if r.passed)
        return f"PipelineFix(score={self.final_score:.1f}, passed={stages_passed}/{len(self.results)})"

    def get_result(self, stage: PipelineStage) -> Optional[PipelineResult]:
        """Get result for a specific stage."""
        for result in self.results:
            if result.stage == stage:
                return result
        return None

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for API response."""
        return {
            "diff": self.fix.unified_diff,
            "explanation": self.fix.explanation,
            "score": self.final_score,
            "passed_all_stages": self.passed_all_stages,
            "strategy": self.fix.strategy,
            "pipeline_results": {
                r.stage.value: {
                    "passed": r.passed,
                    "duration_ms": r.duration_ms,
                    **r.details,
                }
                for r in self.results
            },
        }


@dataclass
class PipelineConfig:
    """Configuration for the repair pipeline."""

    max_candidates: int = 5
    max_fixes_to_return: int = 3
    test_timeout: int = 30  # seconds
    run_tests: bool = True
    run_verification: bool = True
    parallel_validation: bool = True
    min_verification_confidence: float = 0.6
    deduplicate_threshold: float = 0.9  # Similarity threshold for deduplication
    run_mutation_validation: bool = False  # Gate mutation testing (H-138)
    mutation_max_mutants: int = 20  # Max mutants for mutation validation


class FixGeneratorProtocol(Protocol):
    """Protocol for fix generators."""

    async def generate(
        self,
        context: FailureContext,
        temperature: float = 0.2,
        strategy: str = "direct",
    ) -> Optional[GeneratedFix]:
        """Generate a fix for the given context."""
        ...


class RepairPipeline:
    """Multi-stage repair pipeline for high-quality fixes.

    Orchestrates multiple stages to ensure only validated,
    verified fixes reach users.
    """

    def __init__(
        self,
        generator: Optional[FixGeneratorProtocol] = None,
        validator: Optional[FixValidator] = None,
        verifier: Optional[RootCauseVerifier] = None,
        tester: Optional[FixTester] = None,
        ranker: Optional[FixRanker] = None,
        config: Optional[PipelineConfig] = None,
    ):
        """Initialize the repair pipeline.

        Args:
            generator: Fix generator to use
            validator: Fix validator for compilation checks
            verifier: Root cause verifier
            tester: Fix tester for execution
            ranker: Fix ranker for scoring
            config: Pipeline configuration
        """
        self.generator = generator
        self.validator = validator
        self.verifier = verifier
        self.tester = tester
        self.ranker = ranker
        self.config = config or PipelineConfig()

    async def run(self, failure_context: FailureContext) -> List[PipelineFix]:
        """Run the full repair pipeline.

        Args:
            failure_context: Context about the failure to fix

        Returns:
            List of pipeline fixes, ranked by quality
        """
        # Stage 1: Generate multiple fix candidates
        candidates = await self._stage_generate(failure_context)
        if not candidates:
            return []

        # Stage 2: Validate (syntax, compilation, types)
        validated = await self._stage_validate(candidates, failure_context)
        if not validated:
            # Return best invalid candidate with feedback
            return self._create_feedback_response(candidates)

        # Stage 3: Verify root cause is addressed
        verified = validated
        if self.config.run_verification and self.verifier:
            verified = await self._stage_verify(validated, failure_context)
            # Even if no verified fixes, continue with validated (marked unverified)
            if not verified:
                for pf in validated:
                    pf.passed_all_stages = False
                verified = validated

        # Stage 4: Test fixes (if test available)
        tested = verified
        if self.config.run_tests and self.tester and failure_context.test_command:
            tested = await self._stage_test(verified, failure_context)
            # Even if no tested fixes, continue with verified (marked untested)
            if not tested:
                for pf in verified:
                    pf.passed_all_stages = False
                tested = verified

        # Stage 5: Rank and return best fixes
        ranked = await self._stage_rank(tested)

        return ranked

    async def _stage_generate(
        self,
        context: FailureContext,
    ) -> List[GeneratedFix]:
        """Generate multiple fix candidates.

        Args:
            context: Failure context

        Returns:
            List of generated fixes
        """
        if not self.generator:
            return []

        fixes: List[GeneratedFix] = []

        # Strategy 1: Direct fix (temp=0.2)
        fix1 = await self.generator.generate(context, temperature=0.2, strategy="direct")
        if fix1:
            fixes.append(fix1)

        # Strategy 2: Alternative approach (temp=0.7)
        fix2 = await self.generator.generate(context, temperature=0.7, strategy="alternative")
        if fix2:
            fixes.append(fix2)

        # Strategy 3: Minimal change (temp=0.1)
        fix3 = await self.generator.generate(context, temperature=0.1, strategy="minimal")
        if fix3:
            fixes.append(fix3)

        # Strategy 4: Type-safe (with type constraints)
        fix4 = await self.generator.generate(context, temperature=0.3, strategy="type_safe")
        if fix4:
            fixes.append(fix4)

        # Strategy 5: Defense in depth (add validation)
        fix5 = await self.generator.generate(context, temperature=0.4, strategy="defensive")
        if fix5:
            fixes.append(fix5)

        # Deduplicate similar fixes
        fixes = self._deduplicate_fixes(fixes)

        return fixes[: self.config.max_candidates]

    async def _stage_validate(
        self,
        fixes: List[GeneratedFix],
        context: FailureContext,
    ) -> List[PipelineFix]:
        """Validate fixes compile and pass static checks.

        Args:
            fixes: List of generated fixes
            context: Failure context

        Returns:
            List of validated pipeline fixes
        """
        validated: List[PipelineFix] = []

        for fix in fixes:
            start = time.perf_counter()

            # Use validator if available
            if self.validator:
                result = self.validator.validate_diff(
                    fix.unified_diff,
                    context.source_code,
                )

                pipeline_result = PipelineResult(
                    stage=PipelineStage.VALIDATE,
                    passed=result.is_valid,
                    fix=fix,
                    details={
                        "syntax_valid": result.syntax_valid,
                        "compiles": result.compiles,
                        "type_checks": result.type_checks if hasattr(result, "type_checks") else True,
                        "errors": [str(e) for e in result.errors] if result.errors else [],
                    },
                    duration_ms=(time.perf_counter() - start) * 1000,
                )

                if result.is_valid:
                    validated.append(PipelineFix(
                        fix=fix,
                        results=[pipeline_result],
                        final_score=0.0,
                        passed_all_stages=False,
                    ))
            else:
                # No validator — extract added lines from diff for basic syntax check
                syntax_ok = True
                diff_text = getattr(fix, 'unified_diff', None) or getattr(fix, 'diff', None) or str(fix)
                added_lines = [
                    line[1:] for line in diff_text.splitlines()
                    if line.startswith('+') and not line.startswith('+++')
                ]
                if added_lines:
                    try:
                        import ast as _ast
                        _ast.parse('\n'.join(added_lines))
                    except SyntaxError:
                        # Added lines failed to parse — reject the fix
                        syntax_ok = False

                pipeline_result = PipelineResult(
                    stage=PipelineStage.VALIDATE,
                    passed=syntax_ok,
                    fix=fix,
                    details={"syntax_valid": syntax_ok, "validator": "basic_diff"},
                    duration_ms=(time.perf_counter() - start) * 1000,
                )
                validated.append(PipelineFix(
                    fix=fix,
                    results=[pipeline_result],
                    final_score=0.0,
                    passed_all_stages=False,
                ))

        return validated

    async def _stage_verify(
        self,
        fixes: List[PipelineFix],
        context: FailureContext,
    ) -> List[PipelineFix]:
        """Verify fixes address root cause, not just symptoms.

        Args:
            fixes: List of validated fixes
            context: Failure context

        Returns:
            List of verified pipeline fixes
        """
        verified: List[PipelineFix] = []

        for pipeline_fix in fixes:
            start = time.perf_counter()

            if self.verifier:
                result = self.verifier.verify(
                    root_cause=context.stated_root_cause or "Unknown",
                    fix_diff=pipeline_fix.fix.unified_diff,
                    original_code=context.source_code,
                    exception_type=context.exception_type,
                    exception_message=context.exception_message,
                )

                pipeline_result = PipelineResult(
                    stage=PipelineStage.VERIFY,
                    passed=result.addresses_root_cause,
                    fix=pipeline_fix.fix,
                    details={
                        "addresses_root_cause": result.addresses_root_cause,
                        "confidence": result.confidence,
                        "reasoning": result.reasoning,
                        "issues": result.issues,
                    },
                    duration_ms=(time.perf_counter() - start) * 1000,
                )

                pipeline_fix.results.append(pipeline_result)

                # Accept if addresses root cause OR has high confidence
                if result.addresses_root_cause or result.confidence > self.config.min_verification_confidence:
                    verified.append(pipeline_fix)
            else:
                # No verifier, pass through
                verified.append(pipeline_fix)

        return verified

    async def _stage_test(
        self,
        fixes: List[PipelineFix],
        context: FailureContext,
    ) -> List[PipelineFix]:
        """Test fixes by running the failing test.

        Args:
            fixes: List of verified fixes
            context: Failure context

        Returns:
            List of tested pipeline fixes
        """
        if not context.test_command or not self.tester:
            return fixes

        tested: List[PipelineFix] = []

        for pipeline_fix in fixes:
            start = time.perf_counter()

            result = await self.tester.test_fix(
                fix=pipeline_fix.fix,
                test_command=context.test_command,
                file_path=context.file_path,
                original_code=context.source_code,
                timeout=self.config.test_timeout,
            )

            pipeline_result = PipelineResult(
                stage=PipelineStage.TEST,
                passed=result.test_passed,
                fix=pipeline_fix.fix,
                details={
                    "test_passed": result.test_passed,
                    "test_output": result.output[:1000] if result.output else None,
                    "exit_code": result.exit_code,
                },
                duration_ms=(time.perf_counter() - start) * 1000,
            )

            pipeline_fix.results.append(pipeline_result)

            if result.test_passed:
                pipeline_fix.passed_all_stages = True

                # H-136: Run regression check on the broader test module
                if self.tester and hasattr(self.tester, '_run_regression_check') and context.test_command:
                    try:
                        # Extract the test file from the test command
                        test_file = context.test_command.split()[-1] if context.test_command else ""
                        regression_result = await self.tester._run_regression_check(
                            fix=pipeline_fix.fix,
                            test_file=test_file,
                            file_path=context.file_path,
                            original_code=context.source_code,
                            timeout=self.config.test_timeout * 2,
                        )
                        pipeline_fix.fix.metadata["regression_check"] = {
                            "passed": regression_result.test_passed,
                            "exit_code": regression_result.exit_code,
                        }
                        if not regression_result.test_passed:
                            pipeline_fix.passed_all_stages = False
                            logger.warning(
                                f"Fix passed target test but failed regression check"
                            )
                    except Exception as e:
                        logger.warning(f"Regression check skipped: {e}")

                # Optional: run mutation validation to assess fix quality (H-138)
                if self.config.run_mutation_validation:
                    try:
                        from autodebug.quality.mutation_testing import validate_fix_with_mutations

                        mutation_score = await validate_fix_with_mutations(
                            fix_file=context.file_path,
                            test_file=context.test_command.split()[-1] if context.test_command else "",
                            project_root=str(Path(context.file_path).parent),
                            max_mutants=self.config.mutation_max_mutants,
                        )
                        pipeline_fix.fix.metadata["mutation_score"] = mutation_score.to_dict()
                        logger.info(
                            f"Mutation validation: score={mutation_score.mutation_score:.1%}, "
                            f"rating={mutation_score.quality_rating}"
                        )
                    except Exception as e:
                        logger.warning(f"Mutation validation skipped: {e}")

                tested.append(pipeline_fix)

        return tested

    async def _stage_rank(self, fixes: List[PipelineFix]) -> List[PipelineFix]:
        """Rank fixes by quality score.

        Fixes that did not pass all pipeline stages are excluded from
        the final ranking. If no fix passed all stages, the best-scoring
        failed fix is returned with passed_all_stages=False so callers
        can distinguish it from a fully validated fix.

        Args:
            fixes: List of fixes to rank

        Returns:
            Sorted list of fixes
        """
        if self.ranker:
            for fix in fixes:
                fix.final_score = self.ranker.calculate_score(fix)
        else:
            # Simple default scoring
            for fix in fixes:
                fix.final_score = self._default_score(fix)

        # Separate fixes that passed all stages from those that didn't
        passed_fixes = [f for f in fixes if f.passed_all_stages]
        failed_fixes = [f for f in fixes if not f.passed_all_stages]

        # Sort each group by score descending
        passed_fixes.sort(key=lambda f: f.final_score, reverse=True)
        failed_fixes.sort(key=lambda f: f.final_score, reverse=True)

        if passed_fixes:
            return passed_fixes[: self.config.max_fixes_to_return]

        # No fully-passed fixes — return the best failed fix so callers
        # get feedback, but it remains marked passed_all_stages=False.
        if failed_fixes:
            return failed_fixes[:1]

        return []

    def _default_score(self, fix: PipelineFix) -> float:
        """Calculate a default score for a fix.

        Args:
            fix: Pipeline fix to score

        Returns:
            Score value
        """
        score = 0.0

        for result in fix.results:
            if result.passed:
                if result.stage == PipelineStage.VALIDATE:
                    score += 20
                elif result.stage == PipelineStage.VERIFY:
                    score += 30
                elif result.stage == PipelineStage.TEST:
                    score += 40

        # Bonus for minimal changes
        if fix.fix.diff_lines < 5:
            score += 10
        elif fix.fix.diff_lines < 10:
            score += 5

        return score

    def _deduplicate_fixes(self, fixes: List[GeneratedFix]) -> List[GeneratedFix]:
        """Remove duplicate or very similar fixes.

        Args:
            fixes: List of fixes to deduplicate

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

        for fix in fixes:
            # Normalize diff for comparison
            normalized = self._normalize_diff(fix.unified_diff)
            if normalized not in seen_diffs:
                seen_diffs.add(normalized)
                unique.append(fix)

        return unique

    def _normalize_diff(self, diff: str) -> str:
        """Normalize diff for deduplication using AST comparison.

        Tries to parse added lines as Python and compare their AST dump.
        This catches semantically equivalent patches that differ only in
        whitespace, comments, or variable naming style.  Falls back to
        stripped (but unsorted) line comparison when AST parsing fails,
        preserving statement order so that reordered statements are
        correctly treated as distinct patches.

        Args:
            diff: Unified diff string

        Returns:
            Normalized diff string for comparison
        """
        import ast as _ast

        added = []
        removed = []
        for line in diff.split("\n"):
            if line.startswith("+") and not line.startswith("+++"):
                added.append(line[1:])
            elif line.startswith("-") and not line.startswith("---"):
                removed.append(line[1:])

        # Try AST-based normalization for added lines
        try:
            added_code = "\n".join(added)
            added_tree = _ast.parse(added_code)
            added_norm = _ast.dump(added_tree)
        except SyntaxError:
            # Partial diff lines may not parse; fall back to text
            added_norm = "\n".join(line.strip() for line in added)

        # Same for removed lines
        try:
            removed_code = "\n".join(removed)
            removed_tree = _ast.parse(removed_code)
            removed_norm = _ast.dump(removed_tree)
        except SyntaxError:
            removed_norm = "\n".join(line.strip() for line in removed)

        return f"ADD:{added_norm}\nDEL:{removed_norm}"

    def _create_feedback_response(
        self,
        candidates: List[GeneratedFix],
    ) -> List[PipelineFix]:
        """Create feedback response when no valid fixes found.

        Args:
            candidates: Original candidate fixes

        Returns:
            List with feedback about validation failures
        """
        if not candidates:
            return []

        # Return the first candidate with validation feedback
        best = candidates[0]
        return [PipelineFix(
            fix=best,
            results=[PipelineResult(
                stage=PipelineStage.VALIDATE,
                passed=False,
                fix=best,
                details={"message": "No fixes passed validation"},
            )],
            final_score=0.0,
            passed_all_stages=False,
        )]


def create_failure_context(
    source_code: str,
    file_path: str,
    exception_type: str,
    exception_message: str,
    line_number: int,
    **kwargs: Any,
) -> FailureContext:
    """Convenience function to create a FailureContext.

    Args:
        source_code: Source code of the failing file
        file_path: Path to the failing file
        exception_type: Type of exception
        exception_message: Exception message
        line_number: Line number of failure
        **kwargs: Additional context fields

    Returns:
        FailureContext instance
    """
    return FailureContext(
        source_code=source_code,
        file_path=file_path,
        exception_type=exception_type,
        exception_message=exception_message,
        line_number=line_number,
        **kwargs,
    )
