"""Folder management."""

from __future__ import annotations

from fastapi import APIRouter, Depends, Form, HTTPException, Response, status
from sqlalchemy.orm import Session as DBSession

from lembas.api.deps import Db, RequiredUser, require_permission
from lembas.db.models import Folder

# Every route here manages folders, so the guard belongs on the router.
router = APIRouter(
    prefix="/api/folders",
    tags=["folders"],
    dependencies=[Depends(require_permission("folder.manage"))],
)

MAX_DEPTH = 8


def _owned_folder(db: DBSession, folder_id: str, user_id: str) -> Folder:
    folder = db.get(Folder, folder_id)
    if folder is None or folder.user_id != user_id:
        raise HTTPException(status.HTTP_404_NOT_FOUND, "That folder no longer exists.")
    return folder


def _depth_of(db: DBSession, folder: Folder | None) -> int:
    depth = 0
    seen: set[str] = set()
    while folder is not None and folder.id not in seen:
        seen.add(folder.id)
        depth += 1
        folder = db.get(Folder, folder.parent_id) if folder.parent_id else None
    return depth


def _refresh_sidebar() -> Response:
    """Tell the browser to reload so the tree re-renders.

    The folder tree is recursive and a change can move any part of it, so
    re-rendering the whole sidebar server-side is both simpler and less
    error-prone than trying to patch individual nodes over the wire.
    """
    response = Response(status_code=status.HTTP_204_NO_CONTENT)
    response.headers["HX-Refresh"] = "true"
    return response


@router.post("")
async def create_folder(
    db: Db,
    user: RequiredUser,
    name: str = Form("New folder"),
    parent_id: str = Form(""),
) -> Response:
    parent = _owned_folder(db, parent_id, user.id) if parent_id else None

    # A cap on nesting, so a runaway client cannot build a tree deep enough to
    # blow the recursion limit in the template.
    if parent is not None and _depth_of(db, parent) >= MAX_DEPTH:
        raise HTTPException(
            status.HTTP_400_BAD_REQUEST,
            f"Folders cannot be nested more than {MAX_DEPTH} deep.",
        )

    db.add(
        Folder(
            user_id=user.id,
            name=name.strip()[:200] or "New folder",
            parent_id=parent.id if parent else None,
        )
    )
    db.commit()
    return _refresh_sidebar()


@router.patch("/{folder_id}")
async def update_folder(
    db: Db,
    user: RequiredUser,
    folder_id: str,
    name: str | None = Form(None),
    parent_id: str | None = Form(None),
    collapsed: bool | None = Form(None),
) -> Response:
    folder = _owned_folder(db, folder_id, user.id)

    if name is not None and name.strip():
        folder.name = name.strip()[:200]

    if parent_id is not None:
        new_parent = _owned_folder(db, parent_id, user.id) if parent_id else None
        # Reparenting a folder into its own subtree would detach that subtree
        # from the root and make it unreachable.
        cursor = new_parent
        while cursor is not None:
            if cursor.id == folder.id:
                raise HTTPException(
                    status.HTTP_400_BAD_REQUEST,
                    "A folder cannot be moved inside itself.",
                )
            cursor = db.get(Folder, cursor.parent_id) if cursor.parent_id else None
        folder.parent_id = new_parent.id if new_parent else None

    if collapsed is not None:
        folder.collapsed = collapsed

    db.commit()
    return _refresh_sidebar()


@router.delete("/{folder_id}")
async def delete_folder(db: Db, user: RequiredUser, folder_id: str) -> Response:
    """Delete a folder. Child folders go with it; chats do not.

    Chats fall back to the unfiled list (the FK is ON DELETE SET NULL), because
    losing a conversation to a mis-clicked folder delete is unforgivable.
    """
    folder = _owned_folder(db, folder_id, user.id)
    db.delete(folder)
    db.commit()
    return _refresh_sidebar()
