"""
HTML Crypto Scanner - Extracts and scans JavaScript from HTML files.

Since OpenGrep doesn't detect JavaScript inside HTML <script> tags, this module
extracts embedded scripts and scans them for weak cryptography.
"""

import logging
import re
from dataclasses import dataclass
from html.parser import HTMLParser
from pathlib import Path
from typing import List, Dict, Any

logger = logging.getLogger(__name__)


@dataclass
class HTMLFinding:
    """Represents a crypto vulnerability found in HTML/JavaScript 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
    context: str  # 'inline-script', 'inline-event', 'src-attribute'


class ScriptExtractor(HTMLParser):
    """Extract JavaScript from HTML."""
    
    def __init__(self):
        super().__init__()
        self.scripts = []
        self.current_script = None
        self.current_line = 1
        
    def handle_starttag(self, tag, attrs):
        if tag == 'script':
            self.current_script = {
                'start_line': self.current_line,
                'content': '',
                'type': 'script-tag'
            }
        # Check for inline event handlers
        for attr, value in attrs:
            if attr.startswith('on') and value:  # onclick, onload, etc.
                self.scripts.append({
                    'start_line': self.current_line,
                    'content': value,
                    'type': 'inline-event'
                })
                
    def handle_endtag(self, tag):
        if tag == 'script' and self.current_script:
            self.scripts.append(self.current_script)
            self.current_script = None
            
    def handle_data(self, data):
        if self.current_script is not None:
            self.current_script['content'] += data
        self.current_line += data.count('\n')


class HTMLCryptoScanner:
    """Scanner for detecting weak cryptography in HTML files."""

    # ── Quantum Risk Score Framework (0-100) ───────────────────────────────
    # 95  Classically AND quantum broken — MD5, SHA-1, DES, 3DES, RC4
    #     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, RSA-SHA256, DH — 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. Math.random() / non-crypto PRNG used for security-sensitive values
    # ──────────────────────────────────────────────────────────────────────

    # JavaScript crypto patterns
    JS_PATTERNS = {
        'CryptoJS_MD5': {
            'pattern': r'\bCryptoJS\.MD5\s*\(',
            'message': 'CryptoJS.MD5() detected - cryptographically broken and quantum vulnerable',
            'severity': 'ERROR',
            'risk_score': 95
        },
        'CryptoJS_SHA1': {
            'pattern': r'\bCryptoJS\.SHA1\s*\(',
            'message': 'CryptoJS.SHA1() detected - collision attacks possible, quantum vulnerable',
            'severity': 'ERROR',
            'risk_score': 90
        },
        'CryptoJS_DES': {
            'pattern': r'\bCryptoJS\.DES\.encrypt\s*\(',
            'message': 'CryptoJS.DES.encrypt() detected - broken cipher, quantum vulnerable',
            'severity': 'ERROR',
            'risk_score': 95
        },
        'CryptoJS_TripleDES': {
            'pattern': r'\bCryptoJS\.TripleDES\.encrypt\s*\(',
            'message': 'CryptoJS.TripleDES.encrypt() detected - weak cipher, quantum vulnerable',
            'severity': 'ERROR',
            'risk_score': 95
        },
        'CryptoJS_RC4': {
            'pattern': r'\bCryptoJS\.RC4\.encrypt\s*\(',
            'message': 'CryptoJS.RC4.encrypt() detected - broken cipher, quantum vulnerable',
            'severity': 'ERROR',
            'risk_score': 95
        },
        'crypto_createHash_md5': {
            'pattern': r"crypto\.createHash\s*\(\s*['\"]md5['\"]",
            'message': 'crypto.createHash("md5") detected - use SHA-256 or SHA-3',
            'severity': 'ERROR',
            'risk_score': 95
        },
        'crypto_createHash_sha1': {
            'pattern': r"crypto\.createHash\s*\(\s*['\"]sha1['\"]",
            'message': 'crypto.createHash("sha1") detected - use SHA-256 or SHA-3',
            'severity': 'ERROR',
            'risk_score': 90
        },
        'md5_library': {
            'pattern': r'\bmd5\s*\(',
            'message': 'md5() function call detected - likely from md5 npm package, quantum vulnerable',
            'severity': 'ERROR',
            'risk_score': 95
        },
        'sha1_library': {
            'pattern': r'\bsha1\s*\(',
            'message': 'sha1() function call detected - likely from sha1 npm package, quantum vulnerable',
            'severity': 'ERROR',
            'risk_score': 90
        },
        'SubtleCrypto_RSA_1024': {
            'pattern': r'modulusLength\s*:\s*1024',
            'message': 'RSA 1024-bit key detected in SubtleCrypto - use 2048+ bits',
            'severity': 'ERROR',
            'risk_score': 95
        },
        'SubtleCrypto_RSA_512': {
            'pattern': r'modulusLength\s*:\s*512',
            'message': 'RSA 512-bit key detected in SubtleCrypto - use 2048+ bits',
            'severity': 'ERROR',
            'risk_score': 95
        },
        'SubtleCrypto_P256': {
            'pattern': r"namedCurve\s*:\s*['\"]P-256['\"]",
            'message': 'ECC P-256 curve detected in SubtleCrypto - quantum vulnerable',
            'severity': 'WARNING',
            'risk_score': 85
        },
    }
    
    def scan_file(self, file_path: str) -> List[HTMLFinding]:
        """Scan an HTML file for weak cryptography in embedded JavaScript."""
        findings = []
        
        try:
            with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
                html_content = f.read()
                
            # Extract scripts
            extractor = ScriptExtractor()
            extractor.feed(html_content)
            
            # Scan each extracted script
            for script_info in extractor.scripts:
                script_content = script_info['content']
                start_line = script_info['start_line']
                script_type = script_info['type']
                
                # Check each pattern
                # Normalize line endings so \n counts are consistent
                normalized = script_content.replace('\r\n', '\n').replace('\r', '\n')
                script_lines = normalized.split('\n')

                for func_name, pattern_info in self.JS_PATTERNS.items():
                    pattern = pattern_info['pattern']
                    matches = re.finditer(pattern, normalized, re.IGNORECASE)

                    for match in matches:
                        # Calculate line number within the script
                        lines_before = normalized[:match.start()].count('\n')
                        line_num = start_line + lines_before

                        # Get the code snippet for the matched line
                        code_snippet = script_lines[lines_before].strip() if lines_before < len(script_lines) else normalized[:100]

                        # Column is position within the matched line
                        line_start_offset = normalized.rfind('\n', 0, match.start()) + 1
                        col_start = match.start() - line_start_offset + 1
                        col_end = match.end() - line_start_offset + 1

                        finding = HTMLFinding(
                            file_path=file_path,
                            line_number=line_num,
                            column=col_start,
                            end_column=col_end,
                            function_name=func_name,
                            severity=pattern_info['severity'],
                            message=pattern_info['message'],
                            quantum_risk_score=pattern_info['risk_score'],
                            code_snippet=code_snippet,
                            context=script_type
                        )
                        findings.append(finding)

        except Exception as e:
            logger.exception(f"Error scanning {file_path}: {e}")
            
        return findings
    
    def format_findings(self, findings: List[HTMLFinding]) -> Dict[str, Any]:
        """Format findings in scanner-compatible JSON format."""
        results = []
        
        for finding in findings:
            result = {
                "check_id": f"html-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": f"{finding.message} (found in {finding.context})",
                    "metadata": {
                        "category": "security",
                        "technology": ["html", "javascript"],
                        "quantum_vulnerable": True,
                        "quantum_risk_score": finding.quantum_risk_score,
                        "context": finding.context
                    },
                    "severity": finding.severity,
                    "lines": finding.code_snippet
                }
            }
            results.append(result)
            
        return {"results": results, "errors": [], "version": "html-crypto-scanner-1.0"}
