import json
import uuid

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

from app.core.ai_client import chat_completion
from app.core.database import get_db
from app.core.permissions import deny_vendor, require_permission, require_portal_item, resolve_portal_grants
from app.core.security import AuthContext, get_auth_context
from app.crud.base import CRUDBase
from app.crud.dashboard_ai import MAX_WIDGETS, SYSTEM_PROMPT, parse_suggestions
from app.models.master import Master
from app.models.workflow import Form
from app.models.identity import Role, UserRole
from app.models.navigation import Dashboard, DashboardRole, Widget
from app.schemas.navigation import (
    AiWidgetRequest,
    AiWidgetResult,
    DashboardCreate,
    DashboardRead,
    DashboardRoleRead,
    DashboardUpdate,
    WidgetCreate,
    WidgetRead,
    WidgetUpdate,
)

router = APIRouter(prefix="/dashboards", tags=["dashboards"])
crud = CRUDBase[Dashboard, DashboardCreate, DashboardUpdate](Dashboard)


async def _to_read(db: AsyncSession, dashboard: Dashboard) -> DashboardRead:
    role_count = (
        await db.execute(
            select(func.count()).select_from(DashboardRole).where(DashboardRole.dashboard_id == dashboard.id)
        )
    ).scalar_one()
    return DashboardRead.model_validate(dashboard).model_copy(update={"accessible": role_count > 0})


@router.get("", response_model=list[DashboardRead])
async def list_dashboards(
    skip: int = 0,
    limit: int = 100,
    accessible_to_me: bool = False,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("dashboard", "view")),
) -> list[DashboardRead]:
    """FR-DSH-007: a role with access to more than one dashboard can switch
    between them — accessible_to_me=true lists exactly that set for the
    calling user, via their assigned roles."""
    stmt = select(Dashboard).where(Dashboard.tenant_id == ctx.tenant_id)
    if accessible_to_me:
        if ctx.user_id is None:
            raise HTTPException(status_code=400, detail="accessible_to_me requires X-User-Id")
        stmt = stmt.join(DashboardRole, DashboardRole.dashboard_id == Dashboard.id).join(
            UserRole, UserRole.role_id == DashboardRole.role_id
        ).where(UserRole.user_id == ctx.user_id)
    stmt = stmt.order_by(Dashboard.name).offset(skip).limit(limit)
    dashboards = list((await db.execute(stmt)).scalars().all())
    if ctx.vendor_id is not None:
        grants = await resolve_portal_grants(db, ctx.tenant_id, ctx.user_id)
        allowed = grants.get("dashboard", set())
        dashboards = [d for d in dashboards if str(d.id).lower() in allowed]
    return [await _to_read(db, d) for d in dashboards]


@router.get("/{dashboard_id}", response_model=DashboardRead)
async def get_dashboard(
    dashboard_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("dashboard", "view")),
    __: AuthContext = Depends(require_portal_item("dashboard", "dashboard_id")),
) -> DashboardRead:
    dashboard = await crud.get(db, ctx.tenant_id, dashboard_id)
    return await _to_read(db, dashboard)


@router.post("", response_model=DashboardRead, status_code=status.HTTP_201_CREATED)
async def create_dashboard(
    payload: DashboardCreate,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("dashboard", "create")),
    __: AuthContext = Depends(deny_vendor()),
) -> DashboardRead:
    dashboard = await crud.create(db, ctx.tenant_id, payload, created_by=ctx.user_id)
    return await _to_read(db, dashboard)


@router.patch("/{dashboard_id}", response_model=DashboardRead)
async def update_dashboard(
    dashboard_id: uuid.UUID,
    payload: DashboardUpdate,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("dashboard", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> DashboardRead:
    dashboard = await crud.update(db, ctx.tenant_id, dashboard_id, payload)
    return await _to_read(db, dashboard)


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


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


@router.put("/{dashboard_id}/roles", response_model=list[uuid.UUID])
async def set_dashboard_roles(
    dashboard_id: uuid.UUID,
    role_ids: list[uuid.UUID],
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("dashboard", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> list[uuid.UUID]:
    """Replaces the full set of roles that can view this dashboard (FR-DSH-006)."""
    await crud.get(db, ctx.tenant_id, dashboard_id)
    if role_ids:
        valid_count = (
            await db.execute(
                select(func.count()).select_from(Role).where(Role.tenant_id == ctx.tenant_id, Role.id.in_(role_ids))
            )
        ).scalar_one()
        if valid_count != len(set(role_ids)):
            raise HTTPException(status_code=404, detail="One or more roles not found")

    existing = (
        await db.execute(select(DashboardRole).where(DashboardRole.dashboard_id == dashboard_id))
    ).scalars().all()
    for link in existing:
        await db.delete(link)
    await db.flush()

    db.add_all(DashboardRole(dashboard_id=dashboard_id, role_id=rid) for rid in set(role_ids))
    await db.flush()
    return role_ids


# ---- AI widget suggestions ----------------------------------------------------------------------


@router.post("/ai-widgets", response_model=AiWidgetResult)
async def suggest_widgets(
    payload: AiWidgetRequest,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("dashboard", "create")),
    __: AuthContext = Depends(deny_vendor()),
) -> AiWidgetResult:
    """The Dashboard Designer's "Ask AI": widgets described in plain
    language, built from the tenant's own tables. Suggestions only — the
    designer adds the ones the user accepts through the normal create
    endpoint. See app/crud/dashboard_ai.py for how answers are validated."""
    from app.routers.datasets import _build_catalog  # the Data Explorer's table catalog

    masters = list((await db.execute(select(Master).where(Master.tenant_id == ctx.tenant_id).order_by(Master.name))).scalars().all())
    forms = list((await db.execute(select(Form).where(Form.tenant_id == ctx.tenant_id, Form.is_current.is_(True)).order_by(Form.name))).scalars().all())
    if not masters and not forms:
        return AiWidgetResult(error="There are no datasources or forms to build widgets from yet.")
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT.format(catalog=_build_catalog(masters, forms), max_widgets=MAX_WIDGETS)},
        {"role": "user", "content": payload.prompt},
    ]
    try:
        content = await chat_completion(messages)
    except (httpx.HTTPError, KeyError) as exc:
        return AiWidgetResult(error=f"The AI assistant is unavailable ({exc}) — add widgets from the palette instead.", provider_ok=False)
    try:
        message, widgets = parse_suggestions(content, masters, forms)
    except (json.JSONDecodeError, AttributeError, TypeError):
        return AiWidgetResult(error="The AI's answer couldn't be read — try rephrasing the request.")
    if not widgets:
        return AiWidgetResult(message=message, error=message or "Nothing in that request matched your datasources or forms.")
    return AiWidgetResult(message=message, widgets=widgets)


# ---- Widgets -------------------------------------------------------------

@router.get("/{dashboard_id}/widgets", response_model=list[WidgetRead])
async def list_widgets(
    dashboard_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("dashboard", "view")),
    __: AuthContext = Depends(require_portal_item("dashboard", "dashboard_id")),
) -> list[Widget]:
    await crud.get(db, ctx.tenant_id, dashboard_id)
    stmt = (
        select(Widget)
        .where(Widget.tenant_id == ctx.tenant_id, Widget.dashboard_id == dashboard_id)
        .order_by(Widget.sort_order)
    )
    return list((await db.execute(stmt)).scalars().all())


@router.post("/{dashboard_id}/widgets", response_model=WidgetRead, status_code=status.HTTP_201_CREATED)
async def create_widget(
    dashboard_id: uuid.UUID,
    payload: WidgetCreate,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("dashboard", "create")),
    __: AuthContext = Depends(deny_vendor()),
) -> Widget:
    await crud.get(db, ctx.tenant_id, dashboard_id)
    widget = Widget(tenant_id=ctx.tenant_id, dashboard_id=dashboard_id, **payload.model_dump())
    db.add(widget)
    await db.flush()
    await db.refresh(widget)
    return widget


async def _get_widget(db: AsyncSession, tenant_id: uuid.UUID, dashboard_id: uuid.UUID, widget_id: uuid.UUID) -> Widget:
    stmt = select(Widget).where(
        Widget.tenant_id == tenant_id, Widget.dashboard_id == dashboard_id, Widget.id == widget_id
    )
    widget = (await db.execute(stmt)).scalar_one_or_none()
    if widget is None:
        raise HTTPException(status_code=404, detail="Widget not found")
    return widget


@router.patch("/{dashboard_id}/widgets/{widget_id}", response_model=WidgetRead)
async def update_widget(
    dashboard_id: uuid.UUID,
    widget_id: uuid.UUID,
    payload: WidgetUpdate,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("dashboard", "edit")),
    __: AuthContext = Depends(deny_vendor()),
) -> Widget:
    widget = await _get_widget(db, ctx.tenant_id, dashboard_id, widget_id)
    for field, value in payload.model_dump(exclude_unset=True).items():
        setattr(widget, field, value)
    await db.flush()
    await db.refresh(widget)
    return widget


@router.delete("/{dashboard_id}/widgets/{widget_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_widget(
    dashboard_id: uuid.UUID,
    widget_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("dashboard", "delete")),
    __: AuthContext = Depends(deny_vendor()),
) -> None:
    widget = await _get_widget(db, ctx.tenant_id, dashboard_id, widget_id)
    await db.delete(widget)
    await db.flush()
