"""Atomic file write utilities."""

import json
import logging
import os
import tempfile
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from fastapi import UploadFile

logger = logging.getLogger(__name__)


_UPLOAD_CHUNK_SIZE = 1 * 1024 * 1024  # 1 MiB


async def read_upload_capped(file: "UploadFile", max_size: int) -> bytes:
    """Read an uploaded file in chunks, refusing to buffer past ``max_size``.

    A plain ``await file.read()`` will allocate the entire upload body before
    any size check runs, so a 1 GB upload still costs 1 GB of memory even
    against a 50 MB limit. This helper streams the read and aborts as soon
    as the accumulated size exceeds the cap.

    Raises:
        ValueError: When the upload exceeds *max_size*. The caller decides
            how to translate the exception (typically HTTP 400/413).
    """
    buf = bytearray()
    while True:
        chunk = await file.read(_UPLOAD_CHUNK_SIZE)
        if not chunk:
            break
        if len(buf) + len(chunk) > max_size:
            raise ValueError(f"Upload exceeds maximum allowed size of {max_size} bytes")
        buf.extend(chunk)
    return bytes(buf)


def atomic_write_json(file_path: Path, data: dict, indent: int = 2) -> None:
    """Write JSON data to file atomically via temp file + rename.

    Prevents data corruption if the process crashes or loses power
    mid-write. The rename operation is atomic on most filesystems.
    """
    file_path = Path(file_path)
    file_path.parent.mkdir(parents=True, exist_ok=True)

    # Write to a temp file in the same directory (same filesystem for atomic rename)
    fd, tmp_path = tempfile.mkstemp(
        dir=file_path.parent,
        prefix=f".{file_path.stem}_",
        suffix=".tmp",
    )
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            json.dump(data, f, indent=indent, ensure_ascii=False)
        os.replace(tmp_path, file_path)
    except BaseException:
        # Clean up temp file on any error
        try:
            os.unlink(tmp_path)
        except OSError as e:
            logger.debug("Failed to clean up temp file %s: %s", tmp_path, e)
            pass
        raise
