/**
 * GET / PUT per-user permission overrides for the current port.
 *
 * GET returns the effective baseline (role + port-role-overrides + residential
 * toggle) AND the current user-specific override map so the UI can render
 * three states per leaf: inherit, force-grant, force-deny.
 *
 * PUT accepts a Partial<RolePermissions> map (use null at a leaf to clear an
 * override) and upserts it onto user_permission_overrides for (userId, portId).
 * Permission `admin.manage_users` is required - same gate as the user-edit
 * drawer that hosts the matrix.
 */
import { and, eq } from 'drizzle-orm';
import { NextResponse } from 'next/server';

import { withAuth, withPermission } from '@/lib/api/helpers';
import { ALLOWED_RESOURCE_ACTIONS } from '@/lib/auth/permissions';
import { parseBody } from '@/lib/api/route-helpers';
import { db } from '@/lib/db';
import {
  portRoleOverrides,
  roles,
  userPermissionOverrides,
  userPortRoles,
  userProfiles,
  type RolePermissions,
} from '@/lib/db/schema';
import { createAuditLog } from '@/lib/audit';
import { errorResponse, ForbiddenError, NotFoundError, ValidationError } from '@/lib/errors';
import { z } from 'zod';

// The per-user override allow-list is the canonical PERMISSION_CATALOG
// (src/lib/auth/permissions.ts), imported as `ALLOWED_RESOURCE_ACTIONS` above.
// Sharing it with the role validator means the two can't diverge (L23): a
// client can't write arbitrary keys that the resolver would merge into the
// effective permission map, and the catalog stays in lockstep with
// `RolePermissions`.

const updateOverridesSchema = z.object({
  /** Partial<RolePermissions> - passthrough JSON. Validated structurally
   *  by limiting depth + leaf type below. */
  overrides: z.record(z.string(), z.record(z.string(), z.boolean())).default({}),
});

export const GET = withAuth(
  withPermission('admin', 'manage_users', async (_req, ctx, params) => {
    try {
      const targetUserId = params.id!;
      const portId = ctx.portId;

      const profile = await db.query.userProfiles.findFirst({
        where: eq(userProfiles.userId, targetUserId),
      });
      if (!profile) throw new NotFoundError('User');

      // Resolve the role's baseline + port-role override (super-admin
      // edge-case: no role row, so the matrix is empty / not editable).
      let baseline: RolePermissions | null = null;
      if (!profile.isSuperAdmin) {
        const portRole = await db.query.userPortRoles.findFirst({
          where: and(eq(userPortRoles.userId, targetUserId), eq(userPortRoles.portId, portId)),
        });
        if (portRole) {
          const role = await db.query.roles.findFirst({
            where: eq(roles.id, portRole.roleId),
          });
          baseline = (role?.permissions as RolePermissions | null) ?? null;

          const portOverride = await db.query.portRoleOverrides.findFirst({
            where: and(
              eq(portRoleOverrides.portId, portId),
              eq(portRoleOverrides.roleId, portRole.roleId),
            ),
          });
          if (baseline && portOverride?.permissionOverrides) {
            // Cheap structural merge - same shape as helpers.ts's deepMerge.
            baseline = mergePerms(baseline, portOverride.permissionOverrides);
          }
        }
      }

      const userOverride = await db.query.userPermissionOverrides.findFirst({
        where: and(
          eq(userPermissionOverrides.userId, targetUserId),
          eq(userPermissionOverrides.portId, portId),
        ),
      });

      return NextResponse.json({
        data: {
          baseline,
          overrides: userOverride?.permissionOverrides ?? {},
          isSuperAdmin: profile.isSuperAdmin,
        },
      });
    } catch (error) {
      return errorResponse(error);
    }
  }),
);

export const PUT = withAuth(
  withPermission('admin', 'manage_users', async (req, ctx, params) => {
    try {
      const targetUserId = params.id!;
      const portId = ctx.portId;

      // CRITICAL: refuse self-target. Without this an admin with
      // admin.manage_users can grant themselves every permission leaf
      // (incl. permanently_delete_clients, system_backup, etc.) and the
      // override layer in withAuth resolves it on the very next request.
      if (targetUserId === ctx.userId) {
        throw new ForbiddenError('You cannot edit your own permission overrides.');
      }

      // Reject overrides for users that aren't actually assigned to this
      // port - prevents cross-tenant pollution where an admin in port A
      // writes a row keyed on (userIdFromPortB, portA). The withAuth
      // resolver scopes lookups to the caller's port so the row would
      // never apply, but it still consumes a unique slot and confuses
      // future audits.
      const targetPortRole = await db.query.userPortRoles.findFirst({
        where: and(eq(userPortRoles.userId, targetUserId), eq(userPortRoles.portId, portId)),
      });
      if (!targetPortRole) {
        throw new NotFoundError('User not assigned to this port');
      }

      const { overrides } = await parseBody(req, updateOverridesSchema);

      // Strip anything outside the canonical RolePermissions allow-list.
      // The Zod schema only enforces shape (string → string → boolean);
      // here we drop unknown resources/actions so a malicious client
      // can't seed garbage keys that a future resolver might accidentally
      // honour.
      // CALLER-SUPERSET (authz-auditor CRITICAL): an admin with only
      // `admin.manage_users` previously could grant another user any
      // permission leaf - including ones they don't hold themselves
      // (e.g. `permanently_delete_clients`, `system_backup`). Require
      // every `true` write to be a leaf the caller already has.
      // Super-admins bypass (they hold all leaves by definition).
      const callerPerms = ctx.permissions as Record<string, Record<string, boolean>> | null;
      const sanitized: Record<string, Record<string, boolean>> = {};
      for (const [resource, actions] of Object.entries(overrides)) {
        const allowed = ALLOWED_RESOURCE_ACTIONS[resource];
        if (!allowed) continue;
        const cleaned: Record<string, boolean> = {};
        for (const [action, value] of Object.entries(actions)) {
          if (!allowed.has(action)) continue;
          if (typeof value !== 'boolean') {
            throw new ValidationError(
              `permission override for ${resource}.${action} must be true or false`,
            );
          }
          if (value === true && !ctx.isSuperAdmin) {
            const callerHas = callerPerms?.[resource]?.[action] === true;
            if (!callerHas) {
              throw new ForbiddenError(
                `You don't hold ${resource}.${action} yourself, so you can't grant it.`,
              );
            }
          }
          cleaned[action] = value;
        }
        if (Object.keys(cleaned).length > 0) sanitized[resource] = cleaned;
      }

      const existing = await db.query.userPermissionOverrides.findFirst({
        where: and(
          eq(userPermissionOverrides.userId, targetUserId),
          eq(userPermissionOverrides.portId, portId),
        ),
      });

      if (existing) {
        await db
          .update(userPermissionOverrides)
          .set({ permissionOverrides: sanitized, updatedAt: new Date() })
          .where(eq(userPermissionOverrides.id, existing.id));
      } else {
        await db.insert(userPermissionOverrides).values({
          userId: targetUserId,
          portId,
          permissionOverrides: sanitized,
        });
      }

      void createAuditLog({
        userId: ctx.userId,
        portId,
        action: 'update',
        entityType: 'user',
        entityId: targetUserId,
        oldValue: { permissionOverrides: existing?.permissionOverrides ?? {} },
        newValue: { permissionOverrides: sanitized },
        ipAddress: ctx.ipAddress,
        userAgent: ctx.userAgent,
      });

      return NextResponse.json({ data: { overrides: sanitized } });
    } catch (error) {
      return errorResponse(error);
    }
  }),
);

/** Local shallow-merge by resource (matches the RolePermissions shape:
 *  one-level-deep map of resource → action map). Same semantics the
 *  withAuth resolver uses; copied here to avoid pulling that file into
 *  a route module. */
function mergePerms(
  base: RolePermissions,
  patch: Partial<RolePermissions> | Record<string, Record<string, boolean>>,
): RolePermissions {
  const out = { ...(base as unknown as Record<string, Record<string, boolean>>) };
  for (const [resource, actions] of Object.entries(patch)) {
    if (!actions) continue;
    out[resource] = { ...(out[resource] ?? {}), ...(actions as Record<string, boolean>) };
  }
  return out as unknown as RolePermissions;
}
