"""Binary file scanning for embedded scripts and suspicious content.

Scans:
- .pyc files (compiled Python)
- Executables for embedded scripts
- Images for steganography indicators
- Archives for suspicious contents
"""
import re
import struct
from pathlib import Path
from typing import List, Optional, Tuple

from .patterns import Finding, Severity


# Suspicious strings to look for in binaries
SUSPICIOUS_BINARY_PATTERNS = [
    # URLs (potential C2, exfiltration)
    (rb'https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(?:/[^\s\x00]*)?', 'embedded_url', Severity.WARN),
    
    # IP addresses
    (rb'\b(?:\d{1,3}\.){3}\d{1,3}\b', 'embedded_ip', Severity.INFO),
    
    # Shell commands
    (rb'/bin/(?:ba)?sh', 'embedded_shell_path', Severity.WARN),
    (rb'(?:curl|wget|nc|netcat)\s+', 'embedded_network_cmd', Severity.DANGER),
    (rb'rm\s+-rf', 'embedded_rm_rf', Severity.DANGER),
    
    # Python code patterns
    (rb'import\s+(?:os|subprocess|socket|requests)', 'embedded_python_import', Severity.WARN),
    (rb'eval\s*\(|exec\s*\(', 'embedded_eval_exec', Severity.DANGER),
    (rb'__import__\s*\(', 'embedded_dynamic_import', Severity.DANGER),
    
    # JavaScript patterns
    (rb'require\s*\(\s*["\']child_process', 'embedded_child_process', Severity.DANGER),
    (rb'new\s+Function\s*\(', 'embedded_function_constructor', Severity.DANGER),
    
    # Base64-encoded shell commands (common obfuscation)
    (rb'(?:YmFz|L2Jpbi|Y3Vy|d2dl|bmV0Y2F0)', 'embedded_b64_cmd', Severity.WARN),  # bash, /bin, curl, wget, netcat
    
    # Crypto wallet addresses (exfiltration targets)
    (rb'(?:bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}', 'embedded_btc_address', Severity.WARN),
    (rb'0x[a-fA-F0-9]{40}', 'embedded_eth_address', Severity.INFO),
    
    # Private key patterns
    (rb'-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----', 'embedded_private_key', Severity.DANGER),
    (rb'AKIA[0-9A-Z]{16}', 'embedded_aws_key', Severity.DANGER),
]

# Binary file extensions to scan
BINARY_EXTENSIONS = {
    '.pyc', '.pyo',  # Compiled Python
    '.so', '.dll', '.dylib',  # Shared libraries
    '.exe', '.bin',  # Executables
    '.whl', '.egg',  # Python packages
}

# Image extensions (for stego check)
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}

# Archive extensions
ARCHIVE_EXTENSIONS = {'.zip', '.tar', '.gz', '.tgz', '.bz2', '.7z', '.rar'}


def is_binary_file(path: Path) -> bool:
    """Check if a file should be treated as binary."""
    return path.suffix.lower() in BINARY_EXTENSIONS


def is_image_file(path: Path) -> bool:
    """Check if a file is an image."""
    return path.suffix.lower() in IMAGE_EXTENSIONS


def scan_binary_content(content: bytes, filename: str) -> List[Finding]:
    """Scan binary content for suspicious patterns."""
    findings = []
    
    for pattern, pattern_id, severity in SUSPICIOUS_BINARY_PATTERNS:
        for match in re.finditer(pattern, content):
            # Get approximate position
            pos = match.start()
            
            # Get context (surrounding bytes as printable string)
            start = max(0, pos - 20)
            end = min(len(content), pos + 100)
            context_bytes = content[start:end]
            # Convert to printable string
            context = ''.join(chr(b) if 32 <= b < 127 else '.' for b in context_bytes)
            
            matched = match.group(0)
            try:
                match_str = matched.decode('utf-8', errors='replace')[:100]
            except Exception:
                match_str = str(matched[:50])
            
            findings.append(Finding(
                severity=severity,
                pattern_id=f'binary_{pattern_id}',
                description=f'Suspicious content in binary file: {pattern_id}',
                file=filename,
                line=0,  # No line numbers in binary
                match=match_str,
                context=context[:200],
            ))
    
    return findings


def check_pyc_file(content: bytes, filename: str) -> List[Finding]:
    """Check compiled Python file for suspicious indicators."""
    findings = []
    
    # Basic PYC validation
    if len(content) < 16:
        findings.append(Finding(
            severity=Severity.WARN,
            pattern_id='binary_invalid_pyc',
            description='Invalid or truncated .pyc file',
            file=filename,
            line=0,
            match='<invalid pyc>',
        ))
        return findings
    
    # Check magic number (varies by Python version)
    # We won't validate specific versions, just note it exists
    findings.append(Finding(
        severity=Severity.INFO,
        pattern_id='binary_pyc_file',
        description='Compiled Python bytecode - harder to audit than source',
        file=filename,
        line=0,
        match=f'.pyc file ({len(content)} bytes)',
    ))
    
    # Scan the bytecode for embedded strings
    binary_findings = scan_binary_content(content, filename)
    findings.extend(binary_findings)
    
    return findings


def check_image_for_stego(content: bytes, filename: str) -> List[Finding]:
    """Basic check for steganography indicators in images."""
    findings = []
    
    # Check for data appended after image end markers
    # PNG ends with IEND chunk
    if filename.lower().endswith('.png'):
        iend_pos = content.find(b'IEND')
        if iend_pos != -1 and iend_pos + 12 < len(content):
            # There's significant data after IEND
            extra_data = len(content) - (iend_pos + 12)
            if extra_data > 100:
                findings.append(Finding(
                    severity=Severity.WARN,
                    pattern_id='binary_image_extra_data',
                    description=f'PNG has {extra_data} bytes after IEND marker - possible hidden data',
                    file=filename,
                    line=0,
                    match=f'{extra_data} bytes extra',
                ))
    
    # JPEG ends with FFD9
    if filename.lower().endswith(('.jpg', '.jpeg')):
        eoi_pos = content.rfind(b'\xff\xd9')
        if eoi_pos != -1 and eoi_pos + 2 < len(content):
            extra_data = len(content) - (eoi_pos + 2)
            if extra_data > 100:
                findings.append(Finding(
                    severity=Severity.WARN,
                    pattern_id='binary_image_extra_data',
                    description=f'JPEG has {extra_data} bytes after EOI marker - possible hidden data',
                    file=filename,
                    line=0,
                    match=f'{extra_data} bytes extra',
                ))
    
    # Check for embedded scripts in image
    script_patterns = [
        b'<script', b'<?php', b'<%', b'#!/',
        b'import ', b'eval(', b'exec(',
    ]
    for pattern in script_patterns:
        if pattern in content:
            findings.append(Finding(
                severity=Severity.DANGER,
                pattern_id='binary_image_embedded_script',
                description=f'Image contains embedded script pattern: {pattern.decode("utf-8", errors="replace")}',
                file=filename,
                line=0,
                match=pattern.decode('utf-8', errors='replace'),
            ))
    
    return findings


def scan_binary_file(path: Path) -> Tuple[List[Finding], Optional[str]]:
    """Scan a binary file for security issues."""
    try:
        content = path.read_bytes()
    except Exception as e:
        return [], f"Could not read binary file: {e}"
    
    # Size limit for binary files (5MB)
    if len(content) > 5 * 1024 * 1024:
        return [Finding(
            severity=Severity.INFO,
            pattern_id='binary_large_file',
            description='Large binary file - skipped detailed scan',
            file=str(path),
            line=0,
            match=f'{len(content)} bytes',
        )], None
    
    findings = []
    filename = str(path)
    
    # Type-specific checks
    if path.suffix.lower() in ('.pyc', '.pyo'):
        findings.extend(check_pyc_file(content, filename))
    elif is_image_file(path):
        findings.extend(check_image_for_stego(content, filename))
    else:
        # General binary scan
        findings.extend(scan_binary_content(content, filename))
    
    return findings, None
