"""Pattern extraction from proven fixes.

This module extracts anonymized, reusable patterns from fixes that have
been validated as working. Patterns are anonymized to:
- Remove specific variable/function names
- Generalize file paths
- Extract structural patterns

This enables cross-project learning while preserving privacy.
"""

from __future__ import annotations

import ast
import hashlib
import re
import uuid
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional

from autodebug.patterns.store import (
    CodePattern,
    ExceptionSignature,
    FixPattern,
    StackFrame,
)


def extract_pattern_from_fix(
    exception_data: Dict[str, Any],
    fix_diff: str,
    fix_description: str,
    fix_type: Optional[str] = None,
    language: str = "python",
    contributor_id: Optional[str] = None,
    tags: Optional[List[str]] = None,
) -> FixPattern:
    """Extract a reusable pattern from a proven fix.

    Args:
        exception_data: Exception information (type, message, stack)
        fix_diff: The diff that fixed the issue
        fix_description: Human-readable description of the fix
        fix_type: Category of fix (auto-detected if not provided)
        language: Programming language
        contributor_id: Optional contributor identifier (will be hashed)
        tags: Optional tags for categorization

    Returns:
        A FixPattern that can be stored and matched against future failures
    """
    # Extract exception signature
    exception_sig = extract_exception_signature(exception_data)

    # Extract code pattern from diff
    code_pattern = extract_code_pattern(fix_diff, language)

    # Auto-detect fix type if not provided
    if fix_type is None:
        fix_type = infer_fix_type(fix_diff, exception_data, language)

    # Anonymize the diff
    anon_diff = anonymize_diff(fix_diff)

    # Extract fix template if possible
    fix_template = extract_fix_template(fix_diff, language)

    # Hash contributor ID for privacy
    contributor_hash = None
    if contributor_id:
        contributor_hash = hashlib.sha256(
            contributor_id.encode()
        ).hexdigest()[:16]

    now = datetime.now(timezone.utc).isoformat()

    return FixPattern(
        id=str(uuid.uuid4()),
        created_at=now,
        updated_at=now,
        exception_signature=exception_sig,
        code_pattern=code_pattern,
        fix_type=fix_type,
        fix_description=fix_description,
        fix_template=fix_template,
        fix_diff_pattern=anon_diff,
        success_count=1,  # It was proven to work
        failure_count=0,
        total_uses=1,
        source="local",
        contributor_hash=contributor_hash,
        tags=tags or [],
    )


def extract_exception_signature(
    exception_data: Dict[str, Any],
) -> ExceptionSignature:
    """Extract a matchable signature from exception data."""
    exc_type = exception_data.get("exc_type") or exception_data.get("type", "Unknown")
    message = exception_data.get("message", "")
    stack = exception_data.get("stack", [])

    # Anonymize the message
    message_pattern = anonymize_message(message)

    # Extract stack frames
    frames = []
    for frame in stack[:10]:  # Top 10 frames
        if isinstance(frame, dict):
            frames.append(StackFrame(
                file=frame.get("file", frame.get("filename", "")),
                function=frame.get("function", frame.get("name", "")),
                line=frame.get("line", frame.get("lineno", 0)),
                module=frame.get("module"),
            ))
        elif isinstance(frame, str):
            # Parse string frame format
            parsed = parse_frame_string(frame)
            if parsed:
                frames.append(parsed)

    # Create stack signature from top frames
    stack_signature = create_stack_signature(frames[:5])

    return ExceptionSignature(
        exception_type=exc_type,
        message_pattern=message_pattern,
        stack_signature=stack_signature,
        top_frames=frames[:5],
    )


def anonymize_message(message: str) -> str:
    """Anonymize an exception message by replacing specific values.

    Examples:
        "User 'alice' not found" -> "User '{name}' not found"
        "File /home/user/test.py not found" -> "File {path} not found"
        "Expected 5 but got 3" -> "Expected {num} but got {num}"
    """
    pattern = message

    # Replace quoted strings
    pattern = re.sub(r"'[^']+'" , "'{string}'", pattern)
    pattern = re.sub(r'"[^"]+"', '"{string}"', pattern)

    # Replace file paths
    pattern = re.sub(r'/[\w/\-\.]+\.\w+', '{path}', pattern)
    pattern = re.sub(r'[A-Z]:\\[\w\\]+\.\w+', '{path}', pattern)

    # Replace numbers
    pattern = re.sub(r'\b\d+\b', '{num}', pattern)

    # Replace UUIDs
    pattern = re.sub(
        r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
        '{uuid}',
        pattern,
        flags=re.IGNORECASE,
    )

    # Replace email addresses
    pattern = re.sub(r'\S+@\S+\.\S+', '{email}', pattern)

    # Replace URLs
    pattern = re.sub(r'https?://\S+', '{url}', pattern)

    # Replace IP addresses
    pattern = re.sub(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', '{ip}', pattern)

    return pattern


def create_stack_signature(frames: List[StackFrame]) -> str:
    """Create a signature from stack frames for matching."""
    if not frames:
        return "empty"

    parts = []
    for frame in frames:
        sig = frame.to_signature()
        parts.append(sig)

    return " -> ".join(parts)


def parse_frame_string(frame_str: str) -> Optional[StackFrame]:
    """Parse a string stack frame format.

    Examples:
        "File \"test.py\", line 42, in test_func"
        "at test_func (test.py:42)"
    """
    # Python traceback format
    match = re.search(
        r'File "([^"]+)", line (\d+), in (\w+)',
        frame_str
    )
    if match:
        return StackFrame(
            file=match.group(1),
            line=int(match.group(2)),
            function=match.group(3),
        )

    # JavaScript format
    match = re.search(
        r'at (\w+) \(([^:]+):(\d+)',
        frame_str
    )
    if match:
        return StackFrame(
            function=match.group(1),
            file=match.group(2),
            line=int(match.group(3)),
        )

    # Go format
    match = re.search(
        r'([^/]+\.go):(\d+)\s+\+\s+',
        frame_str
    )
    if match:
        return StackFrame(
            file=match.group(1),
            line=int(match.group(2)),
            function="unknown",
        )

    return None


def extract_code_pattern(diff: str, language: str) -> Optional[CodePattern]:
    """Extract a code pattern from a diff."""
    # Parse the diff to find added/removed lines
    added_lines = []
    removed_lines = []

    for line in diff.splitlines():
        if line.startswith('+') and not line.startswith('+++'):
            added_lines.append(line[1:])
        elif line.startswith('-') and not line.startswith('---'):
            removed_lines.append(line[1:])

    if not added_lines and not removed_lines:
        return None

    # Detect pattern type from the changes
    pattern_type = detect_pattern_type(removed_lines, added_lines, language)

    # Create anonymized code signature
    code_sig = anonymize_code(
        "\n".join(removed_lines),
        "\n".join(added_lines),
        language,
    )

    # Try to extract AST pattern for Python
    ast_pattern = None
    if language == "python":
        ast_pattern = extract_ast_pattern(added_lines)

    return CodePattern(
        pattern_type=pattern_type,
        language=language,
        code_signature=code_sig,
        ast_pattern=ast_pattern,
    )


def detect_pattern_type(
    removed: List[str],
    added: List[str],
    language: str,
) -> str:
    """Detect the type of fix from code changes."""
    removed_text = "\n".join(removed).lower()
    added_text = "\n".join(added).lower()

    # Async/await patterns
    if "await" in added_text and "await" not in removed_text:
        return "add_await"
    if "async" in added_text and "async" not in removed_text:
        return "add_async"

    # Sleep/timing patterns
    if re.search(r'sleep\s*\(', removed_text) and 'wait' in added_text:
        return "replace_sleep_with_wait"
    if re.search(r'sleep\s*\(', removed_text):
        return "remove_sleep"

    # Null/None checks
    if re.search(r'if\s+\w+\s+(is not none|!= null|!== null)', added_text):
        return "add_null_check"
    if "optional" in added_text or "?" in added_text:
        return "add_optional"

    # Try/catch patterns
    if re.search(r'try\s*:', added_text) or re.search(r'try\s*\{', added_text):
        return "add_try_catch"
    if "except" in added_text or "catch" in added_text:
        return "add_exception_handler"

    # Retry patterns
    if "retry" in added_text or "retries" in added_text:
        return "add_retry"
    if "backoff" in added_text:
        return "add_backoff"

    # Lock/mutex patterns
    if re.search(r'lock|mutex|synchronized', added_text):
        return "add_synchronization"

    # Timeout patterns
    if "timeout" in added_text and "timeout" not in removed_text:
        return "add_timeout"

    # Cleanup patterns
    if re.search(r'finally|defer|cleanup|teardown', added_text):
        return "add_cleanup"

    # Mock patterns
    if re.search(r'mock|patch|stub|fake', added_text):
        return "add_mock"

    # Fixture patterns
    if re.search(r'@pytest\.fixture|beforeeach|setup', added_text):
        return "add_fixture"

    # Assertion changes
    if re.search(r'assert|expect', added_text):
        if re.search(r'approx|closeto|tolerance', added_text):
            return "use_approximate_assertion"
        return "modify_assertion"

    # Import changes
    if re.search(r'^import\s|^from\s', added_text, re.MULTILINE):
        return "add_import"

    # Type annotation changes
    if re.search(r':\s*(int|str|float|bool|list|dict|optional)', added_text):
        return "add_type_annotation"

    # Default: generic fix
    return "code_change"


def anonymize_code(removed: str, added: str, language: str) -> str:
    """Create an anonymized code signature."""
    combined = f"REMOVED:\n{removed}\nADDED:\n{added}"

    # Replace variable names with placeholders
    # Keep keywords but anonymize identifiers
    keywords = {
        "python": {"def", "class", "if", "else", "elif", "for", "while", "try",
                   "except", "finally", "with", "as", "import", "from", "return",
                   "yield", "raise", "pass", "break", "continue", "and", "or",
                   "not", "in", "is", "None", "True", "False", "async", "await"},
        "javascript": {"function", "const", "let", "var", "if", "else", "for",
                       "while", "try", "catch", "finally", "return", "throw",
                       "async", "await", "class", "import", "export", "from",
                       "true", "false", "null", "undefined"},
        "typescript": {"function", "const", "let", "var", "if", "else", "for",
                       "while", "try", "catch", "finally", "return", "throw",
                       "async", "await", "class", "import", "export", "from",
                       "true", "false", "null", "undefined", "interface", "type"},
        "go": {"func", "if", "else", "for", "switch", "case", "default", "return",
               "go", "defer", "chan", "select", "var", "const", "type", "struct",
               "interface", "package", "import", "true", "false", "nil"},
        "java": {"class", "interface", "public", "private", "protected", "static",
                 "final", "void", "if", "else", "for", "while", "try", "catch",
                 "finally", "return", "throw", "new", "import", "package",
                 "true", "false", "null", "synchronized"},
    }

    lang_keywords = keywords.get(language, set())

    # Simple anonymization: keep structure, replace identifiers
    lines = []
    var_map: Dict[str, str] = {}
    var_counter = 0

    for line in combined.splitlines():
        # Preserve indentation
        indent = len(line) - len(line.lstrip())
        content = line.strip()

        if not content or content.startswith('#') or content.startswith('//'):
            lines.append(line)
            continue

        # Tokenize and replace non-keywords
        tokens = re.findall(r'\b\w+\b|[^\w\s]', content)
        anon_tokens = []

        for token in tokens:
            if token.lower() in lang_keywords:
                anon_tokens.append(token)
            elif token.isidentifier():
                if token not in var_map:
                    var_map[token] = f"$var{var_counter}"
                    var_counter += 1
                anon_tokens.append(var_map[token])
            else:
                anon_tokens.append(token)

        lines.append(" " * indent + " ".join(anon_tokens))

    return "\n".join(lines)


def anonymize_diff(diff: str) -> str:
    """Anonymize a diff for storage."""
    lines = []

    for line in diff.splitlines():
        if line.startswith('---') or line.startswith('+++'):
            # Anonymize file paths in diff headers
            line = re.sub(r'[ab]/[\w/\-\.]+', 'a/file.ext', line)
        elif line.startswith('@@'):
            # Keep hunk headers as-is
            pass
        elif line.startswith('+') or line.startswith('-'):
            # Anonymize code lines
            prefix = line[0]
            content = line[1:]

            # Replace string literals
            content = re.sub(r'"[^"]*"', '"{string}"', content)
            content = re.sub(r"'[^']*'", "'{string}'", content)

            # Replace numbers
            content = re.sub(r'\b\d+\b', '{num}', content)

            line = prefix + content

        lines.append(line)

    return "\n".join(lines)


def extract_fix_template(diff: str, language: str) -> Optional[str]:
    """Extract a reusable fix template from a diff."""
    # Find added lines
    added_lines = []
    for line in diff.splitlines():
        if line.startswith('+') and not line.startswith('+++'):
            added_lines.append(line[1:])

    if not added_lines:
        return None

    # Check if this is a simple template-able fix
    added_code = "\n".join(added_lines)

    # Look for common patterns that can be templated

    # Await pattern
    if re.search(r'await\s+\w+', added_code):
        return "await {expression}"

    # Try-except pattern (Python)
    if language == "python" and "try:" in added_code and "except" in added_code:
        return """try:
    {original_code}
except {exception_type} as e:
    {handler}"""

    # Try-catch pattern (JS/TS)
    if language in ("javascript", "typescript") and "try {" in added_code:
        return """try {
    {original_code}
} catch (error) {
    {handler}
}"""

    # Null check pattern
    if re.search(r'if\s+\w+\s+(is not None|!= null|!== null)', added_code):
        return """if {variable} is not None:
    {original_code}"""

    # Retry pattern
    if "retry" in added_code.lower():
        return """for attempt in range({max_retries}):
    try:
        {original_code}
        break
    except {exception_type}:
        if attempt == {max_retries} - 1:
            raise
        time.sleep({backoff} * (attempt + 1))"""

    return None


def extract_ast_pattern(added_lines: List[str]) -> Optional[Dict[str, Any]]:
    """Extract AST pattern for Python code."""
    code = "\n".join(added_lines)

    try:
        tree = ast.parse(code)
    except SyntaxError:
        # Try wrapping in function
        try:
            tree = ast.parse(f"def _wrapper():\n    {code}")
        except SyntaxError:
            return None

    # Extract structural pattern
    pattern = {"nodes": []}

    for node in ast.walk(tree):
        node_type = type(node).__name__

        if node_type in ("Module", "FunctionDef", "AsyncFunctionDef"):
            continue

        if node_type in ("Await", "Try", "If", "For", "While", "With",
                         "Call", "Attribute", "Subscript"):
            node_info = {"type": node_type}

            if hasattr(node, "lineno"):
                node_info["line"] = node.lineno

            # Add specific info for certain node types
            if node_type == "Call" and isinstance(node.func, ast.Name):
                node_info["func_name"] = node.func.id
            elif node_type == "Call" and isinstance(node.func, ast.Attribute):
                node_info["attr_name"] = node.func.attr

            pattern["nodes"].append(node_info)

    return pattern if pattern["nodes"] else None


def infer_fix_type(diff: str, exception_data: Dict[str, Any], language: str = "python") -> str:
    """Infer the fix type from diff and exception."""
    exc_type = exception_data.get("exc_type", "").lower()
    exc_msg = exception_data.get("message", "").lower()

    # Exception-based inference
    if "attributeerror" in exc_type and "nonetype" in exc_msg:
        return "null_check"
    if "keyerror" in exc_type:
        return "key_check"
    if "indexerror" in exc_type:
        return "bounds_check"
    if "timeout" in exc_type:
        return "timeout_handling"
    if "connection" in exc_type:
        return "connection_handling"
    if "assertionerror" in exc_type:
        return "assertion_fix"

    # Fall back to diff-based detection
    added = []
    removed = []
    for line in diff.splitlines():
        if line.startswith('+') and not line.startswith('+++'):
            added.append(line[1:])
        elif line.startswith('-') and not line.startswith('---'):
            removed.append(line[1:])

    return detect_pattern_type(removed, added, language)
