"""
FastAPI dependencies for authentication and user-scoped data access.
"""
from typing import Optional

from fastapi import Depends, HTTPException, Query, Request, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from app.auth import decode_token
from app.database import get_db
from app.models.user import User
from app.models.photos import Photo
from app.models.folders import Folder, SourceRoot
from app.models.heaps import Heap, heap_photos
from app.models.tags import Tag
from app.models.sharing import HeapShare, FolderShare

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")


async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: AsyncSession = Depends(get_db),
) -> User:
    """Decode JWT, look up user, raise 401 if invalid or inactive."""
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Invalid or expired token",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = decode_token(token)
        user_id: str = payload.get("sub")
        token_type: str = payload.get("type")
        if user_id is None or token_type != "access":
            raise credentials_exception
    except JWTError:
        raise credentials_exception

    result = await db.execute(select(User).where(User.id == user_id))
    user = result.scalar_one_or_none()
    if user is None or not user.is_active:
        raise credentials_exception
    return user


async def get_current_user_media(
    request: Request,
    token: Optional[str] = Query(None, alias="token"),
    db: AsyncSession = Depends(get_db),
) -> User:
    """Authenticate via Authorization header OR ?token= query parameter.

    Used for media endpoints (thumbnails, originals, proxies) where the
    URL is set as an <img src> or <video src> and the browser can't
    attach an Authorization header. The frontend appends ?token=JWT to
    media URLs so they pass auth without custom fetch logic.
    """
    # Try Authorization header first.
    auth_header = request.headers.get("Authorization", "")
    jwt_token = None
    if auth_header.startswith("Bearer "):
        jwt_token = auth_header[7:]
    elif token:
        jwt_token = token

    if not jwt_token:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Missing token",
            headers={"WWW-Authenticate": "Bearer"},
        )

    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Invalid or expired token",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = decode_token(jwt_token)
        user_id: str = payload.get("sub")
        token_type: str = payload.get("type")
        if user_id is None or token_type != "access":
            raise credentials_exception
    except JWTError:
        raise credentials_exception

    result = await db.execute(select(User).where(User.id == user_id))
    user = result.scalar_one_or_none()
    if user is None or not user.is_active:
        raise credentials_exception
    return user


async def require_admin(
    user: User = Depends(get_current_user),
) -> User:
    """Raise 403 if user is not an admin."""
    if user.role != "admin":
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Admin privileges required",
        )
    return user


# ---------------------------------------------------------------------------
# User-scoped query helpers
# ---------------------------------------------------------------------------

def user_photos_query(user: User):
    """Base select for photos owned by user, with tags eager-loaded."""
    return (
        select(Photo)
        .options(selectinload(Photo.tags))
        .where(Photo.user_id == user.id)
    )


async def get_user_photo(
    photo_id: str,
    user: User,
    db: AsyncSession,
) -> Photo:
    """Fetch a single photo by ID, scoped to the user. Raises 404."""
    result = await db.execute(
        select(Photo)
        .options(selectinload(Photo.tags))
        .where(Photo.id == photo_id, Photo.user_id == user.id)
    )
    photo = result.scalar_one_or_none()
    if photo is None:
        raise HTTPException(status_code=404, detail="Photo not found")
    return photo


async def get_user_folder(
    folder_id: str,
    user: User,
    db: AsyncSession,
) -> Folder:
    """Fetch a single folder by ID, scoped to the user. Raises 404."""
    result = await db.execute(
        select(Folder).where(Folder.id == folder_id, Folder.user_id == user.id)
    )
    folder = result.scalar_one_or_none()
    if folder is None:
        raise HTTPException(status_code=404, detail="Folder not found")
    return folder


async def get_user_heap(
    heap_id: str,
    user: User,
    db: AsyncSession,
) -> Heap:
    """Fetch a single heap by ID, scoped to the user. Raises 404."""
    result = await db.execute(
        select(Heap).where(Heap.id == heap_id, Heap.user_id == user.id)
    )
    heap = result.scalar_one_or_none()
    if heap is None:
        raise HTTPException(status_code=404, detail="Heap not found")
    return heap


async def get_user_tag(
    tag_id: str,
    user: User,
    db: AsyncSession,
) -> Tag:
    """Fetch a single tag by ID, scoped to the user. Raises 404."""
    result = await db.execute(
        select(Tag).where(Tag.id == tag_id, Tag.user_id == user.id)
    )
    tag = result.scalar_one_or_none()
    if tag is None:
        raise HTTPException(status_code=404, detail="Tag not found")
    return tag


# ---------------------------------------------------------------------------
# Sharing helpers
# ---------------------------------------------------------------------------

async def get_user_or_shared_heap(
    heap_id: str,
    user: User,
    db: AsyncSession,
) -> tuple:
    """Fetch a heap the user owns OR has a share for.

    Returns ``(heap, permission)`` where *permission* is
    ``'owner'``, ``'read'``, or ``'write'``.  Raises 404 if no access.
    """
    # Fast path: owned by current user.
    result = await db.execute(
        select(Heap).where(Heap.id == heap_id, Heap.user_id == user.id)
    )
    heap = result.scalar_one_or_none()
    if heap:
        return heap, "owner"

    # Shared path.
    result = await db.execute(
        select(HeapShare).where(
            HeapShare.heap_id == heap_id,
            HeapShare.shared_with_id == user.id,
        )
    )
    share = result.scalar_one_or_none()
    if share:
        result = await db.execute(select(Heap).where(Heap.id == heap_id))
        heap = result.scalar_one_or_none()
        if heap:
            return heap, share.permission

    raise HTTPException(status_code=404, detail="Heap not found")


async def get_user_or_shared_folder(
    folder_id: str,
    user: User,
    db: AsyncSession,
) -> tuple:
    """Fetch a folder (or source root) the user owns OR has a share for.

    Returns ``(entity, permission)`` where *entity* is a Folder or
    SourceRoot and *permission* is ``'owner'``, ``'read'``, or ``'write'``.
    """
    # Try owned folder first.
    result = await db.execute(
        select(Folder).where(Folder.id == folder_id, Folder.user_id == user.id)
    )
    folder = result.scalar_one_or_none()
    if folder:
        return folder, "owner"

    # Try owned source root.
    result = await db.execute(
        select(SourceRoot).where(SourceRoot.id == folder_id, SourceRoot.user_id == user.id)
    )
    sr = result.scalar_one_or_none()
    if sr:
        return sr, "owner"

    # Shared path.
    result = await db.execute(
        select(FolderShare).where(
            FolderShare.folder_id == folder_id,
            FolderShare.shared_with_id == user.id,
        )
    )
    share = result.scalar_one_or_none()
    if share:
        if share.folder_type == "source_root":
            result = await db.execute(select(SourceRoot).where(SourceRoot.id == folder_id))
        else:
            result = await db.execute(select(Folder).where(Folder.id == folder_id))
        entity = result.scalar_one_or_none()
        if entity:
            return entity, share.permission

    raise HTTPException(status_code=404, detail="Folder not found")


async def resolve_username(
    username: str,
    db: AsyncSession,
) -> User:
    """Look up an active user by username. Raises 404 if not found."""
    result = await db.execute(
        select(User).where(User.username == username, User.is_active.is_(True))
    )
    user = result.scalar_one_or_none()
    if user is None:
        raise HTTPException(status_code=404, detail="User not found")
    return user


async def can_access_photo_via_share(
    photo_id: str,
    user: User,
    db: AsyncSession,
) -> bool:
    """Check whether *user* can access *photo_id* through any share.

    Returns True if the photo belongs to a heap or folder that has been
    shared with the user.  Used as a fallback in media-serving endpoints
    after the direct ownership check fails.
    """
    import os

    # Check heap shares: photo in any heap shared with user?
    result = await db.execute(
        select(heap_photos.c.photo_id).where(
            heap_photos.c.photo_id == photo_id,
            heap_photos.c.heap_id.in_(
                select(HeapShare.heap_id).where(HeapShare.shared_with_id == user.id)
            ),
        ).limit(1)
    )
    if result.scalar_one_or_none() is not None:
        return True

    # Check folder shares: photo in any folder (or descendant) shared with user?
    result = await db.execute(
        select(Photo.folder_id).where(Photo.id == photo_id)
    )
    photo_folder_id = result.scalar_one_or_none()
    if photo_folder_id is None:
        return False

    # Get the photo's folder path for prefix matching.
    result = await db.execute(
        select(Folder.path, Folder.source_root_id).where(Folder.id == photo_folder_id)
    )
    row = result.one_or_none()
    if row is None:
        return False
    photo_path, photo_sr_id = row

    # Check source root shares — photo's source root matches a shared root?
    result = await db.execute(
        select(FolderShare.folder_id).where(
            FolderShare.shared_with_id == user.id,
            FolderShare.folder_type == "source_root",
            FolderShare.folder_id == photo_sr_id,
        ).limit(1)
    )
    if result.scalar_one_or_none() is not None:
        return True

    # Check folder shares — photo's folder is at or below a shared folder?
    # Single query: join folder_shares → folders to get shared paths, then
    # check if the photo's path starts with any of them.
    result = await db.execute(
        select(Folder.path).where(
            Folder.id.in_(
                select(FolderShare.folder_id).where(
                    FolderShare.shared_with_id == user.id,
                    FolderShare.folder_type == "folder",
                )
            )
        )
    )
    for (shared_path,) in result.all():
        if photo_path == shared_path or photo_path.startswith(shared_path + os.sep):
            return True

    return False
