"""Security utilities — path traversal, secret detection, binary filtering."""

import fnmatch
import os
from pathlib import Path
from typing import Optional


def validate_path(root: Path, target: Path) -> bool:
    """Check that target resolves within root directory."""
    try:
        resolved = target.resolve()
        resolved_root = root.resolve()
        return os.path.commonpath([resolved_root, resolved]) == str(resolved_root)
    except (OSError, ValueError):
        return False


def is_symlink_escape(root: Path, path: Path) -> bool:
    """Check if a symlink points outside the root directory."""
    try:
        if path.is_symlink():
            resolved = path.resolve()
            resolved_root = root.resolve()
            return os.path.commonpath([resolved_root, resolved]) != str(resolved_root)
    except (OSError, ValueError):
        return True
    return False


SECRET_PATTERNS = [
    '*.env', '.env', '.env.*', '*.pem', '*.key', '*.p12', '*.pfx',
    '*.credentials', '*.keystore', '*.jks', '*.token', '*secret*',
    'id_rsa', 'id_rsa.*', 'id_ed25519', 'id_ed25519.*', 'id_dsa',
    'id_ecdsa', '.htpasswd', '.netrc', '.npmrc', '.pypirc',
    'credentials.json', 'service-account*.json', '*.secrets',
]


def is_secret_file(file_path: str) -> bool:
    """Check if a file matches known secret patterns."""
    name = os.path.basename(file_path).lower()
    path_lower = file_path.lower()
    return any(
        fnmatch.fnmatch(name, p) or fnmatch.fnmatch(path_lower, p)
        for p in SECRET_PATTERNS
    )


BINARY_EXTENSIONS = frozenset([
    '.exe', '.dll', '.so', '.dylib', '.bin', '.out',
    '.o', '.obj', '.a', '.lib',
    '.zip', '.tar', '.gz', '.bz2', '.xz', '.7z', '.rar',
    '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp', '.tiff',
    '.mp3', '.mp4', '.avi', '.mov', '.mkv', '.wav', '.flac', '.ogg', '.webm',
    '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
    '.pyc', '.pyo', '.class', '.wasm',
    '.db', '.sqlite', '.sqlite3',
    '.ttf', '.otf', '.woff', '.woff2', '.eot',
    '.jar', '.war', '.ear',
])


def is_binary_extension(file_path: str) -> bool:
    """Check if a file has a known binary extension."""
    _, ext = os.path.splitext(file_path)
    return ext.lower() in BINARY_EXTENSIONS


def is_binary_content(data: bytes, check_size: int = 8192) -> bool:
    """Detect binary content via null-byte scanning."""
    return b'\x00' in data[:check_size]


def is_binary_file(file_path: Path, check_size: int = 8192) -> bool:
    """Check if a file is binary (extension + content sniff)."""
    if is_binary_extension(str(file_path)):
        return True
    try:
        with open(file_path, 'rb') as f:
            return is_binary_content(f.read(check_size), check_size)
    except OSError:
        return True


def safe_decode(data: bytes, encoding: str = 'utf-8') -> str:
    """Decode bytes with replacement for invalid sequences."""
    return data.decode(encoding, errors='replace')


DEFAULT_MAX_FILE_SIZE = 1024 * 1024  # 1MB (raised from jCodeMunch's 500KB)


def should_exclude_file(
    file_path: Path,
    root: Path,
    max_file_size: int = DEFAULT_MAX_FILE_SIZE,
) -> Optional[str]:
    """Run all security checks. Returns reason string if excluded, None if ok."""
    if is_symlink_escape(root, file_path):
        return 'symlink_escape'

    if not validate_path(root, file_path):
        return 'path_traversal'

    try:
        rel_path = file_path.relative_to(root).as_posix()
    except ValueError:
        return 'outside_root'

    if is_secret_file(rel_path):
        return 'secret_file'

    try:
        size = file_path.stat().st_size
        if size > max_file_size:
            return 'file_too_large'
        if size == 0:
            return 'empty_file'
    except OSError:
        return 'unreadable'

    if is_binary_extension(rel_path):
        return 'binary_extension'

    return None
