from __future__ import annotations

import logging
from pathlib import Path
from uuid import uuid4

from sqlmodel.ext.asyncio.session import AsyncSession

from transcription.config import Settings
from transcription.config import get_settings
from transcription.errors import AppError
from transcription.errors import ErrorCategory

from ..models import Document
from ..models import Job
from .documents import UploadJobResult

logger = logging.getLogger(__name__)

SUPPORTED_UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".tif", ".tiff", ".pdf"}


class UploadError(AppError):
    """Raised when uploaded content cannot be persisted safely."""


async def create_upload_job(
    *,
    filename: str,
    file_bytes: bytes,
    session: AsyncSession,
    settings: Settings | None = None,
) -> UploadJobResult:
    """Create upload-backed document and queued job records."""
    runtime_settings = settings or get_settings()
    stored_path = store_file(
        filename=filename,
        file_bytes=file_bytes,
        settings=runtime_settings,
    )
    try:
        document, job = await _create_upload_records(
            session=session,
            original_filename=filename,
            stored_path=stored_path,
        )
    except Exception as exc:
        _best_effort_delete(stored_path)
        raise UploadError(
            "Failed to create upload database records",
            category=ErrorCategory.INFRA_TRANSIENT,
            suggestion="Retry upload. If this keeps happening, verify database availability.",
            retriable=True,
        ) from exc

    logger.info("Created upload job document_id=%s job_id=%s", document.id, job.id)
    return UploadJobResult(
        document_id=document.id,
        job_id=job.id,
        stored_path=stored_path,
        original_filename=Path(filename).name,
    )


async def _create_upload_records(
    *,
    session: AsyncSession,
    original_filename: str,
    stored_path: Path,
) -> tuple[Document, Job]:
    document = Document(
        filename=Path(original_filename).name,
        file_path=str(stored_path),
    )
    session.add(document)
    await session.flush()

    job = Job(document_id=document.id)
    session.add(job)
    await session.commit()
    await session.refresh(document)
    await session.refresh(job)
    return document, job


def _best_effort_delete(path: Path) -> None:
    try:
        if path.exists():
            path.unlink()
    except OSError:
        logger.warning("Failed to clean up upload file after DB error: %s", path)


def store_file(*, filename: str, file_bytes: bytes, settings: Settings | None = None) -> Path:
    """Persist an uploaded file to the configured upload directory."""
    runtime_settings = settings or get_settings()
    _validate_upload(filename=filename, file_bytes=file_bytes)

    upload_dir = runtime_settings.upload_dir
    upload_dir.mkdir(parents=True, exist_ok=True)

    stored_name = _build_stored_filename(filename)
    stored_path = upload_dir / stored_name

    try:
        stored_path.write_bytes(file_bytes)
    except OSError as exc:
        raise UploadError(
            "Failed to persist upload file",
            category=ErrorCategory.INFRA_PERSISTENT,
            suggestion="Check upload directory permissions and available disk space, then retry.",
        ) from exc

    logger.info("Stored uploaded file: %s", stored_path)
    return stored_path


def _validate_upload(*, filename: str, file_bytes: bytes) -> None:
    if not file_bytes:
        raise UploadError(
            "Upload payload is empty",
            category=ErrorCategory.VALIDATION,
            suggestion="Select a non-empty file and try again.",
        )

    safe_name = Path(filename).name
    if not safe_name:
        raise UploadError(
            "Upload filename is required",
            category=ErrorCategory.VALIDATION,
            suggestion="Choose a file with a valid filename and retry.",
        )

    suffix = Path(safe_name).suffix.lower()
    if suffix not in SUPPORTED_UPLOAD_EXTENSIONS:
        raise UploadError(
            f"Unsupported upload extension: {suffix}",
            category=ErrorCategory.USER_INPUT,
            suggestion="Upload JPG, JPEG, PNG, TIFF, or PDF files only.",
        )


def _build_stored_filename(filename: str) -> str:
    safe_name = Path(filename).name
    return f"{uuid4()}_{safe_name}"
