"""Validación de contenido y cifrado autenticado de archivos privados."""
from __future__ import annotations

import base64
import hashlib
import io
import os
import re
import secrets
import zipfile
from dataclasses import dataclass
from pathlib import Path, PurePosixPath

from cryptography.hazmat.primitives.ciphers.aead import AESGCM

from app.core.config import get_settings


MAGIC = b"DOCF1"
AAD = b"documenta:private-file:v1"
PDF_BLOCKED = (b"/javascript", b"/js", b"/launch", b"/embeddedfile", b"/richmedia", b"/openaction", b"/acroform", b"/xfa")
DOCX_BLOCKED_SUFFIXES = (".exe", ".dll", ".com", ".bat", ".cmd", ".ps1", ".js", ".vbs", ".jar", ".scr", ".msi", ".hta", ".lnk", ".iso")


@dataclass(frozen=True)
class CheckedUpload:
    content: bytes
    mime_type: str
    extension: str
    sha256: bytes


def _master_key() -> bytes:
    key = base64.urlsafe_b64decode(get_settings().app_encryption_key.encode())
    if len(key) != 32:
        raise RuntimeError("APP_ENCRYPTION_KEY no contiene una clave de 256 bits")
    return key


def encrypt_private_bytes(content: bytes) -> bytes:
    nonce = secrets.token_bytes(12)
    return MAGIC + nonce + AESGCM(_master_key()).encrypt(nonce, content, AAD)


def decrypt_private_bytes(content: bytes) -> bytes:
    if not content.startswith(MAGIC):
        return content
    if len(content) < len(MAGIC) + 28:
        raise ValueError("Archivo cifrado incompleto")
    nonce = content[len(MAGIC):len(MAGIC) + 12]
    return AESGCM(_master_key()).decrypt(nonce, content[len(MAGIC) + 12:], AAD)


def is_encrypted_file(content: bytes) -> bool:
    return content.startswith(MAGIC)


def _validate_pdf(content: bytes) -> None:
    stripped = content.lstrip()
    if not stripped.startswith(b"%PDF-") or b"%%EOF" not in stripped[-4096:]:
        raise ValueError("El contenido no corresponde a un PDF válido")
    lowered = stripped.lower()
    if b"/encrypt" in lowered:
        raise ValueError("No se aceptan PDF protegidos con contraseña")
    if any(marker in lowered for marker in PDF_BLOCKED):
        raise ValueError("El PDF contiene acciones o contenido activo no permitido")


def _validate_jpeg(content: bytes) -> None:
    if len(content) < 16 or not content.startswith(b"\xff\xd8\xff") or not content.rstrip().endswith(b"\xff\xd9"):
        raise ValueError("El contenido no corresponde a una imagen JPEG válida")


def _validate_png(content: bytes) -> None:
    if not content.startswith(b"\x89PNG\r\n\x1a\n") or b"IEND" not in content[-32:]:
        raise ValueError("El contenido no corresponde a una imagen PNG válida")


def _validate_docx(content: bytes) -> None:
    try:
        with zipfile.ZipFile(io.BytesIO(content)) as archive:
            entries = archive.infolist()
            names = {entry.filename for entry in entries}
            if "[Content_Types].xml" not in names or "word/document.xml" not in names:
                raise ValueError("El contenido no corresponde a un documento DOCX válido")
            if len(entries) > 2500:
                raise ValueError("El DOCX contiene demasiados elementos")
            expanded = 0
            for entry in entries:
                normalized = PurePosixPath(entry.filename)
                if normalized.is_absolute() or ".." in normalized.parts or "\\" in entry.filename:
                    raise ValueError("El DOCX contiene rutas internas no permitidas")
                lowered = entry.filename.lower()
                if lowered.endswith(DOCX_BLOCKED_SUFFIXES) or "vbaproject.bin" in lowered:
                    raise ValueError("El DOCX contiene código ejecutable o macros")
                expanded += entry.file_size
                if entry.file_size > 50_000_000 or expanded > 100_000_000:
                    raise ValueError("El DOCX excede el tamaño expandido permitido")
                if entry.compress_size and entry.file_size / entry.compress_size > 150:
                    raise ValueError("El DOCX presenta una relación de compresión insegura")
            for name in (item for item in names if item.lower().endswith(".rels")):
                if re.search(br'targetmode\s*=\s*["\']external["\']', archive.read(name).lower()):
                    raise ValueError("El DOCX contiene referencias externas no permitidas")
    except zipfile.BadZipFile as exc:
        raise ValueError("El contenido no corresponde a un documento DOCX válido") from exc


def validate_upload(content: bytes, declared_mime: str | None, *, profile: str) -> CheckedUpload:
    if not content:
        raise ValueError("El archivo está vacío")
    profiles = {
        "public": {
            "application/pdf": (".pdf", _validate_pdf),
            "image/jpeg": (".jpg", _validate_jpeg),
            "image/png": (".png", _validate_png),
        },
        "document": {
            "application/pdf": (".pdf", _validate_pdf),
            "application/vnd.openxmlformats-officedocument.wordprocessingml.document": (".docx", _validate_docx),
        },
    }
    allowed = profiles.get(profile, {})
    if declared_mime not in allowed:
        raise ValueError("Tipo de archivo no permitido")
    extension, validator = allowed[str(declared_mime)]
    validator(content)
    return CheckedUpload(content, str(declared_mime), extension, hashlib.sha256(content).digest())


def _safe_path(relative: str) -> Path:
    root = Path(get_settings().storage_path).resolve()
    target = (root / relative).resolve()
    if root != target and root not in target.parents:
        raise ValueError("Ruta privada inválida")
    return target


def write_private_file(relative: str, content: bytes) -> None:
    target = _safe_path(relative)
    target.parent.mkdir(parents=True, exist_ok=True)
    temporary = target.with_name(f".{target.name}.{secrets.token_hex(8)}.tmp")
    temporary.write_bytes(encrypt_private_bytes(content))
    os.chmod(temporary, 0o600)
    os.replace(temporary, target)


def read_private_file(relative: str) -> bytes:
    return decrypt_private_bytes(_safe_path(relative).read_bytes())


def private_file_exists(relative: str) -> bool:
    return _safe_path(relative).is_file()


def safe_download_name(value: str, fallback: str = "documento") -> str:
    cleaned = re.sub(r"[\x00-\x1f\x7f/\\\"]+", "_", value).strip(" ._")
    return cleaned[:180] or fallback
