import uuid

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.database import get_db
from app.core.permissions import deny_vendor, require_permission
from app.core.security import AuthContext, get_auth_context
from app.crud.base import CRUDBase
from app.models.identity import Role, UserRole
from app.models.navigation import Menu, MenuRole
from app.schemas.navigation import MenuCreate, MenuRead, MenuRoleRead, MenuRoleSet, MenuUpdate

router = APIRouter(prefix="/menus", tags=["menus"])
crud = CRUDBase[Menu, MenuCreate, MenuUpdate](Menu)


async def _view_menus(
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
) -> AuthContext:
    """A vendor (Public Portal) account isn't part of the internal RBAC
    system at all — its menu visibility is governed by Menu.audience, not
    by holding a role with "menus":"view" (deny_vendor already blocks it
    from ever mutating a menu regardless). Everyone else still needs the
    usual permission."""
    if ctx.vendor_id is not None:
        return ctx
    return await require_permission("menus", "view")(ctx=ctx, db=db)


@router.get("", response_model=list[MenuRead])
async def list_menus(
    skip: int = 0,
    limit: int = 100,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(_view_menus),
) -> list[Menu]:
    menus = await crud.list(db, ctx.tenant_id, skip=skip, limit=limit)
    if ctx.vendor_id is not None:
        menus = await _portal_sidebar(db, ctx, menus)
    return menus


# Menu.target_type -> the portal role grant module it opens (PORTAL_ITEM_MODULES).
_GRANT_MODULE = {"master": "masters", "form": "forms", "page": "pages", "dashboard": "dashboard"}


async def _portal_sidebar(db: AsyncSession, ctx: AuthContext, menus: list[Menu]) -> list[Menu]:
    """What a portal user's sidebar lists, role by role: a role that shows
    menus (Menus › a menu › role visibility) lists just those; a role that
    has never shown any hasn't used the setting, so it lists what its
    grants let it open. The user sees what any of their roles lists. This
    only shapes the sidebar — what can be opened is still the grants
    (require_portal_item), and the Portal also drops a menu its grants
    can't open (PortalLayout.tsx)."""
    roles = (
        await db.execute(
            select(Role)
            .join(UserRole, UserRole.role_id == Role.id)
            .where(UserRole.user_id == ctx.user_id, Role.tenant_id == ctx.tenant_id, Role.scope == "portal")
        )
    ).scalars().all()
    if not roles:
        return menus
    shown: dict[uuid.UUID, set[uuid.UUID]] = {r.id: set() for r in roles}
    links = await db.execute(select(MenuRole.role_id, MenuRole.menu_id).where(MenuRole.role_id.in_(shown), MenuRole.visible.is_(True)))
    for role_id, menu_id in links.all():
        shown[role_id].add(menu_id)

    def listed_by(role: Role, menu: Menu) -> bool:
        if shown[role.id]:
            return menu.id in shown[role.id]
        module = _GRANT_MODULE.get(menu.target_type or "")
        if module is None or menu.target_id is None:
            return True
        return str(menu.target_id).lower() in {str(i).lower() for i in (role.action_matrix or {}).get(module, [])}

    return [m for m in menus if any(listed_by(r, m) for r in roles)]


@router.get("/{menu_id}", response_model=MenuRead)
async def get_menu(
    menu_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(_view_menus),
) -> Menu:
    return await crud.get(db, ctx.tenant_id, menu_id)


@router.post("", response_model=MenuRead, status_code=status.HTTP_201_CREATED)
async def create_menu(
    payload: MenuCreate,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("menus", "create")),
    __: AuthContext = Depends(deny_vendor()),
) -> Menu:
    """FR-RBA-004: a new menu item is added hidden-by-default to every
    existing role's menu-access configuration, so no role's config silently
    falls out of sync with the sidebar."""
    menu = await crud.create(db, ctx.tenant_id, payload)

    role_ids = (await db.execute(select(Role.id).where(Role.tenant_id == ctx.tenant_id))).scalars().all()
    db.add_all(MenuRole(menu_id=menu.id, role_id=role_id, visible=False) for role_id in role_ids)
    await db.flush()
    return menu


@router.patch("/{menu_id}", response_model=MenuRead)
async def update_menu(
    menu_id: uuid.UUID,
    payload: MenuUpdate,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("menus", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> Menu:
    return await crud.update(db, ctx.tenant_id, menu_id, payload)


@router.delete("/{menu_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_menu(
    menu_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("menus", "delete")),
    __: AuthContext = Depends(deny_vendor()),
) -> None:
    await crud.delete(db, ctx.tenant_id, menu_id)


@router.post("/reorder", status_code=status.HTTP_204_NO_CONTENT)
async def reorder_menus(
    ordered_menu_ids: list[uuid.UUID],
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("menus", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> None:
    for index, menu_id in enumerate(ordered_menu_ids):
        menu = await crud.get(db, ctx.tenant_id, menu_id)
        menu.sort_order = index
    await db.flush()


@router.get("/{menu_id}/roles", response_model=list[MenuRoleRead])
async def list_menu_roles(
    menu_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("menus", "view")),
) -> list[MenuRole]:
    await crud.get(db, ctx.tenant_id, menu_id)
    stmt = select(MenuRole).join(Role, Role.id == MenuRole.role_id).where(
        MenuRole.menu_id == menu_id, Role.tenant_id == ctx.tenant_id
    )
    return list((await db.execute(stmt)).scalars().all())


@router.put("/{menu_id}/roles/{role_id}", response_model=MenuRoleRead)
async def set_menu_role_visibility(
    menu_id: uuid.UUID,
    role_id: uuid.UUID,
    payload: MenuRoleSet,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("menus", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> MenuRole:
    await crud.get(db, ctx.tenant_id, menu_id)
    link = (
        await db.execute(select(MenuRole).where(MenuRole.menu_id == menu_id, MenuRole.role_id == role_id))
    ).scalar_one_or_none()
    if link is None:
        role = (
            await db.execute(select(Role).where(Role.id == role_id, Role.tenant_id == ctx.tenant_id))
        ).scalar_one_or_none()
        if role is None:
            raise HTTPException(status_code=404, detail="Role not found")
        link = MenuRole(menu_id=menu_id, role_id=role_id, visible=payload.visible)
        db.add(link)
    else:
        link.visible = payload.visible
    await db.flush()
    return link
