"""
Quarantine stage — security gate before any parsing occurs.

Checks performed (in order):
1. File existence and readability
2. Size limit (500 MB default)
3. Magic bytes MIME detection
4. PDF encryption check
5. Zip bomb detection (generic)
6. OOXML zip bomb check (.docx, .xlsx, .pptx)
7. Text encoding detection for text-type files
"""

from __future__ import annotations

import zipfile
from pathlib import Path

import puremagic

from .errors import QuarantineError

# 500 MB hard limit for any input file
MAX_FILE_SIZE_BYTES = 500 * 1024 * 1024

# OOXML formats (ZIP containers that must be pre-scanned)
OOXML_EXTENSIONS = frozenset({".docx", ".xlsx", ".pptx", ".odt", ".ods", ".odp"})
OOXML_MIMES = frozenset(
    {
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        "application/vnd.openxmlformats-officedocument.presentationml.presentation",
        "application/vnd.oasis.opendocument.text",
        "application/vnd.oasis.opendocument.spreadsheet",
        "application/vnd.oasis.opendocument.presentation",
    }
)

MAX_COMPRESSION_RATIO = 100  # reject if uncompressed/compressed > 100x
MAX_OOXML_UNCOMPRESSED = 500 * 1024 * 1024  # 500 MB uncompressed ceiling


def detect_mime(path: Path) -> str:
    """Detect MIME type using magic bytes (not file extension)."""
    try:
        matches = puremagic.magic_file(str(path))
        if matches:
            return matches[0].mime_type or "application/octet-stream"
    except Exception:
        pass
    return "application/octet-stream"


def check_pdf_encryption(path: Path) -> None:
    """Reject password-protected PDFs."""
    try:
        import pikepdf

        with pikepdf.open(str(path)) as pdf:
            _ = pdf
    except Exception as exc:
        msg = str(exc).lower()
        if "password" in msg or "encrypted" in msg:
            raise QuarantineError("E_ENCRYPTED", f"PDF is password-protected: {path.name}")


MAX_UNCOMPRESSED_BYTES = 500 * 1024 * 1024  # 500 MB streaming decompression ceiling
_STREAM_CHUNK = 65536


def check_zip_bomb(path: Path) -> None:
    """Detect generic zip bomb via ratio check, then streaming decompression count."""
    if not zipfile.is_zipfile(str(path)):
        return
    try:
        with zipfile.ZipFile(path) as zf:
            entries = zf.infolist()
            if len(entries) > 10_000:
                raise QuarantineError(
                    "E_ZIP_BOMB", f"Zip has {len(entries)} entries (max 10,000): {path}"
                )
            infos = entries

            # Pass 1: fast header-based check (attacker-controlled, but catches obvious cases)
            # Guard against bypass via zeroed file_size headers: if an entry has compressed
            # data but declares file_size=0, the ratio would compute as 0.0 and pass silently.
            for info in infos:
                if info.compress_size > 0 and info.file_size == 0:
                    raise QuarantineError(
                        "E_ZIP_BOMB",
                        f"Zip bomb suspected: entry '{info.filename}' has compress_size"
                        f" {info.compress_size} but declared file_size 0",
                    )
            compressed = sum(i.compress_size for i in infos)
            uncompressed = sum(i.file_size for i in infos)
            if compressed > 0 and (uncompressed / compressed) > MAX_COMPRESSION_RATIO:
                raise QuarantineError(
                    "E_ZIP_BOMB",
                    f"Compression ratio {uncompressed / compressed:.0f}x exceeds {MAX_COMPRESSION_RATIO}x limit",
                )
            elif compressed == 0 and uncompressed > 0:
                raise QuarantineError(
                    "E_ZIP_BOMB",
                    "Zip bomb suspected: member has zero compressed size",
                )
            if uncompressed > MAX_OOXML_UNCOMPRESSED:
                raise QuarantineError(
                    "E_TOO_LARGE",
                    f"Uncompressed size {uncompressed:,} bytes exceeds 500 MB limit",
                )

            # Pass 2: actual streaming decompression — not relying on header metadata
            total_decompressed = 0
            for entry in infos:
                if entry.is_dir():
                    continue
                with zf.open(entry) as fh:
                    while True:
                        chunk = fh.read(_STREAM_CHUNK)
                        if not chunk:
                            break
                        total_decompressed += len(chunk)
                        if total_decompressed >= MAX_UNCOMPRESSED_BYTES:
                            raise QuarantineError(
                                "E_ZIP_BOMB",
                                f"Streaming decompression exceeded {MAX_UNCOMPRESSED_BYTES:,} byte limit",
                            )
    except zipfile.BadZipFile:
        pass


def run(path: Path) -> str:
    """
    Run all quarantine checks. Returns detected MIME type on success.
    Raises QuarantineError on any failure.
    """
    if not path.exists():
        raise QuarantineError("E_NOT_FOUND", f"File not found: {path}")
    if not path.is_file():
        raise QuarantineError("E_NOT_FILE", f"Not a regular file: {path}")
    if path.is_symlink():
        raise QuarantineError("E_SYMLINK", f"Symlink inputs are rejected for security: {path}")

    size = path.stat().st_size
    if size > MAX_FILE_SIZE_BYTES:
        raise QuarantineError(
            "E_TOO_LARGE",
            f"File size {size:,} bytes exceeds {MAX_FILE_SIZE_BYTES:,} byte limit",
        )

    mime_type = detect_mime(path)

    # PDF encryption check
    if mime_type == "application/pdf" or path.suffix.lower() == ".pdf":
        check_pdf_encryption(path)

    # Zip bomb check for all zip-based formats (OOXML, EPUB, ODT, and generic)
    if zipfile.is_zipfile(str(path)):
        check_zip_bomb(path)

    return mime_type
