"""saar deep_extractor -- patterns WITH reasoning, not just labels (OPE-96).

The core problem with label-only extraction:
  label:  "Auth: Depends(reusable_oauth2)"
  rule:   "All protected endpoints use Depends(reusable_oauth2) from app.api.deps.
           The auth context is CurrentUser. Never bypass with manual header parsing."

A label tells Claude what exists. A rule tells Claude what to DO and NOT DO.
Rules are what hand-crafted AGENTS.md files contain. saar should generate them.

Architecture:
  Each DeepRule has:
    - text: the actionable rule sentence (what to put in AGENTS.md)
    - confidence: 0.0-1.0 (how certain we are -- low confidence rules are omitted)
    - evidence: list of file paths that support the rule
    - category: which section it belongs to

This module runs AFTER the main extractor has built CodebaseDNA.
It gets the raw DNA + the source files and generates richer rules.
"""
from __future__ import annotations

import re
from dataclasses import dataclass, field
from pathlib import Path
import logging

logger = logging.getLogger(__name__)


@dataclass
class DeepRule:
    """An actionable rule derived from code analysis, with evidence."""
    text: str                        # the rule sentence
    confidence: float                # 0.0-1.0
    evidence: list[str] = field(default_factory=list)  # file paths
    category: str = "general"        # auth | exceptions | imports | testing | naming | never_do


@dataclass
class DeepExtractionResult:
    """Rules generated by deep analysis."""
    auth_rules: list[DeepRule] = field(default_factory=list)
    exception_rules: list[DeepRule] = field(default_factory=list)
    import_rules: list[DeepRule] = field(default_factory=list)
    testing_rules: list[DeepRule] = field(default_factory=list)
    naming_rules: list[DeepRule] = field(default_factory=list)
    never_do_rules: list[DeepRule] = field(default_factory=list)

    def all_rules(self, min_confidence: float = 0.6) -> list[DeepRule]:
        """Return all rules above confidence threshold, sorted by confidence."""
        all_r = (
            self.auth_rules + self.exception_rules + self.import_rules +
            self.testing_rules + self.naming_rules + self.never_do_rules
        )
        return sorted(
            [r for r in all_r if r.confidence >= min_confidence],
            key=lambda r: -r.confidence,
        )


# ── Auth rule generation ──────────────────────────────────────────────────────

def _derive_auth_rules(files: list[Path], dna, safe_read) -> list[DeepRule]:
    """Generate actionable auth rules from code evidence.

    For each auth pattern found, look at how it's used:
    - Where is it imported from?
    - How is it applied (decorator, Depends, middleware)?
    - What does the auth context look like?
    - Are there any bypass patterns?
    """
    rules = []

    # --- FastAPI Depends() auth ---
    depends_usage: dict[str, list[str]] = {}  # dep_name -> [file paths]
    auth_context_types: dict[str, int] = {}   # type_name -> count
    deps_import_paths: dict[str, str] = {}    # dep_name -> import path

    for file_path in files:
        if file_path.suffix != ".py":
            continue
        content = safe_read(file_path)
        if not content or "Depends(" not in content:
            continue

        # Find where auth deps are imported from
        for match in re.finditer(
            r"^from\s+([\w.]+)\s+import\s+(.*(?:auth|current_user|get_current|oauth2|bearer|security).*)",
            content, re.MULTILINE | re.IGNORECASE
        ):
            import_path = match.group(1)
            names = [n.strip() for n in match.group(2).split(",")]
            for name in names:
                deps_import_paths[name] = import_path

        # Count Depends() usage per dependency
        for match in re.finditer(r"Depends\((\w+)\)", content):
            dep_name = match.group(1)
            if any(kw in dep_name.lower() for kw in ("auth", "current", "user", "oauth", "bearer")):
                if dep_name not in depends_usage:
                    depends_usage[dep_name] = []
                depends_usage[dep_name].append(str(file_path))

        # Detect auth context parameter types
        for match in re.finditer(
            r"(\w+):\s*(CurrentUser|AuthContext|UserModel|User)\s*=\s*Depends",
            content
        ):
            ctx_type = match.group(2)
            auth_context_types[ctx_type] = auth_context_types.get(ctx_type, 0) + 1

    # Generate rules from evidence
    for dep_name, usages in depends_usage.items():
        count = len(usages)
        if count < 2:
            continue
        import_path = deps_import_paths.get(dep_name, "")
        import_hint = f" from `{import_path}`" if import_path else ""

        text = f"Protected endpoints use `Depends({dep_name})`{import_hint} — never bypass with manual header parsing"
        rules.append(DeepRule(
            text=text,
            confidence=min(0.95, 0.6 + count * 0.05),
            evidence=usages[:3],
            category="auth",
        ))

    # Auth context type rule
    if auth_context_types:
        ctx_type, count = max(auth_context_types.items(), key=lambda x: x[1])
        if count >= 2:
            rules.append(DeepRule(
                text=f"Auth context type is `{ctx_type}` — access it as a function parameter via Depends()",
                confidence=min(0.9, 0.6 + count * 0.06),
                evidence=[],
                category="auth",
            ))

    # --- Django @login_required ---
    login_required_files = []
    permission_required_files = []
    for file_path in files:
        if file_path.suffix != ".py":
            continue
        content = safe_read(file_path)
        if not content:
            continue
        if "@login_required" in content and "from django" in content:
            login_required_files.append(str(file_path))
        if "@permission_required" in content:
            permission_required_files.append(str(file_path))

    if len(login_required_files) >= 3:
        rules.append(DeepRule(
            text="Views requiring login use `@login_required` decorator — never skip auth on user-facing views",
            confidence=min(0.9, 0.6 + len(login_required_files) * 0.03),
            evidence=login_required_files[:3],
            category="auth",
        ))

    # --- NestJS @UseGuards ---
    guard_usage: dict[str, list[str]] = {}
    for file_path in files:
        if file_path.suffix not in (".ts", ".tsx"):
            continue
        content = safe_read(file_path)
        if not content or "@UseGuards" not in content:
            continue
        for match in re.finditer(r"@UseGuards\((\w+)\)", content):
            guard = match.group(1)
            if guard not in guard_usage:
                guard_usage[guard] = []
            guard_usage[guard].append(str(file_path))

    if guard_usage:
        top_guard = max(guard_usage, key=lambda g: len(guard_usage[g]))
        count = len(guard_usage[top_guard])
        if count >= 2:
            rules.append(DeepRule(
                text=f"Protected endpoints use `@UseGuards({top_guard})` — apply to all routes requiring auth",
                confidence=min(0.9, 0.6 + count * 0.05),
                evidence=guard_usage[top_guard][:3],
                category="auth",
            ))

    return rules


# ── Exception rule generation ─────────────────────────────────────────────────

def _derive_exception_rules(files: list[Path], dna, safe_read) -> list[DeepRule]:
    """Generate actionable exception rules.

    Goes beyond 'these classes exist' to:
    - Where are exceptions defined vs raised vs caught?
    - Is there a base exception class hierarchy?
    - Are there patterns in how exceptions map to HTTP responses?
    """
    rules = []

    # Find exception hierarchy
    base_classes: dict[str, str] = {}   # ExcName -> base class
    raise_locations: dict[str, list[str]] = {}  # ExcName -> files that raise it
    exc_modules: dict[str, str] = {}    # ExcName -> module where defined

    for file_path in files:
        if file_path.suffix != ".py":
            continue
        content = safe_read(file_path)
        if not content:
            continue

        # Find class definitions with base
        for match in re.finditer(
            r"^class\s+(\w+(?:Error|Exception))\s*\((\w+)\)",
            content, re.MULTILINE
        ):
            exc_name = match.group(1)
            base = match.group(2)
            base_classes[exc_name] = base
            exc_modules[exc_name] = str(file_path)

        # Find raise statements
        for match in re.finditer(r"\braise\s+(\w+(?:Error|Exception))\s*\(", content):
            exc_name = match.group(1)
            if exc_name not in raise_locations:
                raise_locations[exc_name] = []
            raise_locations[exc_name].append(str(file_path))

    # Find the base/root exception (most subclassed)
    subclass_counts: dict[str, int] = {}
    for exc, base in base_classes.items():
        if base not in ("Exception", "BaseException", "Error"):
            subclass_counts[base] = subclass_counts.get(base, 0) + 1

    if subclass_counts:
        root_exc = max(subclass_counts, key=lambda e: subclass_counts[e])
        child_count = subclass_counts[root_exc]
        if child_count >= 3:
            rules.append(DeepRule(
                text=f"`{root_exc}` is the base exception class — all domain exceptions inherit from it. "
                     f"Never raise `Exception` directly.",
                confidence=min(0.95, 0.65 + child_count * 0.04),
                evidence=[exc_modules.get(root_exc, "")],
                category="exceptions",
            ))

    # Find most-raised exceptions (most important for Claude to know)
    top_raised = sorted(raise_locations.items(), key=lambda x: -len(x[1]))[:5]
    if top_raised:
        top_names = [exc for exc, _ in top_raised if exc in base_classes]
        if top_names:
            rules.append(DeepRule(
                text=f"Most commonly raised: `{'`, `'.join(top_names[:3])}`"
                     " — check these exist before creating new ones",
                confidence=0.8,
                evidence=[raise_locations[n][0] for n in top_names[:2] if raise_locations.get(n)],
                category="exceptions",
            ))

    # Check if exceptions are in a dedicated module
    exc_files = set(exc_modules.values())
    dedicated_files = [f for f in exc_files if "exception" in f.lower() or "error" in f.lower()]
    if dedicated_files:
        rel = Path(dedicated_files[0]).name
        rules.append(DeepRule(
            text=f"Custom exceptions are defined in `{rel}` — always check there before adding new ones",
            confidence=0.85,
            evidence=dedicated_files[:2],
            category="exceptions",
        ))

    return rules


# ── Import rule generation ────────────────────────────────────────────────────

def _derive_import_rules(files: list[Path], dna, safe_read) -> list[DeepRule]:
    """Detect import patterns that Claude should follow.

    Looks for:
    - Canonical import paths that appear consistently across files
    - Import patterns that suggest architectural boundaries
    - Package manager constraints
    """
    rules = []

    # Count top import patterns across Python files
    import_counts: dict[str, int] = {}
    for file_path in files:
        if file_path.suffix != ".py":
            continue
        content = safe_read(file_path)
        if not content:
            continue
        for match in re.finditer(r"^from\s+([\w.]+)\s+import\s+(\w+)", content, re.MULTILINE):
            key = f"from {match.group(1)} import {match.group(2)}"
            import_counts[key] = import_counts.get(key, 0) + 1

    # Find the 3 most common non-stdlib imports (canonical imports)
    canonical = sorted(
        [(k, v) for k, v in import_counts.items() if v >= 5 and not k.startswith("from typing")],
        key=lambda x: -x[1],
    )[:3]

    if canonical:
        examples = ", ".join(f"`{k}`" for k, _ in canonical)
        rules.append(DeepRule(
            text=f"Canonical imports used across the codebase: {examples}",
            confidence=0.75,
            evidence=[],
            category="imports",
        ))

    # Detect package manager constraints from lock files
    return rules


# ── Testing rule generation ───────────────────────────────────────────────────

def _derive_testing_rules(files: list[Path], dna, safe_read) -> list[DeepRule]:
    """Generate testing rules from test file patterns."""
    rules = []

    test_files = [f for f in files if "test" in f.name.lower() or "spec" in f.name.lower()]
    if not test_files:
        return rules

    # Detect fixture patterns
    fixture_files = []
    uses_parametrize = []
    uses_factory = []

    for file_path in test_files:
        content = safe_read(file_path)
        if not content:
            continue
        if "conftest.py" in str(file_path):
            fixture_files.append(str(file_path))
        if "@pytest.mark.parametrize" in content:
            uses_parametrize.append(str(file_path))
        if "factory" in content.lower() and ("Factory" in content or "baker" in content):
            uses_factory.append(str(file_path))

    if fixture_files:
        rules.append(DeepRule(
            text="Shared test fixtures live in `conftest.py` — use pytest fixtures, not setUp/tearDown",
            confidence=0.85,
            evidence=fixture_files[:2],
            category="testing",
        ))

    if len(uses_parametrize) >= 3:
        rules.append(DeepRule(
            text="Use `@pytest.mark.parametrize` for multiple test cases — never copy-paste test functions",
            confidence=min(0.9, 0.65 + len(uses_parametrize) * 0.03),
            evidence=uses_parametrize[:2],
            category="testing",
        ))

    if len(uses_factory) >= 2:
        rules.append(DeepRule(
            text="Use factory pattern for test data creation — check existing factories before creating new ones",
            confidence=0.75,
            evidence=uses_factory[:2],
            category="testing",
        ))

    # TypeScript test patterns
    ts_test_files = [f for f in files if f.suffix in (".ts", ".tsx") and
                     ("test" in f.name.lower() or "spec" in f.name.lower())]

    uses_describe = 0
    uses_it = 0
    for file_path in ts_test_files:
        content = safe_read(file_path)
        if not content:
            continue
        if "describe(" in content:
            uses_describe += 1
        if "\nit(" in content or "\n  it(" in content:
            uses_it += 1

    if uses_describe >= 3:
        rules.append(DeepRule(
            text="Tests use `describe()`/`it()` blocks — one top-level describe per test file",
            confidence=min(0.85, 0.6 + uses_describe * 0.04),
            evidence=[],
            category="testing",
        ))

    return rules


# ── Naming rule generation ────────────────────────────────────────────────────

def _derive_naming_rules(files: list[Path], dna, safe_read) -> list[DeepRule]:
    """Generate specific naming rules beyond generic snake_case/camelCase."""
    rules = []

    # TypeScript: detect component naming patterns
    component_names = []
    hook_names = []
    service_names = []

    for file_path in files:
        if file_path.suffix not in (".ts", ".tsx"):
            continue
        name = file_path.stem
        if file_path.suffix == ".tsx" and name[0].isupper():
            component_names.append(name)
        if name.startswith("use") and name[3:4].isupper():
            hook_names.append(name)
        if name.endswith("Service") or name.endswith(".service"):
            service_names.append(name)

    # Detect file naming pattern for components (PascalCase.tsx vs kebab-case.tsx)
    pascal_components = sum(1 for n in component_names if n[0].isupper())
    kebab_files = sum(1 for f in files if "-" in f.stem and f.suffix in (".ts", ".tsx"))

    if pascal_components >= 3:
        rules.append(DeepRule(
            text="React components: `PascalCase.tsx` filename matching the component name",
            confidence=min(0.9, 0.65 + pascal_components * 0.04),
            evidence=[],
            category="naming",
        ))
    elif kebab_files >= 5:
        rules.append(DeepRule(
            text="TypeScript files use `kebab-case` naming (e.g. `user-profile.tsx`)",
            confidence=0.8,
            evidence=[],
            category="naming",
        ))

    if len(hook_names) >= 3:
        rules.append(DeepRule(
            text=f"Custom hooks are prefixed with `use` and live in `hooks/` — "
                 f"example: `{hook_names[0]}` (found {len(hook_names)} hooks)",
            confidence=0.85,
            evidence=[],
            category="naming",
        ))

    # Python: detect specific file naming patterns
    py_service_files = [f for f in files if f.suffix == ".py" and
                        ("service" in f.name.lower() or "services" in str(f).lower())]
    if len(py_service_files) >= 3:
        rules.append(DeepRule(
            text="Service files named `*_service.py` or live in `services/` directory",
            confidence=0.8,
            evidence=[str(f) for f in py_service_files[:2]],
            category="naming",
        ))

    return rules


# ── Never-do rule generation ──────────────────────────────────────────────────

def _derive_never_do_rules(files: list[Path], dna, safe_read) -> list[DeepRule]:
    """Detect patterns that Claude should never introduce.

    These come from anti-patterns that appear in the codebase's guard rails:
    - Linter rules that forbid certain patterns
    - Comments that say 'never', 'do not', 'forbidden'
    - Patterns that appear 0 times suggesting they're avoided
    """
    rules = []

    # Check for print() usage in Python (if logging is used instead)
    has_structlog = dna.logging_patterns.structured_logging if hasattr(dna, 'logging_patterns') else False
    has_logging = bool(getattr(getattr(dna, 'logging_patterns', None), 'logger_import', None))
    print_count = 0
    for file_path in files:
        if file_path.suffix != ".py":
            continue
        content = safe_read(file_path)
        if content and re.search(r"^\s*print\(", content, re.MULTILINE):
            print_count += 1

    if (has_logging or has_structlog) and print_count == 0:
        logger_name = "structlog" if has_structlog else "logging"
        rules.append(DeepRule(
            text=f"Never use `print()` — this project uses `{logger_name}` for all output",
            confidence=0.9,
            evidence=[],
            category="never_do",
        ))

    # Check for package manager: if bun lock exists, never use npm
    return rules


# ── Main entry point ──────────────────────────────────────────────────────────

def run_deep_extraction(
    files: list[Path],
    dna,
    safe_read_fn,
) -> DeepExtractionResult:
    """Run all deep extractors and return consolidated rules.

    Args:
        files: source files to analyze
        dna: CodebaseDNA from main extractor
        safe_read_fn: callable(Path) -> str | None
    """
    result = DeepExtractionResult()

    try:
        result.auth_rules = _derive_auth_rules(files, dna, safe_read_fn)
    except Exception as e:
        logger.warning("Deep auth extraction failed: %s", e)

    try:
        result.exception_rules = _derive_exception_rules(files, dna, safe_read_fn)
    except Exception as e:
        logger.warning("Deep exception extraction failed: %s", e)

    try:
        result.import_rules = _derive_import_rules(files, dna, safe_read_fn)
    except Exception as e:
        logger.warning("Deep import extraction failed: %s", e)

    try:
        result.testing_rules = _derive_testing_rules(files, dna, safe_read_fn)
    except Exception as e:
        logger.warning("Deep testing extraction failed: %s", e)

    try:
        result.naming_rules = _derive_naming_rules(files, dna, safe_read_fn)
    except Exception as e:
        logger.warning("Deep naming extraction failed: %s", e)

    try:
        result.never_do_rules = _derive_never_do_rules(files, dna, safe_read_fn)
    except Exception as e:
        logger.warning("Deep never-do extraction failed: %s", e)

    return result
