"""Git LFS Batch API implementation - Refactored version.

This module implements the Git LFS Batch API specification for handling
large file uploads (>10MB). It provides presigned S3 URLs for direct uploads.
"""

import asyncio
import base64
from datetime import datetime, timedelta, timezone
from typing import Optional

from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel

from kohakuhub.config import cfg
from kohakuhub.db import Repository, User
from kohakuhub.db_operations import get_file_by_sha256, get_organization, get_repository
from kohakuhub.logger import get_logger
from kohakuhub.auth.dependencies import get_optional_user
from kohakuhub.auth.permissions import (
    check_repo_read_permission,
    check_repo_write_permission,
)
from kohakuhub.utils.s3 import (
    generate_download_presigned_url,
    generate_upload_presigned_url,
    get_object_metadata,
    object_exists,
)
from kohakuhub.api.quota.util import check_quota

logger = get_logger("LFS")
router = APIRouter()


class LFSObject(BaseModel):
    """LFS object specification."""

    oid: str  # SHA256 hash
    size: int


class LFSBatchRequest(BaseModel):
    """LFS batch API request."""

    operation: str  # "upload" or "download"
    transfers: list[str] | None = ["basic"]
    objects: list[LFSObject]
    hash_algo: str | None = "sha256"
    is_browser: bool = (
        False  # True if request from browser (needs Content-Type in signature)
    )


class LFSError(BaseModel):
    """LFS error object - must have 'code' (int) and 'message' (str)."""

    code: int  # HTTP status code as integer
    message: str  # Error description


class LFSObjectResponse(BaseModel):
    """LFS object response with actions."""

    oid: str
    size: int
    authenticated: bool | None = True
    actions: dict | None = None  # Contains "upload", "verify", "download"
    error: LFSError | None = None  # Must use LFSError model


class LFSBatchResponse(BaseModel):
    """LFS batch API response."""

    transfer: str = "basic"
    objects: list[LFSObjectResponse]
    hash_algo: str = "sha256"


def get_lfs_key(oid: str) -> str:
    """Generate S3 key for LFS object.

    Args:
        oid: SHA256 hash

    Returns:
        S3 key with balanced directory structure
    """
    return f"lfs/{oid[:2]}/{oid[2:4]}/{oid}"


async def process_upload_object(
    oid: str, size: int, repo_id: str, is_browser: bool = False
) -> LFSObjectResponse:
    """Process single LFS object for upload operation.

    Args:
        oid: Object ID (SHA256)
        size: File size in bytes
        repo_id: Repository ID

    Returns:
        LFS object response with upload actions or error
    """
    lfs_key = get_lfs_key(oid)

    # Check if object exists in S3 (global dedup)
    try:
        s3_exists = await object_exists(cfg.s3.bucket, lfs_key)
    except Exception as e:
        logger.exception(
            f"Failed to check S3 existence for {oid[:8]} (key: {lfs_key})", e
        )
        s3_exists = False

    # Check File table (per-repository check)
    existing_file = get_file_by_sha256(oid)

    if s3_exists or (existing_file and existing_file.size == size):
        # Object exists (either in S3 globally or in File table)
        # Tell client to skip upload
        logger.info(
            f"LFS object {oid[:8]} already exists "
            f"(s3={s3_exists}, db={existing_file is not None}), skipping upload"
        )
        return LFSObjectResponse(
            oid=oid,
            size=size,
            authenticated=True,
            # No actions = already exists
        )

    # Check if multipart upload required (>5GB)
    multipart_threshold = 5 * 1024 * 1024 * 1024  # 5GB

    if size > multipart_threshold:
        return LFSObjectResponse(
            oid=oid,
            size=size,
            error=LFSError(
                code=501,  # Not Implemented
                message="Multipart upload not yet implemented for files >5GB",
            ),
        )

    # Single PUT upload
    try:
        # Convert SHA256 hex to base64 for S3 checksum verification
        checksum_sha256 = base64.b64encode(bytes.fromhex(oid)).decode("utf-8")

        # For browser uploads, we need to include Content-Type in the presigned URL
        # because browsers always send Content-Type header
        # For CLI clients, we don't include it because they might not send it
        content_type = "application/octet-stream" if is_browser else None

        upload_info = await generate_upload_presigned_url(
            bucket=cfg.s3.bucket,
            key=lfs_key,
            expires_in=3600,  # 1 hour
            content_type=content_type,
            checksum_sha256=checksum_sha256,
        )

        return LFSObjectResponse(
            oid=oid,
            size=size,
            authenticated=True,
            actions={
                "upload": {
                    "href": upload_info["url"],
                    "expires_at": upload_info["expires_at"],
                    "header": upload_info.get("headers", {}),
                },
                "verify": {
                    "href": f"{cfg.app.base_url}/api/{repo_id}.git/info/lfs/verify",
                    "expires_at": upload_info["expires_at"],
                },
            },
        )
    except Exception as e:
        logger.exception(
            f"Failed to generate upload URL for LFS object {oid[:8]} "
            f"(size: {size}, key: {lfs_key})",
            e,
        )
        return LFSObjectResponse(
            oid=oid,
            size=size,
            error=LFSError(
                code=500,
                message=f"Failed to generate upload URL: {str(e)}",
            ),
        )


async def process_download_object(oid: str, size: int) -> LFSObjectResponse:
    """Process single LFS object for download operation.

    Args:
        oid: Object ID (SHA256)
        size: File size in bytes

    Returns:
        LFS object response with download actions or error
    """
    lfs_key = get_lfs_key(oid)

    # Check if object exists
    existing = get_file_by_sha256(oid)

    if not existing:
        return LFSObjectResponse(
            oid=oid,
            size=size,
            error=LFSError(code=404, message="Object not found"),
        )

    # Object exists, provide download URL
    try:
        download_url = await generate_download_presigned_url(
            bucket=cfg.s3.bucket,
            key=lfs_key,
            expires_in=3600,
        )

        expires_at = (datetime.now(timezone.utc) + timedelta(seconds=3600)).strftime(
            "%Y-%m-%dT%H:%M:%S.%fZ"
        )

        return LFSObjectResponse(
            oid=oid,
            size=size,
            authenticated=True,
            actions={
                "download": {
                    "href": download_url,
                    "expires_at": expires_at,
                },
            },
        )
    except Exception as e:
        return LFSObjectResponse(
            oid=oid,
            size=size,
            error=LFSError(
                code=500,
                message=f"Failed to generate download URL: {str(e)}",
            ),
        )


@router.post("/{repo_type}s/{namespace}/{name}.git/info/lfs/objects/batch")
@router.post("/{namespace}/{name}.git/info/lfs/objects/batch")
async def lfs_batch(
    namespace: str,
    name: str,
    request: Request,
    repo_type: str | None = "model",
    user: User | None = Depends(get_optional_user),
):
    """Git LFS Batch API endpoint.

    Handles LFS batch requests for upload/download operations.
    Returns presigned URLs for S3 direct upload/download.

    Args:
        namespace: Repository namespace
        name: Repository name
        request: FastAPI request with LFS batch payload
        repo_type: Repository type (default: model)
        user: Current authenticated user (optional for downloads)

    Returns:
        LFS batch response with actions

    Raises:
        HTTPException: If operation fails
    """
    repo_id = f"{namespace}/{name}"

    # Parse request
    try:
        body = await request.json()
        batch_req = LFSBatchRequest(**body)
    except Exception as e:
        raise HTTPException(400, detail={"error": f"Invalid LFS batch request: {e}"})

    # Get repository using get_repository() which returns Repository FK object
    repo = get_repository(repo_type, namespace, name)

    if repo:
        operation = batch_req.operation

        match operation:
            case "upload":
                # Upload requires authentication and write permission
                if not user:
                    raise HTTPException(
                        401, detail={"error": "Authentication required for upload"}
                    )
                check_repo_write_permission(repo, user)

                # Check storage quota for uploads
                total_upload_bytes = sum(obj.size for obj in batch_req.objects)

                # Check if namespace is organization (User with is_org=True)
                org = get_organization(namespace)
                is_org = org is not None

                # Check quota (based on repo privacy)
                is_private = repo.private
                allowed, error_msg = check_quota(
                    namespace, total_upload_bytes, is_private, is_org
                )
                if not allowed:
                    raise HTTPException(
                        status_code=413,  # Payload Too Large
                        detail={
                            "error": "Storage quota exceeded",
                            "message": error_msg,
                        },
                    )

            case "download":
                # Download requires read permission (may be public)
                check_repo_read_permission(repo, user)

    if cfg.app.debug_log_payloads:
        logger.debug("==== LFS Batch Request ====")
        logger.debug(body)

    # Process all objects in parallel
    async def process_object(obj: LFSObject) -> LFSObjectResponse:
        """Process single LFS object based on operation type."""
        match batch_req.operation:
            case "upload":
                return await process_upload_object(
                    obj.oid, obj.size, repo_id, batch_req.is_browser
                )
            case "download":
                return await process_download_object(obj.oid, obj.size)
            case _:
                return LFSObjectResponse(
                    oid=obj.oid,
                    size=obj.size,
                    error=LFSError(
                        code=400, message=f"Unknown operation: {batch_req.operation}"
                    ),
                )

    objects_response = await asyncio.gather(
        *[process_object(obj) for obj in batch_req.objects]
    )

    # Return response with exclude_none to omit null fields
    response = LFSBatchResponse(
        transfer="basic",
        objects=objects_response,
        hash_algo="sha256",
    )

    return JSONResponse(
        content=response.model_dump(exclude_none=True),
        media_type="application/vnd.git-lfs+json",
    )


@router.post("/api/{namespace}/{name}.git/info/lfs/verify")
async def lfs_verify(namespace: str, name: str, request: Request):
    """Verify LFS upload completion.

    Called by client after successful upload to confirm the file.

    Args:
        namespace: Repository namespace
        name: Repository name
        request: FastAPI request with verification data

    Returns:
        Verification result
    """
    repo_id = f"{namespace}/{name}"
    try:
        body = await request.json()
    except Exception as e:
        raise HTTPException(400, detail={"error": f"Invalid verification request: {e}"})

    oid = body.get("oid")
    size = body.get("size")

    if not oid:
        raise HTTPException(400, detail={"error": "Missing OID"})

    lfs_key = get_lfs_key(oid)

    if not await object_exists(cfg.s3.bucket, lfs_key):
        raise HTTPException(404, detail={"error": "Object not found in storage"})

    # Optionally verify size
    if size:
        try:
            metadata = await get_object_metadata(cfg.s3.bucket, lfs_key)
            if metadata["size"] != size:
                raise HTTPException(400, detail={"error": "Size mismatch"})
        except Exception:
            # If metadata retrieval fails, still accept the verification
            pass

    return {"message": "Object verified successfully"}
