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

import { withAuth, withPermission } from '@/lib/api/helpers';
import { parseBody } from '@/lib/api/route-helpers';
import { db } from '@/lib/db';
import { documents, documentSigners } from '@/lib/db/schema/documents';
import { ports } from '@/lib/db/schema/ports';
import {
  sendSigningInvitation,
  type SignerRole,
} from '@/lib/services/document-signing-emails.service';
import { getPortDocumensoConfig } from '@/lib/services/port-config';
import { errorResponse, NotFoundError, ValidationError } from '@/lib/errors';

const bodySchema = z.object({
  /** Optional - defaults to the next pending signer in signing-order. */
  recipientId: z.string().optional(),
});

const DOC_TYPE_LABEL: Record<
  string,
  'Expression of Interest' | 'Sales Contract' | 'Reservation Agreement'
> = {
  eoi: 'Expression of Interest',
  contract: 'Sales Contract',
  reservation_agreement: 'Reservation Agreement',
};

/**
 * Send a branded signing-invitation email to a specific recipient
 * (defaults to the next-pending signer in signing-order). Used by:
 *   - The auto-send path when port `eoi_send_mode = 'auto'`
 *   - The "Send invitation" button in the EOI/Contract tab when
 *     `eoi_send_mode = 'manual'`
 *   - The cascading-email logic on DOCUMENT_SIGNED webhooks
 */
export const POST = withAuth(
  withPermission('documents', 'send_for_signing', async (req, ctx, params) => {
    try {
      const documentId = params.id;
      if (!documentId) throw new NotFoundError('Document');
      const body = await parseBody(req, bodySchema);

      const doc = await db.query.documents.findFirst({
        where: and(eq(documents.id, documentId), eq(documents.portId, ctx.portId)),
      });
      if (!doc) throw new NotFoundError('Document');

      const signers = await db
        .select()
        .from(documentSigners)
        .where(eq(documentSigners.documentId, documentId))
        .orderBy(asc(documentSigners.signingOrder));

      let target = body.recipientId
        ? signers.find((s) => s.id === body.recipientId)
        : signers.find((s) => s.status === 'pending');
      if (!target) {
        throw new ValidationError('No pending signer found to invite');
      }

      // Self-heal flow when target.signingUrl is null. Two scenarios:
      //   1. Envelope was created before the auto-distribute fix shipped
      //      - never distributed, so we must call /envelope/distribute
      //      to mint URLs.
      //   2. Envelope WAS auto-distributed at generate time, but the
      //      response we got didn't carry signingUrls into our DB row
      //      (transient Documenso bug, or response shape mismatch).
      //      In that case the envelope is already PENDING and a second
      //      /distribute call returns 4xx ("already distributed").
      //
      // Defensive flow: try `getEnvelope` FIRST (cheap, always works).
      // If recipients carry signingUrls, persist + skip distribute.
      // If not, fall through to distribute, but catch 4xx so we don't
      // surface a confusing "Documenso upstream error" to the rep -
      // instead we re-fetch via GET one more time and accept whatever
      // URLs the envelope has.
      if (!target.signingUrl && doc.documensoId) {
        const { distributeEnvelopeV2, getDocument } =
          await import('@/lib/services/documenso-client');

        const persistUrlsForDocument = async (
          recipients: Array<{
            signingOrder: number;
            signingUrl?: string;
            embeddedUrl?: string;
            token?: string;
          }>,
        ) => {
          for (const r of recipients) {
            if (!r.signingUrl) continue;
            await db
              .update(documentSigners)
              .set({
                signingUrl: r.signingUrl,
                embeddedUrl: r.embeddedUrl ?? null,
                signingToken: r.token ?? null,
              })
              .where(
                and(
                  eq(documentSigners.documentId, documentId),
                  eq(documentSigners.signingOrder, r.signingOrder),
                ),
              );
          }
        };

        // Step 1: cheap GET.
        let recovered = false;
        try {
          const fetched = await getDocument(doc.documensoId, ctx.portId);
          if (fetched.recipients.some((r) => r.signingUrl)) {
            await persistUrlsForDocument(fetched.recipients);
            recovered = true;
          }
        } catch {
          // ignore - fall through to distribute attempt
        }

        // Step 2: distribute, only if GET didn't recover URLs.
        if (!recovered) {
          try {
            const distributed = await distributeEnvelopeV2(doc.documensoId, ctx.portId);
            await persistUrlsForDocument(distributed.recipients);
          } catch {
            // Probably "already distributed" - last-ditch GET.
            try {
              const fetched = await getDocument(doc.documensoId, ctx.portId);
              await persistUrlsForDocument(fetched.recipients);
            } catch {
              // give up; the validator below surfaces a clean error
            }
          }
        }

        // Re-read target so its signingUrl is now populated.
        const refreshed = await db
          .select()
          .from(documentSigners)
          .where(eq(documentSigners.id, target.id))
          .limit(1);
        target = refreshed[0] ?? target;
      }

      if (!target.signingUrl) {
        throw new ValidationError(
          'Signer has no Documenso URL yet - try regenerating the EOI; v2 envelopes require distribution before the signing link exists.',
        );
      }

      const port = await db.query.ports.findFirst({ where: eq(ports.id, ctx.portId) });
      const docCfg = await getPortDocumensoConfig(ctx.portId);

      await sendSigningInvitation({
        portId: ctx.portId,
        portName: port?.name ?? 'the marina',
        recipient: { name: target.signerName, email: target.signerEmail },
        documensoSigningUrl: target.signingUrl,
        documentLabel: DOC_TYPE_LABEL[doc.documentType] ?? 'Expression of Interest',
        signerRole: (target.signerRole as SignerRole) ?? 'client',
        senderName: docCfg.developerName ?? null,
        // Phase 6 - surface the per-doc rep-authored note when set so
        // every cascaded invite and any manual resend show the same
        // copy. Falls back to the template default when null/empty.
        customMessage: doc.invitationMessage,
      });

      await db
        .update(documentSigners)
        .set({ invitedAt: new Date() })
        .where(eq(documentSigners.id, target.id));

      return NextResponse.json({ data: { recipientId: target.id, sent: true } });
    } catch (error) {
      return errorResponse(error);
    }
  }),
);
