"""DMS plugin routes — folders, files, preview, Collabora, internal sharing, search, bulk."""

from __future__ import annotations

import os
import uuid

from fastapi import (
    APIRouter,
    Body,
    Depends,
    File,
    Form,
    HTTPException,
    Response,
    UploadFile,
    status,
)
from fastapi.responses import StreamingResponse
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.db import get_db
from app.core.storage import get_storage_backend
from app.deps import get_current_user, require_permission
from app.plugins.builtins.dms.models import File as DmsFile
from app.plugins.builtins.dms.models import Folder
from app.plugins.builtins.dms.schemas import (
    BulkDeleteRequest,
    BulkMoveRequest,
    FileUpdate,
    FolderCreate,
    FolderUpdate,
    ShareRemoveRequest,
    ShareRequest,
)
from app.plugins.builtins.permissions.models import Permission

router = APIRouter(prefix="/api/v1/dms", tags=["dms"])

# Office file extensions mapped to Collabora file types
OFFICE_EXTENSIONS = {
    ".docx": "docx",
    ".xlsx": "xlsx",
    ".pptx": "pptx",
}

# Max file size: 100 MB
MAX_FILE_SIZE = 100 * 1024 * 1024


def _parse_uuid(val: str, field: str) -> uuid.UUID:
    try:
        return uuid.UUID(val)
    except (ValueError, TypeError):
        raise HTTPException(
            400, detail={"detail": f"Invalid {field}", "code": "invalid_id"}
        ) from None


def _file_storage_path(tenant_id: uuid.UUID, file_id: uuid.UUID) -> str:
    """Build relative storage path for a file (relative to storage base)."""
    return f"{tenant_id}/{file_id}"


def _get_file_extension(filename: str) -> str:
    """Extract lowercase extension including dot."""
    return os.path.splitext(filename)[1].lower()


# ─── Folders ───


@router.get("/folders", dependencies=[Depends(require_permission("dms:read"))])
async def list_folders(
    parent_id: str | None = None,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC1: GET /api/v1/dms/folders → 200 + folder tree (recursive)."""
    tenant_id = uuid.UUID(current_user["tenant_id"])

    # Fetch all non-deleted folders for tenant
    result = await db.execute(
        select(Folder).where(
            Folder.tenant_id == tenant_id,
            Folder.deleted_at.is_(None),
        )
    )
    all_folders = result.scalars().all()

    # Build lookup map
    folder_map: dict[uuid.UUID, dict] = {}
    for f in all_folders:
        folder_map[f.id] = {
            "id": str(f.id),
            "name": f.name,
            "parent_id": str(f.parent_id) if f.parent_id else None,
            "created_by": str(f.created_by),
            "deleted_at": None,
            "path": "",
            "children": [],
        }

    # Build path for each folder
    def _build_path(folder_id: uuid.UUID) -> str:
        if folder_id not in folder_map:
            return ""
        f = folder_map[folder_id]
        if f["parent_id"] and uuid.UUID(f["parent_id"]) in folder_map:
            parent_path = _build_path(uuid.UUID(f["parent_id"]))
            return f"{parent_path}/{f['name']}"
        return f["name"]

    for fid in folder_map:
        folder_map[fid]["path"] = _build_path(fid)

    # Build tree
    root_nodes: list[dict] = []
    target_parent: uuid.UUID | None = None
    if parent_id is not None:
        target_parent = _parse_uuid(parent_id, "parent_id")

    for f in all_folders:
        node = folder_map[f.id]
        if f.parent_id is not None and f.parent_id in folder_map:
            folder_map[f.parent_id]["children"].append(node)
        elif f.parent_id is None:
            root_nodes.append(node)

    if target_parent is not None:
        # Return children of specified parent
        parent_node = folder_map.get(target_parent)
        if parent_node is None:
            raise HTTPException(
                404, detail={"detail": "Parent folder not found", "code": "not_found"}
            )
        return parent_node["children"]

    return root_nodes


@router.post("/folders", status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_permission("dms:write"))])
async def create_folder(
    body: FolderCreate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC2: POST /api/v1/dms/folders → 201, folder created with path."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    parent_id = _parse_uuid(body.parent_id, "parent_id") if body.parent_id else None

    # Validate parent exists if specified
    if parent_id is not None:
        parent_result = await db.execute(
            select(Folder).where(
                Folder.id == parent_id,
                Folder.tenant_id == tenant_id,
                Folder.deleted_at.is_(None),
            )
        )
        if parent_result.scalar_one_or_none() is None:
            raise HTTPException(
                404, detail={"detail": "Parent folder not found", "code": "not_found"}
            )

    # Check name uniqueness within same parent (non-deleted)
    existing = await db.execute(
        select(Folder).where(
            Folder.tenant_id == tenant_id,
            Folder.name == body.name,
            Folder.parent_id == parent_id if parent_id else Folder.parent_id.is_(None),
            Folder.deleted_at.is_(None),
        )
    )
    if existing.scalar_one_or_none() is not None:
        raise HTTPException(
            409, detail={"detail": "Folder name already exists", "code": "duplicate"}
        )

    folder = Folder(
        tenant_id=tenant_id,
        name=body.name,
        parent_id=parent_id,
        created_by=user_id,
    )
    db.add(folder)
    await db.flush()

    # Build path
    path = body.name
    if parent_id is not None:
        parent_path_result = await db.execute(select(Folder).where(Folder.id == parent_id))
        parent_folder = parent_path_result.scalar_one_or_none()
        if parent_folder:
            # Recursively build path
            path_parts = [body.name]
            current = parent_folder
            while current is not None:
                path_parts.insert(0, current.name)
                if current.parent_id is not None:
                    cur_result = await db.execute(
                        select(Folder).where(Folder.id == current.parent_id)
                    )
                    current = cur_result.scalar_one_or_none()
                else:
                    current = None
            path = "/".join(path_parts)

    return {
        "id": str(folder.id),
        "name": folder.name,
        "parent_id": str(folder.parent_id) if folder.parent_id else None,
        "created_by": str(folder.created_by),
        "deleted_at": None,
        "path": path,
        "children": [],
    }


@router.patch("/folders/{folder_id}", dependencies=[Depends(require_permission("dms:write"))])
async def update_folder(
    folder_id: str,
    body: FolderUpdate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC3: PATCH /api/v1/dms/folders/{id} → 200, rename/move."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    fid = _parse_uuid(folder_id, "folder_id")

    result = await db.execute(
        select(Folder).where(
            Folder.id == fid,
            Folder.tenant_id == tenant_id,
            Folder.deleted_at.is_(None),
        )
    )
    folder = result.scalar_one_or_none()
    if folder is None:
        raise HTTPException(404, detail={"detail": "Folder not found", "code": "not_found"})

    data = body.model_dump(exclude_unset=True)

    if "name" in data and data["name"] is not None:
        # Check uniqueness if name is changing
        new_parent_id = folder.parent_id
        if "parent_id" in data and data["parent_id"] is not None:
            new_parent_id = _parse_uuid(data["parent_id"], "parent_id")

        dup = await db.execute(
            select(Folder).where(
                Folder.tenant_id == tenant_id,
                Folder.name == data["name"],
                Folder.id != fid,
                Folder.parent_id == new_parent_id if new_parent_id else Folder.parent_id.is_(None),
                Folder.deleted_at.is_(None),
            )
        )
        if dup.scalar_one_or_none() is not None:
            raise HTTPException(
                409, detail={"detail": "Folder name already exists", "code": "duplicate"}
            )
        folder.name = data["name"]

    if "parent_id" in data:
        new_parent = _parse_uuid(data["parent_id"], "parent_id") if data["parent_id"] else None
        if new_parent is not None:
            # Validate parent exists and not creating a cycle
            if new_parent == fid:
                raise HTTPException(
                    400, detail={"detail": "Cannot move folder into itself", "code": "invalid_move"}
                )

            parent_result = await db.execute(
                select(Folder).where(
                    Folder.id == new_parent,
                    Folder.tenant_id == tenant_id,
                    Folder.deleted_at.is_(None),
                )
            )
            if parent_result.scalar_one_or_none() is None:
                raise HTTPException(
                    404, detail={"detail": "Parent folder not found", "code": "not_found"}
                )

            # Check for cycle: ensure new_parent is not a descendant of folder
            async def _is_descendant(ancestor_id: uuid.UUID, descendant_id: uuid.UUID) -> bool:
                cur_result = await db.execute(select(Folder).where(Folder.id == descendant_id))
                cur = cur_result.scalar_one_or_none()
                while cur is not None and cur.parent_id is not None:
                    if cur.parent_id == ancestor_id:
                        return True
                    p_result = await db.execute(select(Folder).where(Folder.id == cur.parent_id))
                    cur = p_result.scalar_one_or_none()
                return False

            if await _is_descendant(fid, new_parent):
                raise HTTPException(
                    400,
                    detail={
                        "detail": "Cannot move folder into its own descendant",
                        "code": "invalid_move",
                    },
                )

        folder.parent_id = new_parent

    await db.flush()

    # Build path
    path_parts = [folder.name]
    current_id = folder.parent_id
    while current_id is not None:
        cur_result = await db.execute(select(Folder).where(Folder.id == current_id))
        cur = cur_result.scalar_one_or_none()
        if cur is None:
            break
        path_parts.insert(0, cur.name)
        current_id = cur.parent_id
    path = "/".join(path_parts)

    return {
        "id": str(folder.id),
        "name": folder.name,
        "parent_id": str(folder.parent_id) if folder.parent_id else None,
        "created_by": str(folder.created_by),
        "deleted_at": None,
        "path": path,
        "children": [],
    }


@router.delete("/folders/{folder_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_permission("dms:delete"))])
async def delete_folder(
    folder_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC4: DELETE /api/v1/dms/folders/{id} → 204, soft-delete with cascade."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    fid = _parse_uuid(folder_id, "folder_id")

    result = await db.execute(
        select(Folder).where(
            Folder.id == fid,
            Folder.tenant_id == tenant_id,
            Folder.deleted_at.is_(None),
        )
    )
    folder = result.scalar_one_or_none()
    if folder is None:
        raise HTTPException(404, detail={"detail": "Folder not found", "code": "not_found"})

    from datetime import UTC, datetime

    now = datetime.now(UTC)

    # Recursively collect all descendant folder IDs
    all_folder_ids: list[uuid.UUID] = [fid]
    queue: list[uuid.UUID] = [fid]
    while queue:
        current_id = queue.pop(0)
        children_result = await db.execute(
            select(Folder).where(
                Folder.parent_id == current_id,
                Folder.tenant_id == tenant_id,
                Folder.deleted_at.is_(None),
            )
        )
        for child in children_result.scalars().all():
            all_folder_ids.append(child.id)
            queue.append(child.id)

    # Soft-delete all folders
    await db.execute(update(Folder).where(Folder.id.in_(all_folder_ids)).values(deleted_at=now))

    # Soft-delete all files in those folders
    await db.execute(
        update(DmsFile)
        .where(
            DmsFile.tenant_id == tenant_id,
            DmsFile.folder_id.in_(all_folder_ids),
            DmsFile.deleted_at.is_(None),
        )
        .values(deleted_at=now)
    )

    await db.flush()
    return Response(status_code=status.HTTP_204_NO_CONTENT)


# ─── Files ───


@router.post("/files/upload", status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_permission("dms:write"))])
async def upload_file(
    file: UploadFile = File(...),
    folder_id: str | None = Form(None),
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC5: POST /api/v1/dms/files/upload → 201, file stored + metadata."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])
    fid = _parse_uuid(folder_id, "folder_id") if folder_id else None

    # Validate folder exists if specified
    if fid is not None:
        folder_result = await db.execute(
            select(Folder).where(
                Folder.id == fid,
                Folder.tenant_id == tenant_id,
                Folder.deleted_at.is_(None),
            )
        )
        if folder_result.scalar_one_or_none() is None:
            raise HTTPException(404, detail={"detail": "Folder not found", "code": "not_found"})

    # Read file content
    content = await file.read()
    file_size = len(content)

    if file_size > MAX_FILE_SIZE:
        raise HTTPException(
            413, detail={"detail": "File too large (max 100MB)", "code": "file_too_large"}
        )

    # Create file record
    file_id = uuid.uuid4()
    storage_path = _file_storage_path(tenant_id, file_id)

    # Save file via storage backend
    storage = get_storage_backend()
    await storage.save(storage_path, content)

    mime_type = file.content_type or "application/octet-stream"

    dms_file = DmsFile(
        id=file_id,
        tenant_id=tenant_id,
        name=file.filename or "unnamed",
        folder_id=fid,
        uploaded_by=user_id,
        mime_type=mime_type,
        size_bytes=file_size,
        storage_path=storage_path,
    )
    db.add(dms_file)
    await db.flush()

    return {
        "id": str(dms_file.id),
        "name": dms_file.name,
        "folder_id": str(dms_file.folder_id) if dms_file.folder_id else None,
        "uploaded_by": str(dms_file.uploaded_by),
        "mime_type": dms_file.mime_type,
        "size_bytes": dms_file.size_bytes,
        "storage_path": dms_file.storage_path,
        "deleted_at": None,
        "created_at": dms_file.created_at.isoformat() if dms_file.created_at else None,
        "updated_at": dms_file.updated_at.isoformat() if dms_file.updated_at else None,
    }


@router.get("/files/{file_id}", dependencies=[Depends(require_permission("dms:read"))])
async def get_file(
    file_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC6: GET /api/v1/dms/files/{id} → 200 + file metadata."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    fid = _parse_uuid(file_id, "file_id")

    result = await db.execute(
        select(DmsFile).where(
            DmsFile.id == fid,
            DmsFile.tenant_id == tenant_id,
            DmsFile.deleted_at.is_(None),
        )
    )
    dms_file = result.scalar_one_or_none()
    if dms_file is None:
        raise HTTPException(404, detail={"detail": "File not found", "code": "not_found"})

    return {
        "id": str(dms_file.id),
        "name": dms_file.name,
        "folder_id": str(dms_file.folder_id) if dms_file.folder_id else None,
        "uploaded_by": str(dms_file.uploaded_by),
        "mime_type": dms_file.mime_type,
        "size_bytes": dms_file.size_bytes,
        "storage_path": dms_file.storage_path,
        "deleted_at": None,
        "created_at": dms_file.created_at.isoformat() if dms_file.created_at else None,
        "updated_at": dms_file.updated_at.isoformat() if dms_file.updated_at else None,
    }


@router.get("/files", dependencies=[Depends(require_permission("dms:read"))])
async def list_all_files(
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """List all non-deleted files for the current tenant."""
    tenant_id = uuid.UUID(current_user["tenant_id"])

    result = await db.execute(
        select(DmsFile).where(
            DmsFile.tenant_id == tenant_id,
            DmsFile.deleted_at.is_(None),
        )
    )
    files = result.scalars().all()

    return [
        {
            "id": str(f.id),
            "name": f.name,
            "folder_id": str(f.folder_id) if f.folder_id else None,
            "uploaded_by": str(f.uploaded_by),
            "mime_type": f.mime_type,
            "size_bytes": f.size_bytes,
            "deleted_at": None,
            "created_at": f.created_at.isoformat() if f.created_at else None,
            "updated_at": f.updated_at.isoformat() if f.updated_at else None,
        }
        for f in files
    ]


@router.get("/folders/{folder_id}/files", dependencies=[Depends(require_permission("dms:read"))])
async def list_files_in_folder(
    folder_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """List all non-deleted files in a specific folder (non-recursive)."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    fid = _parse_uuid(folder_id, "folder_id")

    # Validate folder exists
    folder_result = await db.execute(
        select(Folder).where(
            Folder.id == fid,
            Folder.tenant_id == tenant_id,
            Folder.deleted_at.is_(None),
        )
    )
    if folder_result.scalar_one_or_none() is None:
        raise HTTPException(404, detail={"detail": "Folder not found", "code": "not_found"})

    result = await db.execute(
        select(DmsFile).where(
            DmsFile.tenant_id == tenant_id,
            DmsFile.folder_id == fid,
            DmsFile.deleted_at.is_(None),
        )
    )
    files = result.scalars().all()

    return [
        {
            "id": str(f.id),
            "name": f.name,
            "folder_id": str(f.folder_id) if f.folder_id else None,
            "uploaded_by": str(f.uploaded_by),
            "mime_type": f.mime_type,
            "size_bytes": f.size_bytes,
            "deleted_at": None,
            "created_at": f.created_at.isoformat() if f.created_at else None,
            "updated_at": f.updated_at.isoformat() if f.updated_at else None,
        }
        for f in files
    ]


@router.patch("/files/{file_id}", dependencies=[Depends(require_permission("dms:write"))])
async def update_file(
    file_id: str,
    body: FileUpdate,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC7: PATCH /api/v1/dms/files/{id} → 200, rename/move."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    fid = _parse_uuid(file_id, "file_id")

    result = await db.execute(
        select(DmsFile).where(
            DmsFile.id == fid,
            DmsFile.tenant_id == tenant_id,
            DmsFile.deleted_at.is_(None),
        )
    )
    dms_file = result.scalar_one_or_none()
    if dms_file is None:
        raise HTTPException(404, detail={"detail": "File not found", "code": "not_found"})

    data = body.model_dump(exclude_unset=True)

    if "name" in data and data["name"] is not None:
        dms_file.name = data["name"]

    if "folder_id" in data:
        new_folder = _parse_uuid(data["folder_id"], "folder_id") if data["folder_id"] else None
        if new_folder is not None:
            folder_result = await db.execute(
                select(Folder).where(
                    Folder.id == new_folder,
                    Folder.tenant_id == tenant_id,
                    Folder.deleted_at.is_(None),
                )
            )
            if folder_result.scalar_one_or_none() is None:
                raise HTTPException(404, detail={"detail": "Folder not found", "code": "not_found"})
        dms_file.folder_id = new_folder

    await db.flush()
    await db.refresh(dms_file)

    return {
        "id": str(dms_file.id),
        "name": dms_file.name,
        "folder_id": str(dms_file.folder_id) if dms_file.folder_id else None,
        "uploaded_by": str(dms_file.uploaded_by),
        "mime_type": dms_file.mime_type,
        "size_bytes": dms_file.size_bytes,
        "storage_path": dms_file.storage_path,
        "deleted_at": None,
        "created_at": dms_file.created_at.isoformat() if dms_file.created_at else None,
        "updated_at": dms_file.updated_at.isoformat() if dms_file.updated_at else None,
    }


@router.delete("/files/{file_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_permission("dms:delete"))])
async def delete_file(
    file_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC8: DELETE /api/v1/dms/files/{id} → 204, soft-delete."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    fid = _parse_uuid(file_id, "file_id")

    result = await db.execute(
        select(DmsFile).where(
            DmsFile.id == fid,
            DmsFile.tenant_id == tenant_id,
            DmsFile.deleted_at.is_(None),
        )
    )
    dms_file = result.scalar_one_or_none()
    if dms_file is None:
        raise HTTPException(404, detail={"detail": "File not found", "code": "not_found"})

    from datetime import UTC, datetime

    dms_file.deleted_at = datetime.now(UTC)
    await db.flush()
    return Response(status_code=status.HTTP_204_NO_CONTENT)


@router.post("/files/{file_id}/restore", dependencies=[Depends(require_permission("dms:write"))])
async def restore_file(
    file_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC9: POST /api/v1/dms/files/{id}/restore → 200, restored from trash."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    fid = _parse_uuid(file_id, "file_id")

    result = await db.execute(
        select(DmsFile).where(
            DmsFile.id == fid,
            DmsFile.tenant_id == tenant_id,
            DmsFile.deleted_at.is_not(None),
        )
    )
    dms_file = result.scalar_one_or_none()
    if dms_file is None:
        raise HTTPException(404, detail={"detail": "Deleted file not found", "code": "not_found"})

    dms_file.deleted_at = None
    await db.flush()
    await db.refresh(dms_file)

    return {
        "id": str(dms_file.id),
        "name": dms_file.name,
        "folder_id": str(dms_file.folder_id) if dms_file.folder_id else None,
        "uploaded_by": str(dms_file.uploaded_by),
        "mime_type": dms_file.mime_type,
        "size_bytes": dms_file.size_bytes,
        "storage_path": dms_file.storage_path,
        "deleted_at": None,
        "created_at": dms_file.created_at.isoformat() if dms_file.created_at else None,
        "updated_at": dms_file.updated_at.isoformat() if dms_file.updated_at else None,
    }


# ─── Preview & Edit ───


@router.get("/files/{file_id}/preview", dependencies=[Depends(require_permission("dms:read"))])
async def preview_file(
    file_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC10: GET /api/v1/dms/files/{id}/preview → 200 + PDF stream."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    fid = _parse_uuid(file_id, "file_id")

    result = await db.execute(
        select(DmsFile).where(
            DmsFile.id == fid,
            DmsFile.tenant_id == tenant_id,
            DmsFile.deleted_at.is_(None),
        )
    )
    dms_file = result.scalar_one_or_none()
    if dms_file is None:
        raise HTTPException(404, detail={"detail": "File not found", "code": "not_found"})

    if dms_file.mime_type != "application/pdf":
        raise HTTPException(
            400, detail={"detail": "Only PDF files can be previewed", "code": "not_pdf"}
        )

    storage = get_storage_backend()
    if not await storage.exists(dms_file.storage_path):
        raise HTTPException(
            404, detail={"detail": "File not found on disk", "code": "file_missing"}
        )

    content = await storage.read(dms_file.storage_path)

    def _stream():
        yield content

    return StreamingResponse(
        _stream(),
        media_type="application/pdf",
        headers={"Content-Disposition": f'inline; filename="{dms_file.name}"'},
    )


@router.post("/files/{file_id}/edit-session", dependencies=[Depends(require_permission("dms:write"))])
async def create_edit_session(
    file_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC11: POST /api/v1/dms/files/{id}/edit-session → 200 + Collabora config."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = current_user["user_id"]
    user_name = current_user.get("name", "Unknown")
    fid = _parse_uuid(file_id, "file_id")

    result = await db.execute(
        select(DmsFile).where(
            DmsFile.id == fid,
            DmsFile.tenant_id == tenant_id,
            DmsFile.deleted_at.is_(None),
        )
    )
    dms_file = result.scalar_one_or_none()
    if dms_file is None:
        raise HTTPException(404, detail={"detail": "File not found", "code": "not_found"})

    ext = _get_file_extension(dms_file.name)
    if ext not in OFFICE_EXTENSIONS:
        raise HTTPException(
            400,
            detail={
                "detail": "Only Office files (docx, xlsx, pptx) are supported",
                "code": "not_office",
            },
        )

    file_type = OFFICE_EXTENSIONS[ext]
    download_url = f"/api/v1/dms/files/{fid}/preview"
    callback_url = f"/api/v1/dms/files/{fid}/callback"

    config = {
        "document": {
            "fileType": file_type,
            "key": str(uuid.uuid4()),
            "title": dms_file.name,
            "url": download_url,
        },
        "editorConfig": {
            "mode": "edit",
            "callbackUrl": callback_url,
            "user": {
                "id": user_id,
                "name": user_name,
            },
        },
    }

    return config


# ─── Internal Sharing ───


@router.post("/files/{file_id}/share", dependencies=[Depends(require_permission("dms:share"))])
async def share_file(
    file_id: str,
    body: ShareRequest,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC12: POST /api/v1/dms/files/{id}/share → 200, internal share created."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    fid = _parse_uuid(file_id, "file_id")

    # Verify file exists
    file_result = await db.execute(
        select(DmsFile).where(
            DmsFile.id == fid,
            DmsFile.tenant_id == tenant_id,
            DmsFile.deleted_at.is_(None),
        )
    )
    if file_result.scalar_one_or_none() is None:
        raise HTTPException(404, detail={"detail": "File not found", "code": "not_found"})

    created_perms: list[dict] = []

    for uid_str in body.user_ids:
        uid = _parse_uuid(uid_str, "user_id")
        # Check if already exists
        existing = await db.execute(
            select(Permission).where(
                Permission.tenant_id == tenant_id,
                Permission.file_id == fid,
                Permission.user_id == uid,
                Permission.access_level == body.access_level,
            )
        )
        if existing.scalar_one_or_none() is None:
            perm = Permission(
                tenant_id=tenant_id,
                file_id=fid,
                user_id=uid,
                group_id=None,
                access_level=body.access_level,
            )
            db.add(perm)
            await db.flush()
            created_perms.append(
                {
                    "id": str(perm.id),
                    "file_id": str(fid),
                    "user_id": str(uid),
                    "group_id": None,
                    "access_level": body.access_level,
                }
            )

    for gid_str in body.group_ids:
        gid = _parse_uuid(gid_str, "group_id")
        existing = await db.execute(
            select(Permission).where(
                Permission.tenant_id == tenant_id,
                Permission.file_id == fid,
                Permission.group_id == gid,
                Permission.access_level == body.access_level,
            )
        )
        if existing.scalar_one_or_none() is None:
            perm = Permission(
                tenant_id=tenant_id,
                file_id=fid,
                user_id=uuid.uuid4(),  # placeholder user_id for group shares
                group_id=gid,
                access_level=body.access_level,
            )
            db.add(perm)
            await db.flush()
            created_perms.append(
                {
                    "id": str(perm.id),
                    "file_id": str(fid),
                    "user_id": str(perm.user_id),
                    "group_id": str(gid),
                    "access_level": body.access_level,
                }
            )

    return {
        "file_id": str(fid),
        "shared_with": created_perms,
        "count": len(created_perms),
    }


@router.delete("/files/{file_id}/share", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_permission("dms:share"))])
async def remove_share(
    file_id: str,
    body: ShareRemoveRequest = Body(...),
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC13: DELETE /api/v1/dms/files/{id}/share → 204, share removed."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    fid = _parse_uuid(file_id, "file_id")

    if body.user_id:
        uid = _parse_uuid(body.user_id, "user_id")
        result = await db.execute(
            select(Permission).where(
                Permission.tenant_id == tenant_id,
                Permission.file_id == fid,
                Permission.user_id == uid,
            )
        )
        perms = result.scalars().all()
        for p in perms:
            await db.delete(p)

    if body.group_id:
        gid = _parse_uuid(body.group_id, "group_id")
        result = await db.execute(
            select(Permission).where(
                Permission.tenant_id == tenant_id,
                Permission.file_id == fid,
                Permission.group_id == gid,
            )
        )
        perms = result.scalars().all()
        for p in perms:
            await db.delete(p)

    await db.flush()
    return Response(status_code=status.HTTP_204_NO_CONTENT)


# ─── Search & Bulk ───


@router.get("/search", dependencies=[Depends(require_permission("dms:read"))])
async def search_files(
    q: str,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC16: GET /api/v1/dms/search?q=text → 200 + matching files (ILIKE)."""
    tenant_id = uuid.UUID(current_user["tenant_id"])

    result = await db.execute(
        select(DmsFile).where(
            DmsFile.tenant_id == tenant_id,
            DmsFile.deleted_at.is_(None),
            DmsFile.name.ilike(f"%{q}%"),
        )
    )
    files = result.scalars().all()

    return [
        {
            "id": str(f.id),
            "name": f.name,
            "folder_id": str(f.folder_id) if f.folder_id else None,
            "uploaded_by": str(f.uploaded_by),
            "mime_type": f.mime_type,
            "size_bytes": f.size_bytes,
            "deleted_at": None,
            "created_at": f.created_at.isoformat() if f.created_at else None,
        }
        for f in files
    ]


@router.get("/shared-with-me", dependencies=[Depends(require_permission("dms:read"))])
async def shared_with_me(
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC17: GET /api/v1/dms/shared-with-me → 200 + shared files list."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    user_id = uuid.UUID(current_user["user_id"])

    # Query permissions for this user and join with files
    perm_result = await db.execute(
        select(Permission).where(
            Permission.tenant_id == tenant_id,
            Permission.user_id == user_id,
        )
    )
    perms = perm_result.scalars().all()
    file_ids = {p.file_id for p in perms}

    if not file_ids:
        return []

    file_result = await db.execute(
        select(DmsFile).where(
            DmsFile.tenant_id == tenant_id,
            DmsFile.id.in_(file_ids),
            DmsFile.deleted_at.is_(None),
        )
    )
    files = file_result.scalars().all()

    # Map permissions for access_level
    perm_map: dict[uuid.UUID, str] = {}
    for p in perms:
        if p.file_id in file_ids:
            perm_map[p.file_id] = p.access_level

    return [
        {
            "id": str(f.id),
            "name": f.name,
            "folder_id": str(f.folder_id) if f.folder_id else None,
            "uploaded_by": str(f.uploaded_by),
            "mime_type": f.mime_type,
            "size_bytes": f.size_bytes,
            "access_level": perm_map.get(f.id, "read"),
            "created_at": f.created_at.isoformat() if f.created_at else None,
        }
        for f in files
    ]


@router.post("/files/bulk-move", dependencies=[Depends(require_permission("dms:write"))])
async def bulk_move(
    body: BulkMoveRequest,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC18: POST /api/v1/dms/files/bulk-move → 200, files moved."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    target_folder_id = (
        _parse_uuid(body.target_folder_id, "target_folder_id") if body.target_folder_id else None
    )

    # Validate target folder if specified
    if target_folder_id is not None:
        folder_result = await db.execute(
            select(Folder).where(
                Folder.id == target_folder_id,
                Folder.tenant_id == tenant_id,
                Folder.deleted_at.is_(None),
            )
        )
        if folder_result.scalar_one_or_none() is None:
            raise HTTPException(
                404, detail={"detail": "Target folder not found", "code": "not_found"}
            )

    file_ids = [_parse_uuid(fid, "file_id") for fid in body.file_ids]

    result = await db.execute(
        select(DmsFile).where(
            DmsFile.tenant_id == tenant_id,
            DmsFile.id.in_(file_ids),
            DmsFile.deleted_at.is_(None),
        )
    )
    files = result.scalars().all()

    moved_count = 0
    for f in files:
        f.folder_id = target_folder_id
        moved_count += 1

    await db.flush()

    return {
        "moved": moved_count,
        "file_ids": [str(fid) for fid in file_ids],
        "target_folder_id": str(target_folder_id) if target_folder_id else None,
    }


@router.post("/files/bulk-delete", dependencies=[Depends(require_permission("dms:delete"))])
async def bulk_delete(
    body: BulkDeleteRequest,
    db: AsyncSession = Depends(get_db),
    current_user: dict = Depends(get_current_user),
):
    """AC19: POST /api/v1/dms/files/bulk-delete → 200, files soft-deleted."""
    tenant_id = uuid.UUID(current_user["tenant_id"])
    file_ids = [_parse_uuid(fid, "file_id") for fid in body.file_ids]

    from datetime import UTC, datetime

    now = datetime.now(UTC)

    result = await db.execute(
        update(DmsFile)
        .where(
            DmsFile.tenant_id == tenant_id,
            DmsFile.id.in_(file_ids),
            DmsFile.deleted_at.is_(None),
        )
        .values(deleted_at=now)
    )

    deleted_count = result.rowcount
    await db.flush()

    return {
        "deleted": deleted_count,
        "file_ids": body.file_ids,
    }
