"""Folder and document-folder organization endpoints."""
from __future__ import annotations

import uuid
from typing import Optional

from fastapi import APIRouter, Depends, HTTPException, Request, status
from pydantic import BaseModel
from sqlalchemy import select, text
from sqlalchemy.exc import IntegrityError, OperationalError
from sqlalchemy.ext.asyncio import AsyncSession

from db.models import Document, Folder, User
from deps.auth import get_regular_user
from deps.db import get_db
from deps.utils import get_client_ip
from services.audit import write_audit_log
from storage import get_storage_backend

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


class FolderCreate(BaseModel):
    name: str
    parent_id: Optional[str] = None


class FolderRename(BaseModel):
    name: str


class DocumentMove(BaseModel):
    folder_id: Optional[str] = None


def _folder_to_dict(folder: Folder) -> dict:
    return {
        "id": str(folder.id),
        "name": folder.name,
        "parent_id": str(folder.parent_id) if folder.parent_id else None,
        "user_id": str(folder.user_id),
        "created_at": folder.created_at.isoformat() if folder.created_at else None,
    }


def _doc_to_dict(doc: Document) -> dict:
    return {
        "id": str(doc.id),
        "filename": doc.filename,
        "content_type": doc.content_type,
        "size_bytes": doc.size_bytes,
        "status": doc.status,
        "folder_id": str(doc.folder_id) if doc.folder_id else None,
        "user_id": str(doc.user_id) if doc.user_id else None,
        "created_at": doc.created_at.isoformat() if doc.created_at else None,
        "updated_at": doc.updated_at.isoformat() if doc.updated_at else None,
    }


def _parse_uuid(value: str, not_found_detail: str) -> uuid.UUID:
    try:
        return uuid.UUID(value)
    except ValueError:
        raise HTTPException(status_code=404, detail=not_found_detail)


async def _get_owned_folder(
    session: AsyncSession,
    folder_id: str | uuid.UUID,
    user_id: uuid.UUID,
    detail: str = "Folder not found",
) -> Folder:
    uid = folder_id if isinstance(folder_id, uuid.UUID) else _parse_uuid(folder_id, detail)
    folder = await session.get(Folder, uid)
    if folder is None or folder.user_id != user_id:
        raise HTTPException(status_code=404, detail=detail)
    return folder


async def _get_owned_document(
    session: AsyncSession,
    doc_id: str,
    user_id: uuid.UUID,
) -> Document:
    uid = _parse_uuid(doc_id, "Document not found")
    doc = await session.get(Document, uid)
    if doc is None or doc.user_id != user_id:
        raise HTTPException(status_code=404, detail="Document not found")
    return doc


async def _ensure_unique_folder_name(
    session: AsyncSession,
    user_id: uuid.UUID,
    name: str,
    parent_id: Optional[uuid.UUID],
    exclude_id: Optional[uuid.UUID] = None,
) -> None:
    stmt = select(Folder).where(
        Folder.user_id == user_id,
        Folder.name == name,
        Folder.parent_id == parent_id,
    )
    if exclude_id is not None:
        stmt = stmt.where(Folder.id != exclude_id)

    dup = await session.execute(stmt)
    if dup.scalar_one_or_none() is not None:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="A folder with that name already exists here",
        )


def _duplicate_folder_error() -> HTTPException:
    return HTTPException(
        status_code=status.HTTP_409_CONFLICT,
        detail="A folder with that name already exists here",
    )


@router.post("", status_code=status.HTTP_201_CREATED)
async def create_folder(
    body: FolderCreate,
    request: Request,
    session: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_regular_user),
):
    parent_uuid: Optional[uuid.UUID] = None
    if body.parent_id is not None:
        parent = await _get_owned_folder(
            session, body.parent_id, current_user.id, "Parent folder not found"
        )
        parent_uuid = parent.id

    await _ensure_unique_folder_name(session, current_user.id, body.name, parent_uuid)

    folder = Folder(
        user_id=current_user.id,
        name=body.name,
        parent_id=parent_uuid,
    )
    try:
        session.add(folder)
        await session.commit()
    except IntegrityError:
        await session.rollback()
        raise _duplicate_folder_error()

    await write_audit_log(
        session,
        event_type="folder.created",
        user_id=current_user.id,
        actor_id=current_user.id,
        resource_id=folder.id,
        ip_address=get_client_ip(request),
        metadata_={"name": folder.name, "parent_id": str(parent_uuid) if parent_uuid else None},
    )

    return _folder_to_dict(folder)


@router.get("")
async def list_folders(
    parent_id: Optional[str] = None,
    session: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_regular_user),
):
    parent_uuid: Optional[uuid.UUID] = None
    if parent_id is not None:
        parent_folder = await _get_owned_folder(
            session, parent_id, current_user.id, "Parent folder not found"
        )
        parent_uuid = parent_folder.id

    where_clause = Folder.parent_id.is_(None) if parent_uuid is None else Folder.parent_id == parent_uuid

    result = await session.execute(
        select(Folder)
        .where(Folder.user_id == current_user.id, where_clause)
        .order_by(Folder.name)
    )
    folders = result.scalars().all()

    folder_ids = [f.id for f in folders]
    folders_with_children: set = set()
    if folder_ids:
        children_result = await session.execute(
            select(Folder.parent_id.distinct()).where(
                Folder.user_id == current_user.id,
                Folder.parent_id.in_(folder_ids),
            )
        )
        folders_with_children = {row[0] for row in children_result}

    return {
        "items": [
            {**_folder_to_dict(f), "has_children": f.id in folders_with_children}
            for f in folders
        ]
    }


@router.get("/{folder_id}")
async def get_folder(
    folder_id: str,
    session: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_regular_user),
):
    folder = await _get_owned_folder(session, folder_id, current_user.id)

    crumbs = [{"id": str(folder.id), "name": folder.name}]
    current = folder
    visited: set = {current.id}
    while current.parent_id is not None:
        if current.parent_id in visited:
            break
        parent = await session.get(Folder, current.parent_id)
        if parent is None or parent.user_id != current_user.id:
            break
        visited.add(parent.id)
        crumbs.append({"id": str(parent.id), "name": parent.name})
        current = parent

    crumbs.reverse()

    response = _folder_to_dict(folder)
    response["breadcrumb"] = crumbs
    return response


@router.patch("/{folder_id}")
async def rename_folder(
    folder_id: str,
    body: FolderRename,
    request: Request,
    session: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_regular_user),
):
    folder = await _get_owned_folder(session, folder_id, current_user.id)
    old_name = folder.name

    if body.name != folder.name:
        await _ensure_unique_folder_name(
            session, current_user.id, body.name, folder.parent_id, folder.id
        )

    folder.name = body.name
    try:
        await session.commit()
    except IntegrityError:
        await session.rollback()
        raise _duplicate_folder_error()

    await write_audit_log(
        session,
        event_type="folder.renamed",
        user_id=current_user.id,
        actor_id=current_user.id,
        resource_id=folder.id,
        ip_address=get_client_ip(request),
        metadata_={"old_name": old_name, "new_name": folder.name},
    )

    return _folder_to_dict(folder)


@router.delete("/{folder_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_folder(
    folder_id: str,
    request: Request,
    session: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_regular_user),
):
    folder = await _get_owned_folder(session, folder_id, current_user.id)
    folder_name = folder.name

    subtree_folder_ids: list[str] = []
    try:
        cte_result = await session.execute(
            text(
                "WITH RECURSIVE subtree AS ("
                "  SELECT id FROM folders WHERE id = :root_id AND user_id = :uid "
                "  UNION ALL "
                "  SELECT f.id FROM folders f "
                "  JOIN subtree s ON f.parent_id = s.id "
                "  WHERE f.user_id = :uid"
                ") SELECT id FROM subtree"
            ),
            {"root_id": folder.id.hex, "uid": current_user.id.hex},
        )
        subtree_folder_ids = [str(row[0]) for row in cte_result.fetchall()]
    except OperationalError:
        subtree_folder_ids = [str(folder.id)]

    if subtree_folder_ids:
        subtree_uuids = []
        for fid in subtree_folder_ids:
            try:
                subtree_uuids.append(uuid.UUID(fid))
            except ValueError:
                pass

        if subtree_uuids:
            docs_result = await session.execute(
                select(Document).where(
                    Document.folder_id.in_(subtree_uuids),
                    Document.user_id == current_user.id,
                )
            )
            docs = docs_result.scalars().all()
        else:
            docs = []
    else:
        docs = []

    total_bytes = sum(d.size_bytes for d in docs)

    if total_bytes > 0:
        await session.execute(
            text(
                "UPDATE quotas "
                "SET used_bytes = "
                "CASE WHEN used_bytes > :delta THEN used_bytes - :delta ELSE 0 END "
                "WHERE user_id = :uid"
            ),
            {"delta": total_bytes, "uid": current_user.id.hex},
        )

    storage_backend = get_storage_backend()
    for doc in docs:
        try:
            await storage_backend.delete_object(doc.object_key)
        except Exception:
            pass

    for doc in docs:
        await session.delete(doc)

    await session.delete(folder)
    await session.commit()

    await write_audit_log(
        session,
        event_type="folder.deleted",
        user_id=current_user.id,
        actor_id=current_user.id,
        resource_id=folder.id,
        ip_address=get_client_ip(request),
        metadata_={"name": folder_name, "doc_count": len(docs)},
    )


document_move_router = APIRouter(prefix="/api/documents", tags=["folders"])


@document_move_router.patch("/{doc_id}/folder")
async def move_document(
    doc_id: str,
    body: DocumentMove,
    request: Request,
    session: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_regular_user),
):
    doc = await _get_owned_document(session, doc_id, current_user.id)

    target_folder_uuid: Optional[uuid.UUID] = None
    if body.folder_id is not None:
        target_folder = await _get_owned_folder(session, body.folder_id, current_user.id)
        target_folder_uuid = target_folder.id

    doc.folder_id = target_folder_uuid
    await session.commit()

    return _doc_to_dict(doc)
