"""Nextcloud integration router — folder picker + per-user SourceRoots.

Exposes three things:

  - GET  /api/v1/nextcloud/whoami?candidate=<name>
        Validate that a Nextcloud username actually has a files/ tree
        on the mounted homecloud volume. Used by the Settings UI to
        sanity-check the override field before saving.

  - GET  /api/v1/nextcloud/browse?path=<rel>
        List immediate subdirectories of the current user's Nextcloud
        files tree, scoped server-side to their nextcloud_username.
        Powers the folder picker.

  - POST /api/v1/nextcloud/source-roots {name, nextcloud_path}
    DELETE /api/v1/nextcloud/source-roots/{id}
        Add or remove a per-user SourceRoot pointing at a Nextcloud
        subfolder. Adding kicks off an immediate scan_folder task so
        photos start appearing without a full library re-scan.

All paths are normalized with realpath and rejected if they escape the
user's allowed root — defense-in-depth against `..` and symlink
shenanigans.
"""
from __future__ import annotations

import logging
import os
from typing import Optional

from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy import delete, select, update
from sqlalchemy.ext.asyncio import AsyncSession

from app.database import get_db
from app.dependencies import get_current_user
from app.models.folders import SourceRoot, Folder
from app.models.photos import Photo
from app.models.sharing import FolderShare
from app.models.user import User
from app.services.nextcloud_dav import (
    NEXTCLOUD_USERS_ROOT,
    is_nextcloud_path,
    whoami_dir_exists,
)

logger = logging.getLogger(__name__)

router = APIRouter()


def _user_root(nc_username: str) -> str:
    """Absolute path of `<NEXTCLOUD_USERS_ROOT>/<user>/files`."""
    return os.path.join(NEXTCLOUD_USERS_ROOT, nc_username, "files")


def _resolve_under_user_root(nc_username: str, rel: str) -> str:
    """Resolve `rel` (a relative path the client supplied) under the
    user's Nextcloud `files/` directory and ensure the result is still
    inside that root. Returns the absolute, realpath-normalized path.

    Raises 400 on traversal attempts (`..`, absolute paths, symlinks
    that point outside the root)."""
    rel = (rel or "").lstrip("/")
    if any(seg in ("..",) for seg in rel.split("/") if seg):
        raise HTTPException(status_code=400, detail="Invalid path")
    base = _user_root(nc_username)
    candidate = os.path.realpath(os.path.join(base, rel))
    base_real = os.path.realpath(base)
    if candidate != base_real and not candidate.startswith(base_real + os.sep):
        raise HTTPException(status_code=400, detail="Path escapes Nextcloud root")
    return candidate


# ---------------------------------------------------------------------------

class BrowseEntry(BaseModel):
    name: str
    path: str           # path relative to the user's files/ root
    has_children: bool  # True if it contains at least one sub-directory


class BrowseResponse(BaseModel):
    nc_username: str
    rel_path: str
    parent_rel: Optional[str]  # None at the root
    entries: list[BrowseEntry]


@router.get("/whoami")
async def whoami(
    candidate: Optional[str] = Query(None, description="Nextcloud username to validate"),
    current_user: User = Depends(get_current_user),
):
    """Public to authenticated users — returns whether a Nextcloud
    username has a real files/ directory on the bind mount. Used by
    the Settings UI to validate the override field before save."""
    name = (candidate or current_user.nextcloud_username or "").strip()
    if not name:
        return {
            "configured": bool(current_user.nextcloud_username),
            "candidate": None,
            "valid": False,
            "reason": "no_username",
        }
    valid = whoami_dir_exists(name)
    return {
        "configured": bool(current_user.nextcloud_username),
        "candidate": name,
        "valid": valid,
        "reason": None if valid else "no_files_dir",
    }


@router.get("/browse", response_model=BrowseResponse)
async def browse(
    path: str = Query("", description="Path relative to the user's NC files/ root"),
    current_user: User = Depends(get_current_user),
):
    """List immediate subdirectories of the current user's NC tree."""
    nc_user = (current_user.nextcloud_username or "").strip()
    if not nc_user:
        raise HTTPException(
            status_code=412,
            detail="Set your Nextcloud username in Settings → Library first.",
        )
    if not whoami_dir_exists(nc_user):
        raise HTTPException(
            status_code=404,
            detail=(
                f"Nextcloud user '{nc_user}' has no files/ directory on the "
                "mounted homecloud volume. Check your nextcloud_username override."
            ),
        )

    abs_path = _resolve_under_user_root(nc_user, path)
    if not os.path.isdir(abs_path):
        raise HTTPException(status_code=404, detail="Folder not found")

    base = _user_root(nc_user)
    entries: list[BrowseEntry] = []
    try:
        with os.scandir(abs_path) as it:
            for de in it:
                # Skip hidden and Nextcloud's appdata noise.
                if de.name.startswith("."):
                    continue
                if not de.is_dir(follow_symlinks=False):
                    continue
                child_abs = os.path.join(abs_path, de.name)
                rel = os.path.relpath(child_abs, base)
                # Quick has_children probe: any subdir that's a real
                # directory. Cap at first hit so deep trees don't slow
                # the picker.
                has_children = False
                try:
                    with os.scandir(child_abs) as sub:
                        for s in sub:
                            if s.name.startswith("."):
                                continue
                            if s.is_dir(follow_symlinks=False):
                                has_children = True
                                break
                except OSError:
                    has_children = False
                entries.append(BrowseEntry(name=de.name, path=rel, has_children=has_children))
    except PermissionError:
        raise HTTPException(
            status_code=502,
            detail=(
                "Cannot read Nextcloud folder — backend container lacks "
                "filesystem permissions on the mount. Apply ACL fix on the host."
            ),
        )

    entries.sort(key=lambda e: e.name.lower())

    rel_path = os.path.relpath(abs_path, _user_root(nc_user)) if abs_path != _user_root(nc_user) else ""
    if rel_path == ".":
        rel_path = ""
    parent_rel: Optional[str] = None
    if rel_path:
        parent = os.path.dirname(rel_path)
        parent_rel = parent

    return BrowseResponse(
        nc_username=nc_user,
        rel_path=rel_path,
        parent_rel=parent_rel,
        entries=entries,
    )


# ---------------------------------------------------------------------------

class SourceRootCreate(BaseModel):
    name: str
    nextcloud_path: str  # relative to the user's files/ root


@router.post("/source-roots", status_code=201)
async def create_nextcloud_source_root(
    body: SourceRootCreate,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """Register a Nextcloud subfolder as a SourceRoot for the current user
    and kick off the initial scan."""
    nc_user = (current_user.nextcloud_username or "").strip()
    if not nc_user:
        raise HTTPException(
            status_code=412,
            detail="Set your Nextcloud username in Settings → Library first.",
        )

    name = (body.name or "").strip()
    if not name:
        raise HTTPException(status_code=400, detail="Name is required")

    abs_path = _resolve_under_user_root(nc_user, body.nextcloud_path)
    if not os.path.isdir(abs_path):
        raise HTTPException(status_code=404, detail="Folder not found in Nextcloud tree")

    # Don't allow registering the user's `files/` root itself as a
    # SourceRoot — it'd index everything they own (Documents, Notes,
    # appdata noise). Force them to pick a subfolder.
    if abs_path == os.path.realpath(_user_root(nc_user)):
        raise HTTPException(
            status_code=400,
            detail="Pick a subfolder; the whole files/ root is too broad.",
        )

    # Refuse duplicates — the path is uniquely indexed but a clean error
    # beats a 500 from the unique constraint.
    existing = await db.execute(
        select(SourceRoot).where(SourceRoot.path == abs_path)
    )
    if existing.scalar_one_or_none() is not None:
        raise HTTPException(status_code=409, detail="A SourceRoot for that path already exists")

    sr = SourceRoot(
        name=name,
        path=abs_path,
        user_id=current_user.id,
        is_active=True,
    )
    db.add(sr)
    await db.flush()
    await db.commit()
    await db.refresh(sr)

    # Kick off the initial scan. Failures here shouldn't block the
    # SourceRoot creation — the user can hit "Re-scan source folders"
    # from Settings if Celery is wedged.
    try:
        from app.tasks.celery import celery_app
        celery_app.send_task("scan_folder", args=[sr.path, sr.id])
    except Exception as exc:  # noqa: BLE001
        logger.warning("Failed to queue initial scan for new SourceRoot %s: %s", sr.id, exc)

    return {
        "id": sr.id,
        "name": sr.name,
        "path": sr.path,
        "user_id": sr.user_id,
        "is_active": sr.is_active,
        "is_nextcloud": True,
    }


@router.delete("/source-roots/{source_root_id}")
async def delete_nextcloud_source_root(
    source_root_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """Hard-delete: drop the SourceRoot row and cascade-delete every
    Folder and Photo underneath it. The actual files in Nextcloud are
    NOT touched — this is just unwiring the user's library in mule-image.

    Implementation notes:
      - photo_tags and heap_photos cascade via DB-level ON DELETE CASCADE,
        so deleting Photo rows is enough to clean those up.
      - FolderShare uses a stringly-typed folder_id (no FK), so we have
        to clean those rows by hand for both the SourceRoot itself and
        every Folder we're about to delete.
      - Chunked at 500 to mirror prune_missing_photos so postgres doesn't
        choke on a 21k-photo source root.
    """
    sr = (await db.execute(
        select(SourceRoot).where(
            SourceRoot.id == source_root_id,
            SourceRoot.user_id == current_user.id,
        )
    )).scalar_one_or_none()
    if sr is None:
        raise HTTPException(status_code=404, detail="SourceRoot not found")
    if not is_nextcloud_path(sr.path):
        raise HTTPException(
            status_code=400,
            detail="This endpoint only manages Nextcloud-rooted SourceRoots",
        )

    folder_ids = (await db.execute(
        select(Folder.id).where(Folder.source_root_id == sr.id)
    )).scalars().all()

    photo_ids: list[str] = []
    if folder_ids:
        photo_ids = (await db.execute(
            select(Photo.id).where(Photo.folder_id.in_(folder_ids))
        )).scalars().all()

    CHUNK = 500
    for i in range(0, len(photo_ids), CHUNK):
        chunk = photo_ids[i:i + CHUNK]
        await db.execute(delete(Photo).where(Photo.id.in_(chunk)))

    # FolderShare rows: not a real FK, clean both 'source_root' and
    # 'folder' typed shares pointing at anything we're tearing down.
    await db.execute(
        delete(FolderShare).where(
            FolderShare.folder_id == sr.id,
            FolderShare.folder_type == 'source_root',
        )
    )
    if folder_ids:
        await db.execute(
            delete(FolderShare).where(
                FolderShare.folder_id.in_(folder_ids),
                FolderShare.folder_type == 'folder',
            )
        )
        # Folders have a self-referential parent_id FK with no
        # ON DELETE rule. NULL parent_id on every folder that points
        # *into* our delete set — that includes children within this
        # source root AND any folder under a different SourceRoot whose
        # path happens to nest inside this one (e.g. a 'Leóns 1st Year'
        # SourceRoot at `.../Taco and Muli - 2024 onward/Leóns 1st Year`
        # has folder rows whose parent_id points at folder rows under
        # 'Taco and Muli - 2024 onward'). Without this, deleting the
        # outer SourceRoot trips folders_parent_id_fkey from the inner
        # SourceRoot's still-live rows.
        await db.execute(
            update(Folder)
            .where(Folder.parent_id.in_(folder_ids))
            .values(parent_id=None)
        )
        await db.execute(delete(Folder).where(Folder.id.in_(folder_ids)))

    await db.delete(sr)
    await db.commit()

    logger.info(
        f"Deleted SourceRoot {sr.id} ({sr.name}): "
        f"{len(photo_ids)} photos, {len(folder_ids)} folders"
    )
    return {
        "deleted_photos": len(photo_ids),
        "deleted_folders": len(folder_ids),
    }


# ---------------------------------------------------------------------------

@router.get("/source-roots")
async def list_nextcloud_source_roots(
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """All Nextcloud-rooted SourceRoots owned by the current user.
    Used by the Settings panel to render the manage list."""
    rows = (await db.execute(
        select(SourceRoot).where(SourceRoot.user_id == current_user.id)
    )).scalars().all()
    return [
        {
            "id": r.id,
            "name": r.name,
            "path": r.path,
            "is_active": r.is_active,
            "is_nextcloud": True,
        }
        for r in rows
        if is_nextcloud_path(r.path)
    ]
