"""Base64 payload detection - finds hidden code in encoded strings.

Decodes base64 strings and scans contents for dangerous patterns.
High impact: catches payloads that regex patterns miss.
"""
import base64
import re
from typing import List, Optional, Tuple

from .patterns import Finding, Severity


# Patterns that identify base64 usage
BASE64_USAGE_PATTERNS = [
    # JavaScript
    (r'atob\s*\(\s*["\']([A-Za-z0-9+/=]{20,})["\']', 'js_atob'),
    (r'Buffer\.from\s*\(\s*["\']([A-Za-z0-9+/=]{20,})["\'](?:\s*,\s*["\']base64["\'])?', 'node_buffer'),
    # Python
    (r'base64\.b64decode\s*\(\s*[b]?["\']([A-Za-z0-9+/=]{20,})["\']', 'py_b64decode'),
    (r'base64\.decodebytes\s*\(\s*[b]?["\']([A-Za-z0-9+/=]{20,})["\']', 'py_decodebytes'),
    (r'codecs\.decode\s*\(\s*[b]?["\']([A-Za-z0-9+/=]{20,})["\'].*?base64', 'py_codecs'),
    # Shell/Ruby/Other
    (r'echo\s+["\']?([A-Za-z0-9+/=]{20,})["\']?\s*\|\s*base64\s+(?:-d|--decode)', 'shell_base64'),
    (r'Base64\.decode64\s*\(\s*["\']([A-Za-z0-9+/=]{20,})["\']', 'ruby_base64'),
]

# Standalone base64 strings (no decode call, but suspiciously long)
STANDALONE_B64_PATTERN = re.compile(
    r'["\']([A-Za-z0-9+/]{40,}={0,2})["\']',
    re.MULTILINE
)

# Dangerous patterns in decoded content
DANGEROUS_DECODED_PATTERNS = [
    # Code execution
    (r'\b(eval|exec|system|popen|spawn)\s*\(', 'decoded_code_execution', 
     'Code execution function in decoded payload'),
    (r'\b(require|import)\s*\(?["\']?(child_process|subprocess|os|sys)', 'decoded_dangerous_import',
     'Dangerous module import in decoded payload'),
    # Shell commands
    (r'\b(curl|wget|nc|ncat|netcat|bash|sh|powershell)\s+', 'decoded_shell_command',
     'Shell command in decoded payload'),
    (r'(\$\(|\`)[^)\`]*?(curl|wget|exec|eval)', 'decoded_command_substitution',
     'Command substitution in decoded payload'),
    # Network exfil
    (r'https?://[^\s"\'<>]+', 'decoded_url',
     'URL in decoded payload'),
    # Credentials
    (r'(api[_-]?key|secret|token|password|credential)\s*[=:]', 'decoded_credential_pattern',
     'Credential pattern in decoded payload'),
    # Reverse shell signatures
    (r'(\/dev\/tcp|mkfifo|mknod).*?(bash|sh)', 'decoded_reverse_shell',
     'Reverse shell pattern in decoded payload'),
    (r'socket\.connect|socket\.socket.*?SOCK_STREAM', 'decoded_socket',
     'Socket connection in decoded payload'),
]


def is_valid_base64(s: str) -> bool:
    """Check if string is valid base64 (with reasonable confidence)."""
    if len(s) < 16:
        return False
    
    # Must have valid base64 characters
    if not re.match(r'^[A-Za-z0-9+/]*={0,2}$', s):
        return False
    
    # Check length is valid (multiple of 4, accounting for padding)
    unpadded = s.rstrip('=')
    padding_needed = (4 - len(unpadded) % 4) % 4
    if len(s) != len(unpadded) + padding_needed and len(unpadded) % 4 != 0:
        # Allow strings that would be valid with proper padding
        pass
    
    return True


def try_decode_base64(s: str) -> Optional[str]:
    """Attempt to decode a base64 string, return None if invalid."""
    try:
        # Add padding if needed
        padding = 4 - (len(s) % 4)
        if padding != 4:
            s = s + '=' * padding
        
        decoded = base64.b64decode(s)
        
        # Try to decode as text
        try:
            text = decoded.decode('utf-8')
            # Check if it looks like text (printable characters)
            printable_ratio = sum(1 for c in text if c.isprintable() or c in '\n\r\t') / len(text)
            if printable_ratio > 0.8:
                return text
        except UnicodeDecodeError:
            # Try latin-1
            try:
                text = decoded.decode('latin-1')
                printable_ratio = sum(1 for c in text if c.isprintable() or c in '\n\r\t') / len(text)
                if printable_ratio > 0.7:
                    return text
            except:
                pass
        
        return None
    except Exception:
        return None


def scan_decoded_content(decoded: str, original_match: str, filename: str, line_num: int) -> List[Finding]:
    """Scan decoded base64 content for dangerous patterns."""
    findings = []
    
    for pattern, pattern_id, description in DANGEROUS_DECODED_PATTERNS:
        for match in re.finditer(pattern, decoded, re.IGNORECASE):
            # URLs in decoded content are only WARN if not suspicious
            severity = Severity.DANGER
            if pattern_id == 'decoded_url':
                url = match.group(0)
                if any(safe in url for safe in ['github.com', 'githubusercontent.com', 'npmjs.org', 'pypi.org']):
                    severity = Severity.INFO
                else:
                    severity = Severity.WARN
            
            # Preview of decoded content
            preview_start = max(0, match.start() - 20)
            preview_end = min(len(decoded), match.end() + 20)
            preview = decoded[preview_start:preview_end].replace('\n', '\\n')
            
            findings.append(Finding(
                severity=severity,
                pattern_id=f'base64_{pattern_id}',
                description=f'{description}',
                file=filename,
                line=line_num,
                match=match.group(0)[:80],
                context=f'Decoded: ...{preview}...',
            ))
    
    return findings


def detect_base64_payloads(content: str, filename: str) -> List[Finding]:
    """
    Detect and analyze base64-encoded payloads.
    
    Returns findings for:
    1. Base64 strings being decoded (atob, b64decode, etc.)
    2. Large standalone base64 strings that decode to dangerous content
    """
    findings = []
    lines = content.split('\n')
    
    # 1. Find explicit base64 decode operations
    for pattern, source in BASE64_USAGE_PATTERNS:
        for match in re.finditer(pattern, content, re.IGNORECASE):
            b64_string = match.group(1)
            line_num = content[:match.start()].count('\n') + 1
            
            if not is_valid_base64(b64_string):
                continue
            
            decoded = try_decode_base64(b64_string)
            if decoded:
                # Found decodable base64 - scan it
                decoded_findings = scan_decoded_content(decoded, b64_string, filename, line_num)
                
                if decoded_findings:
                    findings.extend(decoded_findings)
                elif len(decoded) > 50:
                    # No dangerous patterns but still notable
                    preview = decoded[:100].replace('\n', '\\n')
                    findings.append(Finding(
                        severity=Severity.INFO,
                        pattern_id='base64_decoded_content',
                        description=f'Base64 decoded content ({len(decoded)} chars)',
                        file=filename,
                        line=line_num,
                        match=f'{source}: {b64_string[:40]}...',
                        context=f'Decodes to: {preview}...',
                    ))
    
    # 2. Find standalone base64 strings (not in obvious decode calls)
    # Only flag if they decode to something dangerous
    for match in STANDALONE_B64_PATTERN.finditer(content):
        b64_string = match.group(1)
        
        # Skip if already found via explicit decode pattern
        if any(b64_string in f.match for f in findings):
            continue
        
        # Skip very long strings (likely binary/image data)
        if len(b64_string) > 10000:
            continue
        
        if not is_valid_base64(b64_string):
            continue
        
        decoded = try_decode_base64(b64_string)
        if decoded and len(decoded) > 30:
            line_num = content[:match.start()].count('\n') + 1
            decoded_findings = scan_decoded_content(decoded, b64_string, filename, line_num)
            
            if decoded_findings:
                # Add a header finding for the base64 itself
                findings.append(Finding(
                    severity=Severity.WARN,
                    pattern_id='base64_suspicious_standalone',
                    description='Standalone base64 string with dangerous decoded content',
                    file=filename,
                    line=line_num,
                    match=b64_string[:60] + '...',
                    context=f'Decodes to {len(decoded)} chars of text',
                ))
                findings.extend(decoded_findings)
    
    return findings
