"""Checker for insecure random number generation in security contexts."""

import re
from pathlib import Path
from typing import List

from stablestack.checkers.base import BaseChecker
from stablestack.models import Finding, Severity, Tier


class InsecureRandomChecker(BaseChecker):
    """Detects use of non-cryptographic random for security-sensitive operations.

    Supports: Python, JavaScript/TypeScript, Go, Rust, Ruby
    """

    rule_id = "SEC004"
    rule_name = "insecure-random"
    description = "Non-cryptographic random is predictable and insecure for tokens/passwords"
    why = (
        "Python's random module and JavaScript's Math.random() are not cryptographically "
        "secure. Attackers can predict the output! For passwords, tokens, or any security "
        "purpose, use secrets (Python) or crypto.randomBytes (Node.js)."
    )
    severity = Severity.ERROR
    tier = Tier.CRITICAL  # Security issues are always critical
    file_extensions = {".py", ".js", ".ts", ".jsx", ".tsx", ".go", ".rs", ".rb"}

    # Context keywords that suggest security-sensitive usage
    _SECURITY_CONTEXTS = [
        'token', 'password', 'secret', 'key', 'auth', 'session',
        'csrf', 'nonce', 'salt', 'hash', 'encrypt', 'secure', 'credential',
    ]

    # Variable names where Math.random()/random is acceptable (not security-sensitive)
    _NON_SECURITY_NAMES = re.compile(
        r'\b(requestId|request_id|correlationId|correlation_id|'
        r'idempotencyKey|idempotency_key|traceId|trace_id|'
        r'uniqueId|unique_id|messageId|message_id|'
        r'eventId|event_id|transactionId|transaction_id|'
        r'jitter|delay|retry|timeout|offset|color|opacity|'
        r'position|width|height|angle|rotation|shuffle|sample|'
        r'placeholder|example|demo|test|mock|fixture)\b',
        re.IGNORECASE,
    )

    # Python patterns
    _PY_RANDOM = re.compile(r'\brandom\.(choice|choices|randint|random|sample|shuffle)\s*\(')

    # JavaScript patterns
    _JS_RANDOM = re.compile(r'\bMath\.random\s*\(\s*\)')

    # Go patterns - math/rand is not crypto-safe
    _GO_RAND = re.compile(r'\brand\.(Intn|Int31|Int63|Float32|Float64|Uint32|Uint64|Perm|Shuffle)\s*\(')

    # Rust patterns - rand crate without OsRng for security
    _RUST_RAND = re.compile(r'\brand::(random|thread_rng|Rng)\b')

    # Ruby patterns - rand() is not crypto-safe
    _RUBY_RAND = re.compile(r'\brand\s*\(|\.rand\s*\(')

    def check_file(self, path: Path, content: str) -> List[Finding]:
        findings = []
        lines = content.splitlines()

        if path.suffix == '.py':
            findings.extend(self._check_python(path, content, lines))
        elif path.suffix == '.go':
            findings.extend(self._check_go(path, content, lines))
        elif path.suffix == '.rs':
            findings.extend(self._check_rust(path, content, lines))
        elif path.suffix == '.rb':
            findings.extend(self._check_ruby(path, content, lines))
        else:
            findings.extend(self._check_javascript(path, content, lines))

        return findings

    def _check_python(self, path: Path, content: str, lines: List[str]) -> List[Finding]:
        """Check Python code for insecure random usage."""
        findings = []

        # Check if secrets module is available but not used
        uses_secrets = 'import secrets' in content or 'from secrets' in content

        for match in self._PY_RANDOM.finditer(content):
            line_num = content[:match.start()].count('\n') + 1
            line_content = lines[line_num - 1] if line_num <= len(lines) else ""

            # Skip non-security usages (e.g., request_id, trace_id)
            if self._is_non_security_usage(content, match.start()):
                continue

            # Check if this is in a security-sensitive context
            context = self._get_context(content, match.start())
            is_security = any(kw in context for kw in self._SECURITY_CONTEXTS)

            if is_security:
                func_name = match.group(1)
                findings.append(
                    self.create_finding(
                        path=path,
                        line=line_num,
                        column=match.start() - content.rfind('\n', 0, match.start()),
                        code=line_content.strip(),
                        message=(
                            f"random.{func_name}() used in security context. "
                            "This is predictable and not secure!"
                        ),
                        fix_hint=(
                            "Use the secrets module for security:\n"
                            "import secrets\n\n"
                            "# Generate a secure token\n"
                            "token = secrets.token_hex(32)\n\n"
                            "# Secure random choice\n"
                            "secrets.choice(alphabet)\n\n"
                            "# Secure random integer\n"
                            "secrets.randbelow(n)"
                        ),
                    )
                )

        return findings

    def _check_javascript(self, path: Path, content: str, lines: List[str]) -> List[Finding]:
        """Check JavaScript code for insecure random usage."""
        findings = []

        for match in self._JS_RANDOM.finditer(content):
            line_num = content[:match.start()].count('\n') + 1
            line_content = lines[line_num - 1] if line_num <= len(lines) else ""

            # Skip non-security usages (e.g., requestId, idempotencyKey)
            if self._is_non_security_usage(content, match.start()):
                continue

            # Check if this is in a security-sensitive context
            context = self._get_context(content, match.start())
            is_security = any(kw in context for kw in self._SECURITY_CONTEXTS)

            if is_security:
                findings.append(
                    self.create_finding(
                        path=path,
                        line=line_num,
                        column=match.start() - content.rfind('\n', 0, match.start()),
                        code=line_content.strip(),
                        message=(
                            "Math.random() used in security context. "
                            "This is predictable and not secure!"
                        ),
                        fix_hint=(
                            "Use crypto for security:\n\n"
                            "Node.js:\n"
                            "const crypto = require('crypto');\n"
                            "const token = crypto.randomBytes(32).toString('hex');\n\n"
                            "Browser:\n"
                            "const array = new Uint8Array(32);\n"
                            "crypto.getRandomValues(array);"
                        ),
                    )
                )

        return findings

    def _check_go(self, path: Path, content: str, lines: List[str]) -> List[Finding]:
        """Check Go code for insecure random usage."""
        findings = []

        # Skip if using crypto/rand
        uses_crypto = 'crypto/rand' in content

        for match in self._GO_RAND.finditer(content):
            line_num = content[:match.start()].count('\n') + 1
            line_content = lines[line_num - 1] if line_num <= len(lines) else ""

            # Check if this is in a security-sensitive context
            context = self._get_context(content, match.start())
            is_security = any(kw in context for kw in self._SECURITY_CONTEXTS)

            if is_security:
                func_name = match.group(1)
                findings.append(
                    self.create_finding(
                        path=path,
                        line=line_num,
                        column=match.start() - content.rfind('\n', 0, match.start()),
                        code=line_content.strip(),
                        message=(
                            f"rand.{func_name}() used in security context. "
                            "math/rand is predictable and not secure!"
                        ),
                        fix_hint=(
                            "Use crypto/rand for security:\n\n"
                            "import \"crypto/rand\"\n\n"
                            "// Generate secure random bytes\n"
                            "token := make([]byte, 32)\n"
                            "rand.Read(token)\n\n"
                            "// Or use a helper function\n"
                            "func generateToken() string {\n"
                            "    b := make([]byte, 32)\n"
                            "    rand.Read(b)\n"
                            "    return base64.URLEncoding.EncodeToString(b)\n"
                            "}"
                        ),
                    )
                )

        return findings

    def _check_rust(self, path: Path, content: str, lines: List[str]) -> List[Finding]:
        """Check Rust code for insecure random usage."""
        findings = []

        for match in self._RUST_RAND.finditer(content):
            line_num = content[:match.start()].count('\n') + 1
            line_content = lines[line_num - 1] if line_num <= len(lines) else ""

            # Check if this is in a security-sensitive context
            context = self._get_context(content, match.start())
            is_security = any(kw in context for kw in self._SECURITY_CONTEXTS)

            # Skip if OsRng or getrandom is nearby (proper crypto usage)
            if 'OsRng' in context or 'getrandom' in context:
                continue

            if is_security:
                func_name = match.group(1)
                findings.append(
                    self.create_finding(
                        path=path,
                        line=line_num,
                        column=match.start() - content.rfind('\n', 0, match.start()),
                        code=line_content.strip(),
                        message=(
                            f"rand::{func_name} used in security context. "
                            "Use OsRng for cryptographic security!"
                        ),
                        fix_hint=(
                            "Use OsRng for security:\n\n"
                            "use rand::rngs::OsRng;\n"
                            "use rand::RngCore;\n\n"
                            "// Generate secure random bytes\n"
                            "let mut token = [0u8; 32];\n"
                            "OsRng.fill_bytes(&mut token);\n\n"
                            "// Or use the getrandom crate directly\n"
                            "use getrandom::getrandom;\n"
                            "let mut buf = [0u8; 32];\n"
                            "getrandom(&mut buf).expect(\"failed to get random\");"
                        ),
                    )
                )

        return findings

    def _check_ruby(self, path: Path, content: str, lines: List[str]) -> List[Finding]:
        """Check Ruby code for insecure random usage."""
        findings = []

        for match in self._RUBY_RAND.finditer(content):
            line_num = content[:match.start()].count('\n') + 1
            line_content = lines[line_num - 1] if line_num <= len(lines) else ""

            # Check if this is in a security-sensitive context
            context = self._get_context(content, match.start())
            is_security = any(kw in context for kw in self._SECURITY_CONTEXTS)

            # Skip if SecureRandom is used nearby
            if 'SecureRandom' in content:
                continue

            if is_security:
                findings.append(
                    self.create_finding(
                        path=path,
                        line=line_num,
                        column=match.start() - content.rfind('\n', 0, match.start()),
                        code=line_content.strip(),
                        message=(
                            "rand() used in security context. "
                            "Kernel#rand is predictable and not secure!"
                        ),
                        fix_hint=(
                            "Use SecureRandom for security:\n\n"
                            "require 'securerandom'\n\n"
                            "# Generate secure random token\n"
                            "token = SecureRandom.hex(32)\n\n"
                            "# Generate UUID\n"
                            "uuid = SecureRandom.uuid\n\n"
                            "# Generate URL-safe base64\n"
                            "token = SecureRandom.urlsafe_base64(32)"
                        ),
                    )
                )

        return findings

    def _get_context(self, content: str, position: int) -> str:
        """Get surrounding context for a position in the content."""
        start = max(0, position - 200)
        end = min(len(content), position + 200)
        return content[start:end].lower()

    def _is_non_security_usage(self, content: str, position: int) -> bool:
        """Check if the random usage is for a non-security purpose (e.g., request IDs)."""
        # Check the immediate line for non-security variable names
        line_start = content.rfind('\n', 0, position) + 1
        line_end = content.find('\n', position)
        if line_end == -1:
            line_end = len(content)
        line = content[line_start:line_end]
        return bool(self._NON_SECURITY_NAMES.search(line))
