"""Strategy definitions for different error types.

Each strategy provides guidance for fixing a specific class of errors.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import List


@dataclass
class FixStrategy:
    """A strategy for fixing a specific type of error."""

    id: str
    name: str
    description: str
    applicable_errors: List[str]  # Exception types this applies to
    priority: int  # Base priority (used as tiebreaker)
    prompt_guidance: str
    negative_guidance: str = ""  # What NOT to do
    examples: List[str] = field(default_factory=list)

    def matches_error(self, error_type: str) -> bool:
        """Check if this strategy applies to an error type.

        Args:
            error_type: Exception type (e.g., "KeyError", "TypeError")

        Returns:
            True if strategy is applicable
        """
        if "*" in self.applicable_errors:
            return True
        for applicable in self.applicable_errors:
            if error_type == applicable:
                return True
            if error_type.startswith(applicable):
                return True
            if error_type.endswith(applicable):
                return True
        return False


# Strategy definitions organized by error type
STRATEGIES = {
    # ========================================
    # KeyError strategies
    # ========================================
    "use_get_default": FixStrategy(
        id="use_get_default",
        name="Use .get() with default",
        description="Replace dict[key] with dict.get(key) and handle missing case",
        applicable_errors=["KeyError"],
        priority=10,
        prompt_guidance="""
Replace dict[key] with dict.get(key, default) or dict.get(key) with explicit None handling.
If the key is required, raise a descriptive error. If optional, provide a sensible default.

Example:
- Before: value = data['user_id']
- After: value = data.get('user_id')
        if value is None:
            raise ValueError("Missing required field: user_id")
""",
        negative_guidance="""
DO NOT just wrap in try/except KeyError without fixing the root cause.
DO NOT use empty dict as default unless semantically correct.
""",
    ),
    "validate_keys_early": FixStrategy(
        id="validate_keys_early",
        name="Validate required keys upfront",
        description="Add validation at data entry point",
        applicable_errors=["KeyError"],
        priority=8,
        prompt_guidance="""
Add validation where data enters the system (API response, config load).
Check for required keys and raise descriptive errors early.

Example:
- Add: required_keys = {'user_id', 'email'}
       missing = required_keys - data.keys()
       if missing:
           raise ValueError(f"Missing required keys: {missing}")
""",
        negative_guidance="""
DO NOT add validation in every function - only at system boundaries.
""",
    ),
    "use_defaultdict": FixStrategy(
        id="use_defaultdict",
        name="Use defaultdict",
        description="Replace dict with defaultdict for auto-initialization",
        applicable_errors=["KeyError"],
        priority=6,
        prompt_guidance="""
If the pattern is accessing/updating keys that may not exist, use defaultdict.

Example:
- Before: counts = {}; counts[key] += 1  # KeyError
- After: counts = defaultdict(int); counts[key] += 1
""",
        negative_guidance="""
DO NOT use defaultdict if the key should be required - it hides bugs.
""",
    ),

    # ========================================
    # TypeError strategies
    # ========================================
    "add_type_check": FixStrategy(
        id="add_type_check",
        name="Add type checking",
        description="Add isinstance() check before operation",
        applicable_errors=["TypeError"],
        priority=10,
        prompt_guidance="""
Add isinstance() check to verify type before the failing operation.
Handle the wrong-type case appropriately (convert, raise, or use default).

Example:
- Before: result = x + y  # TypeError if x or y is wrong type
- After: if not isinstance(x, (int, float)):
             x = float(x)  # or raise TypeError
         result = x + y
""",
        negative_guidance="""
DO NOT add type checks everywhere - only where needed.
DO NOT catch TypeError and silently return None.
""",
    ),
    "add_none_check": FixStrategy(
        id="add_none_check",
        name="Add None check",
        description="Check for None before attribute/method access",
        applicable_errors=["TypeError", "AttributeError"],
        priority=10,
        prompt_guidance="""
Add 'if obj is not None' check before accessing the attribute.
Consider using Optional type hints to document nullability.

Example:
- Before: result = obj.method()  # TypeError: NoneType has no method
- After: if obj is None:
             raise ValueError("Expected object, got None")
         result = obj.method()
""",
        negative_guidance="""
DO NOT just add 'or default' without understanding why value is None.
DO NOT silently skip processing if None - log or raise if unexpected.
""",
    ),
    "fix_type_conversion": FixStrategy(
        id="fix_type_conversion",
        name="Fix type conversion",
        description="Add or fix type conversion",
        applicable_errors=["TypeError", "ValueError"],
        priority=9,
        prompt_guidance="""
Add appropriate type conversion (str(), int(), etc.) or fix existing conversion.
Handle conversion errors gracefully.

Example:
- Before: int(user_input)  # ValueError if not numeric
- After: try:
             value = int(user_input)
         except ValueError:
             raise ValueError(f"Expected number, got: {user_input!r}")
""",
    ),

    # ========================================
    # AttributeError strategies
    # ========================================
    "fix_attribute_name": FixStrategy(
        id="fix_attribute_name",
        name="Fix attribute name typo",
        description="Correct misspelled attribute name",
        applicable_errors=["AttributeError"],
        priority=8,
        prompt_guidance="""
Check if the attribute name is misspelled. Look at similar attributes on the object.

Example:
- Before: obj.lenght  # Typo
- After: obj.length
""",
    ),
    "check_object_type": FixStrategy(
        id="check_object_type",
        name="Check object type",
        description="Ensure object is the expected type before accessing attribute",
        applicable_errors=["AttributeError"],
        priority=7,
        prompt_guidance="""
The object may be a different type than expected. Add type check or trace back
to find where the wrong type was assigned.
""",
    ),

    # ========================================
    # ValueError strategies
    # ========================================
    "validate_input_range": FixStrategy(
        id="validate_input_range",
        name="Validate input range",
        description="Add validation for valid input ranges",
        applicable_errors=["ValueError"],
        priority=9,
        prompt_guidance="""
Add validation to ensure input is within expected range or format.

Example:
- Before: int(user_input, 16)  # ValueError if not hex
- After: if not all(c in '0123456789abcdefABCDEF' for c in user_input):
             raise ValueError(f"Invalid hex string: {user_input}")
         int(user_input, 16)
""",
    ),
    "fix_enum_value": FixStrategy(
        id="fix_enum_value",
        name="Fix enum/literal value",
        description="Correct invalid enum or literal value",
        applicable_errors=["ValueError"],
        priority=8,
        prompt_guidance="""
Check if the value is from a fixed set of options and fix accordingly.
""",
    ),

    # ========================================
    # IndexError strategies
    # ========================================
    "check_list_bounds": FixStrategy(
        id="check_list_bounds",
        name="Check list bounds",
        description="Add bounds checking before list access",
        applicable_errors=["IndexError"],
        priority=10,
        prompt_guidance="""
Add bounds checking before accessing list by index.

Example:
- Before: items[index]  # IndexError
- After: if index < len(items):
             result = items[index]
         else:
             result = default  # or raise descriptive error
""",
    ),
    "use_safe_list_access": FixStrategy(
        id="use_safe_list_access",
        name="Use safe list access",
        description="Use try/except or conditional for list access",
        applicable_errors=["IndexError"],
        priority=8,
        prompt_guidance="""
Use pattern that handles missing index gracefully.

Example:
- items[0] if items else None
- next(iter(items), None)
""",
    ),

    # ========================================
    # AssertionError strategies
    # ========================================
    "fix_actual_value": FixStrategy(
        id="fix_actual_value",
        name="Fix code producing wrong value",
        description="Fix the code that produces the incorrect actual value",
        applicable_errors=["AssertionError"],
        priority=10,
        prompt_guidance="""
The actual value is wrong. Trace back through the code to find where the bug is.
Fix the logic that produces the incorrect value, not the test assertion.

Focus on:
- Off-by-one errors
- Wrong variable used
- Missing step in calculation
- Wrong condition in if statement
""",
        negative_guidance="""
DO NOT just update the expected value unless you're certain the behavior is correct.
DO NOT remove the assertion.
""",
    ),
    "fix_expected_value": FixStrategy(
        id="fix_expected_value",
        name="Fix expected value",
        description="Update expected value in assertion if behavior is correct",
        applicable_errors=["AssertionError"],
        priority=7,
        prompt_guidance="""
If the actual value is correct and the expected is wrong, update the expected.
Only do this if you're confident the actual behavior is correct.

This applies when:
- Test was written with wrong expectation
- Specification changed
- Test is for documentation purposes
""",
        negative_guidance="""
DO NOT change expected value without understanding the intent.
""",
    ),

    # ========================================
    # HTTP Error strategies
    # ========================================
    "add_retry_logic": FixStrategy(
        id="add_retry_logic",
        name="Add retry with backoff",
        description="Add retry logic for transient errors",
        applicable_errors=["HTTPError_5xx", "ConnectionError", "TimeoutError"],
        priority=9,
        prompt_guidance="""
Add retry logic with exponential backoff for transient errors.
Use tenacity or implement simple retry loop. Don't retry 4xx errors.

Example with tenacity:
@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=4, max=10),
    retry=retry_if_exception_type(HTTPError)
)
def fetch_data():
    ...
""",
    ),
    "fix_auth_header": FixStrategy(
        id="fix_auth_header",
        name="Fix authentication",
        description="Fix authentication header or token",
        applicable_errors=["HTTPError_401", "HTTPError_403", "AuthenticationError"],
        priority=10,
        prompt_guidance="""
Check if Authorization header is present and correctly formatted.
Check if token is expired and needs refresh.

Common issues:
- Missing "Bearer " prefix
- Token expired
- Wrong environment credentials
""",
    ),
    "handle_http_error": FixStrategy(
        id="handle_http_error",
        name="Add HTTP error handling",
        description="Add proper handling for HTTP errors",
        applicable_errors=["HTTPError_4xx", "HTTPError_5xx", "HTTPError"],
        priority=8,
        prompt_guidance="""
Add try/except for HTTP errors. Check response status before using body.
Provide meaningful error messages based on status code.

Example:
response = client.get(url)
if not response.is_success:
    if response.status_code == 404:
        return None  # Not found is expected
    raise APIError(f"Request failed: {response.status_code}")
""",
    ),

    # ========================================
    # SQL Error strategies
    # ========================================
    "fix_sql_constraint": FixStrategy(
        id="fix_sql_constraint",
        name="Handle constraint violation",
        description="Handle unique/foreign key constraint violations",
        applicable_errors=["IntegrityError", "UniqueViolation", "ForeignKeyViolation"],
        priority=9,
        prompt_guidance="""
Add handling for constraint violations. For unique violations, consider upsert.
For foreign key violations, ensure referenced records exist first.

Example (PostgreSQL upsert):
INSERT INTO users (email, name) VALUES ($1, $2)
ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name
""",
    ),
    "fix_null_constraint": FixStrategy(
        id="fix_null_constraint",
        name="Fix NOT NULL violation",
        description="Provide value for NOT NULL column",
        applicable_errors=["IntegrityError", "NotNullViolation"],
        priority=8,
        prompt_guidance="""
Either provide a value for the column or add a default in the schema.
""",
    ),

    # ========================================
    # Timeout strategies
    # ========================================
    "increase_timeout": FixStrategy(
        id="increase_timeout",
        name="Increase timeout",
        description="Increase timeout for slow operations",
        applicable_errors=["TimeoutError", "ReadTimeout", "ConnectTimeout"],
        priority=7,
        prompt_guidance="""
If the operation legitimately takes longer, increase the timeout.
But first check if there's a performance issue to fix.
""",
        negative_guidance="""
DO NOT just increase timeout without understanding why it's slow.
""",
    ),
    "add_async_handling": FixStrategy(
        id="add_async_handling",
        name="Add async handling",
        description="Convert blocking operation to async or add background processing",
        applicable_errors=["TimeoutError"],
        priority=6,
        prompt_guidance="""
If operation is inherently slow, consider making it async or running in background.
""",
    ),

    # ========================================
    # TBar-equivalent templates (E6/F-08)
    # High-yield APR patterns from TBar (arXiv:1903.08409)
    # ========================================
    "fix_conditional": FixStrategy(
        id="fix_conditional",
        name="Fix conditional expression (TBar FP6)",
        description="Fix wrong conditional expression (operator, boundary, logic)",
        applicable_errors=["AssertionError", "ValueError", "IndexError"],
        priority=9,
        prompt_guidance="""
Fix the conditional expression that produces the wrong result.
Common issues:
- Off-by-one: < vs <=, > vs >=
- Wrong logic: and vs or
- Inverted condition: if x vs if not x
- Wrong comparison: == vs !=, is vs ==

Example:
- Before: if index < len(items):  # Off-by-one
- After: if index <= len(items):
""",
        negative_guidance="""
DO NOT change the condition without understanding the intended behavior.
DO NOT add try/except around the condition -- fix the logic.
""",
    ),
    "fix_operator": FixStrategy(
        id="fix_operator",
        name="Fix operator (TBar FP11)",
        description="Fix wrong arithmetic or comparison operator",
        applicable_errors=["AssertionError", "TypeError", "ValueError"],
        priority=8,
        prompt_guidance="""
Fix the incorrect operator in the expression.
Common issues:
- Arithmetic: + vs -, * vs /, // vs /
- Comparison: == vs !=, < vs >
- Bitwise: & vs |, << vs >>
- String: + vs join()

Example:
- Before: total = count - offset  # Should be addition
- After: total = count + offset
""",
    ),
    "fix_method_call": FixStrategy(
        id="fix_method_call",
        name="Fix method invocation (TBar FP10)",
        description="Replace wrong method call with correct one",
        applicable_errors=["AttributeError", "TypeError", "ValueError"],
        priority=7,
        prompt_guidance="""
The wrong method is being called. Find the correct method.
Common issues:
- Similar names: strip() vs rstrip(), encode() vs decode()
- Wrong receiver: calling on wrong object
- Missing arguments or wrong argument order

Example:
- Before: text.encode('utf-8')  # Should decode
- After: text.decode('utf-8')
""",
    ),
    "fix_return_value": FixStrategy(
        id="fix_return_value",
        name="Fix return value (TBar FP12)",
        description="Fix function returning wrong value or missing return",
        applicable_errors=["AssertionError", "TypeError"],
        priority=7,
        prompt_guidance="""
The function returns an incorrect value. Check:
- Missing return statement (returns None implicitly)
- Wrong variable returned
- Missing conversion before return
- Early return skipping final computation

Example:
- Before: def calculate(x):
             result = x * 2
             # Missing return
- After: def calculate(x):
             result = x * 2
             return result
""",
    ),
    "remove_dead_code": FixStrategy(
        id="remove_dead_code",
        name="Remove buggy statement (TBar FP15)",
        description="Remove duplicate or incorrect statement causing the error",
        applicable_errors=["AssertionError", "TypeError", "ValueError"],
        priority=5,
        prompt_guidance="""
A statement is causing the error and should be removed:
- Duplicate initialization overwriting correct value
- Stale assignment that conflicts with new logic
- Debug/test code left in production

Only remove if you're confident the statement is wrong.
""",
        negative_guidance="""
DO NOT remove code without understanding its purpose.
DO NOT remove error handling or validation code.
""",
    ),

    # ========================================
    # Generic fallback
    # ========================================
    "llm_guided_repair": FixStrategy(
        id="llm_guided_repair",
        name="LLM-guided contextual repair",
        description="Use LLM with rich context for repair when no specific template applies",
        applicable_errors=["*"],
        priority=5,
        prompt_guidance="""
No specific repair template matched this error. Construct a detailed
repair prompt including:
1. The full error type and message
2. The stack trace (especially the failing line)
3. 20 lines of code context around the failure point
4. The test intent (what the test is checking)

Ask the LLM to:
- Identify the root cause
- Propose a minimal fix
- Explain why the fix is correct
""",
        negative_guidance="""
DO NOT generate a fix without understanding the error.
DO NOT apply generic patterns (try/except) when a specific fix exists.
""",
    ),
    "add_error_handling": FixStrategy(
        id="add_error_handling",
        name="Add try/except",
        description="Add targeted exception handling",
        applicable_errors=["*"],
        priority=3,
        prompt_guidance="""
Only use if more specific strategies don't apply.
Add targeted exception handling - don't catch all exceptions.

Example:
try:
    result = risky_operation()
except SpecificError as e:
    logger.warning(f"Operation failed: {e}")
    result = fallback_value
""",
        negative_guidance="""
DO NOT use bare except.
DO NOT catch Exception unless you're at a top-level handler.
DO NOT silently swallow errors - log them at minimum.
""",
    ),
    "add_logging": FixStrategy(
        id="add_logging",
        name="Add logging/debugging",
        description="Add logging to understand the issue",
        applicable_errors=["*"],
        priority=2,
        prompt_guidance="""
Add logging to capture more context about the error.
Useful when the root cause is unclear.
""",
    ),
}


def get_applicable_strategies(error_type: str) -> List[FixStrategy]:
    """Get all strategies applicable to an error type.

    Args:
        error_type: Exception type (e.g., "KeyError", "TypeError")

    Returns:
        List of applicable strategies, sorted by priority
    """
    applicable = []
    for strategy in STRATEGIES.values():
        if strategy.matches_error(error_type):
            applicable.append(strategy)

    # Sort by priority (highest first)
    applicable.sort(key=lambda s: -s.priority)
    return applicable


def get_strategy(strategy_id: str) -> FixStrategy:
    """Get a strategy by ID.

    Args:
        strategy_id: Strategy ID

    Returns:
        FixStrategy or raises KeyError
    """
    return STRATEGIES[strategy_id]


def list_all_strategies() -> List[FixStrategy]:
    """Get all registered strategies.

    Returns:
        List of all strategies
    """
    return list(STRATEGIES.values())
