"""Общая валидация загружаемых картинок: тип по содержимому и реальный размер.

Выделено из `services/avatars.py`, когда те же правила понадобились фонам
видео (`services/backgrounds.py`): списки допустимых форматов и сигнатур
должны быть в одном месте — разъехавшись, они дали бы дыру ровно в том
месте, ради которого проверка и написана.

Правила намеренно не смягчаются для «уже проверенных на клиенте» файлов:
браузер жмёт картинку фона перед отправкой (canvas → WebP), но запрос к API
может прийти и мимо интерфейса — клиенту верить нельзя.
"""

from collections.abc import Callable

from fastapi import UploadFile

# Читаем файл чанками, не доверяя заголовку `Content-Length` (клиент может
# солгать о размере) — реальный размер считается по факту прочитанных байт.
_CHUNK_SIZE_BYTES = 64 * 1024

# Допустимые типы изображений -> расширение файла на диске.
ALLOWED_IMAGE_CONTENT_TYPES: dict[str, str] = {
    "image/jpeg": "jpg",
    "image/png": "png",
    "image/webp": "webp",
}

# Магические байты (сигнатуры) форматов — заголовку `Content-Type` от клиента
# доверять нельзя (легко подделать), реальный формат определяется по
# содержимому файла.
_MAGIC_CHECKS: dict[str, Callable[[bytes], bool]] = {
    "image/jpeg": lambda head: head[:3] == b"\xff\xd8\xff",
    "image/png": lambda head: head[:8] == b"\x89PNG\r\n\x1a\n",
    "image/webp": lambda head: head[:4] == b"RIFF" and head[8:12] == b"WEBP",
}

# Достаточно первых 12 байт, чтобы проверить все сигнатуры выше (WebP —
# самая длинная проверка, требует байты 8..11 включительно).
_MAGIC_HEAD_SIZE = 12


class ImageTooLargeError(Exception):
    """Загружаемый файл превышает переданный лимит размера (413)."""


class ImageInvalidTypeError(Exception):
    """`Content-Type` не входит в список допустимых либо не совпадает с содержимым (415)."""


async def read_and_validate_image(file: UploadFile, max_size_bytes: int) -> tuple[bytes, str]:
    """Прочитать файл чанками и провалидировать тип/размер.

    Возвращает `(содержимое, расширение)`. Порядок проверок: сначала
    заявленный `Content-Type` (быстрый отсев), затем фактический размер по
    мере чтения, затем магические байты содержимого — заявленный тип должен
    совпасть с реальным (иначе подделка `Content-Type` не даст загрузить,
    например, исполняемый файл под видом `image/png`).
    """
    declared_type = file.content_type
    if declared_type not in ALLOWED_IMAGE_CONTENT_TYPES:
        raise ImageInvalidTypeError(f"unsupported_content_type: {declared_type}")

    chunks: list[bytes] = []
    total_size = 0
    while True:
        chunk = await file.read(_CHUNK_SIZE_BYTES)
        if not chunk:
            break
        total_size += len(chunk)
        if total_size > max_size_bytes:
            raise ImageTooLargeError(f"file exceeds {max_size_bytes} bytes")
        chunks.append(chunk)
    content = b"".join(chunks)

    magic_check = _MAGIC_CHECKS[declared_type]
    if not magic_check(content[:_MAGIC_HEAD_SIZE]):
        raise ImageInvalidTypeError("content_does_not_match_declared_content_type")

    return content, ALLOWED_IMAGE_CONTENT_TYPES[declared_type]
