import { Router, type IRouter } from "express";
import { eq, asc, inArray, sql } from "drizzle-orm";
import { db } from "@workspace/db";
import {
  appsTable,
  appPermissionsTable,
  userRolesTable,
  rolePermissionsTable,
  rolesTable,
  userAppOrdersTable,
} from "@workspace/db";
import { requireAuth, requireAdmin } from "../middlewares/auth";
import {
  CreateAppBody,
  UpdateAppBody,
  GetAppParams,
  UpdateAppParams,
  DeleteAppParams,
  UpdateMyAppOrderBody,
} from "@workspace/api-zod";

const router: IRouter = Router();

async function getVisibleAppsForUser(userId: number): Promise<typeof appsTable.$inferSelect[]> {
  const userRoleRows = await db
    .select({ roleName: rolesTable.name, roleId: userRolesTable.roleId })
    .from(userRolesTable)
    .innerJoin(rolesTable, eq(userRolesTable.roleId, rolesTable.id))
    .where(eq(userRolesTable.userId, userId));

  const isAdmin = userRoleRows.some((r) => r.roleName === "admin");

  // Pull the user's preferred order, then order by COALESCE(user_order, app.sort_order, name).
  const orderedRows = await db
    .select({
      app: appsTable,
      userSort: userAppOrdersTable.sortOrder,
    })
    .from(appsTable)
    .leftJoin(
      userAppOrdersTable,
      sql`${userAppOrdersTable.appId} = ${appsTable.id} and ${userAppOrdersTable.userId} = ${userId}`,
    )
    .where(eq(appsTable.isActive, true))
    .orderBy(
      sql`COALESCE(${userAppOrdersTable.sortOrder}, ${appsTable.sortOrder})`,
      asc(appsTable.nameEn),
    );

  const apps = orderedRows.map((r) => r.app);

  if (isAdmin) return apps;

  const userRoleIds = userRoleRows.map((r) => r.roleId);
  const userPermissionIds = userRoleIds.length > 0
    ? (await db
        .select({ permissionId: rolePermissionsTable.permissionId })
        .from(rolePermissionsTable)
        .where(inArray(rolePermissionsTable.roleId, userRoleIds))
      ).map((r) => r.permissionId)
    : [];

  const appsWithPermissions = await db
    .select({ appId: appPermissionsTable.appId })
    .from(appPermissionsTable);
  const restrictedAppIds = new Set(appsWithPermissions.map((r) => r.appId));

  if (restrictedAppIds.size === 0) return apps;

  const allowedRestrictedAppIds = userPermissionIds.length > 0
    ? (await db
        .select({ appId: appPermissionsTable.appId })
        .from(appPermissionsTable)
        .where(inArray(appPermissionsTable.permissionId, userPermissionIds))
      ).map((r) => r.appId)
    : [];

  const allowedAppIdSet = new Set(allowedRestrictedAppIds);
  return apps.filter(
    (app) => !restrictedAppIds.has(app.id) || allowedAppIdSet.has(app.id),
  );
}

router.get("/apps", requireAuth, async (req, res): Promise<void> => {
  const userId = req.session.userId!;
  const visible = await getVisibleAppsForUser(userId);
  res.json(visible);
});

router.put("/me/app-order", requireAuth, async (req, res): Promise<void> => {
  const userId = req.session.userId!;
  const parsed = UpdateMyAppOrderBody.safeParse(req.body);
  if (!parsed.success) {
    res.status(400).json({ error: parsed.error.message });
    return;
  }

  const { order } = parsed.data;

  const seen = new Set<number>();
  for (const id of order) {
    if (seen.has(id)) {
      res.status(400).json({ error: `Duplicate app id in order: ${id}` });
      return;
    }
    seen.add(id);
  }

  const visible = await getVisibleAppsForUser(userId);
  const visibleIds = new Set(visible.map((a) => a.id));
  const invalid = order.filter((id) => !visibleIds.has(id));
  if (invalid.length > 0) {
    res.status(400).json({
      error: `Unknown or unauthorized app id(s): ${invalid.join(", ")}`,
    });
    return;
  }

  await db.transaction(async (tx) => {
    await tx.delete(userAppOrdersTable).where(eq(userAppOrdersTable.userId, userId));
    if (order.length > 0) {
      await tx.insert(userAppOrdersTable).values(
        order.map((appId, idx) => ({ userId, appId, sortOrder: idx })),
      );
    }
  });

  const updated = await getVisibleAppsForUser(userId);
  res.json(updated);
});

router.post("/apps", requireAdmin, async (req, res): Promise<void> => {
  const parsed = CreateAppBody.safeParse(req.body);
  if (!parsed.success) {
    res.status(400).json({ error: parsed.error.message });
    return;
  }

  const [app] = await db.insert(appsTable).values(parsed.data).returning();
  res.status(201).json(app);
});

router.get("/apps/:id", requireAuth, async (req, res): Promise<void> => {
  const params = GetAppParams.safeParse(req.params);
  if (!params.success) {
    res.status(400).json({ error: params.error.message });
    return;
  }

  const [app] = await db
    .select()
    .from(appsTable)
    .where(eq(appsTable.id, params.data.id));

  if (!app) {
    res.status(404).json({ error: "App not found" });
    return;
  }

  res.json(app);
});

router.patch("/apps/:id", requireAdmin, async (req, res): Promise<void> => {
  const params = UpdateAppParams.safeParse(req.params);
  if (!params.success) {
    res.status(400).json({ error: params.error.message });
    return;
  }

  const parsed = UpdateAppBody.safeParse(req.body);
  if (!parsed.success) {
    res.status(400).json({ error: parsed.error.message });
    return;
  }

  const [app] = await db
    .update(appsTable)
    .set(parsed.data)
    .where(eq(appsTable.id, params.data.id))
    .returning();

  if (!app) {
    res.status(404).json({ error: "App not found" });
    return;
  }

  res.json(app);
});

router.delete("/apps/:id", requireAdmin, async (req, res): Promise<void> => {
  const params = DeleteAppParams.safeParse(req.params);
  if (!params.success) {
    res.status(400).json({ error: params.error.message });
    return;
  }

  const [app] = await db
    .delete(appsTable)
    .where(eq(appsTable.id, params.data.id))
    .returning();

  if (!app) {
    res.status(404).json({ error: "App not found" });
    return;
  }

  res.sendStatus(204);
});

export default router;
