"""Secure coding rules integration from TikiTribe/claude-secure-coding-rules.

This module provides intelligent fetching of security rules based on
detected project languages and frameworks.

Migrated from zerg/security_rules.py — complete copy of all functions,
classes, and constants.
"""

import json
import subprocess
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

from zerg.fs_utils import collect_files
from zerg.logging import get_logger

logger = get_logger(__name__)

# GitHub repository for secure coding rules
RULES_REPO = "TikiTribe/claude-secure-coding-rules"
RULES_RAW_URL = f"https://raw.githubusercontent.com/{RULES_REPO}/main"


@dataclass
class ProjectStack:
    """Detected project technology stack."""

    languages: set[str] = field(default_factory=set)
    frameworks: set[str] = field(default_factory=set)
    databases: set[str] = field(default_factory=set)
    infrastructure: set[str] = field(default_factory=set)
    ai_ml: bool = False
    rag: bool = False

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary."""
        return {
            "languages": sorted(self.languages),
            "frameworks": sorted(self.frameworks),
            "databases": sorted(self.databases),
            "infrastructure": sorted(self.infrastructure),
            "ai_ml": self.ai_ml,
            "rag": self.rag,
        }


# Mapping of file patterns to languages
LANGUAGE_DETECTION: dict[str, str] = {
    "*.py": "python",
    "pyproject.toml": "python",
    "requirements.txt": "python",
    "setup.py": "python",
    "Pipfile": "python",
    "*.js": "javascript",
    "*.mjs": "javascript",
    "*.ts": "typescript",
    "*.tsx": "typescript",
    "package.json": "javascript",  # Could be JS or TS
    "tsconfig.json": "typescript",
    "*.go": "go",
    "go.mod": "go",
    "*.rs": "rust",
    "Cargo.toml": "rust",
    "*.java": "java",
    "pom.xml": "java",
    "build.gradle": "java",
    "*.cs": "csharp",
    "*.csproj": "csharp",
    "*.rb": "ruby",
    "Gemfile": "ruby",
    "*.r": "r",
    "*.R": "r",
    "*.cpp": "cpp",
    "*.cc": "cpp",
    "*.hpp": "cpp",
    "CMakeLists.txt": "cpp",
    "*.jl": "julia",
    "*.sql": "sql",
}

# Mapping of package/import patterns to frameworks
FRAMEWORK_DETECTION: dict[str, tuple[str, str]] = {
    # Python frameworks
    "fastapi": ("python", "fastapi"),
    "django": ("python", "django"),
    "flask": ("python", "flask"),
    "langchain": ("python", "langchain"),
    "llama_index": ("python", "llamaindex"),
    "llama-index": ("python", "llamaindex"),
    "crewai": ("python", "crewai"),
    "autogen": ("python", "autogen"),
    "transformers": ("python", "transformers"),
    "torch": ("python", "pytorch"),
    "tensorflow": ("python", "tensorflow"),
    "mlflow": ("python", "mlflow"),
    "bentoml": ("python", "bentoml"),
    "ray": ("python", "ray"),
    # JavaScript/TypeScript frameworks
    "react": ("javascript", "react"),
    "next": ("javascript", "nextjs"),
    "vue": ("javascript", "vue"),
    "angular": ("typescript", "angular"),
    "svelte": ("javascript", "svelte"),
    "express": ("javascript", "express"),
    "nestjs": ("typescript", "nestjs"),
    "@nestjs": ("typescript", "nestjs"),
    "fastify": ("javascript", "fastify"),
    # Databases
    "pinecone": ("database", "pinecone"),
    "weaviate": ("database", "weaviate"),
    "chromadb": ("database", "chroma"),
    "qdrant": ("database", "qdrant"),
    "milvus": ("database", "milvus"),
    "pgvector": ("database", "pgvector"),
    "neo4j": ("database", "neo4j"),
    "mongodb": ("database", "mongodb"),
    "psycopg": ("database", "postgresql"),
    "asyncpg": ("database", "postgresql"),
    "sqlalchemy": ("database", "sql"),
}

# Infrastructure detection patterns
INFRASTRUCTURE_DETECTION: dict[str, str] = {
    "Dockerfile": "docker",
    ".devcontainer/Dockerfile": "docker",
    "docker-compose.yml": "docker",
    "docker-compose.yaml": "docker",
    "kubernetes": "kubernetes",
    "k8s": "kubernetes",
    "*.tf": "terraform",
    "terraform": "terraform",
    "Pulumi.yaml": "pulumi",
    ".github/workflows": "github-actions",
    ".gitlab-ci.yml": "gitlab-ci",
}

# Mapping of detected tech to rule paths in the upstream repository.
# Core rules are flat .md files; everything else uses {category}/{name}/CLAUDE.md.
RULE_PATHS: dict[str, list[str]] = {
    # Core rules (always included)
    "_core": [
        "rules/_core/owasp-2025.md",
    ],
    # AI/ML core rules
    "ai_ml": [
        "rules/_core/ai-security.md",
        "rules/_core/agent-security.md",
    ],
    # RAG core rules
    "rag": [
        "rules/_core/rag-security.md",
    ],
    # Languages
    "python": ["rules/languages/python/CLAUDE.md"],
    "javascript": ["rules/languages/javascript/CLAUDE.md"],
    "typescript": ["rules/languages/typescript/CLAUDE.md"],
    "go": ["rules/languages/go/CLAUDE.md"],
    "rust": ["rules/languages/rust/CLAUDE.md"],
    "java": ["rules/languages/java/CLAUDE.md"],
    "csharp": ["rules/languages/csharp/CLAUDE.md"],
    "ruby": ["rules/languages/ruby/CLAUDE.md"],
    "r": ["rules/languages/r/CLAUDE.md"],
    "cpp": ["rules/languages/cpp/CLAUDE.md"],
    "julia": ["rules/languages/julia/CLAUDE.md"],
    "sql": ["rules/languages/sql/CLAUDE.md"],
    # Backend frameworks
    "fastapi": ["rules/backend/fastapi/CLAUDE.md"],
    "django": ["rules/backend/django/CLAUDE.md"],
    "flask": ["rules/backend/flask/CLAUDE.md"],
    "express": ["rules/backend/express/CLAUDE.md"],
    "nestjs": ["rules/backend/nestjs/CLAUDE.md"],
    "langchain": ["rules/backend/langchain/CLAUDE.md"],
    "crewai": ["rules/backend/crewai/CLAUDE.md"],
    "autogen": ["rules/backend/autogen/CLAUDE.md"],
    "transformers": ["rules/backend/transformers/CLAUDE.md"],
    "mlflow": ["rules/backend/mlflow/CLAUDE.md"],
    "bentoml": ["rules/backend/bentoml/CLAUDE.md"],
    "ray": ["rules/backend/ray-serve/CLAUDE.md"],
    # Frontend frameworks
    "react": ["rules/frontend/react/CLAUDE.md"],
    "nextjs": ["rules/frontend/nextjs/CLAUDE.md"],
    "vue": ["rules/frontend/vue/CLAUDE.md"],
    "angular": ["rules/frontend/angular/CLAUDE.md"],
    "svelte": ["rules/frontend/svelte/CLAUDE.md"],
    # RAG tools
    "llamaindex": ["rules/rag/orchestration/llamaindex/CLAUDE.md"],
    "pinecone": ["rules/rag/vector-managed/pinecone/CLAUDE.md"],
    "weaviate": ["rules/rag/vector-selfhosted/weaviate/CLAUDE.md"],
    "chroma": ["rules/rag/vector-selfhosted/chroma/CLAUDE.md"],
    "qdrant": ["rules/rag/vector-selfhosted/qdrant/CLAUDE.md"],
    "milvus": ["rules/rag/vector-selfhosted/milvus/CLAUDE.md"],
    "pgvector": ["rules/rag/vector-selfhosted/pgvector/CLAUDE.md"],
    "neo4j": ["rules/rag/graph/neo4j/CLAUDE.md"],
    # Infrastructure
    "docker": ["rules/containers/docker/CLAUDE.md"],
    "kubernetes": ["rules/containers/kubernetes/CLAUDE.md"],
    "terraform": ["rules/iac/terraform/CLAUDE.md"],
    "pulumi": ["rules/iac/pulumi/CLAUDE.md"],
    "github-actions": ["rules/cicd/github-actions/CLAUDE.md"],
    "gitlab-ci": ["rules/cicd/gitlab-ci/CLAUDE.md"],
}


def detect_project_stack(project_path: Path) -> ProjectStack:
    """Detect the technology stack of a project.

    Uses single-pass directory traversal for performance.

    Args:
        project_path: Path to the project root

    Returns:
        ProjectStack with detected technologies
    """
    stack = ProjectStack()
    project_path = Path(project_path)

    # Single traversal - collect and classify all files
    files_by_ext: dict[str, list[Path]] = {}  # "*.py" -> [files...]
    exact_files: set[str] = set()  # exact filenames found (e.g., "Dockerfile")

    for filepath in project_path.rglob("*"):
        if not filepath.is_file():
            continue

        name = filepath.name
        exact_files.add(name)

        # Build extension pattern (e.g., "*.py" from "foo.py")
        if "." in name:
            ext = name.rsplit(".", 1)[-1]
            ext_pattern = f"*.{ext}"
            files_by_ext.setdefault(ext_pattern, []).append(filepath)

    # Detect languages from collected file data
    for pattern, language in LANGUAGE_DETECTION.items():
        if pattern.startswith("*"):
            # Glob pattern - check our collected extension map
            if pattern in files_by_ext and files_by_ext[pattern]:
                stack.languages.add(language)
        else:
            # Exact file match - check our collected filenames
            if pattern in exact_files:
                stack.languages.add(language)

    # Detect frameworks from dependency files
    _detect_python_frameworks(project_path, stack)
    _detect_js_frameworks(project_path, stack)
    _detect_go_frameworks(project_path, stack)
    _detect_rust_frameworks(project_path, stack)

    # Detect infrastructure from collected file data
    for pattern, infra in INFRASTRUCTURE_DETECTION.items():
        if pattern.startswith("*"):
            # Glob pattern - check our collected extension map
            if pattern in files_by_ext and files_by_ext[pattern]:
                stack.infrastructure.add(infra)
        elif "/" in pattern:
            # Path-based check (e.g., ".github/workflows")
            if (project_path / pattern).exists():
                stack.infrastructure.add(infra)
        else:
            # Exact file match - check our collected filenames
            if pattern in exact_files:
                stack.infrastructure.add(infra)

    # Set AI/ML and RAG flags based on detected frameworks
    ai_ml_frameworks = {
        "langchain",
        "llamaindex",
        "crewai",
        "autogen",
        "transformers",
        "pytorch",
        "tensorflow",
        "mlflow",
        "bentoml",
        "ray",
    }
    rag_frameworks = {
        "langchain",
        "llamaindex",
        "pinecone",
        "weaviate",
        "chroma",
        "qdrant",
        "milvus",
        "pgvector",
        "neo4j",
    }

    stack.ai_ml = bool(stack.frameworks & ai_ml_frameworks)
    stack.rag = bool(stack.frameworks & rag_frameworks) or bool(stack.databases & rag_frameworks)

    logger.info(f"Detected stack: {stack.to_dict()}")
    return stack


def _detect_python_frameworks(project_path: Path, stack: ProjectStack) -> None:
    """Detect Python frameworks from requirements/pyproject."""
    deps: set[str] = set()

    # Check requirements.txt
    req_file = project_path / "requirements.txt"
    if req_file.exists():
        content = req_file.read_text()
        for line in content.splitlines():
            line = line.strip().lower()
            if line and not line.startswith("#"):
                # Extract package name (before any version specifier)
                pkg = line.split("==")[0].split(">=")[0].split("<=")[0].split("[")[0]
                deps.add(pkg.replace("-", "_").replace(".", "_"))

    # Check pyproject.toml
    pyproject = project_path / "pyproject.toml"
    if pyproject.exists():
        content = pyproject.read_text().lower()
        for pkg in FRAMEWORK_DETECTION:
            if pkg.lower() in content:
                deps.add(pkg.replace("-", "_"))

    # Map dependencies to frameworks
    for dep in deps:
        for pattern, (lang, framework) in FRAMEWORK_DETECTION.items():
            if pattern.replace("-", "_") in dep or dep in pattern.replace("-", "_"):
                if lang == "python":
                    stack.frameworks.add(framework)
                elif lang == "database":
                    stack.databases.add(framework)


def _detect_js_frameworks(project_path: Path, stack: ProjectStack) -> None:
    """Detect JavaScript/TypeScript frameworks from package.json."""
    pkg_file = project_path / "package.json"
    if not pkg_file.exists():
        return

    try:
        content = json.loads(pkg_file.read_text())
        all_deps = set()
        all_deps.update(content.get("dependencies", {}).keys())
        all_deps.update(content.get("devDependencies", {}).keys())

        for dep in all_deps:
            dep_lower = dep.lower()
            for pattern, (lang, framework) in FRAMEWORK_DETECTION.items():
                if pattern in dep_lower:
                    if lang in ("javascript", "typescript"):
                        stack.frameworks.add(framework)
                    elif lang == "database":
                        stack.databases.add(framework)
    except (json.JSONDecodeError, OSError):
        pass  # Config file not found or malformed


def _detect_go_frameworks(project_path: Path, stack: ProjectStack) -> None:
    """Detect Go frameworks from go.mod."""
    go_mod = project_path / "go.mod"
    if not go_mod.exists():
        return

    content = go_mod.read_text().lower()
    # Add Go-specific framework detection as needed
    if "gin-gonic" in content:
        stack.frameworks.add("gin")
    if "echo" in content:
        stack.frameworks.add("echo")


def _detect_rust_frameworks(project_path: Path, stack: ProjectStack) -> None:
    """Detect Rust frameworks from Cargo.toml."""
    cargo = project_path / "Cargo.toml"
    if not cargo.exists():
        return

    content = cargo.read_text().lower()
    if "actix" in content:
        stack.frameworks.add("actix")
    if "axum" in content:
        stack.frameworks.add("axum")
    if "rocket" in content:
        stack.frameworks.add("rocket")


def get_required_rules(stack: ProjectStack) -> list[str]:
    """Get list of rule paths needed for the detected stack.

    Args:
        stack: Detected project stack

    Returns:
        List of rule file paths to fetch
    """
    rules: set[str] = set()

    # Always include core OWASP rules
    rules.update(RULE_PATHS.get("_core", []))

    # Add AI/ML rules if applicable
    if stack.ai_ml:
        rules.update(RULE_PATHS.get("ai_ml", []))

    # Add RAG rules if applicable
    if stack.rag:
        rules.update(RULE_PATHS.get("rag", []))

    # Add language-specific rules
    for lang in stack.languages:
        rules.update(RULE_PATHS.get(lang, []))

    # Add framework rules
    for framework in stack.frameworks:
        rules.update(RULE_PATHS.get(framework, []))

    # Add database rules
    for db in stack.databases:
        rules.update(RULE_PATHS.get(db, []))

    # Add infrastructure rules
    for infra in stack.infrastructure:
        rules.update(RULE_PATHS.get(infra, []))

    return sorted(rules)


def filter_rules_for_files(
    file_paths: list[str],
    rules_dir: str | Path = ".claude/rules/security",
) -> list[Path]:
    """Filter security rule files relevant to specific task files.

    Maps file extensions to languages, returns matching security rule paths.
    Always includes _core/owasp-2025.md for code implementation tasks.
    """
    rules_path = Path(rules_dir)
    result: list[Path] = []

    # Always include core OWASP rules
    core = rules_path / "_core" / "owasp-2025.md"
    if core.exists():
        result.append(core)

    # Detect languages from file extensions
    detected_langs: set[str] = set()
    has_docker = False

    for fp in file_paths:
        p = Path(fp)
        ext = p.suffix.lower()
        name = p.name.lower()

        # Map extensions to languages
        if ext in (".py", ".pyx", ".pyi"):
            detected_langs.add("python")
        elif ext in (".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx"):
            detected_langs.add("javascript")

        # Check for Docker
        if "dockerfile" in name or name.startswith("docker-compose"):
            has_docker = True

    # Map languages to rule files
    for lang in detected_langs:
        lang_rule = rules_path / "languages" / lang / "CLAUDE.md"
        if lang_rule.exists():
            result.append(lang_rule)

    if has_docker:
        docker_rule = rules_path / "containers" / "docker" / "CLAUDE.md"
        if docker_rule.exists():
            result.append(docker_rule)

    return result


def summarize_rules(rule_paths: list[Path], max_tokens: int = 2000) -> str:
    """Read and summarize security rules within token budget.

    Extracts ## Rule headers and **Level** lines only, skipping code blocks.
    Returns markdown summary.
    """
    parts: list[str] = []
    chars_budget = max_tokens * 4  # ~4 chars per token
    chars_used = 0

    for rule_path in rule_paths:
        if not rule_path.exists():
            continue

        content = rule_path.read_text()
        in_code_block = False
        file_parts: list[str] = []

        for line in content.split("\n"):
            # Track code blocks
            if line.strip().startswith("```"):
                in_code_block = not in_code_block
                continue
            if in_code_block:
                continue

            # Include rule headers and level indicators
            if line.startswith("## Rule:") or line.startswith("### Rule:"):
                file_parts.append(line)
            elif line.startswith("**Level**:") or line.startswith("**Level**"):
                file_parts.append(line)
            elif line.startswith("**When**:") or line.startswith("**When**"):
                file_parts.append(line)

        if file_parts:
            section = f"### {rule_path.parent.name}/{rule_path.name}\n" + "\n".join(file_parts)
            if chars_used + len(section) <= chars_budget:
                parts.append(section)
                chars_used += len(section)

    return "\n\n".join(parts)


def fetch_rules(
    rule_paths: list[str],
    output_dir: Path,
    use_cache: bool = True,
) -> dict[str, Path]:
    """Fetch security rules from GitHub.

    Args:
        rule_paths: List of rule file paths to fetch
        output_dir: Directory to save fetched rules
        use_cache: Skip fetching if file already exists

    Returns:
        Dictionary mapping rule path to local file path
    """
    output_dir = Path(output_dir)
    output_dir.mkdir(parents=True, exist_ok=True)

    fetched: dict[str, Path] = {}

    for rule_path in rule_paths:
        # Create local path preserving directory structure
        local_path = output_dir / rule_path.replace("rules/", "")
        local_path.parent.mkdir(parents=True, exist_ok=True)

        if use_cache and local_path.exists():
            logger.debug(f"Using cached rule: {rule_path}")
            fetched[rule_path] = local_path
            continue

        # Fetch from GitHub
        url = f"{RULES_RAW_URL}/{rule_path}"
        try:
            result = subprocess.run(
                ["curl", "-fsSL", url],
                capture_output=True,
                text=True,
                timeout=30,
            )
            if result.returncode == 0:
                local_path.write_text(result.stdout)
                fetched[rule_path] = local_path
                logger.info(f"Fetched rule: {rule_path}")
            else:
                logger.warning(f"Failed to fetch {rule_path}: {result.stderr}")
        except subprocess.TimeoutExpired:
            logger.warning(f"Timeout fetching {rule_path}")
        except (subprocess.SubprocessError, OSError) as e:
            logger.warning(f"Error fetching {rule_path}: {e}")

    return fetched


def generate_claude_md_section(
    stack: ProjectStack,
    rules_dir: Path,
) -> str:
    """Generate CLAUDE.md security rules section.

    Args:
        stack: Detected project stack
        rules_dir: Directory containing fetched rules

    Returns:
        Markdown content for CLAUDE.md
    """
    lines = [
        "# Security Rules",
        "",
        f"Auto-generated from [TikiTribe/claude-secure-coding-rules](https://github.com/{RULES_REPO})",
        "",
        "## Detected Stack",
        "",
    ]

    if stack.languages:
        lines.append(f"- **Languages**: {', '.join(sorted(stack.languages))}")
    if stack.frameworks:
        lines.append(f"- **Frameworks**: {', '.join(sorted(stack.frameworks))}")
    if stack.databases:
        lines.append(f"- **Databases**: {', '.join(sorted(stack.databases))}")
    if stack.infrastructure:
        lines.append(f"- **Infrastructure**: {', '.join(sorted(stack.infrastructure))}")
    if stack.ai_ml:
        lines.append("- **AI/ML**: Yes")
    if stack.rag:
        lines.append("- **RAG**: Yes")

    lines.extend(["", "## Fetched Rules", ""])

    # List fetched rule files for reference (no @-imports needed;
    # Claude Code auto-loads everything under .claude/rules/).
    rules_dir = Path(rules_dir)
    if rules_dir.exists():
        for rule_file in collect_files(rules_dir, extensions={".md"}).get(".md", []):
            rel_path = rule_file.relative_to(rules_dir)
            lines.append(f"- `{rel_path}`")

    lines.append("")
    return "\n".join(lines)


def integrate_security_rules(
    project_path: Path,
    output_dir: Path | None = None,
    update_claude_md: bool = True,
) -> dict[str, Any]:
    """Full integration: detect, fetch, and integrate security rules.

    Args:
        project_path: Path to the project root
        output_dir: Where to store rules (default: .claude/rules/security)
        update_claude_md: Whether to update/create CLAUDE.md

    Returns:
        Dictionary with integration results
    """
    project_path = Path(project_path)
    output_dir = output_dir or project_path / ".claude" / "rules" / "security"

    # Step 1: Detect stack
    logger.info("Detecting project stack...")
    stack = detect_project_stack(project_path)

    # Step 2: Get required rules
    rule_paths = get_required_rules(stack)
    logger.info(f"Identified {len(rule_paths)} relevant rule files")

    # Step 3: Fetch rules
    logger.info("Fetching security rules...")
    fetched = fetch_rules(rule_paths, output_dir)

    # Step 4: Generate CLAUDE.md section
    claude_md_section = generate_claude_md_section(stack, output_dir)

    # Step 5: Update CLAUDE.md if requested
    if update_claude_md:
        claude_md_path = project_path / "CLAUDE.md"
        _update_claude_md(claude_md_path, claude_md_section)

    return {
        "stack": stack.to_dict(),
        "rules_fetched": len(fetched),
        "rules_dir": str(output_dir),
        "rule_paths": list(fetched.keys()),
    }


def _update_claude_md(claude_md_path: Path, security_section: str) -> None:
    """Update or create CLAUDE.md with security rules section.

    Args:
        claude_md_path: Path to CLAUDE.md
        security_section: Security rules section content
    """
    marker_start = "<!-- SECURITY_RULES_START -->"
    marker_end = "<!-- SECURITY_RULES_END -->"

    if claude_md_path.exists():
        content = claude_md_path.read_text()

        # Check if markers exist
        if marker_start in content and marker_end in content:
            # Replace existing section
            import re

            pattern = f"{re.escape(marker_start)}.*?{re.escape(marker_end)}"
            replacement = f"{marker_start}\n{security_section}\n{marker_end}"
            content = re.sub(pattern, replacement, content, flags=re.DOTALL)
        else:
            # Append section
            content = f"{content}\n\n{marker_start}\n{security_section}\n{marker_end}\n"
    else:
        # Create new file
        content = f"{marker_start}\n{security_section}\n{marker_end}\n"

    claude_md_path.write_text(content)
    logger.info(f"Updated {claude_md_path}")
