import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { and, asc, eq, isNull, lt } from 'drizzle-orm';
import { PDFDocument } from '@pdfme/pdf-lib';
import {
  crmDocumentTemplateVersions,
  crmDocumentTemplates,
} from '@/db/schema';
import { db } from '@/lib/db';
import { AuthError } from '@/lib/auth/session';
import { generatePdfFromTemplate } from '@/features/foundation/pdf-generator/server/service';
import { buildQuotationPdfRuntime } from '@/features/crm/quotations/document/server/quotation-pdf-runtime';
import type { QuotationDocumentData } from '@/features/crm/quotations/document/types';
import {
  PDFME_STATIC_LABEL_FIELD_KEYS,
  buildAuditPayload,
  createSqlClient,
  extractTemplateFields,
  getEffectiveMappedKeys,
  normalizeTemplateInputAliases,
  summarizeIssues,
} from '@/features/foundation/pdf-audit/server/shared';
import {
  getDocumentTemplate,
  mapDocumentDataToTemplateInput,
  resolveTemplateMappings,
} from './service';
import {
  CURRENT_TEMPLATE_RUNTIME_VERSION,
  getManagementMetadata,
  mergeManagementMetadata,
  sanitizeTemplateSchemaJson,
} from './metadata';
import type {
  DocumentTemplateDetail,
  DocumentTemplateLifecycleStatus,
  DocumentTemplateVersionAuditSummary,
  DocumentTemplateVersionCompareResponse,
  DocumentTemplateVersionDetail,
  DocumentTemplateVersionPreviewResponse,
  DocumentTemplateVersionVisualSummary,
  DocumentTemplateVersionValidationSummary,
} from '../types';

type VersionRow = typeof crmDocumentTemplateVersions.$inferSelect;
const REQUIRED_TEMPLATE_PLACEHOLDERS: string[] = [
  'customer_name',
  'quotation_code_data',
  'quotation_date_data',
  'app1',
  'app2',
  'app3',
] as const;

function inferLifecycleStatus(row: VersionRow): DocumentTemplateLifecycleStatus {
  if (row.isActive) {
    return 'active';
  }
  return getManagementMetadata({
    metadataJson: row.metadataJson,
    schemaJson: row.schemaJson,
  }).lifecycleStatus ?? 'draft';
}

async function getVersionRow(versionId: string, organizationId: string) {
  const [row] = await db
    .select()
    .from(crmDocumentTemplateVersions)
    .where(
      and(
        eq(crmDocumentTemplateVersions.id, versionId),
        eq(crmDocumentTemplateVersions.organizationId, organizationId),
        isNull(crmDocumentTemplateVersions.deletedAt),
      ),
    );

  if (!row) {
    throw new AuthError('Document template version not found', 404);
  }

  return row;
}

async function listSiblingVersionRows(templateId: string, organizationId: string) {
  return db
    .select()
    .from(crmDocumentTemplateVersions)
    .where(
      and(
        eq(crmDocumentTemplateVersions.templateId, templateId),
        eq(crmDocumentTemplateVersions.organizationId, organizationId),
        isNull(crmDocumentTemplateVersions.deletedAt),
      ),
    )
    .orderBy(asc(crmDocumentTemplateVersions.createdAt));
}

function decorateVersionDetail(args: {
  version: DocumentTemplateVersionDetail;
  siblings: DocumentTemplateVersionDetail[];
  auditSummary: DocumentTemplateVersionAuditSummary | null;
  validationSummary?: DocumentTemplateVersionValidationSummary | null;
}): DocumentTemplateVersionDetail {
  const metadata = getManagementMetadata({
    metadataJson: args.version.metadataJson,
    schemaJson: args.version.schemaJson,
  });
  const siblingIds = args.siblings.map((item) => item.id);
  const currentIndex = siblingIds.indexOf(args.version.id);

  return {
    ...args.version,
    lifecycleStatus: args.version.isActive
      ? 'active'
      : metadata.lifecycleStatus ?? args.version.lifecycleStatus ?? 'draft',
    runtimeVersion:
      metadata.runtimeVersion ??
      args.version.runtimeVersion ??
      CURRENT_TEMPLATE_RUNTIME_VERSION,
    templateVariant:
      metadata.templateVariant ?? args.version.templateVariant ?? null,
    brand: metadata.brand ?? args.version.brand ?? null,
    publishedAt: metadata.publishedAt ?? args.version.publishedAt ?? null,
    publishedBy: metadata.publishedBy ?? args.version.publishedBy ?? null,
    activatedAt: metadata.activatedAt ?? args.version.activatedAt ?? null,
    activatedBy: metadata.activatedBy ?? args.version.activatedBy ?? null,
    archivedAt: metadata.archivedAt ?? args.version.archivedAt ?? null,
    archivedBy: metadata.archivedBy ?? args.version.archivedBy ?? null,
    previousVersionId:
      metadata.previousVersionId ??
      args.version.previousVersionId ??
      (currentIndex > 0 ? siblingIds[currentIndex - 1] : null),
    nextVersionId:
      metadata.nextVersionId ??
      args.version.nextVersionId ??
      (currentIndex >= 0 && currentIndex < siblingIds.length - 1
        ? siblingIds[currentIndex + 1]
        : null),
    auditSummary: args.auditSummary ?? metadata.auditSummary ?? null,
    validationSummary: args.validationSummary ?? metadata.validationSummary ?? null,
    visualSummary: metadata.visualSummary ?? null,
  };
}

async function loadAuditSummary(
  version: DocumentTemplateVersionDetail,
): Promise<DocumentTemplateVersionAuditSummary | null> {
  const auditReportPath = path.resolve(
    process.cwd(),
    'docs/implementation/pdf-audit-report.json',
  );
  const visualReportPath = path.resolve(
    process.cwd(),
    'artifacts/pdf-visual/visual-regression-report.json',
  );

  const auditSummary: DocumentTemplateVersionAuditSummary = {
    status: 'WARNING',
    auditedAt: null,
    runtimeVersion: version.runtimeVersion ?? CURRENT_TEMPLATE_RUNTIME_VERSION,
    visualRegressionStatus: 'NOT_RUN',
    visualRegressionAt: null,
  };

  try {
    const auditReport = JSON.parse(await readFile(auditReportPath, 'utf8')) as {
      generatedAt?: string;
      overallStatus?: 'PASS' | 'WARNING' | 'FAIL';
    };
    auditSummary.auditedAt = auditReport.generatedAt ?? null;
    auditSummary.status = auditReport.overallStatus ?? 'WARNING';
  } catch {}

  try {
    const visualReport = JSON.parse(await readFile(visualReportPath, 'utf8')) as {
      generatedAt?: string;
      overallStatus?: 'PASS' | 'WARNING' | 'FAIL';
    };
    auditSummary.visualRegressionAt = visualReport.generatedAt ?? null;
    auditSummary.visualRegressionStatus =
      version.templateVariant === 'product-v1'
        ? visualReport.overallStatus ?? 'WARNING'
        : 'NOT_RUN';
  } catch {}

  return auditSummary;
}

async function loadVisualSummary(): Promise<DocumentTemplateVersionVisualSummary | null> {
  const visualReportPath = path.resolve(
    process.cwd(),
    'artifacts/pdf-visual/visual-regression-report.json',
  );

  try {
    const visualReport = JSON.parse(await readFile(visualReportPath, 'utf8')) as {
      generatedAt?: string;
      overallStatus?: 'PASS' | 'WARNING' | 'FAIL';
      comparisons?: Array<{ changedPages?: number; diffCount?: number }>;
    };

    const comparisons = Array.isArray(visualReport.comparisons)
      ? visualReport.comparisons
      : [];

    return {
      status: visualReport.overallStatus ?? 'NOT_RUN',
      generatedAt: visualReport.generatedAt ?? null,
      changedPages: comparisons.reduce(
        (sum, item) => sum + (item.changedPages ?? 0),
        0,
      ),
      layoutDifferences: comparisons.reduce(
        (sum, item) => sum + (item.diffCount ?? 0),
        0,
      ),
    };
  } catch {
    return null;
  }
}

async function buildFixtureDocumentData(): Promise<QuotationDocumentData> {
  const sql = createSqlClient();
  try {
    const payload = await buildAuditPayload(sql);
    return payload.documentData as unknown as QuotationDocumentData;
  } finally {
    await sql.end({ timeout: 1 });
  }
}

async function getTemplateContext(versionId: string, organizationId: string): Promise<{
  template: DocumentTemplateDetail;
  version: DocumentTemplateVersionDetail;
  siblings: DocumentTemplateVersionDetail[];
}> {
  const row = await getVersionRow(versionId, organizationId);
  const template = await getDocumentTemplate(row.templateId, organizationId);
  const siblings = template.versions;
  const version = siblings.find((item) => item.id === versionId);

  if (!version) {
    throw new AuthError('Document template version not found', 404);
  }

  return {
    template,
    version,
    siblings,
  };
}

export async function validateDocumentTemplateVersion(
  versionId: string,
  organizationId: string,
): Promise<DocumentTemplateVersionValidationSummary> {
  const { template, version } = await getTemplateContext(versionId, organizationId);
  const fields = extractTemplateFields(version.schemaJson);
  const mappedKeys = getEffectiveMappedKeys(fields, version.mappings);
  const schemaFieldNames = new Set(fields.map((field) => field.name).filter(Boolean));
  const hasProductSection = fields.some(
    (field) => field.name === '__section_role__product_items',
  );
  const requiredFieldsMissing = REQUIRED_TEMPLATE_PLACEHOLDERS.filter(
    (field) => !schemaFieldNames.has(field),
  );

  if (hasProductSection && !schemaFieldNames.has('items_table')) {
    requiredFieldsMissing.push('items_table');
  }

  const duplicateFieldCounts = new Map<string, number>();
  for (const field of fields) {
    if (!field.name) {
      continue;
    }
    duplicateFieldCounts.set(
      field.name,
      (duplicateFieldCounts.get(field.name) ?? 0) + 1,
    );
  }
  const duplicateFields = [...duplicateFieldCounts.entries()]
    .filter(([, count]) => count > 1)
    .map(([name]) => name)
    .sort();

  const orphanMappings = version.mappings
    .filter((mapping) => !schemaFieldNames.has(mapping.placeholderKey))
    .map((mapping) => mapping.placeholderKey)
    .sort();

  const unmappedPlaceholders = [...schemaFieldNames]
    .filter(
      (fieldName) =>
        !fieldName.startsWith('__section_role__') &&
        !PDFME_STATIC_LABEL_FIELD_KEYS.includes(
          fieldName as (typeof PDFME_STATIC_LABEL_FIELD_KEYS)[number],
        ) &&
        !mappedKeys.has(fieldName) &&
        !fieldName.startsWith('topic') &&
        !fieldName.startsWith('data_topic') &&
        !fieldName.startsWith('item_topic'),
    )
    .sort();

  const unsupportedSchemaTypes = fields
    .filter(
      (field) =>
        ![
          'text',
          'line',
          'rectangle',
          'ellipse',
          'table',
          'image',
          'pdf',
        ].includes(field.type),
    )
    .map((field) => `${field.name}:${field.type}`)
    .sort();

  const unknownPlugins: string[] = [];
  const sectionMarkerIssues: string[] = [];
  if (hasProductSection && !schemaFieldNames.has('items_table')) {
    sectionMarkerIssues.push(
      'Product section marker exists but items_table placeholder is missing',
    );
  }

  const documentData = await buildFixtureDocumentData();
  const templateInput = mapDocumentDataToTemplateInput(
    documentData as unknown as Record<string, unknown>,
    version.mappings,
  );
  const normalizedTemplateInput = normalizeTemplateInputAliases(fields, templateInput);
  const runtime = await buildQuotationPdfRuntime({
    documentData,
    template: {
      template,
      version,
    },
    mappings: version.mappings,
    templateInput: normalizedTemplateInput,
  });

  const issues: DocumentTemplateVersionValidationSummary['issues'] = [];
  for (const field of requiredFieldsMissing) {
    issues.push({
      code: 'missing_required_placeholder',
      severity: 'error',
      location: field,
      message: `Required placeholder ${field} is missing`,
      suggestedFix: 'Add the placeholder into the template schema.',
    });
  }
  for (const field of duplicateFields) {
    issues.push({
      code: 'duplicate_field_name',
      severity: 'error',
      location: field,
      message: `Field ${field} is duplicated in the template schema`,
      suggestedFix: 'Rename or remove duplicated fields so each placeholder is unique.',
    });
  }
  for (const field of orphanMappings) {
    issues.push({
      code: 'orphan_mapping',
      severity: 'warning',
      location: field,
      message: `Mapping ${field} does not exist in the current schema`,
      suggestedFix: 'Remove the mapping or restore the matching placeholder in the schema.',
    });
  }
  for (const field of unmappedPlaceholders) {
    issues.push({
      code: 'unmapped_placeholder',
      severity: 'warning',
      location: field,
      message: `Placeholder ${field} does not have a mapping`,
      suggestedFix: 'Add a mapping or convert the field into a static designer label.',
    });
  }
  for (const field of unsupportedSchemaTypes) {
    issues.push({
      code: 'unsupported_schema_type',
      severity: 'error',
      location: field,
      message: `Unsupported schema field type detected: ${field}`,
      suggestedFix: 'Use only supported PDFMe field types for runtime rendering.',
    });
  }
  for (const issue of sectionMarkerIssues) {
    issues.push({
      code: 'section_marker_invalid',
      severity: 'error',
      location: 'items_table',
      message: issue,
      suggestedFix: 'Add items_table to the template and map it to quotation items.',
    });
  }
  for (const issue of runtime.issues) {
    issues.push({
      code: issue.code,
      severity: issue.severity,
      location: 'runtime',
      message: issue.message,
      suggestedFix:
        issue.severity === 'error'
          ? 'Resolve runtime compatibility before publish.'
          : 'Review runtime warning and confirm it is acceptable.',
    });
  }

  const statuses: Array<'PASS' | 'WARNING' | 'FAIL'> = [];
  if (requiredFieldsMissing.length) {
    statuses.push('FAIL');
  }
  if (duplicateFields.length) {
    statuses.push('FAIL');
  }
  if (orphanMappings.length) {
    statuses.push('WARNING');
  }
  if (unmappedPlaceholders.length) {
    statuses.push('WARNING');
  }
  if (unsupportedSchemaTypes.length || sectionMarkerIssues.length || unknownPlugins.length) {
    statuses.push('FAIL');
  }
  statuses.push(
    ...runtime.issues.map((issue) => (issue.severity === 'error' ? 'FAIL' : 'WARNING')),
  );

  const status = summarizeIssues(statuses.length ? statuses : ['PASS']);

  return {
    status,
    validatedAt: new Date().toISOString(),
    requiredFieldsMissing,
    duplicateFields,
    unmappedPlaceholders,
    orphanMappings,
    unsupportedSchemaTypes,
    unknownPlugins,
    sectionMarkerIssues,
    healthStatus:
      status === 'FAIL' ? 'fail' : status === 'WARNING' ? 'warning' : 'pass',
    issues,
    suggestedNextStep:
      status === 'FAIL'
        ? 'Fix blocking template issues before publish.'
        : status === 'WARNING'
          ? 'Review warnings and confirm template intent.'
          : 'Template is ready for publish.',
    runtimeIssues: runtime.issues.map((issue) => ({
      code: issue.code,
      severity: issue.severity,
      message: issue.message,
    })),
  };
}

async function persistVersionManagementMetadata(args: {
  versionId: string;
  organizationId: string;
  schemaJson?: unknown;
  metadataJson?: unknown;
  isActive?: boolean;
}) {
  const [updated] = await db
    .update(crmDocumentTemplateVersions)
    .set({
      ...(args.schemaJson === undefined ? {} : { schemaJson: args.schemaJson }),
      ...(args.metadataJson === undefined ? {} : { metadataJson: args.metadataJson }),
      ...(args.isActive === undefined ? {} : { isActive: args.isActive }),
      updatedAt: new Date(),
    })
    .where(
      and(
        eq(crmDocumentTemplateVersions.id, args.versionId),
        eq(crmDocumentTemplateVersions.organizationId, args.organizationId),
      ),
    )
    .returning();

  return updated;
}

export async function getManagedDocumentTemplateVersion(
  versionId: string,
  organizationId: string,
) {
  const { version, siblings } = await getTemplateContext(versionId, organizationId);
  const auditSummary = await loadAuditSummary(version);
  const visualSummary = await loadVisualSummary();
  return decorateVersionDetail({
    version: {
      ...version,
      visualSummary,
    },
    siblings,
    auditSummary,
  });
}

export async function validateAndPersistDocumentTemplateVersion(args: {
  versionId: string;
  organizationId: string;
}) {
  const validation = await validateDocumentTemplateVersion(
    args.versionId,
    args.organizationId,
  );
  const { version } = await getTemplateContext(args.versionId, args.organizationId);
  const row = await getVersionRow(args.versionId, args.organizationId);
  const metadata = getManagementMetadata({
    metadataJson: row.metadataJson,
    schemaJson: row.schemaJson,
  });
  const auditSummary = await loadAuditSummary(version);
  const visualSummary = await loadVisualSummary();

  if (validation.status !== 'FAIL') {
    await persistVersionManagementMetadata({
      versionId: args.versionId,
      organizationId: args.organizationId,
      metadataJson: mergeManagementMetadata({
        currentMetadataJson: row.metadataJson,
        currentSchemaJson: row.schemaJson,
        patch: {
        lifecycleStatus:
          inferLifecycleStatus(row) === 'draft' ? 'validated' : inferLifecycleStatus(row),
        validatedAt: validation.validatedAt,
          validationSummary: validation,
          auditSummary,
          visualSummary,
        },
      }),
    });
  }

  return validation;
}

export async function publishDocumentTemplateVersion(args: {
  versionId: string;
  organizationId: string;
  userId: string;
}) {
  const validation = await validateAndPersistDocumentTemplateVersion(args);
  if (validation.status === 'FAIL') {
    throw new AuthError('Template version validation failed. Publish rejected.', 409);
  }

  const row = await getVersionRow(args.versionId, args.organizationId);
  const metadata = getManagementMetadata({
    metadataJson: row.metadataJson,
    schemaJson: row.schemaJson,
  });
  await persistVersionManagementMetadata({
    versionId: args.versionId,
    organizationId: args.organizationId,
    isActive: false,
    metadataJson: mergeManagementMetadata({
      currentMetadataJson: row.metadataJson,
      currentSchemaJson: row.schemaJson,
      patch: {
        lifecycleStatus: 'published',
        runtimeVersion: metadata.runtimeVersion ?? CURRENT_TEMPLATE_RUNTIME_VERSION,
        publishedAt: new Date().toISOString(),
        publishedBy: args.userId,
        validatedAt: validation.validatedAt,
        validationSummary: validation,
      },
    }),
  });

  return getManagedDocumentTemplateVersion(args.versionId, args.organizationId);
}

export async function activateDocumentTemplateVersion(args: {
  versionId: string;
  organizationId: string;
  userId: string;
}) {
  const row = await getVersionRow(args.versionId, args.organizationId);
  const currentStatus = inferLifecycleStatus(row);
  if (!['validated', 'published', 'active'].includes(currentStatus)) {
    throw new AuthError('Only validated or published versions can be activated.', 409);
  }

  const siblings = await listSiblingVersionRows(row.templateId, args.organizationId);
  const currentActive = siblings.find((item) => item.isActive && item.id !== row.id) ?? null;

  if (currentActive) {
    const previousMetadata = getManagementMetadata({
      metadataJson: currentActive.metadataJson,
      schemaJson: currentActive.schemaJson,
    });
    await persistVersionManagementMetadata({
      versionId: currentActive.id,
      organizationId: args.organizationId,
      isActive: false,
      metadataJson: mergeManagementMetadata({
        currentMetadataJson: currentActive.metadataJson,
        currentSchemaJson: currentActive.schemaJson,
        patch: {
          ...previousMetadata,
          lifecycleStatus: 'published',
          nextVersionId: row.id,
        },
      }),
    });
  }

  const metadata = getManagementMetadata({
    metadataJson: row.metadataJson,
    schemaJson: row.schemaJson,
  });
  await persistVersionManagementMetadata({
    versionId: row.id,
    organizationId: args.organizationId,
    isActive: true,
    schemaJson: sanitizeTemplateSchemaJson(row.schemaJson),
    metadataJson: mergeManagementMetadata({
      currentMetadataJson: row.metadataJson,
      currentSchemaJson: row.schemaJson,
      patch: {
        lifecycleStatus: 'active',
        runtimeVersion: metadata.runtimeVersion ?? CURRENT_TEMPLATE_RUNTIME_VERSION,
        publishedAt: metadata.publishedAt ?? new Date().toISOString(),
        publishedBy: metadata.publishedBy ?? args.userId,
        activatedAt: new Date().toISOString(),
        activatedBy: args.userId,
        previousVersionId: currentActive?.id ?? metadata.previousVersionId ?? null,
        nextVersionId: null,
      },
    }),
  });

  return getManagedDocumentTemplateVersion(row.id, args.organizationId);
}

export async function rollbackDocumentTemplateVersion(args: {
  versionId: string;
  organizationId: string;
  userId: string;
}) {
  const row = await getVersionRow(args.versionId, args.organizationId);
  if (!row.isActive) {
    throw new AuthError('Rollback is only allowed from the current active version.', 409);
  }

  const metadata = getManagementMetadata({
    metadataJson: row.metadataJson,
    schemaJson: row.schemaJson,
  });
  let targetVersionId = metadata.previousVersionId ?? null;

  if (!targetVersionId) {
    const [fallback] = await db
      .select()
      .from(crmDocumentTemplateVersions)
      .where(
        and(
          eq(crmDocumentTemplateVersions.templateId, row.templateId),
          eq(crmDocumentTemplateVersions.organizationId, args.organizationId),
          isNull(crmDocumentTemplateVersions.deletedAt),
          lt(crmDocumentTemplateVersions.createdAt, row.createdAt),
        ),
      )
      .orderBy(asc(crmDocumentTemplateVersions.createdAt));
    targetVersionId = fallback?.id ?? null;
  }

  if (!targetVersionId) {
    throw new AuthError('No previous version available for rollback.', 409);
  }

  return activateDocumentTemplateVersion({
    versionId: targetVersionId,
    organizationId: args.organizationId,
    userId: args.userId,
  });
}

export async function archiveDocumentTemplateVersion(args: {
  versionId: string;
  organizationId: string;
  userId: string;
}) {
  const row = await getVersionRow(args.versionId, args.organizationId);
  if (row.isActive) {
    throw new AuthError('Active versions must be rolled back before archive.', 409);
  }

  const metadata = getManagementMetadata({
    metadataJson: row.metadataJson,
    schemaJson: row.schemaJson,
  });
  await persistVersionManagementMetadata({
    versionId: row.id,
    organizationId: args.organizationId,
    isActive: false,
    metadataJson: mergeManagementMetadata({
      currentMetadataJson: row.metadataJson,
      currentSchemaJson: row.schemaJson,
      patch: {
        lifecycleStatus: 'archived',
        archivedAt: new Date().toISOString(),
        archivedBy: args.userId,
      },
    }),
  });

  return getManagedDocumentTemplateVersion(row.id, args.organizationId);
}

export async function compareDocumentTemplateVersions(args: {
  templateId: string;
  leftVersionId: string;
  rightVersionId: string;
  organizationId: string;
}): Promise<DocumentTemplateVersionCompareResponse['comparison']> {
  const template = await getDocumentTemplate(args.templateId, args.organizationId);
  const left = template.versions.find((item) => item.id === args.leftVersionId);
  const right = template.versions.find((item) => item.id === args.rightVersionId);

  if (!left || !right) {
    throw new AuthError('Unable to compare template versions.', 404);
  }

  const metadataFields: Array<keyof DocumentTemplateVersionDetail> = [
    'version',
    'filePath',
    'isActive',
    'lifecycleStatus',
    'runtimeVersion',
    'templateVariant',
    'brand',
  ];

  const metadataChanges = metadataFields
    .map((field) => ({
      field,
      left: (left[field] as string | number | boolean | null | undefined) ?? null,
      right: (right[field] as string | number | boolean | null | undefined) ?? null,
    }))
    .filter((item) => item.left !== item.right);

  const leftPlaceholderSet = new Set(left.mappings.map((item) => item.placeholderKey));
  const rightPlaceholderSet = new Set(right.mappings.map((item) => item.placeholderKey));
  const leftFields = extractTemplateFields(left.schemaJson);
  const rightFields = extractTemplateFields(right.schemaJson);
  const leftFieldNames = new Set(leftFields.map((field) => field.name).filter(Boolean));
  const rightFieldNames = new Set(rightFields.map((field) => field.name).filter(Boolean));

  const mappingDifferences = [
    ...left.mappings.flatMap((mapping) => {
      const other = right.mappings.find(
        (candidate) => candidate.placeholderKey === mapping.placeholderKey,
      );
      if (!other) {
        return [];
      }
      const currentSignature = JSON.stringify({
        sourcePath: mapping.sourcePath,
        dataType: mapping.dataType,
        columns: mapping.columns.map((column) => ({
          columnName: column.columnName,
          sourceField: column.sourceField,
          formatMask: column.formatMask,
        })),
      });
      const otherSignature = JSON.stringify({
        sourcePath: other.sourcePath,
        dataType: other.dataType,
        columns: other.columns.map((column) => ({
          columnName: column.columnName,
          sourceField: column.sourceField,
          formatMask: column.formatMask,
        })),
      });
      return currentSignature === otherSignature
        ? []
        : [{
            placeholderKey: mapping.placeholderKey,
            difference: 'Mapping contract changed',
            leftValue: currentSignature,
            rightValue: otherSignature,
          }];
    }),
  ];

  const leftValidation = await validateDocumentTemplateVersion(left.id, args.organizationId);
  const rightValidation = await validateDocumentTemplateVersion(right.id, args.organizationId);
  const jsonDiff: DocumentTemplateVersionCompareResponse['comparison']['jsonDiff'] = [];

  const fieldSchemaChanges = {
    addedFields: [...rightFieldNames].filter((field) => !leftFieldNames.has(field)).sort(),
    removedFields: [...leftFieldNames].filter((field) => !rightFieldNames.has(field)).sort(),
    changedFields: leftFields
      .flatMap((leftField) => {
        const rightField = rightFields.find((item) => item.name === leftField.name);
        if (!rightField) {
          return [];
        }
        if (leftField.type === rightField.type) {
          return [];
        }
        return [
          {
            field: leftField.name,
            difference: `${leftField.type} -> ${rightField.type}`,
          },
        ];
      })
      .sort((a, b) => a.field.localeCompare(b.field)),
  };

  const leftSchema = sanitizeTemplateSchemaJson(left.schemaJson) as Record<string, unknown>;
  const rightSchema = sanitizeTemplateSchemaJson(right.schemaJson) as Record<string, unknown>;
  const leftPages = Array.isArray(leftSchema.schemas) ? leftSchema.schemas.length : 0;
  const rightPages = Array.isArray(rightSchema.schemas) ? rightSchema.schemas.length : 0;
  if (leftPages !== rightPages) {
    jsonDiff.push({
      path: 'schemas.length',
      left: leftPages,
      right: rightPages,
    });
  }

  return {
    leftVersionId: left.id,
    rightVersionId: right.id,
    metadataChanges,
    placeholderOnlyLeft: [...leftPlaceholderSet].filter((item) => !rightPlaceholderSet.has(item)),
    placeholderOnlyRight: [...rightPlaceholderSet].filter((item) => !leftPlaceholderSet.has(item)),
    mappingDifferences,
    fieldSchemaChanges,
    jsonDiff,
    runtimeCompatibility: {
      leftStatus: leftValidation.status,
      rightStatus: rightValidation.status,
    },
    leftValidation,
    rightValidation,
  };
}

export async function previewDocumentTemplateVersion(args: {
  versionId: string;
  organizationId: string;
}): Promise<DocumentTemplateVersionPreviewResponse['preview']> {
  const { template, version } = await getTemplateContext(args.versionId, args.organizationId);
  const documentData = await buildFixtureDocumentData();
  const fields = extractTemplateFields(version.schemaJson);
  const templateInput = normalizeTemplateInputAliases(
    fields,
    mapDocumentDataToTemplateInput(
      documentData as unknown as Record<string, unknown>,
      version.mappings,
    ),
  );
  const runtime = await buildQuotationPdfRuntime({
    documentData,
    template: {
      template,
      version,
    },
    mappings: version.mappings,
    templateInput,
  });
  const buffer = await generatePdfFromTemplate({
    template: runtime.assembled.template,
    inputs: [runtime.assembled.templateInput],
  });
  const pdfDocument = await PDFDocument.load(buffer);

  return {
    fileName: `${template.templateName}-${version.version}-preview.pdf`,
    contentType: 'application/pdf',
    base64: Buffer.from(buffer).toString('base64'),
    pageCount: pdfDocument.getPageCount(),
    source: 'audit-fixture',
  };
}
