"""File upload endpoint + image serving endpoint — auto-converts to WebP"""
import io
import os
import re
import uuid
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
from fastapi.responses import Response

from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select

from app.api.deps import get_admin_user, get_db
from app.core.models.user import User
from app.core.storage import get_storage, check_storage_deps
from app.core.services.storage_quota import mb_from_bytes

try:
    from PIL import Image as PILImage
    _PIL_OK = True
except ImportError:
    _PIL_OK = False

# 与 storage.py 保持一致：从 routers/ 往上 4 层才到项目根 sme-omnistore/
# storage.py 在 core/，往上 3 层就到根；upload.py 在 api/routers/，需要 4 层
_HERE = os.path.dirname(os.path.abspath(__file__))
_ROOT = os.path.abspath(os.path.join(_HERE, "../../../.."))
UPLOAD_DIR = os.path.join(_ROOT, "static", "uploads")
os.makedirs(UPLOAD_DIR, exist_ok=True)

ALLOWED_TYPES = {"image/jpeg", "image/png", "image/webp", "image/gif", "image/jpg"}
MIME_MAP = {
    ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
    ".png": "image/png", ".webp": "image/webp",
    ".gif": "image/gif",
    ".mp4": "video/mp4", ".webm": "video/webm",
    ".ogg": "video/ogg", ".mov": "video/quicktime",
    ".pdf": "application/pdf",
    ".doc": "application/msword",
    ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    ".xls": "application/vnd.ms-excel",
    ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    ".csv": "text/csv",
    ".txt": "text/plain",
}
MAX_SIZE = 5 * 1024 * 1024

ATTACHMENT_ALLOWED_TYPES = {
    "application/pdf",
    "application/msword",
    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    "application/vnd.ms-excel",
    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    "text/csv",
    "text/plain",
}
ATTACHMENT_SAFE_EXT = {".pdf", ".doc", ".docx", ".xls", ".xlsx", ".csv", ".txt"}
ATTACHMENT_MAX_SIZE = 20 * 1024 * 1024

VIDEO_ALLOWED_TYPES = {
    "video/mp4", "video/webm", "video/ogg",
    "video/quicktime",   # .mov
    "video/mpeg", "video/x-msvideo", "video/x-ms-wmv",
}
VIDEO_SAFE_EXT = {".mp4", ".webm", ".ogg", ".mov", ".mpeg", ".avi", ".wmv"}
VIDEO_MAX_SIZE = 100 * 1024 * 1024  # 100 MB
WEBP_QUALITY = 85          # WebP 压缩质量 (1-100)
MAX_DIMENSION = 1600       # 长边最大像素，超出则等比缩小

router = APIRouter(tags=["upload"])


def _to_webp(raw: bytes, animated_ok: bool = False) -> bytes:
    """Convert raw image bytes to WebP. Returns WebP bytes."""
    img = PILImage.open(io.BytesIO(raw))

    # GIF 动图：保留原格式
    if getattr(img, "is_animated", False) and animated_ok:
        return raw

    # 转 RGB（去掉 alpha 时用白底合成，保留 RGBA 透明）
    if img.mode not in ("RGB", "RGBA", "L"):
        img = img.convert("RGBA" if "A" in img.mode else "RGB")

    # 等比缩小长边超过 MAX_DIMENSION 的图
    w, h = img.size
    if max(w, h) > MAX_DIMENSION:
        ratio = MAX_DIMENSION / max(w, h)
        img = img.resize((int(w * ratio), int(h * ratio)), PILImage.LANCZOS)

    out = io.BytesIO()
    img.save(out, format="WEBP", quality=WEBP_QUALITY, method=4)
    return out.getvalue()


_SAFE_EXT = {".jpg", ".jpeg", ".png", ".webp", ".gif"}


def _safe_ext(filename: str) -> str:
    """从文件名中提取安全的扩展名，拒绝双扩展名攻击（如 evil.php.jpg）。"""
    # 只取最后一个 . 后的内容，且必须在白名单中
    ext = os.path.splitext(os.path.basename(filename or ""))[1].lower()
    return ext if ext in _SAFE_EXT else ".jpg"


def _safe_attachment_ext(filename: str) -> str:
    ext = os.path.splitext(os.path.basename(filename or ""))[1].lower()
    return ext if ext in ATTACHMENT_SAFE_EXT else ".pdf"


async def _consume_quota_and_get_storage(db: AsyncSession, tenant_id: int, incoming_mb: int):
    """加锁查询租户存储配额，校验后立即累加用量并返回对应的存储后端。

    使用 with_for_update() 防止同一租户并发上传时计数丢失（参考 inventory.deduct_stock 的并发安全模式）。
    """
    from app.core.models.tenant import Tenant
    from app.core.models.storage_profile import StorageProfile
    from app.core.services.storage_quota import check_quota
    from app.core.storage import get_storage_for_profile

    tr = await db.execute(select(Tenant).where(Tenant.id == tenant_id).with_for_update())
    tenant = tr.scalar_one()

    ok, quota_msg = check_quota(tenant.storage_used_mb, tenant.storage_quota_mb, incoming_mb)
    if not ok:
        raise HTTPException(status_code=413, detail=quota_msg)

    profile = None
    if tenant.storage_profile_id:
        pr = await db.execute(select(StorageProfile).where(StorageProfile.id == tenant.storage_profile_id))
        profile = pr.scalar_one_or_none()

    # 注意：先提交配额计数再做存储写入，避免行锁持有跨越外部网络IO（S3/OSS上传）。
    # 极端情况下写入失败会导致配额计数偏多但文件未真正写入，这是可接受的取舍——
    # 配额走低风险方向偏差（少给可用空间）好于长时间锁表阻塞同租户并发上传。
    tenant.storage_used_mb += incoming_mb
    await db.commit()

    return get_storage_for_profile(profile), profile


async def _refund_quota(db: AsyncSession, tenant_id: int, incoming_mb: int) -> None:
    """存储写入失败时退回之前预先累加的配额计数，避免反复失败重试导致配额虚高。"""
    from app.core.models.tenant import Tenant
    tr = await db.execute(select(Tenant).where(Tenant.id == tenant_id).with_for_update())
    tenant = tr.scalar_one()
    tenant.storage_used_mb = max(0, tenant.storage_used_mb - incoming_mb)
    await db.commit()


@router.post("/upload", summary="Upload image or product attachment")
async def upload_image(
    file: UploadFile = File(...),
    admin_user: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    content_type = (file.content_type or "").lower()
    original_ext = os.path.splitext(os.path.basename(file.filename or ""))[1].lower()
    is_image = content_type in ALLOWED_TYPES
    is_attachment = content_type in ATTACHMENT_ALLOWED_TYPES or original_ext in ATTACHMENT_SAFE_EXT
    if not is_image and not is_attachment:
        raise HTTPException(status_code=400, detail="Only image, PDF, Word, Excel, CSV, or TXT files allowed")

    content = await file.read()
    max_size = MAX_SIZE if is_image else ATTACHMENT_MAX_SIZE
    if len(content) > max_size:
        if not is_image:
            raise HTTPException(status_code=400, detail=f"Attachment must be under {ATTACHMENT_MAX_SIZE // 1024 // 1024}MB")
        raise HTTPException(status_code=400, detail="Image must be under 5MB")

    # ── WebP 转换 ──────────────────────────────────────────────
    if is_image and _PIL_OK:
        try:
            content = _to_webp(content)
            ext = ".webp"
        except Exception:
            ext = _safe_ext(file.filename or "")
    elif is_image:
        ext = _safe_ext(file.filename or "")
    else:
        ext = _safe_attachment_ext(file.filename or "")

    # 文件名：租户前缀 + UUID + 安全扩展名，不含任何用户输入
    folder = "attachments" if not is_image else ""
    filename = f"tenant_{admin_user.tenant_id}/{folder + '/' if folder else ''}{uuid.uuid4().hex}{ext}"

    incoming_mb = mb_from_bytes(len(content))
    storage, profile = await _consume_quota_and_get_storage(db, admin_user.tenant_id, incoming_mb)

    provider = profile.provider if profile else "local"
    dep = check_storage_deps(provider)
    if not dep["ok"]:
        raise HTTPException(
            status_code=501,
            detail=f"CDN 存储依赖未安装：{dep['missing']}。"
                   f"请在服务器执行：{dep['install_cmd']}，然后重启后端。",
        )

    try:
        stored_content_type = "image/webp" if ext == ".webp" else MIME_MAP.get(ext, content_type or "application/octet-stream")
        url = await storage.save(filename, content, content_type=stored_content_type)
    except Exception as e:
        await _refund_quota(db, admin_user.tenant_id, incoming_mb)
        raise HTTPException(status_code=502, detail=f"存储写入失败：{e}")

    return {"url": url, "filename": filename, "mime_type": MIME_MAP.get(ext, content_type), "file_size": len(content)}


@router.get("/static/uploads/{filename:path}", summary="Serve uploaded file", include_in_schema=False)
async def serve_upload(filename: str, request: Request):
    # filename 可能含 tenant_{id}/ 子目录，仅做路径穿越防护，不强行 basename
    filename = os.path.normpath(filename).lstrip("/\\")
    if filename.startswith("..") or ".." in filename.split(os.sep):
        raise HTTPException(status_code=400, detail="非法文件路径")
    filepath = os.path.join(UPLOAD_DIR, filename)
    if not os.path.isfile(filepath):
        raise HTTPException(status_code=404, detail="File not found")
    ext = os.path.splitext(filename)[1].lower()
    mime = MIME_MAP.get(ext, "application/octet-stream")

    file_size = os.path.getsize(filepath)
    range_header = request.headers.get("range")

    # ── 支持 Range 请求（视频 seek 必须）──────────────────────────
    if range_header:
        import re as _re
        m = _re.match(r"bytes=(\d+)-(\d*)", range_header)
        if m:
            start = int(m.group(1))
            end = int(m.group(2)) if m.group(2) else file_size - 1
            end = min(end, file_size - 1)
            length = end - start + 1
            with open(filepath, "rb") as f:
                f.seek(start)
                chunk = f.read(length)
            return Response(
                content=chunk,
                status_code=206,
                media_type=mime,
                headers={
                    "Content-Range": f"bytes {start}-{end}/{file_size}",
                    "Accept-Ranges": "bytes",
                    "Content-Length": str(length),
                },
            )

    with open(filepath, "rb") as f:
        content = f.read()
    return Response(
        content=content,
        media_type=mime,
        headers={"Accept-Ranges": "bytes", "Content-Length": str(file_size)},
    )


@router.post("/upload/video", summary="Upload video")
async def upload_video(
    file: UploadFile = File(...),
    admin_user: User = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
):
    """上传视频文件（mp4 / webm / mov / ogg），返回 {url, filename}"""
    # 宽松检查：有些浏览器对 .mov 报 application/octet-stream
    ct = (file.content_type or "").lower()
    ext = os.path.splitext(os.path.basename(file.filename or ""))[1].lower()
    if ct not in VIDEO_ALLOWED_TYPES and ext not in VIDEO_SAFE_EXT:
        raise HTTPException(status_code=400, detail="仅支持 MP4 / WebM / MOV / OGG 视频文件")

    content = await file.read()
    if len(content) > VIDEO_MAX_SIZE:
        raise HTTPException(status_code=400, detail=f"视频文件不能超过 {VIDEO_MAX_SIZE // 1024 // 1024} MB")

    safe_ext = ext if ext in VIDEO_SAFE_EXT else ".mp4"
    filename = f"tenant_{admin_user.tenant_id}/{uuid.uuid4().hex}{safe_ext}"

    incoming_mb = mb_from_bytes(len(content))
    storage, profile = await _consume_quota_and_get_storage(db, admin_user.tenant_id, incoming_mb)

    dep = check_storage_deps(profile.provider if profile else "local")
    if not dep["ok"]:
        raise HTTPException(
            status_code=501,
            detail=f"CDN 存储依赖未安装：{dep['missing']}。请执行：{dep['install_cmd']} 后重启后端。",
        )

    video_ct = ct if ct in VIDEO_ALLOWED_TYPES else f"video/{safe_ext.lstrip('.')}"
    try:
        url = await storage.save(filename, content, content_type=video_ct)
    except Exception as e:
        await _refund_quota(db, admin_user.tenant_id, incoming_mb)
        raise HTTPException(status_code=502, detail=f"存储写入失败：{e}")

    return {"url": url, "filename": filename}
