"""
Sharing API router — manage cross-user access to heaps and folders.
"""
import logging
from typing import Literal, Optional

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession

from app.database import get_db
from app.models.heaps import Heap, heap_photos
from app.models.folders import Folder, SourceRoot
from app.models.photos import Photo
from app.models.sharing import HeapShare, FolderShare
from app.models.user import User
from app.dependencies import (
    get_current_user,
    get_user_heap,
    get_user_folder,
    resolve_username,
)
from app.services.gravatar import gravatar_url


def _user_avatar(user: User) -> Optional[str]:
    """OIDC `picture` claim wins, Gravatar fills the gap. Returns None
    when neither source can produce a URL so the frontend can fall back
    to the initials bubble."""
    return user.avatar_url or gravatar_url(user.email)

logger = logging.getLogger(__name__)

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


# ── Schemas ──────────────────────────────────────────────────────────────

class ShareCreate(BaseModel):
    username: str
    permission: Literal["read", "write"] = "read"


class ShareResponse(BaseModel):
    id: str
    shared_with_id: str
    shared_with_username: str
    shared_with_avatar_url: Optional[str] = None
    shared_with_display_name: Optional[str] = None
    permission: str
    status: str  # 'pending' | 'accepted'
    created_at: str


class SharedHeapResponse(BaseModel):
    # `id` is the heap id (used for navigation). `share_id` is the
    # heap_shares row id, needed so the recipient can "Leave" via the
    # existing DELETE endpoint without a separate lookup.
    id: str
    share_id: str
    name: str
    owner_username: str
    owner_avatar_url: Optional[str] = None
    owner_display_name: Optional[str] = None
    permission: str
    photo_count: int


class SharedFolderResponse(BaseModel):
    id: str
    share_id: str
    name: str
    folder_type: str
    owner_username: str
    owner_avatar_url: Optional[str] = None
    owner_display_name: Optional[str] = None
    permission: str
    photo_count: int


class PendingInvite(BaseModel):
    """A share that exists in the DB but hasn't been accepted yet. Powers
    the notification bell in the left-sidebar user section."""
    share_id: str
    target_id: str  # heap id or folder id
    target_name: str
    owner_username: str
    owner_avatar_url: Optional[str] = None
    owner_display_name: Optional[str] = None
    permission: str
    created_at: str


class PendingInvitesResponse(BaseModel):
    heaps: list[PendingInvite]
    folders: list[PendingInvite]


class ShareableUser(BaseModel):
    id: str
    username: str
    avatar_url: Optional[str] = None
    display_name: Optional[str] = None


# ── Shareable users ──────────────────────────────────────────────────────

@router.get("/users", response_model=list[ShareableUser])
async def list_shareable_users(
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """List every active user other than the caller, for the share-picker
    dropdown. Sharing only requires knowing a username today, so surfacing
    the list is no wider an attack surface than the free-text input it
    replaces. Inactive accounts are filtered out."""
    result = await db.execute(
        select(User)
        .where(User.id != current_user.id)
        .where(User.is_active.is_(True))
        .order_by(User.username)
    )
    return [
        ShareableUser(
            id=str(u.id),
            username=u.username,
            avatar_url=_user_avatar(u),
            display_name=u.display_name,
        )
        for u in result.scalars().all()
    ]


# ── Pending invites (recipient-facing, cross-type) ───────────────────────

@router.get("/pending", response_model=PendingInvitesResponse)
async def list_pending_invites(
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """Every share targeting the current user that's still waiting on
    them to accept. Feeds the notification bell in the sidebar."""
    heap_rows = (await db.execute(
        select(HeapShare, Heap, User)
        .join(Heap, HeapShare.heap_id == Heap.id)
        .join(User, HeapShare.owner_id == User.id)
        .where(HeapShare.shared_with_id == current_user.id)
        .where(HeapShare.status == "pending")
    )).all()

    heaps = [
        PendingInvite(
            share_id=share.id,
            target_id=heap.id,
            target_name=heap.name,
            owner_username=owner.username,
            owner_avatar_url=_user_avatar(owner),
            owner_display_name=owner.display_name,
            permission=share.permission,
            created_at=share.created_at.isoformat() if share.created_at else "",
        )
        for share, heap, owner in heap_rows
    ]

    folder_rows = (await db.execute(
        select(FolderShare, User)
        .join(User, FolderShare.owner_id == User.id)
        .where(FolderShare.shared_with_id == current_user.id)
        .where(FolderShare.status == "pending")
    )).all()

    folders: list[PendingInvite] = []
    for share, owner in folder_rows:
        if share.folder_type == "source_root":
            entity = (await db.execute(
                select(SourceRoot).where(SourceRoot.id == share.folder_id)
            )).scalar_one_or_none()
        else:
            entity = (await db.execute(
                select(Folder).where(Folder.id == share.folder_id)
            )).scalar_one_or_none()
        # If the underlying folder was deleted while an invite was
        # still pending, just skip — the share is effectively orphaned
        # and the owner's revoke path will clean it up.
        if entity is None:
            continue
        folders.append(PendingInvite(
            share_id=share.id,
            target_id=share.folder_id,
            target_name=entity.name,
            owner_username=owner.username,
            owner_avatar_url=_user_avatar(owner),
            owner_display_name=owner.display_name,
            permission=share.permission,
            created_at=share.created_at.isoformat() if share.created_at else "",
        ))

    return PendingInvitesResponse(heaps=heaps, folders=folders)


# ── Heap sharing ─────────────────────────────────────────────────────────

@router.get("/heaps/shared-with-me")
async def list_shared_heaps(
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """List all accepted heap shares for the current user. Pending
    invites are hidden here and surfaced via /sharing/pending instead."""
    result = await db.execute(
        select(HeapShare, Heap, User)
        .join(Heap, HeapShare.heap_id == Heap.id)
        .join(User, HeapShare.owner_id == User.id)
        .where(HeapShare.shared_with_id == current_user.id)
        .where(HeapShare.status == "accepted")
    )
    rows = result.all()

    items = []
    for share, heap, owner in rows:
        # Count photos in this heap.
        count_result = await db.execute(
            select(func.count()).select_from(heap_photos).where(
                heap_photos.c.heap_id == heap.id
            )
        )
        count = count_result.scalar() or 0

        items.append(SharedHeapResponse(
            id=heap.id,
            share_id=share.id,
            name=heap.name,
            owner_username=owner.username,
            owner_avatar_url=_user_avatar(owner),
            owner_display_name=owner.display_name,
            permission=share.permission,
            photo_count=count,
        ))
    return items


@router.get("/heaps/{heap_id}")
async def list_heap_shares(
    heap_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """List all shares for a heap (owner only)."""
    heap = await get_user_heap(heap_id, current_user, db)

    result = await db.execute(
        select(HeapShare, User)
        .join(User, HeapShare.shared_with_id == User.id)
        .where(HeapShare.heap_id == heap.id)
    )
    return [
        ShareResponse(
            id=share.id,
            shared_with_id=user.id,
            shared_with_username=user.username,
            shared_with_avatar_url=_user_avatar(user),
            shared_with_display_name=user.display_name,
            permission=share.permission,
            status=share.status,
            created_at=share.created_at.isoformat() if share.created_at else "",
        )
        for share, user in result.all()
    ]


@router.post("/heaps/{heap_id}", status_code=201)
async def share_heap(
    heap_id: str,
    body: ShareCreate,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """Share a heap with another user (owner only)."""
    heap = await get_user_heap(heap_id, current_user, db)
    target_user = await resolve_username(body.username, db)

    if target_user.id == current_user.id:
        raise HTTPException(status_code=400, detail="Cannot share with yourself")

    # Check for existing share.
    existing = await db.execute(
        select(HeapShare).where(
            HeapShare.heap_id == heap.id,
            HeapShare.shared_with_id == target_user.id,
        )
    )
    if existing.scalar_one_or_none():
        raise HTTPException(status_code=409, detail="Already shared with this user")

    share = HeapShare(
        heap_id=heap.id,
        owner_id=current_user.id,
        shared_with_id=target_user.id,
        permission=body.permission,
    )
    db.add(share)
    await db.commit()

    logger.info("Heap %s shared with %s (%s)", heap.name, target_user.username, body.permission)
    return {"status": "shared", "share_id": share.id}


@router.post("/heaps/{heap_id}/accept", status_code=200)
async def accept_heap_share(
    heap_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """Recipient accepts a pending heap invite. Idempotent — if the
    share is already accepted, returns 200 anyway so double-clicks in
    the notification popover are harmless."""
    result = await db.execute(
        select(HeapShare).where(
            HeapShare.heap_id == heap_id,
            HeapShare.shared_with_id == current_user.id,
        )
    )
    share = result.scalar_one_or_none()
    if share is None:
        raise HTTPException(status_code=404, detail="Invite not found")
    if share.status != "accepted":
        share.status = "accepted"
        share.accepted_at = func.now()
        await db.commit()
    return {"status": "accepted"}


@router.post("/heaps/{heap_id}/decline", status_code=200)
async def decline_heap_share(
    heap_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """Recipient declines a pending heap invite. The share row is
    deleted — there's no separate 'declined' status. A re-invite just
    creates a fresh pending row."""
    result = await db.execute(
        select(HeapShare).where(
            HeapShare.heap_id == heap_id,
            HeapShare.shared_with_id == current_user.id,
        )
    )
    share = result.scalar_one_or_none()
    if share is None:
        raise HTTPException(status_code=404, detail="Invite not found")
    await db.delete(share)
    await db.commit()
    return {"status": "declined"}


@router.delete("/heaps/{heap_id}/{share_id}", status_code=204)
async def revoke_heap_share(
    heap_id: str,
    share_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """Revoke a heap share. The owner can revoke any share; a recipient
    can revoke their own share (i.e. leave)."""
    result = await db.execute(
        select(HeapShare).where(HeapShare.id == share_id, HeapShare.heap_id == heap_id)
    )
    share = result.scalar_one_or_none()
    if share is None:
        raise HTTPException(status_code=404, detail="Share not found")

    # Must be the owner or the recipient themselves.
    if share.owner_id != current_user.id and share.shared_with_id != current_user.id:
        raise HTTPException(status_code=403, detail="Not authorized")

    await db.delete(share)
    await db.commit()


# ── Folder sharing ───────────────────────────────────────────────────────

@router.get("/folders/shared-with-me")
async def list_shared_folders(
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """List all accepted folder/source-root shares for the current
    user. Pending invites are hidden here and surfaced via
    /sharing/pending instead."""
    result = await db.execute(
        select(FolderShare, User)
        .join(User, FolderShare.owner_id == User.id)
        .where(FolderShare.shared_with_id == current_user.id)
        .where(FolderShare.status == "accepted")
    )
    rows = result.all()

    items = []
    for share, owner in rows:
        # Resolve the folder/source root name and photo count.
        if share.folder_type == "source_root":
            sr_result = await db.execute(
                select(SourceRoot).where(SourceRoot.id == share.folder_id)
            )
            entity = sr_result.scalar_one_or_none()
            if not entity:
                continue
            name = entity.name
            # Count all photos under this source root's folders.
            count_result = await db.execute(
                select(func.count()).select_from(Photo).where(
                    Photo.folder_id.in_(
                        select(Folder.id).where(Folder.source_root_id == entity.id)
                    ),
                    Photo.is_discarded.is_(False),
                )
            )
        else:
            folder_result = await db.execute(
                select(Folder).where(Folder.id == share.folder_id)
            )
            entity = folder_result.scalar_one_or_none()
            if not entity:
                continue
            name = entity.name
            import os
            target_path = os.path.normpath(entity.path).rstrip(os.sep)
            count_result = await db.execute(
                select(func.count()).select_from(Photo).where(
                    Photo.folder_id.in_(
                        select(Folder.id).where(
                            (Folder.path == target_path)
                            | (Folder.path.like(target_path + os.sep + "%"))
                        )
                    ),
                    Photo.is_discarded.is_(False),
                )
            )

        count = count_result.scalar() or 0
        items.append(SharedFolderResponse(
            id=share.folder_id,
            share_id=share.id,
            name=name,
            folder_type=share.folder_type,
            owner_username=owner.username,
            owner_avatar_url=_user_avatar(owner),
            owner_display_name=owner.display_name,
            permission=share.permission,
            photo_count=count,
        ))
    return items


@router.get("/folders/{folder_id}")
async def list_folder_shares(
    folder_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """List all shares for a folder (owner only)."""
    # Verify ownership — try folder then source root.
    owned = False
    result = await db.execute(
        select(Folder).where(Folder.id == folder_id, Folder.user_id == current_user.id)
    )
    if result.scalar_one_or_none():
        owned = True
    else:
        result = await db.execute(
            select(SourceRoot).where(SourceRoot.id == folder_id, SourceRoot.user_id == current_user.id)
        )
        if result.scalar_one_or_none():
            owned = True

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

    result = await db.execute(
        select(FolderShare, User)
        .join(User, FolderShare.shared_with_id == User.id)
        .where(FolderShare.folder_id == folder_id)
    )
    return [
        ShareResponse(
            id=share.id,
            shared_with_id=user.id,
            shared_with_username=user.username,
            shared_with_avatar_url=_user_avatar(user),
            shared_with_display_name=user.display_name,
            permission=share.permission,
            status=share.status,
            created_at=share.created_at.isoformat() if share.created_at else "",
        )
        for share, user in result.all()
    ]


@router.post("/folders/{folder_id}", status_code=201)
async def share_folder(
    folder_id: str,
    body: ShareCreate,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """Share a folder or source root with another user (owner only)."""
    # Determine folder_type and verify ownership.
    folder_type = "folder"
    result = await db.execute(
        select(Folder).where(Folder.id == folder_id, Folder.user_id == current_user.id)
    )
    entity = result.scalar_one_or_none()
    if entity is None:
        result = await db.execute(
            select(SourceRoot).where(SourceRoot.id == folder_id, SourceRoot.user_id == current_user.id)
        )
        entity = result.scalar_one_or_none()
        if entity is None:
            raise HTTPException(status_code=404, detail="Folder not found")
        folder_type = "source_root"

    target_user = await resolve_username(body.username, db)
    if target_user.id == current_user.id:
        raise HTTPException(status_code=400, detail="Cannot share with yourself")

    existing = await db.execute(
        select(FolderShare).where(
            FolderShare.folder_id == folder_id,
            FolderShare.shared_with_id == target_user.id,
        )
    )
    if existing.scalar_one_or_none():
        raise HTTPException(status_code=409, detail="Already shared with this user")

    share = FolderShare(
        folder_id=folder_id,
        folder_type=folder_type,
        owner_id=current_user.id,
        shared_with_id=target_user.id,
        permission=body.permission,
    )
    db.add(share)
    await db.commit()

    logger.info("Folder %s shared with %s (%s)", entity.name, target_user.username, body.permission)
    return {"status": "shared", "share_id": share.id}


@router.post("/folders/{folder_id}/accept", status_code=200)
async def accept_folder_share(
    folder_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """Recipient accepts a pending folder invite. Idempotent."""
    result = await db.execute(
        select(FolderShare).where(
            FolderShare.folder_id == folder_id,
            FolderShare.shared_with_id == current_user.id,
        )
    )
    share = result.scalar_one_or_none()
    if share is None:
        raise HTTPException(status_code=404, detail="Invite not found")
    if share.status != "accepted":
        share.status = "accepted"
        share.accepted_at = func.now()
        await db.commit()
    return {"status": "accepted"}


@router.post("/folders/{folder_id}/decline", status_code=200)
async def decline_folder_share(
    folder_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """Recipient declines a pending folder invite. Row is deleted."""
    result = await db.execute(
        select(FolderShare).where(
            FolderShare.folder_id == folder_id,
            FolderShare.shared_with_id == current_user.id,
        )
    )
    share = result.scalar_one_or_none()
    if share is None:
        raise HTTPException(status_code=404, detail="Invite not found")
    await db.delete(share)
    await db.commit()
    return {"status": "declined"}


@router.delete("/folders/{folder_id}/{share_id}", status_code=204)
async def revoke_folder_share(
    folder_id: str,
    share_id: str,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
):
    """Revoke a folder share (owner or self-remove)."""
    result = await db.execute(
        select(FolderShare).where(FolderShare.id == share_id, FolderShare.folder_id == folder_id)
    )
    share = result.scalar_one_or_none()
    if share is None:
        raise HTTPException(status_code=404, detail="Share not found")

    if share.owner_id != current_user.id and share.shared_with_id != current_user.id:
        raise HTTPException(status_code=403, detail="Not authorized")

    await db.delete(share)
    await db.commit()
