"""
Elixir Crypto Scanner - Regex-based detection for Erlang :crypto module.

Since OpenGrep patterns don't match Elixir/Erlang crypto calls, this module
provides regex-based detection of weak cryptography in Elixir code.
"""

import logging
import re
from dataclasses import dataclass
from pathlib import Path
from typing import List, Dict, Any

logger = logging.getLogger(__name__)


@dataclass
class ElixirFinding:
    """Represents a crypto vulnerability found in Elixir code."""
    file_path: str
    line_number: int
    column: int
    end_column: int
    function_name: str
    severity: str
    message: str
    quantum_risk_score: int
    code_snippet: str


class ElixirCryptoScanner:
    """Scanner for detecting weak cryptography in Elixir files."""

    # ── Quantum Risk Score Framework (0-100) ───────────────────────────────
    # 95  Classically AND quantum broken — MD5, SHA-1, DES, 3DES, RC4, MD4
    #     Broken even without quantum computers; Grover's/Shor's makes it worse
    # 90  Quantum-broken (Shor's) with additional classical weakness
    #     e.g. RSA-SHA1 (RSA broken by Shor's + SHA-1 is weak)
    # 85  Quantum-broken (Shor's), still classically sound
    #     e.g. ECC curves — safe today but broken by large quantum computer
    # 75  Quantum-weakened (Grover's halves effective key size)
    #     e.g. AES-128 → 64-bit effective; still needs quantum advantage to break
    # 70  Security hygiene — not an algorithm break, but unsafe practice
    #     e.g. :rand / :random modules — non-crypto PRNG for security-sensitive values
    # ──────────────────────────────────────────────────────────────────────

    # Regex patterns for Erlang :crypto and :public_key modules
    PATTERNS = {
        'rsa_1024': {
            'pattern': r':public_key\.generate_key\s*\(\s*\{\s*:rsa\s*,\s*1024',
            'message': 'RSA 1024-bit key generation detected - use 2048+ bits',
            'severity': 'ERROR',
            'risk_score': 95
        },
        'rsa_512': {
            'pattern': r':public_key\.generate_key\s*\(\s*\{\s*:rsa\s*,\s*512',
            'message': 'RSA 512-bit key generation detected - use 2048+ bits',
            'severity': 'ERROR',
            'risk_score': 95
        },
        'ecdh_secp256r1': {
            'pattern': r':crypto\.generate_key\s*\(\s*:ecdh\s*,\s*:secp256r1',
            'message': 'ECC secp256r1 curve detected - quantum vulnerable',
            'severity': 'WARNING',
            'risk_score': 85
        },
        'ecdh_secp256k1': {
            'pattern': r':crypto\.generate_key\s*\(\s*:ecdh\s*,\s*:secp256k1',
            'message': 'ECC secp256k1 curve detected - quantum vulnerable',
            'severity': 'WARNING',
            'risk_score': 85
        },
        'ecdh_secp384r1': {
            'pattern': r':crypto\.generate_key\s*\(\s*:ecdh\s*,\s*:secp384r1',
            'message': 'ECC secp384r1 curve detected - quantum vulnerable',
            'severity': 'WARNING',
            'risk_score': 85
        },
        'ecdh_secp521r1': {
            'pattern': r':crypto\.generate_key\s*\(\s*:ecdh\s*,\s*:secp521r1',
            'message': 'ECC secp521r1 curve detected - quantum vulnerable',
            'severity': 'WARNING',
            'risk_score': 85
        },
        'hash_md5': {
            'pattern': r':crypto\.hash\s*\(\s*:md5',
            'message': ':crypto.hash with :md5 detected - use :sha256 or :sha3_256',
            'severity': 'ERROR',
            'risk_score': 95
        },
        'hash_sha': {
            'pattern': r':crypto\.hash\s*\(\s*:sha\b',
            'message': ':crypto.hash with :sha (SHA-1) detected - use :sha256 or :sha3_256',
            'severity': 'ERROR',
            'risk_score': 90
        },
        'block_encrypt_des_ecb': {
            'pattern': r':crypto\.block_encrypt\s*\(\s*:des_ecb',
            'message': ':crypto.block_encrypt with :des_ecb detected - use :aes_gcm',
            'severity': 'ERROR',
            'risk_score': 95
        },
        'block_encrypt_des_cbc': {
            'pattern': r':crypto\.block_encrypt\s*\(\s*:des_cbc',
            'message': ':crypto.block_encrypt with :des_cbc detected - use :aes_gcm',
            'severity': 'ERROR',
            'risk_score': 95
        },
        'block_encrypt_des3_cbc': {
            'pattern': r':crypto\.block_encrypt\s*\(\s*:des3_cbc',
            'message': ':crypto.block_encrypt with :des3_cbc (3DES) detected - use :aes_gcm',
            'severity': 'ERROR',
            'risk_score': 95
        },
        'block_encrypt_aes_cbc128': {
            'pattern': r':crypto\.block_encrypt\s*\(\s*:aes_cbc128',
            'message': ':crypto.block_encrypt with :aes_cbc128 detected - use :aes_gcm with 256-bit keys',
            'severity': 'WARNING',
            'risk_score': 75
        },
        'stream_encrypt_rc4': {
            'pattern': r':crypto\.stream_encrypt\s*\(\s*:rc4',
            'message': ':crypto.stream_encrypt with :rc4 detected - broken cipher',
            'severity': 'ERROR',
            'risk_score': 95
        },
        'block_decrypt_des': {
            'pattern': r':crypto\.block_decrypt\s*\(\s*:des_',
            'message': ':crypto.block_decrypt with DES detected - use :aes_gcm',
            'severity': 'ERROR',
            'risk_score': 95
        },
        'block_decrypt_des3': {
            'pattern': r':crypto\.block_decrypt\s*\(\s*:des3_',
            'message': ':crypto.block_decrypt with 3DES detected - use :aes_gcm',
            'severity': 'ERROR',
            'risk_score': 95
        },
        'stream_decrypt_rc4': {
            'pattern': r':crypto\.stream_decrypt\s*\(\s*:rc4',
            'message': ':crypto.stream_decrypt with :rc4 detected - broken cipher',
            'severity': 'ERROR',
            'risk_score': 95
        },
        # MD4 hash (completely broken)
        'hash_md4': {
            'pattern': r':crypto\.hash\s*\(\s*:md4',
            'message': ':crypto.hash with :md4 detected - MD4 is completely broken; use :sha256 or :sha3_256',
            'severity': 'ERROR',
            'risk_score': 95
        },
        # Non-cryptographic random number generators
        'weak_random_rand': {
            'pattern': r':rand\.(uniform|normal|seed)\s*\(',
            'message': ':rand module is not cryptographically secure - use :crypto.strong_rand_bytes/1 for security-sensitive operations',
            'severity': 'WARNING',
            'risk_score': 70
        },
        'weak_random_random': {
            'pattern': r':random\.(uniform|seed)\s*\(',
            'message': 'Deprecated :random module is not cryptographically secure - use :crypto.strong_rand_bytes/1',
            'severity': 'WARNING',
            'risk_score': 70
        },
    }
    
    def scan_file(self, file_path: str) -> List[ElixirFinding]:
        """Scan an Elixir file for weak cryptography."""
        findings = []
        
        try:
            with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
                lines = f.readlines()
                
            for line_num, line in enumerate(lines, start=1):
                for func_name, pattern_info in self.PATTERNS.items():
                    pattern = pattern_info['pattern']
                    matches = re.finditer(pattern, line, re.IGNORECASE)
                    
                    for match in matches:
                        finding = ElixirFinding(
                            file_path=file_path,
                            line_number=line_num,
                            column=match.start() + 1,
                            end_column=match.end() + 1,
                            function_name=func_name,
                            severity=pattern_info['severity'],
                            message=pattern_info['message'],
                            quantum_risk_score=pattern_info['risk_score'],
                            code_snippet=line.strip()
                        )
                        findings.append(finding)

        except Exception as e:
            logger.exception(f"Error scanning {file_path}: {e}")
            
        return findings
    
    def format_findings(self, findings: List[ElixirFinding]) -> Dict[str, Any]:
        """Format findings in scanner-compatible JSON format."""
        results = []
        
        for finding in findings:
            result = {
                "check_id": f"elixir-crypto-scanner.{finding.function_name.lower()}",
                "path": finding.file_path,
                "start": {"line": finding.line_number, "col": finding.column},
                "end": {"line": finding.line_number, "col": finding.end_column},
                "start_line": finding.line_number,
                "end_line": finding.line_number,
                "extra": {
                    "message": finding.message,
                    "metadata": {
                        "category": "security",
                        "technology": ["elixir", "erlang"],
                        "quantum_vulnerable": True,
                        "quantum_risk_score": finding.quantum_risk_score
                    },
                    "severity": finding.severity,
                    "lines": finding.code_snippet
                }
            }
            results.append(result)
            
        return {"results": results, "errors": [], "version": "elixir-crypto-scanner-1.0"}
