/**
 * Automate Signing orchestration. Wraps the per-signer invitation
 * fire so the rep can flip a document from manual → auto with one
 * click. Once enabled, the existing
 * `sendCascadingInviteForNextSigner` path in documents.service.ts
 * handles the rest of the cascade (it fires regardless of
 * automation_mode), and `handleDocumentCompleted` handles the
 * completion broadcast (signed PDF to every recipient).
 *
 * Modes:
 *   - manual           Default. Rep fires each signer's invitation
 *                      individually. Existing webhook cascade still
 *                      auto-fires subsequent signers when each
 *                      completes.
 *   - sequential_auto  First signer invited on enable; webhook cascade
 *                      handles the rest. Used when the envelope is
 *                      SEQUENTIAL.
 *   - concurrent_auto  All signers invited in parallel on enable.
 *                      Used when the envelope is PARALLEL.
 *
 * Decisions locked 2026-05-26:
 *   - Mid-flow enable picks up from next-in-order signer (find
 *     invitedAt=null, fire from there).
 *   - Completion broadcast goes to ALL recipients (signers + approvers
 *     + CCs). Already wired in handleDocumentCompleted.
 *   - Single combined mode (no partial-automate option).
 *   - Manual override buttons stay visible during automation with
 *     "Auto-firing soon" tooltip.
 */

import { and, asc, eq } from 'drizzle-orm';

import { db } from '@/lib/db';
import { documents, documentSigners } from '@/lib/db/schema/documents';
import { ports } from '@/lib/db/schema/ports';
import { ConflictError, NotFoundError, ValidationError } from '@/lib/errors';
import { createAuditLog, type AuditMeta } from '@/lib/audit';
import { getPortDocumensoConfig } from '@/lib/services/port-config';
import {
  getDocument as getDocumensoDoc,
  distributeEnvelopeV2,
} from '@/lib/services/documenso-client';
import {
  sendSigningInvitation,
  type SignerRole,
} from '@/lib/services/document-signing-emails.service';
import { DOC_TYPE_LABEL } from '@/lib/services/documenso-signers';
import { logger } from '@/lib/logger';

export type AutomationMode = 'manual' | 'sequential_auto' | 'concurrent_auto';

interface SignerRow {
  id: string;
  signerName: string;
  signerEmail: string;
  signerRole: string;
  signingOrder: number;
  signingUrl: string | null;
  invitedAt: Date | null;
  status: string;
}

/**
 * Fire a Documenso signing invitation to one signer + stamp invitedAt.
 * Mirrors what the manual POST /api/v1/documents/:id/send-invitation
 * route does so the email a rep receives is identical regardless of
 * whether the rep clicked Send or the orchestrator auto-fired.
 */
async function dispatchInvitationToSigner(args: {
  portId: string;
  portName: string;
  signer: SignerRow;
  documentLabel: string;
  customMessage: string | null;
  senderName: string | null;
}): Promise<void> {
  if (!args.signer.signingUrl) {
    throw new ValidationError(
      `Signer ${args.signer.signerEmail} has no signing URL — cannot dispatch invitation`,
    );
  }
  await sendSigningInvitation({
    portId: args.portId,
    portName: args.portName,
    recipient: { name: args.signer.signerName, email: args.signer.signerEmail },
    documensoSigningUrl: args.signer.signingUrl,
    documentLabel: args.documentLabel as never,
    signerRole: (args.signer.signerRole as SignerRole) ?? 'client',
    senderName: args.senderName,
    customMessage: args.customMessage,
  });
  await db
    .update(documentSigners)
    .set({ invitedAt: new Date() })
    .where(eq(documentSigners.id, args.signer.id));
}

/**
 * Recover signing URLs for the document's signers when any are
 * missing. v2 envelopes don't populate signingUrls until distribution
 * lands; fetch the envelope, persist URLs onto local signer rows, and
 * return the refreshed signer list. Idempotent.
 */
async function ensureSigningUrls(
  documentId: string,
  documensoId: string,
  portId: string,
): Promise<SignerRow[]> {
  const rows = await db
    .select()
    .from(documentSigners)
    .where(eq(documentSigners.documentId, documentId))
    .orderBy(asc(documentSigners.signingOrder));

  if (rows.every((r) => r.signingUrl)) return rows as SignerRow[];

  let fetched: Awaited<ReturnType<typeof getDocumensoDoc>> | null = null;
  try {
    fetched = await getDocumensoDoc(documensoId, portId);
  } catch {
    /* fall through to distribute */
  }
  const haveUrlsFromGet = fetched?.recipients.some((r) => r.signingUrl);
  if (!haveUrlsFromGet) {
    try {
      fetched = await distributeEnvelopeV2(documensoId, portId);
    } catch {
      try {
        fetched = await getDocumensoDoc(documensoId, portId);
      } catch {
        /* give up */
      }
    }
  }
  if (!fetched) return rows as SignerRow[];

  for (const r of fetched.recipients) {
    if (!r.email || !r.signingUrl) continue;
    await db
      .update(documentSigners)
      .set({ signingUrl: r.signingUrl })
      .where(
        and(eq(documentSigners.documentId, documentId), eq(documentSigners.signerEmail, r.email)),
      );
  }
  const refreshed = await db
    .select()
    .from(documentSigners)
    .where(eq(documentSigners.documentId, documentId))
    .orderBy(asc(documentSigners.signingOrder));
  return refreshed as SignerRow[];
}

/**
 * Enable Automate Signing on a document. Resolves the mode from the
 * envelope's signing order and fires the appropriate invitation set:
 * first uninvited signer for sequential, all uninvited signers for
 * concurrent.
 */
export async function enableSigningAutomation(
  documentId: string,
  portId: string,
  meta: AuditMeta,
): Promise<{ mode: AutomationMode; invitedCount: number }> {
  const doc = await db.query.documents.findFirst({
    where: and(eq(documents.id, documentId), eq(documents.portId, portId)),
  });
  if (!doc) throw new NotFoundError('document');
  if (!doc.documensoId) {
    throw new ConflictError(
      'Document has no Documenso envelope — cannot automate. Generate / upload + send first.',
    );
  }
  if (!['draft', 'sent', 'partially_signed'].includes(doc.status)) {
    throw new ConflictError(
      `Document is ${doc.status} — automation only applies to in-flight envelopes.`,
    );
  }
  if (doc.automationMode === 'sequential_auto' || doc.automationMode === 'concurrent_auto') {
    throw new ConflictError(`Automation already enabled (mode=${doc.automationMode}).`);
  }

  const port = await db.query.ports.findFirst({ where: eq(ports.id, portId) });
  if (!port) throw new NotFoundError('port');
  const docCfg = await getPortDocumensoConfig(portId);

  // Determine mode from the envelope's actual signing order. Falls
  // back to the per-port `documenso_signing_order` setting when the
  // remote response doesn't echo signingOrder; falls back to PARALLEL
  // when neither is set (Documenso's own default).
  const remote = await getDocumensoDoc(doc.documensoId, portId);
  const remoteOrder = (
    remote as { signingOrder?: string } & typeof remote
  ).signingOrder?.toString();
  const isSequential = remoteOrder === 'SEQUENTIAL' || docCfg.signingOrder === 'SEQUENTIAL';
  const mode: AutomationMode = isSequential ? 'sequential_auto' : 'concurrent_auto';

  const signers = await ensureSigningUrls(documentId, doc.documensoId, portId);
  if (signers.length < 2) {
    throw new ConflictError('Automation requires at least two signers on the envelope.');
  }

  // Persist the mode FIRST so the audit trail captures the rep's
  // intent even if the invitation dispatch fails part-way through.
  await db
    .update(documents)
    .set({ automationMode: mode, updatedAt: new Date() })
    .where(eq(documents.id, documentId));

  const docLabel = DOC_TYPE_LABEL[doc.documentType] ?? 'Expression of Interest';
  let invitedCount = 0;

  if (mode === 'sequential_auto') {
    // Find the next-in-order signer with invitedAt=NULL and fire just
    // that one. The webhook cascade in documents.service.ts handles
    // the rest as each completes (mode-independent).
    const next = signers.find(
      (s) => s.invitedAt === null && s.status === 'pending' && s.signingUrl,
    );
    if (next) {
      await dispatchInvitationToSigner({
        portId,
        portName: port.name,
        signer: next,
        documentLabel: docLabel,
        customMessage: doc.invitationMessage,
        senderName: docCfg.developerName ?? null,
      });
      invitedCount = 1;
    }
  } else {
    // concurrent_auto: fire all uninvited signers in parallel.
    const candidates = signers.filter(
      (s) => s.invitedAt === null && s.status === 'pending' && s.signingUrl,
    );
    const results = await Promise.allSettled(
      candidates.map((s) =>
        dispatchInvitationToSigner({
          portId,
          portName: port.name,
          signer: s,
          documentLabel: docLabel,
          customMessage: doc.invitationMessage,
          senderName: docCfg.developerName ?? null,
        }),
      ),
    );
    invitedCount = results.filter((r) => r.status === 'fulfilled').length;
    const failures = results.filter((r) => r.status === 'rejected');
    if (failures.length > 0) {
      logger.warn(
        { documentId, failureCount: failures.length, total: candidates.length },
        'Some invitations failed during concurrent_auto enable — partial state',
      );
    }
  }

  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'update',
    entityType: 'document',
    entityId: documentId,
    newValue: { automationMode: mode, invitedCount },
    metadata: { type: 'signing_automation_enabled' },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });

  return { mode, invitedCount };
}

/**
 * Disable Automate Signing — reverts to manual. Doesn't undo any
 * invitations already sent.
 */
export async function disableSigningAutomation(
  documentId: string,
  portId: string,
  meta: AuditMeta,
): Promise<void> {
  const doc = await db.query.documents.findFirst({
    where: and(eq(documents.id, documentId), eq(documents.portId, portId)),
    columns: { id: true, automationMode: true },
  });
  if (!doc) throw new NotFoundError('document');
  if (doc.automationMode === 'manual') return;
  await db
    .update(documents)
    .set({ automationMode: 'manual', updatedAt: new Date() })
    .where(eq(documents.id, documentId));
  void createAuditLog({
    userId: meta.userId,
    portId,
    action: 'update',
    entityType: 'document',
    entityId: documentId,
    oldValue: { automationMode: doc.automationMode },
    newValue: { automationMode: 'manual' },
    metadata: { type: 'signing_automation_disabled' },
    ipAddress: meta.ipAddress,
    userAgent: meta.userAgent,
  });
}
