"""Obfuscation detection via entropy and statistical analysis.

Detects obfuscated/minified malicious code through:
- Shannon entropy of identifiers
- Average identifier length
- Base64 pattern density
- Hex escape sequence density
"""
import math
import re
from typing import List, Tuple

from .patterns import Finding, Severity


# Patterns to extract identifiers
JS_IDENTIFIER_PATTERN = re.compile(r'\b([a-zA-Z_$][a-zA-Z0-9_$]*)\b')
PY_IDENTIFIER_PATTERN = re.compile(r'\b([a-zA-Z_][a-zA-Z0-9_]*)\b')

# Common keywords/builtins to exclude from identifier analysis
JS_KEYWORDS = {
    'var', 'let', 'const', 'function', 'return', 'if', 'else', 'for', 'while', 'do',
    'switch', 'case', 'break', 'continue', 'try', 'catch', 'finally', 'throw',
    'new', 'this', 'class', 'extends', 'super', 'import', 'export', 'default',
    'true', 'false', 'null', 'undefined', 'typeof', 'instanceof', 'in', 'of',
    'async', 'await', 'yield', 'delete', 'void', 'with', 'debugger',
    'console', 'window', 'document', 'Object', 'Array', 'String', 'Number',
    'Boolean', 'Function', 'Math', 'Date', 'JSON', 'Promise', 'Map', 'Set',
    'module', 'exports', 'require', 'process', 'global', 'Buffer',
}

PY_KEYWORDS = {
    'def', 'class', 'if', 'elif', 'else', 'for', 'while', 'try', 'except',
    'finally', 'with', 'as', 'import', 'from', 'return', 'yield', 'raise',
    'pass', 'break', 'continue', 'and', 'or', 'not', 'in', 'is', 'lambda',
    'True', 'False', 'None', 'self', 'cls', 'global', 'nonlocal', 'assert',
    'async', 'await', 'print', 'len', 'range', 'str', 'int', 'float', 'list',
    'dict', 'set', 'tuple', 'type', 'open', 'file', 'input', 'output',
}


def calculate_shannon_entropy(text: str) -> float:
    """Calculate Shannon entropy of a string (bits per character)."""
    if not text:
        return 0.0
    
    # Count character frequencies
    freq = {}
    for char in text:
        freq[char] = freq.get(char, 0) + 1
    
    # Calculate entropy
    length = len(text)
    entropy = 0.0
    for count in freq.values():
        if count > 0:
            p = count / length
            entropy -= p * math.log2(p)
    
    return entropy


def extract_identifiers(content: str, language: str = 'auto') -> List[str]:
    """Extract variable/function names from code."""
    # Detect language if auto
    if language == 'auto':
        if 'function ' in content or 'const ' in content or 'let ' in content:
            language = 'js'
        elif 'def ' in content or 'import ' in content:
            language = 'py'
        else:
            language = 'js'  # Default
    
    if language == 'js':
        pattern = JS_IDENTIFIER_PATTERN
        keywords = JS_KEYWORDS
    else:
        pattern = PY_IDENTIFIER_PATTERN
        keywords = PY_KEYWORDS
    
    identifiers = []
    for match in pattern.finditer(content):
        ident = match.group(1)
        # Skip keywords and very short identifiers
        if ident.lower() not in keywords and len(ident) > 1:
            identifiers.append(ident)
    
    return identifiers


def calculate_identifier_entropy(identifiers: List[str]) -> float:
    """Calculate average Shannon entropy of identifier names."""
    if not identifiers:
        return 0.0
    
    entropies = [calculate_shannon_entropy(ident) for ident in identifiers]
    return sum(entropies) / len(entropies)


def average_identifier_length(identifiers: List[str]) -> float:
    """Calculate average length of identifiers."""
    if not identifiers:
        return 10.0  # Default to non-suspicious
    
    return sum(len(ident) for ident in identifiers) / len(identifiers)


def base64_pattern_density(content: str) -> float:
    """Calculate density of base64-like patterns."""
    if not content:
        return 0.0
    
    # Find base64-like strings (long alphanumeric with +/=)
    b64_pattern = re.compile(r'[A-Za-z0-9+/]{20,}={0,2}')
    matches = b64_pattern.findall(content)
    
    total_b64_chars = sum(len(m) for m in matches)
    return total_b64_chars / len(content)


def hex_escape_density(content: str) -> float:
    """Calculate density of hex escape sequences."""
    if not content:
        return 0.0
    
    # \x00 style
    hex_pattern = re.compile(r'\\x[0-9a-fA-F]{2}')
    matches = hex_pattern.findall(content)
    
    # Each \xNN is 4 chars in source
    total_hex_chars = len(matches) * 4
    return total_hex_chars / len(content)


def unicode_escape_density(content: str) -> float:
    """Calculate density of unicode escape sequences."""
    if not content:
        return 0.0
    
    # \uXXXX style
    unicode_pattern = re.compile(r'\\u[0-9a-fA-F]{4}')
    matches = unicode_pattern.findall(content)
    
    # Each \uXXXX is 6 chars in source
    total_chars = len(matches) * 6
    return total_chars / len(content)


def string_literal_ratio(content: str) -> float:
    """Calculate ratio of string literals to total content."""
    if not content:
        return 0.0
    
    # Count characters inside strings
    in_string = False
    string_char = None
    escaped = False
    string_chars = 0
    
    for char in content:
        if escaped:
            escaped = False
            if in_string:
                string_chars += 1
            continue
        
        if char == '\\':
            escaped = True
            if in_string:
                string_chars += 1
            continue
        
        if char in '"\'`':
            if not in_string:
                in_string = True
                string_char = char
            elif char == string_char:
                in_string = False
                string_char = None
        elif in_string:
            string_chars += 1
    
    return string_chars / len(content)


def calculate_obfuscation_score(content: str, language: str = 'auto') -> Tuple[float, dict]:
    """
    Calculate overall obfuscation score.
    
    Returns:
        Tuple of (score 0-1, details dict)
        
    Score interpretation:
        >0.7 = WARN (likely obfuscated)
        >0.85 = DANGER (highly suspicious)
    """
    identifiers = extract_identifiers(content, language)
    
    # Calculate metrics
    ident_entropy = calculate_identifier_entropy(identifiers)
    avg_ident_len = average_identifier_length(identifiers)
    b64_density = base64_pattern_density(content)
    hex_density = hex_escape_density(content)
    unicode_density = unicode_escape_density(content)
    str_ratio = string_literal_ratio(content)
    
    # Normalize and weight metrics
    # Entropy: max useful entropy ~4-5 bits for identifiers
    entropy_component = min(ident_entropy / 4.5, 1.0) * 0.25
    
    # Identifier length: short names (avg <4) suspicious
    length_component = max(0, 1 - avg_ident_len / 8) * 0.15
    
    # Base64 density: any significant amount is suspicious
    b64_component = min(b64_density * 10, 1.0) * 0.20
    
    # Hex/Unicode escape density
    escape_component = min((hex_density + unicode_density) * 5, 1.0) * 0.20
    
    # High string ratio can indicate packed/obfuscated code
    string_component = min(str_ratio * 2, 1.0) * 0.10
    
    # Bonus: very few identifiers relative to content size
    ident_scarcity = 0.0
    if len(content) > 500 and len(identifiers) < 10:
        ident_scarcity = 0.10
    
    score = (
        entropy_component +
        length_component +
        b64_component +
        escape_component +
        string_component +
        ident_scarcity
    )
    
    details = {
        'identifier_count': len(identifiers),
        'identifier_entropy': round(ident_entropy, 3),
        'avg_identifier_length': round(avg_ident_len, 2),
        'base64_density': round(b64_density, 4),
        'hex_escape_density': round(hex_density, 4),
        'unicode_escape_density': round(unicode_density, 4),
        'string_literal_ratio': round(str_ratio, 3),
        'components': {
            'entropy': round(entropy_component, 3),
            'length': round(length_component, 3),
            'base64': round(b64_component, 3),
            'escapes': round(escape_component, 3),
            'strings': round(string_component, 3),
            'scarcity': round(ident_scarcity, 3),
        }
    }
    
    return min(score, 1.0), details


def analyze_obfuscation(content: str, filename: str) -> List[Finding]:
    """
    Analyze code for obfuscation indicators.
    
    Returns findings if obfuscation score exceeds thresholds.
    """
    findings = []
    
    # Skip very short files
    if len(content) < 200:
        return findings
    
    # Skip markdown and config files
    if filename.endswith(('.md', '.markdown', '.json', '.yaml', '.yml', '.toml')):
        return findings
    
    # Detect language
    language = 'auto'
    if filename.endswith(('.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx')):
        language = 'js'
    elif filename.endswith('.py'):
        language = 'py'
    
    score, details = calculate_obfuscation_score(content, language)
    
    # Thresholds
    if score > 0.85:
        severity = Severity.DANGER
        description = 'Highly obfuscated code detected - manual review required'
    elif score > 0.70:
        severity = Severity.WARN
        description = 'Code appears obfuscated - harder to audit'
    elif score > 0.55:
        severity = Severity.INFO
        description = 'Code has some obfuscation indicators'
    else:
        return findings
    
    # Build context string
    context_parts = []
    if details['identifier_entropy'] > 3.5:
        context_parts.append(f"high identifier entropy ({details['identifier_entropy']:.2f})")
    if details['avg_identifier_length'] < 4:
        context_parts.append(f"short identifiers (avg {details['avg_identifier_length']:.1f} chars)")
    if details['base64_density'] > 0.05:
        context_parts.append(f"base64 content ({details['base64_density']*100:.1f}%)")
    if details['hex_escape_density'] > 0.01:
        context_parts.append(f"hex escapes ({details['hex_escape_density']*100:.1f}%)")
    if details['identifier_count'] < 10 and len(content) > 500:
        context_parts.append(f"few identifiers ({details['identifier_count']})")
    
    context = ', '.join(context_parts) if context_parts else f"score components: {details['components']}"
    
    findings.append(Finding(
        severity=severity,
        pattern_id='obfuscation_detected',
        description=description,
        file=filename,
        line=1,
        match=f'Obfuscation score: {score:.2f}',
        context=context,
    ))
    
    return findings
