import {
  downloadEnvelopeItemPdf,
  getTemplate,
  type DocumensoTemplate,
} from '@/lib/services/documenso-client';
import { getPortDocumensoConfig } from '@/lib/services/port-config';
import { writeSetting } from '@/lib/settings/resolver';
import { getSetting as getSettingLegacy } from '@/lib/services/settings.service';
import { inspectPdfAcroForm, type AcroFormField } from '@/lib/pdf/inspect-acroform';
import { logger } from '@/lib/logger';
import type { AuditMeta } from '@/lib/audit';

export interface TemplateFieldMap {
  [label: string]: number;
}

/**
 * Field labels the CRM expects to fill on every EOI. Kept in sync with the
 * keys of `formValues` inside `buildDocumensoPayload`. When a template's
 * field labels diverge from this set, the unmatched entries are surfaced in
 * the sync result so the admin can rename in the Documenso template editor.
 */
const CRM_EXPECTED_EOI_FIELD_LABELS = [
  'Name',
  'Email',
  'Address',
  'Yacht Name',
  'Length',
  'Width',
  'Draft',
  'Berth Number',
  'Lease_10',
  'Purchase',
] as const;

export interface TemplateSyncResult {
  /** ISO timestamp of when this sync ran. Surfaced as "Last synced X ago"
   *  so the admin can tell when the cached report is from. */
  syncedAt: string;
  templateId: number;
  title: string;
  /** Pre-filled into the matching documenso_*_recipient_id settings. */
  recipients: Array<{
    role: string;
    signingOrder: number;
    id: number;
    name?: string;
    email?: string;
    /** Which CRM setting key this row was written to (null = no match). */
    mappedSettingKey: string | null;
  }>;
  /** Stored at `documenso_eoi_field_map` for v2 prefillFields-by-ID. */
  fieldMap: TemplateFieldMap;
  fieldCount: number;
  /**
   * Template fields the CRM knows how to fill at send time. The admin doesn't
   * need to do anything for these - they'll flow through prefillFields on
   * the next EOI send.
   */
  matchedFields: Array<{ label: string; fieldId: number }>;
  /**
   * Template fields discovered on Documenso side but with labels the CRM
   * doesn't recognize. These will NOT be filled. The admin should rename
   * them in the Documenso template editor to match a CRM expected label.
   */
  unmatchedTemplateFields: Array<{ label: string; fieldId: number }>;
  /**
   * CRM-expected fields that don't exist on the template. The CRM will skip
   * them at send time. May be benign (e.g. you removed a field from the
   * template intentionally) or a typo (rename a template field to match).
   */
  missingFromTemplate: string[];
  /**
   * The template's stored meta - what every envelope generated from this
   * template inherits at creation time. signingOrder is bound to the
   * template (v2's /template/use does NOT accept an override), so this
   * is the authoritative value the admin sees.
   */
  templateMeta: {
    signingOrder: 'PARALLEL' | 'SEQUENTIAL' | null;
    distributionMethod: 'EMAIL' | 'NONE' | null;
    redirectUrl: string | null;
  };
  /**
   * v2 only. Each entry is one underlying PDF file behind the template,
   * with its AcroForm field roster and a diff against the CRM's expected
   * EOI field labels. The admin uses this to verify their fillable PDF
   * actually has the named fields the CRM will fill via `formValues`.
   *
   * Empty array on v1 or when the PDF download / parse fails - diagnostic
   * messages go to pino so the admin still sees the rest of the sync result.
   */
  acroForm: Array<{
    envelopeItemId: string;
    fields: AcroFormField[];
    /** Names from the PDF that match a CRM-expected EOI label. */
    matchedFieldNames: string[];
    /**
     * CRM-expected labels missing from the PDF's AcroForm. These won't
     * be filled at send time on the AcroForm path. (Independent of the
     * Documenso-overlay-field diff above.)
     */
    missingFieldNames: string[];
    /**
     * Names in the PDF AcroForm the CRM has no token for. Usually
     * harmless leftover fields from the original PDF authoring tool
     * (signature blocks, etc.).
     */
    extraFieldNames: string[];
    /** Set when download or parse failed - string surfaces in the UI. */
    error?: string;
  }>;
}

/**
 * Map a template recipient to its CRM **recipient-id** setting key by role +
 * signing order. These are the Documenso-internal numeric IDs that bind a
 * /template/use call's recipients block to the template's slots.
 *
 * Distinct from `documenso_developer_user_id` / `documenso_approver_user_id`,
 * which are CRM user UUIDs for in-CRM notification routing (RBAC binding).
 *
 * Convention (matches buildDocumensoPayload's signing order):
 *   signingOrder 1, role=SIGNER    → client recipient slot
 *   signingOrder 2, role=SIGNER    → developer recipient slot
 *   signingOrder 3, role=APPROVER  → approver recipient slot
 *
 * Returns null if the role/order combination doesn't match any known slot -
 * useful future-proofing for templates that add CC / VIEWER recipients.
 */
function mapRecipientToSettingKey(role: string, signingOrder: number): string | null {
  const r = role.toUpperCase();
  if (r === 'SIGNER' && signingOrder === 1) return 'documenso_client_recipient_id';
  if (r === 'SIGNER' && signingOrder === 2) return 'documenso_developer_recipient_id';
  if (r === 'APPROVER' && signingOrder === 3) return 'documenso_approval_recipient_id';
  return null;
}

/**
 * Fetch the Documenso template, write the discovered recipient IDs into the
 * matching CRM settings, and cache the field name→ID map for v2 prefillFields.
 *
 * Throws when the template fetch fails (bad credentials, wrong template ID,
 * network) - caller surfaces the error to the admin via the form's mutation
 * onError + toastError.
 */
export async function syncDocumensoTemplate(
  templateId: number,
  portId: string,
  meta: AuditMeta,
): Promise<TemplateSyncResult> {
  const template: DocumensoTemplate = await getTemplate(templateId, portId);

  // Build the recipient-slot map and persist to system_settings.
  const recipientRows: TemplateSyncResult['recipients'] = [];
  for (const rec of template.recipients) {
    const settingKey = mapRecipientToSettingKey(rec.role, rec.signingOrder);
    recipientRows.push({
      role: rec.role,
      signingOrder: rec.signingOrder,
      id: rec.id,
      name: rec.name,
      email: rec.email,
      mappedSettingKey: settingKey,
    });
    if (settingKey) {
      await writeSetting(settingKey, rec.id, portId, meta);
    }
  }

  // Build the field-label → field-id map. Drives v2's prefillFields. The
  // field-map is itself a registry-unaware setting (the registry only knows
  // about scalar credentials/URLs, not arbitrary JSON blobs), so write
  // through the legacy upsertSetting path to avoid registry validation.
  const fieldMap: TemplateFieldMap = {};
  for (const f of template.fields) {
    if (f.label) fieldMap[f.label] = f.id;
  }
  await persistFieldMap(portId, fieldMap, meta);

  // Persist the canonical template ID so a subsequent send doesn't depend on
  // the admin form remembering it after the sync click.
  await writeSetting('documenso_eoi_template_id', templateId, portId, meta);

  // Diff the discovered field labels against what the CRM expects to fill.
  // Drives the matched / unmatched / missing lists the admin sees post-sync.
  const expected = new Set<string>(CRM_EXPECTED_EOI_FIELD_LABELS);
  const discovered = new Set<string>(Object.keys(fieldMap));
  const matchedFields: TemplateSyncResult['matchedFields'] = [];
  const unmatchedTemplateFields: TemplateSyncResult['unmatchedTemplateFields'] = [];
  for (const [label, fieldId] of Object.entries(fieldMap)) {
    if (expected.has(label)) matchedFields.push({ label, fieldId });
    else unmatchedTemplateFields.push({ label, fieldId });
  }
  const missingFromTemplate: string[] = CRM_EXPECTED_EOI_FIELD_LABELS.filter(
    (lbl) => !discovered.has(lbl),
  );

  // v2 only: download each underlying PDF and inspect its AcroForm. v1
  // doesn't expose envelope items so the array stays empty there.
  const acroForm: TemplateSyncResult['acroForm'] = [];
  const cfg = await getPortDocumensoConfig(portId);
  if (cfg.apiVersion === 'v2' && template.envelopeItems.length > 0) {
    for (const item of template.envelopeItems) {
      try {
        const pdfBytes = await downloadEnvelopeItemPdf(item.id, portId);
        const fields = await inspectPdfAcroForm(pdfBytes);
        const pdfNames = new Set(fields.map((f) => f.name));
        const matchedFieldNames = CRM_EXPECTED_EOI_FIELD_LABELS.filter((lbl) => pdfNames.has(lbl));
        const missingFieldNames = CRM_EXPECTED_EOI_FIELD_LABELS.filter((lbl) => !pdfNames.has(lbl));
        const extraFieldNames = fields.map((f) => f.name).filter((n) => !expected.has(n));
        acroForm.push({
          envelopeItemId: item.id,
          fields,
          matchedFieldNames,
          missingFieldNames,
          extraFieldNames,
        });
      } catch (err) {
        // Surface the failure in the UI rather than failing the whole
        // sync - the recipient + field-map writes already succeeded
        // and an admin can still progress without the AcroForm diff.
        logger.warn(
          { err, envelopeItemId: item.id, templateId },
          'AcroForm inspection failed during template sync',
        );
        acroForm.push({
          envelopeItemId: item.id,
          fields: [],
          matchedFieldNames: [],
          missingFieldNames: [...CRM_EXPECTED_EOI_FIELD_LABELS],
          extraFieldNames: [],
          error: err instanceof Error ? err.message : 'PDF inspection failed',
        });
      }
    }
  }

  const result: TemplateSyncResult = {
    syncedAt: new Date().toISOString(),
    templateId: template.id || templateId,
    title: template.title,
    recipients: recipientRows,
    fieldMap,
    fieldCount: Object.keys(fieldMap).length,
    matchedFields,
    unmatchedTemplateFields,
    missingFromTemplate,
    templateMeta: template.meta,
    acroForm,
  };

  // Cache the full report so the admin's status panel survives page reloads.
  // Free-form JSON, written via the legacy upsertSetting path (the registry
  // resolver only handles scalar credentials/URLs). Last sync wins.
  const { upsertSetting } = await import('@/lib/services/settings.service');
  await upsertSetting('documenso_eoi_template_sync_report', result, portId, meta);

  return result;
}

/**
 * Read the cached sync report written on the most recent successful sync.
 * Drives the post-reload status panel - returns null when no sync has
 * ever run for this port.
 */
export async function getEoiTemplateSyncReport(portId: string): Promise<TemplateSyncResult | null> {
  const row = await getSettingLegacy('documenso_eoi_template_sync_report', portId);
  const stored = row?.value as unknown;
  if (!stored || typeof stored !== 'object') return null;
  return stored as TemplateSyncResult;
}

/**
 * Read the cached field-name → field-id map for this port. Used by
 * `buildDocumensoPayload` when emitting v2's `prefillFields` array.
 * Returns null when no sync has run yet - caller falls back to v1's
 * `formValues`-by-name shape.
 */
export async function getEoiFieldMap(portId: string): Promise<TemplateFieldMap | null> {
  // The field map is a free-form JSON blob that doesn't fit the registry's
  // scalar-credential model - read directly via the legacy settings.service
  // path so the registry-aware resolver doesn't reject the unknown key.
  const row = await getSettingLegacy('documenso_eoi_field_map', portId);
  const stored = row?.value;
  if (!stored || typeof stored !== 'object') return null;
  const out: TemplateFieldMap = {};
  for (const [k, v] of Object.entries(stored as Record<string, unknown>)) {
    if (typeof v === 'number') out[k] = v;
    else if (typeof v === 'string' && /^\d+$/.test(v)) out[k] = Number(v);
  }
  return Object.keys(out).length > 0 ? out : null;
}

/**
 * Write the field map via the legacy settings.service path so we don't have
 * to add an arbitrary-JSON entry to the registry. The map is per-port and
 * gets blown away + rewritten on every sync click.
 */
async function persistFieldMap(
  portId: string,
  fieldMap: TemplateFieldMap,
  meta: AuditMeta,
): Promise<void> {
  const { upsertSetting } = await import('@/lib/services/settings.service');
  await upsertSetting('documenso_eoi_field_map', fieldMap, portId, meta);
}
