"""
BouncyCastle Crypto Scanner - Regex-based detection for org.bouncycastle APIs.

Complements OpenGrep YAML rules with regex patterns that catch string-based
algorithm identifiers, import declarations, and BC-specific class usage.
"""

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

logger = logging.getLogger(__name__)


@dataclass
class BouncyCastleFinding:
    """Represents a crypto vulnerability found in Java/BouncyCastle code."""
    file_path: str
    line_number: int
    column: int
    end_column: int
    rule_id: str
    severity: str
    message: str
    quantum_risk_score: int
    code_snippet: str


class BouncyCastleCryptoScanner:
    """Scanner for detecting weak cryptography in Java files using BouncyCastle."""

    # ── Quantum Risk Score Framework (0-100) ───────────────────────────────
    # 95  Classically AND quantum broken — DES, 3DES, RC4, MD5, SHA-1
    # 90  Quantum-broken (Shor's) with classical weakness — RSA-SHA1
    # 85  Quantum-broken (Shor's), classically sound — RSA, ECC, DSA
    # 75  Quantum-weakened (Grover's) — AES-128
    # 70  Security hygiene — misuse, disabled checks, hardcoded keys
    # ──────────────────────────────────────────────────────────────────────

    PATTERNS = {
        # ── RSA ─────────────────────────────────────────────────────────────
        'bc_rsa_keypair_generator': {
            'pattern': r'new\s+RSAKeyPairGenerator\s*\(',
            'message': 'BouncyCastle RSAKeyPairGenerator: RSA is broken by Shor\'s algorithm. Migrate to CRYSTALS-Dilithium or CRYSTALS-Kyber.',
            'severity': 'ERROR',
            'risk_score': 85,
        },
        'bc_rsa_engine': {
            'pattern': r'new\s+RSAEngine\s*\(',
            'message': 'BouncyCastle RSAEngine: RSA encryption is broken by Shor\'s algorithm.',
            'severity': 'ERROR',
            'risk_score': 85,
        },
        'bc_pkcs1_encoding': {
            'pattern': r'new\s+PKCS1Encoding\s*\(',
            'message': 'BouncyCastle PKCS1Encoding (PKCS#1 v1.5): quantum-vulnerable RSA padding. Use OAEP at minimum; migrate to PQC.',
            'severity': 'ERROR',
            'risk_score': 90,
        },
        'bc_rsa_blinded_engine': {
            'pattern': r'new\s+RSABlindedEngine\s*\(',
            'message': 'BouncyCastle RSABlindedEngine: RSA is broken by Shor\'s algorithm regardless of blinding.',
            'severity': 'ERROR',
            'risk_score': 85,
        },
        'bc_rsa_keypair_gen_string': {
            'pattern': r'KeyPairGenerator\.getInstance\s*\(\s*["\']RSA["\']\s*,\s*["\']BC["\']',
            'message': 'RSA key generation via BouncyCastle provider: quantum-vulnerable. Migrate to CRYSTALS-Kyber/Dilithium.',
            'severity': 'ERROR',
            'risk_score': 85,
        },

        # ── ECC ─────────────────────────────────────────────────────────────
        'bc_ec_keypair_generator': {
            'pattern': r'new\s+ECKeyPairGenerator\s*\(',
            'message': 'BouncyCastle ECKeyPairGenerator: ECC is broken by Shor\'s algorithm. Migrate to CRYSTALS-Dilithium (signatures) or CRYSTALS-Kyber (key exchange).',
            'severity': 'ERROR',
            'risk_score': 85,
        },
        'bc_ecdsa_signer': {
            'pattern': r'new\s+ECDSASigner\s*\(',
            'message': 'BouncyCastle ECDSASigner: ECDSA is broken by Shor\'s algorithm. Migrate to CRYSTALS-Dilithium or FALCON.',
            'severity': 'ERROR',
            'risk_score': 85,
        },
        'bc_ecdh_agreement': {
            'pattern': r'new\s+ECDHBasicAgreement\s*\(\s*\)',
            'message': 'BouncyCastle ECDHBasicAgreement: ECDH is broken by Shor\'s algorithm. Migrate to CRYSTALS-Kyber (ML-KEM).',
            'severity': 'ERROR',
            'risk_score': 85,
        },
        'bc_ec_provider_string': {
            'pattern': r'KeyPairGenerator\.getInstance\s*\(\s*["\']EC(?:DSA)?["\']\s*,\s*["\']BC["\']',
            'message': 'ECC key generation via BouncyCastle provider: quantum-vulnerable.',
            'severity': 'ERROR',
            'risk_score': 85,
        },

        # ── DSA ─────────────────────────────────────────────────────────────
        'bc_dsa_keypair_generator': {
            'pattern': r'new\s+DSAKeyPairGenerator\s*\(',
            'message': 'BouncyCastle DSAKeyPairGenerator: DSA is broken by Shor\'s algorithm. Migrate to CRYSTALS-Dilithium.',
            'severity': 'ERROR',
            'risk_score': 85,
        },
        'bc_dsa_signer': {
            'pattern': r'new\s+DSASigner\s*\(',
            'message': 'BouncyCastle DSASigner: DSA is broken by Shor\'s algorithm.',
            'severity': 'ERROR',
            'risk_score': 85,
        },

        # ── Broken hashes ────────────────────────────────────────────────────
        'bc_md5_digest': {
            'pattern': r'new\s+MD5Digest\s*\(',
            'message': 'BouncyCastle MD5Digest: MD5 is cryptographically broken and quantum-weakened by Grover\'s. Use SHA-256 or SHA-3.',
            'severity': 'ERROR',
            'risk_score': 95,
        },
        'bc_sha1_digest': {
            'pattern': r'new\s+SHA1Digest\s*\(',
            'message': 'BouncyCastle SHA1Digest: SHA-1 is deprecated and quantum-weakened. Use SHA-256 or SHA-3.',
            'severity': 'WARNING',
            'risk_score': 90,
        },
        'bc_md4_digest': {
            'pattern': r'new\s+MD4Digest\s*\(',
            'message': 'BouncyCastle MD4Digest: MD4 is completely broken. Use SHA-256 or SHA-3.',
            'severity': 'ERROR',
            'risk_score': 95,
        },
        'bc_ripemd160': {
            'pattern': r'new\s+RIPEMD160Digest\s*\(',
            'message': 'BouncyCastle RIPEMD-160: quantum-weakened by Grover\'s algorithm (effective 80-bit security). Prefer SHA-256 or SHA-3.',
            'severity': 'WARNING',
            'risk_score': 75,
        },

        # ── Broken ciphers ────────────────────────────────────────────────────
        'bc_des_engine': {
            'pattern': r'new\s+DESEngine\s*\(',
            'message': 'BouncyCastle DESEngine: DES is classically broken (56-bit key) and quantum-vulnerable. Use AES-256-GCM.',
            'severity': 'ERROR',
            'risk_score': 95,
        },
        'bc_desede_engine': {
            'pattern': r'new\s+DESedeEngine\s*\(',
            'message': 'BouncyCastle DESedeEngine (3DES): deprecated by NIST (2023), 112-bit effective security, quantum-vulnerable. Use AES-256-GCM.',
            'severity': 'ERROR',
            'risk_score': 95,
        },
        'bc_rc4_engine': {
            'pattern': r'new\s+(?:RC4|ARC4)Engine\s*\(',
            'message': 'BouncyCastle RC4/ARC4 stream cipher: cryptographically broken (RFC 7465). Use ChaCha20-Poly1305 or AES-256-GCM.',
            'severity': 'ERROR',
            'risk_score': 95,
        },
        'bc_rc2_engine': {
            'pattern': r'new\s+RC2Engine\s*\(',
            'message': 'BouncyCastle RC2Engine: RC2 is broken. Use AES-256-GCM.',
            'severity': 'ERROR',
            'risk_score': 95,
        },
        'bc_blowfish_engine': {
            'pattern': r'new\s+BlowfishEngine\s*\(',
            'message': 'BouncyCastle BlowfishEngine: Blowfish has a 64-bit block size (birthday-bound attacks) and is quantum-weakened. Use AES-256-GCM.',
            'severity': 'WARNING',
            'risk_score': 80,
        },
        'bc_aes_ecb': {
            'pattern': r'new\s+AESEngine\s*\(\s*\)',
            'message': 'BouncyCastle AESEngine used directly (likely ECB mode): ECB mode leaks plaintext structure. Use AESGCMBlockCipher or GCMBlockCipher.',
            'severity': 'WARNING',
            'risk_score': 75,
        },

        # ── String-based algorithm identifiers ──────────────────────────────
        'cipher_des_string': {
            'pattern': r'Cipher\.getInstance\s*\(\s*["\']DES(?:/[^"\']+)?["\']',
            'message': 'DES cipher via string identifier: broken algorithm. Use AES-256-GCM.',
            'severity': 'ERROR',
            'risk_score': 95,
        },
        'cipher_3des_string': {
            'pattern': r'Cipher\.getInstance\s*\(\s*["\'](?:DESede|TripleDES|3DES)(?:/[^"\']+)?["\']',
            'message': '3DES/DESede cipher via string identifier: deprecated by NIST. Use AES-256-GCM.',
            'severity': 'ERROR',
            'risk_score': 95,
        },
        'cipher_rc4_string': {
            'pattern': r'Cipher\.getInstance\s*\(\s*["\'](?:RC4|ARCFOUR|RC4/NONE)["\']',
            'message': 'RC4 cipher via string identifier: banned by RFC 7465.',
            'severity': 'ERROR',
            'risk_score': 95,
        },
        'message_digest_md5_string': {
            'pattern': r'MessageDigest\.getInstance\s*\(\s*["\']MD5["\']',
            'message': 'MD5 MessageDigest via string identifier: broken hash. Use SHA-256 or SHA-3.',
            'severity': 'ERROR',
            'risk_score': 95,
        },
        'message_digest_sha1_string': {
            'pattern': r'MessageDigest\.getInstance\s*\(\s*["\']SHA-?1["\']',
            'message': 'SHA-1 MessageDigest via string identifier: deprecated. Use SHA-256 or SHA-3.',
            'severity': 'WARNING',
            'risk_score': 90,
        },

        # ── BouncyCastle provider registration ──────────────────────────────
        'bc_provider_registration': {
            'pattern': r'Security\.addProvider\s*\(\s*new\s+BouncyCastleProvider\s*\(',
            'message': 'BouncyCastle provider registered. Verify all algorithms used are post-quantum safe.',
            'severity': 'WARNING',
            'risk_score': 50,
        },
    }

    def scan_file(self, file_path: str) -> List[BouncyCastleFinding]:
        """Scan a Java file for BouncyCastle crypto vulnerabilities."""
        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):
                # Skip comment lines
                stripped = line.strip()
                if stripped.startswith('//') or stripped.startswith('*'):
                    continue
                for rule_id, pattern_info in self.PATTERNS.items():
                    for match in re.finditer(pattern_info['pattern'], line, re.IGNORECASE):
                        findings.append(BouncyCastleFinding(
                            file_path=file_path,
                            line_number=line_num,
                            column=match.start() + 1,
                            end_column=match.end() + 1,
                            rule_id=rule_id,
                            severity=pattern_info['severity'],
                            message=pattern_info['message'],
                            quantum_risk_score=pattern_info['risk_score'],
                            code_snippet=line.strip(),
                        ))
        except Exception as e:
            logger.exception(f"Error scanning {file_path}: {e}")
        return findings

    def format_findings(self, findings: List[BouncyCastleFinding]) -> Dict[str, Any]:
        """Format findings in scanner-compatible JSON format."""
        results = []
        for finding in findings:
            results.append({
                'check_id': f'bouncycastle-crypto-scanner.{finding.rule_id}',
                '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': ['java', 'bouncycastle'],
                        'quantum_vulnerable': True,
                        'quantum_risk_score': finding.quantum_risk_score,
                    },
                    'severity': finding.severity,
                    'lines': finding.code_snippet,
                },
            })
        return {'results': results, 'errors': [], 'version': 'bouncycastle-crypto-scanner-1.0'}
