"""Dataset ingestion pipeline for BugsInPy and SWE-bench."""

from __future__ import annotations

import ast
import difflib
import hashlib
import json
import re
from dataclasses import dataclass, field
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

from ai_orchestrator.schemas.rag_models import BugFixExample
from ai_orchestrator.services.embedding_service import EmbeddingService
from ai_orchestrator.services.rag_knowledge_base import RAGKnowledgeBase


@dataclass
class DatasetConfig:
    """Configuration for a bug-fix dataset."""
    name: str
    path: Path
    format_type: str  # "bugsinpy", "swe-bench", "stackoverflow", or "custom"
    error_classes: tuple[str, ...] = field(default_factory=tuple)
    max_entries: int | None = None


class RAGDatasetIngester:
    """Service for ingesting bug-fix datasets into the RAG knowledge base."""

    ERROR_CLASS_TARGETS = {
        "attributeerror": {
            "category": "attribute-error",
            "patterns": [r"\battributeerror\b", r"has no attribute"],
            "symptoms": ["attributeerror", "attribute-error"],
        },
        "keyerror": {
            "category": "key-error",
            "patterns": [r"\bkeyerror\b", r"missing key", r"dictionary key"],
            "symptoms": ["keyerror", "key-error"],
        },
        "unicodedecodeerror": {
            "category": "encoding-issue",
            "patterns": [r"\bunicodedecodeerror\b", r"\bdecode\b", r"\bencoding\b", r"\bunicode\b"],
            "symptoms": ["unicodedecodeerror", "encoding", "encoding-issue"],
        },
    }

    # Common bug patterns for categorization
    BUG_PATTERNS = {
        "off-by-one": [r"\+ 1", r"- 1", r"\.index\(", r"\.find\("],
        "missing-null-check": [r"if \w+\[", r"if not \w+", r"\.get\("],
        "type-error": [r"int\(", r"str\(", r"float\(", r"list\("],
        "unbound-variable": [r"NameError", r"undefined", r"not defined"],
        "import-error": [r"ImportError", r"ModuleNotFoundError"],
        "attribute-error": [r"AttributeError", r"has no attribute"],
        "key-error": [r"KeyError", r"\[.*\].*Key"],
        "index-error": [r"IndexError", r"list index"],
        "value-error": [r"ValueError", r"invalid literal"],
        "division-by-zero": [r"ZeroDivisionError"],
        "file-not-found": [r"FileNotFoundError", r"No such file"],
        "race-condition": [r"threading", r"multiprocessing", r"concurrent"],
        "memory-leak": [r"memory", r"leak", r"not closed"],
        "infinite-loop": [r"while True", r"recursion"],
        "deprecated-api": [r"deprecated", r"RemovedIn"],
        "string-formatting": [r"%s", r"%d", r"\.format\(", r"f'"],
        "encoding-issue": [r"decode", r"encode", r"utf-8", r"Unicode"],
    }

    def __init__(
        self,
        knowledge_base: RAGKnowledgeBase,
        embedding_service: EmbeddingService,
    ) -> None:
        self.knowledge_base = knowledge_base
        self.embedding_service = embedding_service

    def ingest_dataset(self, config: DatasetConfig) -> dict[str, Any]:
        """Ingest a dataset into the knowledge base."""
        if config.format_type == "bugsinpy":
            return self._ingest_bugsinpy(config)
        if config.format_type == "swe-bench":
            return self._ingest_swe_bench(config)
        if config.format_type == "stackoverflow":
            return self._ingest_stackoverflow(config)
        raise ValueError(f"Unknown dataset format: {config.format_type}")

    def _ingest_bugsinpy(self, config: DatasetConfig) -> dict[str, Any]:
        """Ingest BugsInPy dataset."""
        results = {
            "dataset": config.name,
            "processed": 0,
            "added": 0,
            "errors": [],
            "skipped": 0,
        }

        dataset_path = config.path

        # BugsInPy structure: projects/{repo}/bugs/{bug_id}/bug_patch.txt
        # Check for both structures
        projects_dir = dataset_path / "projects"
        if not projects_dir.exists():
            # Try direct dataset structure
            projects_dir = dataset_path

        if not projects_dir.exists():
            results["errors"].append(f"Dataset path not found: {dataset_path}")
            return results

        # Find all bug directories
        bug_dirs = []
        for repo_dir in projects_dir.iterdir():
            if not repo_dir.is_dir() or repo_dir.name.startswith('.'):
                continue

            bugs_dir = repo_dir / "bugs"
            if not bugs_dir.exists():
                continue

            for bug_dir in bugs_dir.iterdir():
                if not bug_dir.is_dir():
                    continue

                patch_file = bug_dir / "bug_patch.txt"
                info_file = bug_dir / "bug.info"

                if patch_file.exists():
                    bug_dirs.append((repo_dir.name, bug_dir, patch_file, info_file))

        # Process bug directories
        for repo_name, bug_dir, patch_file, info_file in bug_dirs:
            if config.max_entries is not None and results["processed"] >= config.max_entries:
                break
            try:
                results["processed"] += 1
                example = self._process_bugsinpy_patch_entry(
                    repo_name=repo_name,
                    bug_id=bug_dir.name,
                    patch_file=patch_file,
                    info_file=info_file,
                )

                if example:
                    if not self._matches_target_error_classes(example, config.error_classes):
                        results["skipped"] += 1
                        continue
                    # Generate embedding
                    text_to_embed = self._create_embedding_text(example)
                    example.embedding = self.embedding_service.embed_code(text_to_embed)

                    self._store_example(example)
                    results["added"] += 1
                else:
                    results["skipped"] += 1

            except Exception as e:
                results["errors"].append(f"{repo_name}/{bug_dir.name}: {e}")

        return results

    def _process_bugsinpy_entry(
        self,
        repo_name: str,
        bug_id: str,
        buggy_file: Path,
        fixed_file: Path,
        metadata_file: Path,
    ) -> BugFixExample | None:
        """Process a single BugsInPy entry."""
        try:
            buggy_code = buggy_file.read_text(encoding="utf-8")
            fixed_code = fixed_file.read_text(encoding="utf-8")
        except (OSError, UnicodeDecodeError):
            return None

        # Generate diff
        diff = self._generate_diff(buggy_code, fixed_code)

        # Extract metadata
        metadata = self._parse_metadata(metadata_file, buggy_code)

        # Categorize the bug
        category = self._categorize_bug(buggy_code, fixed_code, diff, metadata.get("error_type"))

        # Create example ID
        example_id = f"bugsinpy-{repo_name}-{bug_id}"

        # Extract function name if possible
        function_name = self._extract_function_name(fixed_code, diff)

        return BugFixExample(
            id=example_id,
            dataset="BugsInPy",
            repository=repo_name,
            bug_description=metadata.get("description", f"Bug fix in {repo_name}"),
            bug_summary=self._generate_summary(category, diff),
            bug_category=category,
            file_path=metadata.get("file_path", str(buggy_file.relative_to(buggy_file.parent.parent.parent))),
            function_name=function_name,
            buggy_code=buggy_code,
            fixed_code=fixed_code,
            diff=diff,
            severity=metadata.get("severity", "medium"),
            complexity=self._assess_complexity(diff),
            root_cause=metadata.get("root_cause"),
            symptoms=metadata.get("symptoms", []),
        )

    def _process_bugsinpy_patch_entry(
        self,
        repo_name: str,
        bug_id: str,
        patch_file: Path,
        info_file: Path,
    ) -> BugFixExample | None:
        """Process a BugsInPy entry from patch file format."""
        try:
            patch_content = patch_file.read_text(encoding="utf-8")
        except (OSError, UnicodeDecodeError):
            return None

        # Parse the diff to extract code changes
        code_changes = self._parse_patch(patch_content)

        if not code_changes or not code_changes.get("before") or not code_changes.get("after"):
            # The diff might not have complete before/after, use the patch itself
            diff = patch_content
            buggy_code = code_changes.get("before", "")
            fixed_code = code_changes.get("after", "")
        else:
            diff = patch_content
            buggy_code = code_changes.get("before", "")
            fixed_code = code_changes.get("after", "")

        # Parse bug.info if available
        metadata = {}
        if info_file.exists():
            try:
                info_content = info_file.read_text(encoding="utf-8")
                # Parse key-value pairs
                for line in info_content.split('\n'):
                    if ':' in line:
                        key, value = line.split(':', 1)
                        metadata[key.strip().lower()] = value.strip()
            except (OSError, UnicodeDecodeError):
                pass

        # Categorize the bug
        category = self._categorize_bug(buggy_code, fixed_code, diff, metadata.get("type"))

        # Create example ID
        example_id = f"bugsinpy-{repo_name}-{bug_id}"

        # Extract function name if possible
        function_name = code_changes.get("function_name")

        return BugFixExample(
            id=example_id,
            dataset="BugsInPy",
            repository=repo_name,
            bug_description=metadata.get("description", diff[:200] if len(diff) < 200 else f"Bug fix in {repo_name}"),
            bug_summary=self._generate_summary(category, diff, metadata.get("short_desc")),
            bug_category=category,
            file_path=code_changes.get("file_path", f"{repo_name}/bug_{bug_id}.py"),
            function_name=function_name,
            line_number=code_changes.get("line_number"),
            buggy_code=buggy_code,
            fixed_code=fixed_code,
            diff=diff,
            severity="medium",
            complexity=self._assess_complexity(diff),
            root_cause=metadata.get("root_cause"),
            symptoms=self._extract_symptoms_from_metadata(metadata),
        )

    def _extract_symptoms_from_metadata(self, metadata: dict[str, str]) -> list[str]:
        """Extract symptoms from BugsInPy metadata."""
        symptoms = []

        # Common symptom indicators in BugsInPy
        if "error" in metadata.get("type", "").lower():
            symptoms.append("runtime_error")
        if "crash" in metadata.get("type", "").lower():
            symptoms.append("crash")

        return symptoms

    def _ingest_swe_bench(self, config: DatasetConfig) -> dict[str, Any]:
        """Ingest SWE-bench dataset from HuggingFace."""
        results = {
            "dataset": config.name,
            "processed": 0,
            "added": 0,
            "errors": [],
            "skipped": 0,
        }

        try:
            from datasets import load_dataset
        except ImportError:
            results["errors"].append("datasets library not installed. Install with: pip install datasets")
            return results

        # Load SWE-bench Lite from HuggingFace
        try:
            dataset = load_dataset('princeton-nlp/SWE-bench_Lite', split='test')
        except Exception as e:
            results["errors"].append(f"Failed to load SWE-bench_Lite from HuggingFace: {e}")
            return results

        for item in dataset:
            if config.max_entries is not None and results["processed"] >= config.max_entries:
                break
            try:
                results["processed"] += 1
                example = self._process_swe_bench_item(item)

                if example:
                    if not self._matches_target_error_classes(example, config.error_classes):
                        results["skipped"] += 1
                        continue
                    # Generate embedding
                    text_to_embed = self._create_embedding_text(example)
                    example.embedding = self.embedding_service.embed_code(text_to_embed)

                    self._store_example(example)
                    results["added"] += 1
                else:
                    results["skipped"] += 1

            except Exception as e:
                results["errors"].append(f"{item.get('instance_id', 'unknown')}: {e}")

        return results

    def _ingest_stackoverflow(self, config: DatasetConfig) -> dict[str, Any]:
        """Ingest StackOverflow question/answer dumps from JSON or JSONL."""
        results = {
            "dataset": config.name,
            "processed": 0,
            "added": 0,
            "errors": [],
            "skipped": 0,
        }

        try:
            records = self._iter_stackoverflow_records(config.path)
        except ValueError as exc:
            results["errors"].append(str(exc))
            return results

        for record in records:
            if config.max_entries is not None and results["processed"] >= config.max_entries:
                break
            try:
                results["processed"] += 1
                example = self._process_stackoverflow_item(record)
                if example is None:
                    results["skipped"] += 1
                    continue
                if not self._matches_target_error_classes(example, config.error_classes):
                    results["skipped"] += 1
                    continue

                text_to_embed = self._create_embedding_text(example)
                example.embedding = self.embedding_service.embed_code(text_to_embed)
                self._store_example(example)
                results["added"] += 1
            except Exception as exc:
                question_id = record.get("question_id") or record.get("id") or "unknown"
                results["errors"].append(f"{question_id}: {exc}")

        return results

    def _iter_stackoverflow_records(self, dataset_path: Path) -> list[dict[str, Any]]:
        if not dataset_path.exists():
            raise ValueError(f"Dataset path not found: {dataset_path}")

        files: list[Path]
        if dataset_path.is_dir():
            files = sorted(
                path for path in dataset_path.rglob("*")
                if path.is_file() and path.suffix.lower() in {".json", ".jsonl"}
            )
        else:
            files = [dataset_path]

        records: list[dict[str, Any]] = []
        for path in files:
            records.extend(self._load_stackoverflow_records(path))
        return records

    def _load_stackoverflow_records(self, path: Path) -> list[dict[str, Any]]:
        suffix = path.suffix.lower()
        if suffix == ".jsonl":
            rows: list[dict[str, Any]] = []
            for line in path.read_text(encoding="utf-8").splitlines():
                line = line.strip()
                if not line:
                    continue
                payload = json.loads(line)
                if isinstance(payload, dict):
                    rows.append(payload)
            return rows

        if suffix == ".json":
            payload = json.loads(path.read_text(encoding="utf-8"))
            if isinstance(payload, list):
                return [item for item in payload if isinstance(item, dict)]
            if isinstance(payload, dict):
                for key in ("items", "questions", "rows"):
                    value = payload.get(key)
                    if isinstance(value, list):
                        return [item for item in value if isinstance(item, dict)]
                return [payload]

        raise ValueError(f"Unsupported StackOverflow dataset file: {path}")

    def _process_stackoverflow_item(self, item: dict[str, Any]) -> BugFixExample | None:
        """Process a StackOverflow dump entry into a bug-fix example."""
        question = item.get("question") if isinstance(item.get("question"), dict) else item
        title = self._first_text(
            question.get("title"),
            item.get("title"),
            item.get("question_title"),
        )
        body = self._first_text(
            question.get("body"),
            item.get("body"),
            item.get("question_body"),
        )
        answer = self._select_stackoverflow_answer(item)
        answer_body = self._first_text(
            item.get("accepted_answer_body"),
            answer.get("body") if answer else None,
            item.get("answer_body"),
        )
        if not title and not body:
            return None

        buggy_code = self._first_text(item.get("buggy_code"), item.get("before_code"))
        if not buggy_code:
            question_blocks = self._extract_stackoverflow_code_blocks(body)
            buggy_code = question_blocks[0] if question_blocks else ""

        fixed_code = self._first_text(item.get("fixed_code"), item.get("after_code"))
        if not fixed_code:
            answer_blocks = self._extract_stackoverflow_code_blocks(answer_body)
            fixed_code = answer_blocks[0] if answer_blocks else ""

        if not buggy_code or not fixed_code:
            return None

        tags = self._coerce_string_list(question.get("tags"), item.get("tags"))
        normalized_error_classes = self._normalize_error_classes([
            item.get("error_class"),
            item.get("exception_name"),
            item.get("exception_type"),
            *tags,
            title,
            body,
            answer_body,
        ])

        issue_id = item.get("question_id") or question.get("question_id") or item.get("id") or question.get("id") or hashlib.md5(
            f"{title}|{buggy_code[:120]}".encode("utf-8")
        ).hexdigest()[:12]
        diff = self._generate_diff(buggy_code, fixed_code)
        description = self._build_stackoverflow_description(title=title, body=body, answer_body=answer_body)
        category = self._category_from_error_classes(normalized_error_classes) or self._categorize_bug(
            buggy_code,
            fixed_code,
            diff,
            description,
        )
        function_name = self._extract_function_name(fixed_code, diff)

        return BugFixExample(
            id=f"stackoverflow-{issue_id}",
            dataset="StackOverflow",
            repository="stackoverflow",
            issue_url=self._stackoverflow_issue_url(item, question, issue_id),
            bug_description=description,
            bug_summary=self._generate_summary(category, diff, title or description[:100]),
            bug_category=category,
            file_path=f"stackoverflow/question_{issue_id}.py",
            function_name=function_name,
            buggy_code=buggy_code,
            fixed_code=fixed_code,
            diff=diff,
            severity="medium",
            complexity=self._assess_complexity(diff),
            root_cause=self._truncate_text(self._strip_html(answer_body), 400),
            symptoms=sorted(set(self._extract_symptoms(description) + list(normalized_error_classes))),
            related_patterns=tags,
        )

    def _select_stackoverflow_answer(self, item: dict[str, Any]) -> dict[str, Any] | None:
        accepted = item.get("accepted_answer")
        if isinstance(accepted, dict):
            return accepted

        answers = item.get("answers")
        if not isinstance(answers, list):
            return None

        accepted_id = item.get("accepted_answer_id")
        if accepted_id is not None:
            for answer in answers:
                if isinstance(answer, dict) and answer.get("id") == accepted_id:
                    return answer

        ranked = [answer for answer in answers if isinstance(answer, dict)]
        if not ranked:
            return None
        ranked.sort(key=lambda answer: answer.get("score", 0), reverse=True)
        return ranked[0]

    @staticmethod
    def _extract_stackoverflow_code_blocks(text: str) -> list[str]:
        if not text:
            return []

        code_blocks: list[str] = []
        for match in re.findall(r"<code>(.*?)</code>", text, flags=re.IGNORECASE | re.DOTALL):
            snippet = match.strip()
            if snippet:
                code_blocks.append(snippet)

        if code_blocks:
            return code_blocks

        for match in re.findall(r"```(?:python)?\s*(.*?)```", text, flags=re.IGNORECASE | re.DOTALL):
            snippet = match.strip()
            if snippet:
                code_blocks.append(snippet)
        return code_blocks

    @staticmethod
    def _build_stackoverflow_description(*, title: str, body: str, answer_body: str) -> str:
        pieces = [piece for piece in (title, RAGDatasetIngester._strip_html(body), RAGDatasetIngester._strip_html(answer_body)) if piece]
        return RAGDatasetIngester._truncate_text(" ".join(pieces), 800)

    @staticmethod
    def _stackoverflow_issue_url(item: dict[str, Any], question: dict[str, Any], issue_id: str) -> str | None:
        direct = item.get("link") or item.get("url") or question.get("link") or question.get("url")
        if isinstance(direct, str) and direct:
            return direct
        if str(issue_id).isdigit():
            return f"https://stackoverflow.com/q/{issue_id}"
        return None

    def _process_swe_bench_entry(self, item: dict[str, Any], patches_dir: Path = None) -> BugFixExample | None:
        """Process a single SWE-bench entry."""
        instance_id = item.get("instance_id", "")
        repo = item.get("repo", "")
        base_commit = item.get("base_commit", "")
        problem_statement = item.get("problem_statement", "")
        hints_text = item.get("hints_text", "")
        patch = item.get("patch", "")

        if not patch:
            return None

        # Extract code changes from the patch
        code_changes = self._parse_patch(patch)

        # Categorize the bug
        category = self._categorize_bug(
            code_changes.get("before", ""),
            code_changes.get("after", ""),
            patch,
            problem_statement
        )

        return BugFixExample(
            id=f"swe-bench-{instance_id}",
            dataset="SWE-bench",
            repository=repo,
            commit_hash=base_commit,
            bug_description=problem_statement,
            bug_summary=self._generate_summary(category, patch, problem_statement[:200] if problem_statement else ""),
            bug_category=category,
            file_path=code_changes.get("file_path", f"{repo}/changes.patch"),
            function_name=code_changes.get("function_name"),
            line_number=code_changes.get("line_number"),
            buggy_code=code_changes.get("before", ""),
            fixed_code=code_changes.get("after", ""),
            diff=patch,
            severity="medium",
            complexity=self._assess_complexity(patch),
            root_cause=hints_text if hints_text else None,
            symptoms=self._extract_symptoms(problem_statement),
        )

    def _process_swe_bench_item(self, item: dict[str, Any]) -> BugFixExample | None:
        """Process a single SWE-bench item from HuggingFace dataset."""
        return self._process_swe_bench_entry(item)

    @staticmethod
    def _first_text(*values: Any) -> str:
        for value in values:
            if isinstance(value, str) and value.strip():
                return value.strip()
        return ""

    @staticmethod
    def _coerce_string_list(*values: Any) -> list[str]:
        results: list[str] = []
        for value in values:
            if isinstance(value, str):
                if value.strip():
                    results.append(value.strip())
                continue
            if isinstance(value, list):
                for item in value:
                    if isinstance(item, str) and item.strip():
                        results.append(item.strip())
        return results

    @staticmethod
    def _strip_html(text: str) -> str:
        if not text:
            return ""
        text = re.sub(r"<code>.*?</code>", " ", text, flags=re.IGNORECASE | re.DOTALL)
        text = re.sub(r"<[^>]+>", " ", text)
        return re.sub(r"\s+", " ", text).strip()

    @staticmethod
    def _truncate_text(text: str, max_chars: int) -> str:
        if len(text) <= max_chars:
            return text
        return text[: max_chars - 3].rstrip() + "..."

    def _normalize_error_classes(self, values: list[Any] | tuple[Any, ...]) -> set[str]:
        normalized: set[str] = set()
        for value in values:
            if isinstance(value, list):
                normalized.update(self._normalize_error_classes(value))
                continue
            if not isinstance(value, str):
                continue
            lowered = re.sub(r"[^a-z0-9]+", "", value.lower())
            for candidate in self.ERROR_CLASS_TARGETS:
                if candidate in lowered:
                    normalized.add(candidate)
                    continue
            for candidate, metadata in self.ERROR_CLASS_TARGETS.items():
                if any(re.search(pattern, value, re.IGNORECASE) for pattern in metadata["patterns"]):
                    normalized.add(candidate)
        return normalized

    def _category_from_error_classes(self, error_classes: set[str]) -> str | None:
        for error_class in sorted(error_classes):
            metadata = self.ERROR_CLASS_TARGETS.get(error_class)
            if metadata:
                return str(metadata["category"])
        return None

    def _matches_target_error_classes(self, example: BugFixExample, target_error_classes: tuple[str, ...]) -> bool:
        normalized_targets = self._normalize_error_classes(list(target_error_classes))
        if not normalized_targets:
            return True

        text_parts = [
            example.bug_category,
            example.bug_description,
            example.bug_summary,
            example.root_cause or "",
            " ".join(example.symptoms),
            " ".join(example.related_patterns),
        ]
        haystack = " ".join(part for part in text_parts if part)
        for target in normalized_targets:
            metadata = self.ERROR_CLASS_TARGETS[target]
            if example.bug_category == metadata["category"]:
                return True
            if any(re.search(pattern, haystack, re.IGNORECASE) for pattern in metadata["patterns"]):
                return True
            if any(symptom.lower() in metadata["symptoms"] for symptom in example.symptoms):
                return True
        return False

    def _create_embedding_text(self, example: BugFixExample) -> str:
        """Create a text representation for embedding."""
        parts = [
            example.bug_summary,
            example.bug_category,
            example.bug_description,
        ]

        if example.root_cause:
            parts.append(example.root_cause)

        # Include key parts of the code change
        if example.diff:
            # Extract the most relevant lines from the diff
            diff_lines = example.diff.split('\n')
            relevant_lines = [l for l in diff_lines if l.startswith('+') or l.startswith('-')]
            if relevant_lines:
                parts.append('\n'.join(relevant_lines[:20]))

        return ' | '.join(parts)

    def _generate_diff(self, before: str, after: str) -> str:
        """Generate a unified diff between two code snippets."""
        before_lines = before.splitlines(keepends=True)
        after_lines = after.splitlines(keepends=True)

        diff = difflib.unified_diff(
            before_lines,
            after_lines,
            fromfile="before",
            tofile="after",
            lineterm=""
        )
        return ''.join(diff)

    def _parse_patch(self, patch: str) -> dict[str, Any]:
        """Parse a patch file to extract code changes."""
        if not patch:
            return {}

        result = {
            "file_path": "",
            "before": "",
            "after": "",
            "function_name": None,
            "line_number": None,
        }

        # Extract file path
        for line in patch.split('\n')[:10]:
            if line.startswith('+++') or line.startswith('---'):
                match = re.search(r'b/(.+)', line)
                if match:
                    result["file_path"] = match.group(1)
                    break

        # Extract before and after code
        before_lines = []
        after_lines = []
        line_num = None

        for line in patch.split('\n'):
            if line.startswith('@@'):
                # Extract line number
                match = re.search(r'-(\d+)', line)
                if match:
                    line_num = int(match.group(1))
            elif line.startswith('-') and not line.startswith('---'):
                before_lines.append(line[1:])
            elif line.startswith('+') and not line.startswith('+++'):
                after_lines.append(line[1:])

        result["before"] = '\n'.join(before_lines)
        result["after"] = '\n'.join(after_lines)
        result["line_number"] = line_num

        # Try to extract function name from context
        for line in patch.split('\n'):
            if re.match(r'^[\+\-]\s*def\s+(\w+)', line):
                match = re.search(r'def\s+(\w+)', line)
                if match:
                    result["function_name"] = match.group(1)
                    break

        return result

    def _categorize_bug(
        self,
        buggy_code: str,
        fixed_code: str,
        diff: str,
        description: str | None = None,
    ) -> str:
        """Categorize the bug based on patterns."""
        code_to_check = diff.lower() if diff else (buggy_code + fixed_code).lower()

        # Check against known patterns
        for category, patterns in self.BUG_PATTERNS.items():
            for pattern in patterns:
                if re.search(pattern, code_to_check, re.IGNORECASE):
                    return category

        # Try to infer from description
        if description:
            desc_lower = description.lower()
            if "attributeerror" in desc_lower or "has no attribute" in desc_lower:
                return "attribute-error"
            if "keyerror" in desc_lower or "key error" in desc_lower:
                return "key-error"
            if "unicodedecodeerror" in desc_lower or ("unicode" in desc_lower and ("decode" in desc_lower or "encoding" in desc_lower)):
                return "encoding-issue"
            if "type" in desc_lower and "error" in desc_lower:
                return "type-error"
            if "null" in desc_lower or "none" in desc_lower:
                return "missing-null-check"
            if "index" in desc_lower:
                return "index-error"
            if "key" in desc_lower:
                return "key-error"

        # Default category
        return "logic-error"

    def _assess_complexity(self, diff: str) -> str:
        """Assess the complexity of a fix based on the diff."""
        if not diff:
            return "simple"

        # Count changed lines
        added = len([l for l in diff.split('\n') if l.startswith('+')])
        removed = len([l for l in diff.split('\n') if l.startswith('-')])

        total_changes = added + removed

        if total_changes <= 3:
            return "simple"
        elif total_changes <= 10:
            return "moderate"
        else:
            return "complex"

    def _generate_summary(self, category: str, diff: str, description: str = "") -> str:
        """Generate a one-line summary of the bug fix."""
        if description:
            return description[:100]

        # Extract key change from diff
        for line in diff.split('\n'):
            if line.startswith('-') and not line.startswith('---'):
                return f"Fix {category}: removed '{line[1:50].strip()}'"
            if line.startswith('+') and not line.startswith('+++'):
                return f"Fix {category}: added '{line[1:50].strip()}'"

        return f"Fix for {category}"

    def _extract_function_name(self, code: str, diff: str) -> str | None:
        """Extract the function name from code or diff."""
        # Try to find function definition in changed lines
        for line in diff.split('\n'):
            if line.startswith('+') or line.startswith('-'):
                match = re.search(r'def\s+(\w+)\s*\(', line[1:])
                if match:
                    return match.group(1)

        # Fallback: find first function in the fixed code
        try:
            tree = ast.parse(code)
        except SyntaxError:
            return None
        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                return node.name

        return None

    def _parse_metadata(self, metadata_file: Path, code: str) -> dict[str, Any]:
        """Parse metadata file if it exists."""
        metadata = {}

        if not metadata_file.exists():
            # Try to infer from code
            return self._infer_metadata_from_code(code)

        try:
            if metadata_file.suffix == '.json':
                metadata = json.loads(metadata_file.read_text(encoding="utf-8"))
            else:
                # Try to parse text format
                content = metadata_file.read_text(encoding="utf-8")
                for line in content.split('\n'):
                    if ':' in line:
                        key, value = line.split(':', 1)
                        metadata[key.strip().lower()] = value.strip()
        except (OSError, json.JSONDecodeError):
            pass

        return metadata

    def _infer_metadata_from_code(self, code: str) -> dict[str, Any]:
        """Infer metadata from the code itself."""
        metadata = {}

        # Look for common error patterns in comments
        error_patterns = {
            "TODO": "potential issue",
            "FIXME": "needs fixing",
            "BUG": "known bug",
            "HACK": "workaround",
        }

        for line in code.split('\n'):
            for pattern, issue_type in error_patterns.items():
                if pattern in line:
                    metadata.setdefault("symptoms", []).append(issue_type)

        return metadata

    def _extract_symptoms(self, description: str) -> list[str]:
        """Extract symptoms from a bug description."""
        symptoms = []

        symptom_keywords = [
            "crash", "error", "exception", "fail", "broken",
            "incorrect", "wrong", "unexpected", "undefined",
            "missing", "duplicate", "leak", "hang", "freeze"
        ]

        for keyword in symptom_keywords:
            if keyword in description.lower():
                symptoms.append(keyword)

        return symptoms

    def create_synthetic_examples(self, patterns: dict[str, tuple[str, str]]) -> list[BugFixExample]:
        """
        Create synthetic bug-fix examples from patterns.

        Useful for seeding the knowledge base with common Python gotchas.
        """
        examples = []

        for category, (buggy, fixed) in patterns.items():
            diff = self._generate_diff(buggy, fixed)
            example_id = f"synthetic-{category}-{hashlib.md5(buggy.encode()).hexdigest()[:8]}"

            examples.append(BugFixExample(
                id=example_id,
                dataset="synthetic",
                repository="python-common-patterns",
                bug_description=f"Common {category} bug pattern",
                bug_summary=f"Fix for {category}",
                bug_category=category,
                file_path="example.py",
                buggy_code=buggy,
                fixed_code=fixed,
                diff=diff,
                severity="medium",
                complexity="simple",
                root_cause=f"Common Python {category} anti-pattern",
                symptoms=[category],
            ))

        return examples

    def _store_example(self, example: BugFixExample) -> None:
        ingest_family = getattr(self.knowledge_base, "ingest_example_family", None)
        if callable(ingest_family):
            ingest_family(example, self.embedding_service)
            return
        self.knowledge_base.add_example(example)


# Common Python bug patterns for synthetic examples
COMMON_PYTHON_PATTERNS = {
    "mutable-default-arg": (
        """def foo(items=[]):
    items.append(1)
    return items""",
        """def foo(items=None):
    if items is None:
        items = []
    items.append(1)
    return items"""
    ),
    "missing-none-check": (
        """def get_value(data, key):
    return data[key]""",
        """def get_value(data, key):
    if key not in data:
        return None
    return data[key]"""
    ),
    "off-by-one": (
        """for i in range(len(items)):
    print(items[i])""",
        """for i in range(len(items) - 1):
    print(items[i])"""
    ),
    "resource-leak": (
        """f = open('file.txt')
data = f.read()
process(data)""",
        """with open('file.txt') as f:
    data = f.read()
    process(data)"""
    ),
    "string-bytes-confusion": (
        """data = b'hello'
print(data.upper())""",
        """data = b'hello'
print(data.decode().upper())"""
    ),
}
