"""Fix-related tools for the autonomous debugging agent.

This module provides tools for searching historical fixes and
generating new fix hypotheses.
"""

from __future__ import annotations

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

from autodebug.llm_first.autonomous import BaseTool
from autodebug.llm_first.models import (
    FailureContext,
    FailureQuery,
    FixHypothesis,
    FixResponse,
    FlakinessCategory,
    RootCauseExplanation,
    SimilarFix,
)

if TYPE_CHECKING:
    from autodebug.llm_first.hybrid_search import HybridFixSearch
    from autodebug.llm_first.multi_hypothesis import MultiHypothesisFixer

logger = logging.getLogger(__name__)


class SearchSimilarFixesTool(BaseTool):
    """Search for similar historical fixes.

    This tool delegates to HybridFixSearch to find fixes from the
    knowledge base that addressed similar errors in the past.
    """

    name = "search_similar_fixes"
    description = """
    Search for similar fixes from the knowledge base.

    Parameters:
    - error_type: The type of error (e.g., "AssertionError")
    - error_message: The error message
    - category: Optional category filter (TIMING, ORDERING, etc.)
    - limit: Number of results (default 5)

    Returns: List of similar fixes with:
    - fix_id: Unique identifier
    - description: What the fix does
    - diff: The actual fix diff
    - similarity_score: How similar to current error
    - success_rate: Verification pass rate
    - times_used: How often this fix pattern was used

    Use this tool to:
    - Find fixes that worked for similar errors
    - Learn from past debugging patterns
    - Get inspiration for fix approaches
    """

    def __init__(
        self,
        fix_search: Optional[HybridFixSearch] = None,
        db=None,
        embedding_pipeline=None,
    ):
        """Initialize SearchSimilarFixesTool.

        Args:
            fix_search: HybridFixSearch instance.
            db: Database client for fallback search.
            embedding_pipeline: Embedding pipeline for queries.
        """
        self.fix_search = fix_search
        self.db = db
        self.embedding_pipeline = embedding_pipeline

    async def execute(
        self,
        error_type: str,
        error_message: str,
        category: Optional[str] = None,
        limit: int = 5,
    ) -> List[Dict[str, Any]]:
        """Search for similar fixes.

        Args:
            error_type: Error type to search for.
            error_message: Error message.
            category: Optional category filter.
            limit: Maximum results to return.

        Returns:
            List of similar fix dictionaries.
        """
        try:
            # Parse category if provided
            category_enum = None
            if category:
                try:
                    category_enum = FlakinessCategory(category.lower())
                except ValueError:
                    logger.warning(f"Invalid category: {category}")

            # Use HybridFixSearch if available
            if self.fix_search:
                query = FailureQuery(
                    error_type=error_type,
                    error_message=error_message,
                    category=category_enum,
                )

                results = await self.fix_search.search(query, top_k=limit)
                return self._format_results(results)

            # Fall back to direct database search
            if self.db:
                return await self._fallback_search(
                    error_type, error_message, category_enum, limit
                )

            return [{
                "info": "No search backend available. "
                        "Configure fix_search or db to enable this tool."
            }]

        except Exception as e:
            logger.error(f"Search error: {e}")
            return [{"error": f"Search failed: {e}"}]

    def _format_results(self, results: List[SimilarFix]) -> List[Dict[str, Any]]:
        """Format SimilarFix results for output.

        Args:
            results: List of SimilarFix objects.

        Returns:
            List of formatted dictionaries.
        """
        formatted = []

        for fix in results:
            formatted.append({
                "fix_id": fix.fix_id,
                "similarity_score": round(fix.similarity_score, 3),
                "error_type": fix.error_type,
                "category": fix.category.value,
                "description": fix.description,
                "diff": self._truncate_diff(fix.diff, 1000),
                "success_rate": round(fix.verification_pass_rate, 2),
                "user_acceptance_rate": (
                    round(fix.user_acceptance_rate, 2)
                    if fix.user_acceptance_rate else None
                ),
                "times_successful": fix.times_successful,
                "strategy": fix.strategy.value if fix.strategy else None,
            })

        return formatted

    async def _fallback_search(
        self,
        error_type: str,
        error_message: str,
        category: Optional[FlakinessCategory],
        limit: int,
    ) -> List[Dict[str, Any]]:
        """Fallback to direct database search.

        Args:
            error_type: Error type.
            error_message: Error message.
            category: Optional category filter.
            limit: Maximum results.

        Returns:
            List of fix dictionaries.
        """
        query = """
            SELECT
                id,
                error_type,
                category,
                description,
                diff,
                verification_pass_rate,
                user_acceptance_rate,
                times_led_to_success
            FROM verified_fixes
            WHERE error_type ILIKE $1
            AND ($2::text IS NULL OR category = $2)
            ORDER BY
                verification_pass_rate DESC,
                times_led_to_success DESC
            LIMIT $3
        """

        try:
            results = await self.db.fetch(
                query,
                f"%{error_type}%",
                category.value if category else None,
                limit,
            )

            return [
                {
                    "fix_id": str(r["id"]),
                    "similarity_score": 0.5,  # No embedding score in fallback
                    "error_type": r["error_type"],
                    "category": r["category"],
                    "description": r["description"],
                    "diff": self._truncate_diff(r["diff"], 1000),
                    "success_rate": float(r["verification_pass_rate"] or 0),
                    "user_acceptance_rate": (
                        float(r["user_acceptance_rate"])
                        if r["user_acceptance_rate"] else None
                    ),
                    "times_successful": int(r["times_led_to_success"] or 0),
                }
                for r in results
            ]

        except Exception as e:
            logger.error(f"Fallback search failed: {e}")
            return []

    def _truncate_diff(self, diff: str, max_len: int) -> str:
        """Truncate diff to max length."""
        if len(diff) <= max_len:
            return diff
        return diff[:max_len] + "\n... (truncated)"


class GenerateFixTool(BaseTool):
    """Generate a fix for the identified root cause.

    This tool delegates to MultiHypothesisFixer to generate fix
    hypotheses using multiple strategies (conservative, RAG-based,
    framework-specific, creative, ensemble).
    """

    name = "generate_fix"
    description = """
    Generate fix hypotheses for the identified root cause.

    Parameters:
    - root_cause: Summary of the root cause
    - category: Flakiness category (TIMING, ORDERING, etc.)
    - code_context: Relevant source code
    - error_type: The exception type
    - error_message: The error message
    - file_path: Path to the file to fix
    - similar_fixes: Optional list of similar fix IDs to use as reference

    Returns: List of fix hypotheses with:
    - id: Unique fix ID
    - strategy: The generation strategy used
    - diff: The fix as a unified diff
    - description: Explanation of the fix
    - confidence: Confidence score (0-1)
    - quality_score: Quality score (0-1)

    Use this tool to:
    - Generate candidate fixes after identifying the root cause
    - Get multiple fix options to choose from
    - Apply different fix strategies
    """

    def __init__(
        self,
        instructor_client=None,
        fixer: Optional[MultiHypothesisFixer] = None,
        fix_search: Optional[HybridFixSearch] = None,
    ):
        """Initialize GenerateFixTool.

        Args:
            instructor_client: LLM client for generation.
            fixer: MultiHypothesisFixer instance.
            fix_search: HybridFixSearch for RAG-based fixes.
        """
        self.instructor_client = instructor_client
        self.fixer = fixer
        self.fix_search = fix_search

    async def execute(
        self,
        root_cause: str,
        category: str,
        code_context: str,
        error_type: str = "Unknown",
        error_message: str = "",
        file_path: str = "",
        similar_fixes: Optional[List[str]] = None,
    ) -> List[Dict[str, Any]]:
        """Generate fix hypotheses.

        Args:
            root_cause: Root cause summary.
            category: Flakiness category.
            code_context: Relevant code.
            error_type: Exception type.
            error_message: Error message.
            file_path: Path to file to fix.
            similar_fixes: Optional similar fix IDs.

        Returns:
            List of fix hypothesis dictionaries.
        """
        try:
            # Parse category
            try:
                category_enum = FlakinessCategory(category.lower())
            except ValueError:
                category_enum = FlakinessCategory.UNKNOWN

            # Build context
            context = FailureContext(
                run_id="",
                test_name="",
                test_file=file_path,
                error_type=error_type,
                error_message=error_message,
                stack_trace="",
                code_snippet=code_context,
                category=category_enum,
            )

            # Build root cause explanation
            root_cause_exp = RootCauseExplanation(
                summary=root_cause,
                category=category_enum,
                confidence=0.8,
                evidence=[],
            )

            # Use MultiHypothesisFixer if available
            if self.fixer:
                hypotheses = await self.fixer.generate_fixes(root_cause_exp, context)
                return self._format_hypotheses(hypotheses)

            # Fall back to simple LLM generation
            if self.instructor_client:
                return await self._generate_simple(
                    root_cause=root_cause,
                    category=category_enum,
                    code_context=code_context,
                    error_type=error_type,
                    error_message=error_message,
                    file_path=file_path,
                )

            return [{
                "error": "No fix generator available. "
                         "Configure instructor_client or fixer."
            }]

        except Exception as e:
            logger.error(f"Fix generation error: {e}")
            return [{"error": f"Fix generation failed: {e}"}]

    def _format_hypotheses(
        self,
        hypotheses: List[FixHypothesis],
    ) -> List[Dict[str, Any]]:
        """Format FixHypothesis objects for output.

        Args:
            hypotheses: List of FixHypothesis objects.

        Returns:
            List of formatted dictionaries.
        """
        return [
            {
                "id": h.id,
                "strategy": h.strategy.value,
                "diff": h.diff,
                "description": h.description,
                "confidence": round(h.confidence, 3),
                "quality_score": round(h.quality_score, 3),
                "iteration": h.iteration,
                "syntax_valid": h.syntax_valid,
                "types_valid": h.types_valid,
                "based_on": h.based_on,
            }
            for h in hypotheses
        ]

    async def _generate_simple(
        self,
        root_cause: str,
        category: FlakinessCategory,
        code_context: str,
        error_type: str,
        error_message: str,
        file_path: str,
    ) -> List[Dict[str, Any]]:
        """Simple single-strategy fix generation.

        Args:
            root_cause: Root cause summary.
            category: Flakiness category.
            code_context: Code context.
            error_type: Error type.
            error_message: Error message.
            file_path: File path.

        Returns:
            List of fix dictionaries.
        """
        # Get category-specific hints
        hints = self._get_category_hints(category)

        prompt = f"""
Generate a fix for this test failure.

Error: {error_type}: {error_message}
File: {file_path}
Root cause: {root_cause}
Category: {category.value}

Code context:
```python
{code_context[:3000]}
```

Category-specific guidance for {category.value} issues:
{hints}

Requirements:
1. Generate a minimal fix that addresses the root cause
2. Don't just suppress the error - fix the underlying issue
3. Use proper Python idioms and patterns
4. Consider edge cases

Before generating the fix, think step by step:
1. What is the exact line causing the issue?
2. What change would fix the root cause (not just suppress the symptom)?
3. Are there side effects of this change?

Example of a good response:
- diff:
  --- a/tests/test_cache.py
  +++ b/tests/test_cache.py
  @@ -15,7 +15,7 @@
  -    time.sleep(0.5)
  +    await asyncio.wait_for(cache.is_populated(), timeout=2.0)
- description: Replaced fixed sleep with an explicit condition wait, eliminating timing dependency.
- confidence: 0.85

Return the fix as:
- diff: A unified diff format patch
- description: Brief explanation of what the fix does
- confidence: How confident you are this fix is correct (0.0-1.0)
- reasoning: Your step-by-step analysis of the root cause and fix
"""

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

            fix_id = str(uuid.uuid4())

            return [{
                "id": fix_id,
                "strategy": "conservative",
                "diff": response.diff,
                "description": response.description,
                "confidence": response.confidence,
                "quality_score": response.confidence * 0.8,
                "iteration": 0,
                "syntax_valid": False,  # Not validated yet
                "types_valid": False,
                "based_on": [],
            }]

        except Exception as e:
            logger.error(f"Simple generation failed: {e}")
            return [{"error": f"Generation failed: {e}"}]

    def _get_category_hints(self, category: FlakinessCategory) -> str:
        """Get hints for a flakiness category.

        Args:
            category: Flakiness category.

        Returns:
            Hint text.
        """
        hints = {
            FlakinessCategory.TIMING: """
For timing issues:
- Replace time.sleep() with polling/waiting for conditions
- Use asyncio.wait_for() with proper timeout handling
- Mock time.time() and datetime.now() in tests
- Use freezegun or similar for deterministic time
- Add proper async/await patterns
""",
            FlakinessCategory.ORDERING: """
For ordering issues:
- Add ORDER BY to SQL queries that return multiple rows
- Sort results before comparison: sorted(actual) == sorted(expected)
- Use list comparisons instead of set for ordered data
- Convert sets/dicts to sorted lists for assertions
- Check if the ordering matters for the test
""",
            FlakinessCategory.RESOURCE: """
For resource issues:
- Use try/finally to ensure cleanup
- Use context managers (with statement)
- Generate unique resource names (ports, files)
- Implement retry with exponential backoff
- Add proper connection pool management
""",
            FlakinessCategory.STATE: """
For state issues:
- Reset global/class variables in setUp/tearDown
- Use function-scoped fixtures instead of module-scoped
- Clear caches and memoization between tests
- Use fresh database transactions per test
- Isolate singleton state
""",
            FlakinessCategory.NETWORK: """
For network issues:
- Mock external HTTP calls with responses library
- Use testcontainers for local service dependencies
- Add retry logic with exponential backoff
- Handle connection errors gracefully
- Use VCR.py to record/replay network calls
""",
            FlakinessCategory.CONCURRENCY: """
For concurrency issues:
- Use proper locks and synchronization
- Avoid shared mutable state between threads
- Use threading.Event or asyncio.Event for coordination
- Consider using queue-based communication
- Ensure thread-safe data structures
""",
            FlakinessCategory.UNKNOWN: """
General debugging tips:
- Look for non-deterministic operations
- Check for implicit ordering dependencies
- Verify resource cleanup
- Check for timing assumptions
""",
        }

        return hints.get(category, hints[FlakinessCategory.UNKNOWN])
