"""Attachment storage for application documents.

Files land in UPLOAD_DIR/<entity>/<id>/<random>.<ext>. The stored name is generated,
never taken from the client, so a malicious filename cannot escape the upload root.
"""
import re
import secrets
from pathlib import Path

from fastapi import HTTPException, UploadFile, status

from app.core.config import settings

#: Attachments are scans and photos of paperwork — no executables, no archives.
ALLOWED_CONTENT_TYPES: dict[str, str] = {
    "image/jpeg": ".jpg",
    "image/png": ".png",
    "image/webp": ".webp",
    "application/pdf": ".pdf",
}

_SAFE_NAME = re.compile(r"[^A-Za-z0-9._ \-]")
_CHUNK = 1024 * 1024


def _upload_root() -> Path:
    root = Path(settings.UPLOAD_DIR).resolve()
    root.mkdir(parents=True, exist_ok=True)
    return root


def sanitize_display_name(name: str | None) -> str:
    """Keep a readable original name for the UI, stripped of anything path-like."""
    base = Path(name or "attachment").name
    cleaned = _SAFE_NAME.sub("_", base).strip() or "attachment"
    return cleaned[:255]


def save_upload(upload: UploadFile, *, entity: str, entity_id: int) -> tuple[str, int]:
    """Persist an upload. Returns (path relative to UPLOAD_DIR, size in bytes)."""
    suffix = ALLOWED_CONTENT_TYPES.get((upload.content_type or "").lower())
    if suffix is None:
        raise HTTPException(
            status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
            detail=f"Unsupported file type. Allowed: {', '.join(sorted(ALLOWED_CONTENT_TYPES))}",
        )

    root = _upload_root()
    target_dir = root / entity / str(entity_id)
    target_dir.mkdir(parents=True, exist_ok=True)
    target = target_dir / f"{secrets.token_hex(16)}{suffix}"

    limit = settings.MAX_UPLOAD_MB * 1024 * 1024
    size = 0
    upload.file.seek(0)
    try:
        with target.open("wb") as out:
            while chunk := upload.file.read(_CHUNK):
                size += len(chunk)
                if size > limit:
                    raise HTTPException(
                        status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
                        detail=f"File exceeds the {settings.MAX_UPLOAD_MB} MB limit",
                    )
                out.write(chunk)
    except Exception:
        target.unlink(missing_ok=True)
        raise

    if size == 0:
        target.unlink(missing_ok=True)
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="File is empty")

    return str(target.relative_to(root).as_posix()), size


def resolve_stored_path(stored_path: str) -> Path:
    """Map a DB-stored relative path back to disk, refusing anything outside the root."""
    root = _upload_root()
    candidate = (root / stored_path).resolve()
    if not candidate.is_relative_to(root) or not candidate.is_file():
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="File not found")
    return candidate


def delete_stored_file(stored_path: str) -> None:
    root = _upload_root()
    candidate = (root / stored_path).resolve()
    if candidate.is_relative_to(root):
        candidate.unlink(missing_ok=True)
