"""
Attachment API endpoints.

Two upload flows:
  (A) Small files (< 10 MB): multipart/form-data → FastAPI → MinIO
  (B) Large files:           client requests signed URL → uploads direct to MinIO
"""
from __future__ import annotations

import io
from typing import Annotated

from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field

from app.auth.dependencies import get_current_user, User
from app.db import get_db
from app.storage import storage, settings, ALLOWED_MIME_TYPES, StorageError
from app.repositories import attachment_repo, application_repo

router = APIRouter(prefix="/applications/{app_id}/attachments", tags=["attachments"])

SMALL_FILE_LIMIT_BYTES = 10 * 1024 * 1024  # 10 MB


# ---------------------------------------------------------------------- #
# Request / response models
# ---------------------------------------------------------------------- #
class AttachmentOut(BaseModel):
    attachment_id: int
    file_name: str
    file_size: int
    mime_type: str
    kind: str | None
    uploaded_at: str
    download_url: str | None = None


class PresignedUploadRequest(BaseModel):
    file_name: str = Field(min_length=1, max_length=255)
    mime_type: str
    file_size: int = Field(gt=0)
    kind: str | None = None


class PresignedUploadResponse(BaseModel):
    upload_url: str
    headers: dict[str, str]
    object_key: str
    # Client calls POST /attachments/confirm with this after upload completes
    pending_attachment_id: int


# ---------------------------------------------------------------------- #
# (A) Direct upload: multipart → FastAPI → MinIO
# ---------------------------------------------------------------------- #
@router.post("", response_model=AttachmentOut, status_code=status.HTTP_201_CREATED)
async def upload_attachment(
    app_id: int,
    file: Annotated[UploadFile, File(description="Attachment file")],
    kind: str | None = None,
    user: User = Depends(get_current_user),
    db=Depends(get_db),
):
    """
    Upload a small file (< 10 MB) through the API.

    For files larger than this, call POST /attachments/presign instead —
    proxying large uploads through FastAPI wastes memory and CPU.
    """
    # 1. Authorization: user must be author of the app, and app must be DRAFT
    app = await application_repo.get(db, app_id)
    if app is None:
        raise HTTPException(404, "Application not found")
    if app.status != "DRAFT":
        raise HTTPException(422, "Attachments can only be added to DRAFT applications")
    if not await application_repo.is_author(db, app_id, user.id):
        raise HTTPException(403, "Only authors may upload attachments")

    # 2. Validate MIME type — never trust the client alone
    if file.content_type not in ALLOWED_MIME_TYPES:
        raise HTTPException(422, f"MIME type {file.content_type} not allowed")

    # 3. Check declared size
    if file.size and file.size > SMALL_FILE_LIMIT_BYTES:
        raise HTTPException(
            413,
            "File too large for direct upload. Use POST /presign for large files.",
        )

    # 4. Stream to MinIO
    object_key = storage.build_key(app_id, file.filename or "file")
    try:
        result = await storage.upload(
            bucket=settings.s3_bucket_attachments,
            key=object_key,
            fileobj=file.file,
            mime_type=file.content_type,
            metadata={"uploaded_by": str(user.id), "app_id": str(app_id)},
        )
    except ValueError as exc:
        raise HTTPException(422, str(exc)) from exc
    except StorageError as exc:
        raise HTTPException(502, f"Storage unavailable: {exc}") from exc

    # 5. Write metadata to Postgres (storage is the source of truth for bytes,
    #    DB is the source of truth for metadata — two-phase write)
    attachment = await attachment_repo.insert(
        db,
        application_id=app_id,
        file_name=file.filename,
        file_path=object_key,
        file_size=result["size"],
        mime_type=file.content_type,
        kind=kind,
        uploaded_by=user.id,
    )
    await db.commit()

    return AttachmentOut(
        attachment_id=attachment.attachment_id,
        file_name=attachment.file_name,
        file_size=attachment.file_size,
        mime_type=attachment.mime_type,
        kind=attachment.kind,
        uploaded_at=attachment.uploaded_at.isoformat(),
    )


# ---------------------------------------------------------------------- #
# (B) Large-file upload: presigned URL flow
# ---------------------------------------------------------------------- #
@router.post("/presign", response_model=PresignedUploadResponse)
async def presign_upload(
    app_id: int,
    req: PresignedUploadRequest,
    user: User = Depends(get_current_user),
    db=Depends(get_db),
):
    """
    Step 1 of large-file upload: client requests a signed URL.

    Flow:
      1. Client POSTs metadata → receives {upload_url, pending_attachment_id}
      2. Client PUTs the file bytes to upload_url (direct to MinIO)
      3. Client POSTs /attachments/confirm with pending_attachment_id
    """
    app = await application_repo.get(db, app_id)
    if app is None or app.status != "DRAFT":
        raise HTTPException(422, "Invalid application state")
    if not await application_repo.is_author(db, app_id, user.id):
        raise HTTPException(403, "Only authors may upload attachments")
    if req.mime_type not in ALLOWED_MIME_TYPES:
        raise HTTPException(422, "MIME type not allowed")
    if req.file_size > settings.max_upload_size_mb * 1024 * 1024:
        raise HTTPException(413, "File exceeds maximum size")

    object_key = storage.build_key(app_id, req.file_name)

    # Write a PENDING attachment row — gets finalized on /confirm
    pending = await attachment_repo.insert_pending(
        db,
        application_id=app_id,
        file_name=req.file_name,
        file_path=object_key,
        file_size=req.file_size,
        mime_type=req.mime_type,
        kind=req.kind,
        uploaded_by=user.id,
    )
    await db.commit()

    presigned = await storage.get_upload_url(
        bucket=settings.s3_bucket_attachments,
        key=object_key,
        mime_type=req.mime_type,
    )

    return PresignedUploadResponse(
        upload_url=presigned["url"],
        headers=presigned["headers"],
        object_key=object_key,
        pending_attachment_id=pending.attachment_id,
    )


@router.post("/{pending_id}/confirm", response_model=AttachmentOut)
async def confirm_upload(
    app_id: int,
    pending_id: int,
    user: User = Depends(get_current_user),
    db=Depends(get_db),
):
    """Step 3: confirm the direct upload succeeded; verify the object exists."""
    pending = await attachment_repo.get(db, pending_id)
    if pending is None or pending.application_id != app_id:
        raise HTTPException(404, "Pending attachment not found")
    if pending.uploaded_by != user.id:
        raise HTTPException(403, "Not your upload")

    # Verify MinIO actually received the file
    try:
        head = await storage.head(
            bucket=settings.s3_bucket_attachments, key=pending.file_path
        )
    except FileNotFoundError:
        raise HTTPException(422, "Upload never completed")

    # Optionally verify size matches declaration
    actual_size = head["ContentLength"]
    if actual_size != pending.file_size:
        # Client lied about size — quarantine and reject
        await storage.move(
            settings.s3_bucket_attachments,
            pending.file_path,
            settings.s3_bucket_quarantine,
            pending.file_path,
        )
        await attachment_repo.delete(db, pending_id)
        await db.commit()
        raise HTTPException(422, "File size mismatch")

    await attachment_repo.mark_confirmed(db, pending_id)
    await db.commit()

    return AttachmentOut(
        attachment_id=pending.attachment_id,
        file_name=pending.file_name,
        file_size=actual_size,
        mime_type=pending.mime_type,
        kind=pending.kind,
        uploaded_at=pending.uploaded_at.isoformat(),
    )


# ---------------------------------------------------------------------- #
# Download
# ---------------------------------------------------------------------- #
@router.get("/{attachment_id}")
async def download_attachment(
    app_id: int,
    attachment_id: int,
    user: User = Depends(get_current_user),
    db=Depends(get_db),
):
    """
    Returns a signed URL. The browser follows it to download directly from
    MinIO — our API doesn't proxy the bytes, which saves significant bandwidth
    and CPU.
    """
    attachment = await attachment_repo.get(db, attachment_id)
    if attachment is None or attachment.application_id != app_id:
        raise HTTPException(404)

    # Authorization
    if not await application_repo.user_can_read(db, app_id, user.id):
        raise HTTPException(403)

    url = await storage.get_download_url(
        bucket=settings.s3_bucket_attachments,
        key=attachment.file_path,
        filename=attachment.file_name,
    )
    return {"download_url": url, "expires_in": settings.s3_signed_url_ttl}


@router.get("/{attachment_id}/stream")
async def stream_attachment(
    app_id: int,
    attachment_id: int,
    user: User = Depends(get_current_user),
    db=Depends(get_db),
):
    """
    Alternative: proxy the stream through FastAPI. Use this when you need
    to apply additional authorization/watermarking server-side, at the cost
    of extra bandwidth on your API servers.
    """
    attachment = await attachment_repo.get(db, attachment_id)
    if attachment is None or attachment.application_id != app_id:
        raise HTTPException(404)
    if not await application_repo.user_can_read(db, app_id, user.id):
        raise HTTPException(403)

    return StreamingResponse(
        storage.download_stream(
            bucket=settings.s3_bucket_attachments, key=attachment.file_path
        ),
        media_type=attachment.mime_type,
        headers={
            "Content-Disposition": f'attachment; filename="{attachment.file_name}"'
        },
    )


# ---------------------------------------------------------------------- #
# Delete
# ---------------------------------------------------------------------- #
@router.delete("/{attachment_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_attachment(
    app_id: int,
    attachment_id: int,
    user: User = Depends(get_current_user),
    db=Depends(get_db),
):
    app = await application_repo.get(db, app_id)
    if app.status != "DRAFT":
        raise HTTPException(422, "Can only delete attachments on DRAFT applications")
    if not await application_repo.is_author(db, app_id, user.id):
        raise HTTPException(403)

    attachment = await attachment_repo.get(db, attachment_id)
    if attachment is None or attachment.application_id != app_id:
        raise HTTPException(404)

    # Delete from MinIO first. If the DB delete fails, we have an orphan
    # in storage (recoverable — versioning), which is better than an
    # orphan in the DB that points to nothing.
    await storage.delete(settings.s3_bucket_attachments, attachment.file_path)
    await attachment_repo.delete(db, attachment_id)
    await db.commit()
