import { NextResponse } from 'next/server';
import { z } from 'zod';
import { eq, and } from 'drizzle-orm';

import { withAuth, withRateLimit } from '@/lib/api/helpers';
import { CLIENT_ARCHIVE_ACTION } from '@/lib/auth/permissions';
import { parseBody } from '@/lib/api/route-helpers';
import { runBulk } from '@/lib/api/bulk-helpers';
import { db } from '@/lib/db';
import { clients, clientTags } from '@/lib/db/schema/clients';
import { setClientTags } from '@/lib/services/clients.service';
import {
  getClientArchiveDossier,
  type ClientArchiveDossier,
} from '@/lib/services/client-archive-dossier.service';
import {
  archiveClientWithDecisions,
  type ArchiveResult,
} from '@/lib/services/client-archive.service';
import { notifyNextInLine } from '@/lib/services/next-in-line-notify.service';
import { getQueue } from '@/lib/queue';
import { logger } from '@/lib/logger';
import { errorResponse } from '@/lib/errors';

const bulkSchema = z.discriminatedUnion('action', [
  z.object({
    action: z.literal('archive'),
    ids: z.array(z.string().min(1)).min(1).max(100),
    /** When provided, lifts the high-stakes block on listed clients
     *  individually. The bulk-archive wizard collects these from the
     *  operator one client at a time. Reasons must be ≥5 characters. */
    reasonsByClientId: z.record(z.string(), z.string().min(5).max(2000)).optional(),
  }),
  z.object({
    action: z.literal('add_tag'),
    ids: z.array(z.string().min(1)).min(1).max(100),
    tagId: z.string().min(1),
  }),
  z.object({
    action: z.literal('remove_tag'),
    ids: z.array(z.string().min(1)).min(1).max(100),
    tagId: z.string().min(1),
  }),
]);

const PERMISSION_BY_ACTION = {
  // Archiving is reversible -> edit-level (see CLIENT_ARCHIVE_ACTION). Not the
  // destructive `delete`, which only super_admin holds.
  archive: CLIENT_ARCHIVE_ACTION,
  add_tag: 'edit' as const,
  remove_tag: 'edit' as const,
};

export const POST = withAuth(
  withRateLimit('bulk', async (req, ctx) => {
    let body: z.infer<typeof bulkSchema>;
    try {
      body = await parseBody(req, bulkSchema);
    } catch (error) {
      return errorResponse(error);
    }

    const allowed = ctx.isSuperAdmin
      ? true
      : !!ctx.permissions?.clients?.[PERMISSION_BY_ACTION[body.action]];
    if (!allowed) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });

    const meta = {
      userId: ctx.userId,
      portId: ctx.portId,
      ipAddress: ctx.ipAddress,
      userAgent: ctx.userAgent,
    };

    const reasonsByClientId = body.action === 'archive' ? (body.reasonsByClientId ?? {}) : {};

    // Collect per-archive side-effects so we can fan out Documenso voids
    // + next-in-line notifications AFTER the bulk loop completes (mirrors
    // the single-client route's post-commit behaviour). Without this the
    // bulk path silently dropped both side-effect streams (audit R2-C1).
    const archiveSideEffects: Array<{
      dossier: ClientArchiveDossier;
      result: ArchiveResult;
    }> = [];

    const { results, summary } = await runBulk(body.ids, async (id) => {
      if (body.action === 'archive') {
        // Bulk archive uses the smart-archive backend with sensible
        // low-stakes defaults: release available/under-offer berths,
        // retain sold ones, cancel active reservations, leave invoices,
        // leave Documenso envelopes pending. High-stakes clients require
        // a per-client reason supplied via reasonsByClientId; the bulk-
        // archive wizard captures these one at a time before submitting.
        const dossier = await getClientArchiveDossier(id, ctx.portId);
        // Idempotent: if a previous request already archived this client
        // (e.g. a network retry / double-click), treat it as success
        // rather than letting `archiveClientWithDecisions` throw a
        // ConflictError that runBulk will surface as a per-row failure.
        if (dossier.client.archivedAt) {
          return;
        }
        const perClientReason = reasonsByClientId[id];
        if (dossier.stakeLevel === 'high' && !perClientReason) {
          throw new Error(
            `Client at ${dossier.highStakesStage} requires a per-client reason; supply one in reasonsByClientId.`,
          );
        }
        if (dossier.blockers.length > 0) {
          throw new Error(`Cannot archive: ${dossier.blockers[0]}`);
        }
        const hasSignedDocs = dossier.documents.some(
          (d) => d.status === 'completed' || d.status === 'signed',
        );
        const reason = perClientReason ?? 'Bulk archive (low-stakes auto-mode)';
        // Pick the berth's first linked interest from the dossier
        // (authoritative interest_berths join). Berths with no linked
        // interest for this client are dropped - emitting an empty
        // interestId causes the delete to silently match zero rows
        // (audit R2-H3).
        const berthDecisions = dossier.berths
          .map((b) => {
            const interestId = b.linkedInterestIds[0];
            if (!interestId) return null;
            return {
              berthId: b.berthId,
              interestId,
              action: b.status === 'sold' ? ('retain' as const) : ('release' as const),
            };
          })
          .filter(
            (x): x is { berthId: string; interestId: string; action: 'retain' | 'release' } =>
              x !== null,
          );

        const result = await archiveClientWithDecisions({
          dossier,
          decisions: {
            reason,
            acknowledgedSignedDocuments: hasSignedDocs,
            berthDecisions,
            yachtDecisions: dossier.yachts.map((y) => ({ yachtId: y.yachtId, action: 'retain' })),
            tenancyDecisions: dossier.tenancies.map((r) => ({
              tenancyId: r.tenancyId,
              action: 'cancel',
            })),
            invoiceDecisions: dossier.invoices.map((i) => ({
              invoiceId: i.invoiceId,
              action: 'leave',
            })),
            documentDecisions: dossier.documents.map((d) => ({
              documentId: d.documentId,
              action: 'leave',
            })),
          },
          meta,
        });
        archiveSideEffects.push({ dossier, result });
        return;
      }
      const client = await db.query.clients.findFirst({
        where: and(eq(clients.id, id), eq(clients.portId, ctx.portId)),
      });
      if (!client) throw new Error('Client not found');
      const existing = await db
        .select({ tagId: clientTags.tagId })
        .from(clientTags)
        .where(eq(clientTags.clientId, id));
      const current = new Set(existing.map((t) => t.tagId));
      if (body.action === 'add_tag') current.add(body.tagId);
      else current.delete(body.tagId);
      await setClientTags(id, ctx.portId, Array.from(current), meta);
    });

    // Post-commit side-effects, identical pattern to the single-client
    // route at /api/v1/clients/[id]/archive. Documenso voids → BullMQ
    // documents queue; next-in-line notifications fire-and-forget per
    // released berth.
    if (archiveSideEffects.length > 0) {
      const queue = getQueue('documents');
      for (const { dossier, result } of archiveSideEffects) {
        for (const c of result.externalCleanups) {
          if (c.kind === 'documenso_void') {
            await queue
              .add('documenso-void', {
                documentId: c.documentId,
                documensoId: c.documensoId,
                portId: ctx.portId,
              })
              .catch((err) =>
                logger.error(
                  { err, documentId: c.documentId, clientId: result.clientId },
                  'Bulk archive: failed to enqueue Documenso void',
                ),
              );
          }
        }

        for (const released of result.releasedBerths) {
          if (released.nextInLineInterestIds.length === 0) continue;
          const otherInterests =
            dossier.berths
              .find((b) => b.berthId === released.berthId)
              ?.otherInterests.map((o) => ({
                interestId: o.interestId,
                clientName: o.clientName,
                pipelineStage: o.pipelineStage,
              })) ?? [];
          void notifyNextInLine({
            portId: ctx.portId,
            berthId: released.berthId,
            mooringNumber: released.mooringNumber,
            archivedClientName: dossier.client.fullName,
            nextInLineInterests: otherInterests,
          }).catch((err) =>
            logger.error(
              { err, berthId: released.berthId, clientId: result.clientId },
              'Bulk archive: failed to fire next-in-line notification',
            ),
          );
        }
      }
    }

    return NextResponse.json({ data: { results, summary } });
  }),
);
