"""File upload/download endpoints for post attachments.

Handles multipart file upload proxied to MinIO and signed URL generation
for downloads. All uploads require authentication and post ownership.
"""

from __future__ import annotations

import asyncio
import logging
import os
import re
import uuid
from typing import Annotated

from fastapi import APIRouter, Depends, Form, HTTPException, UploadFile, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from auth import get_current_user
from database import get_session
from minio_client import generate_download_url, upload_file
from models import Creator, Post, PostAttachment, User
from schemas import PostAttachmentRead

logger = logging.getLogger("chrysopedia.files")

router = APIRouter(prefix="/files", tags=["files"])

# Max filename length after sanitization
_MAX_FILENAME_LEN = 200


def _sanitize_filename(filename: str) -> str:
    """Strip path separators, limit length, preserve extension.

    Prevents path traversal and overly long filenames while keeping
    the original extension for content-type detection downstream.
    """
    # Strip directory components
    name = os.path.basename(filename)
    # Remove any remaining path-separator-like chars
    name = re.sub(r'[/\\]', '_', name)
    # Replace sequences of whitespace/special chars with underscore
    name = re.sub(r'[^\w.\-]', '_', name)
    # Remove leading dots (hidden files)
    name = name.lstrip('.')

    if not name:
        name = "unnamed"

    # Preserve extension while limiting total length
    root, ext = os.path.splitext(name)
    max_root = _MAX_FILENAME_LEN - len(ext)
    if len(root) > max_root:
        root = root[:max_root]
    return root + ext


@router.post("/upload", response_model=PostAttachmentRead, status_code=status.HTTP_201_CREATED)
async def upload_attachment(
    file: UploadFile,
    post_id: Annotated[uuid.UUID, Form()],
    current_user: Annotated[User, Depends(get_current_user)],
    db: AsyncSession = Depends(get_session),
) -> PostAttachmentRead:
    """Upload a file attachment to a post.

    The file is stored in MinIO and a PostAttachment record is created.
    Requires auth and ownership of the target post.
    """
    if current_user.creator_id is None:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="No creator profile linked to this account",
        )

    # Validate post exists and user owns it
    q = (
        select(Post)
        .where(Post.id == post_id)
        .options(selectinload(Post.creator))
    )
    result = await db.execute(q)
    post = result.scalar_one_or_none()

    if post is None:
        raise HTTPException(status_code=404, detail="Post not found")
    if post.creator_id != current_user.creator_id:
        logger.warning(
            "Upload ownership violation: user %s tried to upload to post %s owned by creator %s",
            current_user.id, post_id, post.creator_id,
        )
        raise HTTPException(status_code=403, detail="Not your post")

    # Validate file was provided
    if file.filename is None or file.filename == "":
        raise HTTPException(status_code=400, detail="No file provided")

    # Read file content
    content = await file.read()
    if len(content) == 0:
        raise HTTPException(status_code=400, detail="Empty file")

    # Build object key: posts/{creator_slug}/{post_id}/{sanitized_filename}
    creator_slug = post.creator.slug if post.creator else str(post.creator_id)
    safe_name = _sanitize_filename(file.filename)
    object_key = f"posts/{creator_slug}/{post_id}/{safe_name}"

    content_type = file.content_type or "application/octet-stream"
    size_bytes = len(content)

    # Upload to MinIO (sync I/O via run_in_executor)
    loop = asyncio.get_event_loop()
    try:
        await loop.run_in_executor(
            None,
            upload_file,
            object_key,
            content,
            size_bytes,
            content_type,
        )
    except Exception as exc:
        logger.error("MinIO upload failed for %s: %s", object_key, exc)
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail="File storage unavailable",
        )

    # Create DB record
    attachment = PostAttachment(
        post_id=post_id,
        filename=safe_name,
        object_key=object_key,
        content_type=content_type,
        size_bytes=size_bytes,
    )
    db.add(attachment)
    await db.commit()
    await db.refresh(attachment)

    # Generate download URL
    try:
        url = generate_download_url(object_key)
    except Exception:
        logger.warning("Failed to generate download URL for %s after upload", object_key)
        url = None

    return PostAttachmentRead(
        id=attachment.id,
        filename=attachment.filename,
        content_type=attachment.content_type,
        size_bytes=attachment.size_bytes,
        download_url=url,
        created_at=attachment.created_at,
    )


@router.get("/{attachment_id}/download")
async def get_download_url(
    attachment_id: uuid.UUID,
    db: AsyncSession = Depends(get_session),
):
    """Generate a signed download URL for an attachment.

    Returns JSON with the presigned URL (1-hour expiry) so the frontend
    can control the download UX rather than getting a redirect.
    """
    q = select(PostAttachment).where(PostAttachment.id == attachment_id)
    result = await db.execute(q)
    attachment = result.scalar_one_or_none()

    if attachment is None:
        raise HTTPException(status_code=404, detail="Attachment not found")

    try:
        url = generate_download_url(attachment.object_key)
    except Exception as exc:
        logger.error("MinIO presigned URL failed for %s: %s", attachment.object_key, exc)
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail="File storage unavailable",
        )

    return {"url": url, "filename": attachment.filename}
