import fs from 'node:fs';
import { mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { createHash } from 'node:crypto';
import postgres from 'postgres';
import { resolveQuotationStaticTemplateInputs } from '../../../crm/quotations/document/server/static-input-resolver.ts';

export const AUDIT_QUOTATION_CODE = 'QT-H5-AUDIT';
export const AUDIT_ARTIFACTS_DIR = path.resolve(
  /* turbopackIgnore: true */ process.cwd(),
  'artifacts',
  'pdf-audit',
);
export const REPORT_JSON_PATH = path.resolve(
  /* turbopackIgnore: true */ process.cwd(),
  'docs/implementation/pdf-audit-report.json'
);
export const REPORT_MARKDOWN_PATH = path.resolve(
  /* turbopackIgnore: true */ process.cwd(),
  'docs/implementation/pdf-audit-report.md'
);

export type SqlClient = ReturnType<typeof postgres>;

export interface TemplateFieldRecord {
  pageIndex: number;
  fieldIndex: number;
  name: string;
  type: string;
  tokens: string[];
}

export interface ActiveTemplateVersionRecord {
  templateId: string;
  templateName: string;
  organizationId: string;
  documentType: string;
  productType: string;
  fileType: string;
  versionId: string;
  version: string;
  filePath: string | null;
  schemaJson: unknown;
}

export interface MappingRecord {
  id: string;
  organizationId: string;
  templateVersionId: string;
  placeholderKey: string;
  sourcePath: string;
  dataType: 'scalar' | 'multiline' | 'table' | 'image';
  defaultValue: string | null;
  formatMask: string | null;
  sortOrder: number;
}

export interface MappingColumnRecord {
  mappingId: string;
  columnName: string;
  sourceField: string;
  sortOrder: number;
  formatMask: string | null;
}

export type TemplateFieldClassification =
  | 'static input field'
  | 'dynamic topic template'
  | 'readonly/static visual field'
  | 'system field'
  | 'unknown field';

export interface TemplateMappingSeed {
  placeholderKey: string;
  sourcePath: string;
  dataType: 'scalar' | 'multiline' | 'table' | 'image';
  defaultValue?: string | null;
  formatMask?: string | null;
  sortOrder: number;
  columns?: Array<{
    columnName: string;
    sourceField: string;
    columnLetter?: string;
    sortOrder: number;
    formatMask?: string | null;
  }>;
}

export interface AuditPayload {
  organizationId: string;
  quotationId: string;
  quotationCode: string;
  templateVersionId: string;
  approvedTemplateVersionId: string | null;
  templateName: string;
  templateVersion: string;
  templateSchema: unknown;
  mappings: MappingRecord[];
  mappingColumns: MappingColumnRecord[];
  documentData: Record<string, unknown>;
  templateInput: Record<string, unknown>;
  topicInputs: Record<string, string[][]>;
  quotationMetrics: {
    topicCount: number;
    itemCount: number;
    partyCount: number;
  };
  approvedSnapshot: Record<string, unknown> | null;
  approvedArtifactReference: string | null;
}

const SYSTEM_FIELD_NAMES = new Set([
  '',
  'company1',
  'company',
  'company2',
  'company_addr1',
  'company_addr2',
  'company_tel',
  'company_email',
  'company_tax',
  'Please_do_not',
  'yours_faithfuly'
]);

const DYNAMIC_TOPIC_FIELD_NAMES = new Set(['topic', 'data_topic']);
const LEGACY_DYNAMIC_SEED_TOKENS = new Set(['item_topic']);
const DYNAMIC_TOPIC_TOKEN_PREFIXES = ['topic_', 'item_topic_'];
const DESIGNER_LABEL_NAME_PATTERNS = [/_label$/i, /_lable$/i];
const STATIC_VISUAL_FIELD_NAMES = new Set([
  'colon',
  'colon copy',
  'colon_att',
  'colon_email',
  'colon_fax',
  'colon_project',
  'colon_site',
  'colon_tel',
  'dear_sirs',
  'line',
  'yours_faithfuly',
  'quotation_code_lable',
  'quotation_date_labe'
]);
export const PRICE_TABLE_LABEL = 'Price (Exclude VAT)';
export const PDFME_STATIC_LABELS = {
  tel: 'Tel',
  email: 'Email',
  att: 'Att',
  project: 'Project',
  location: 'Location'
} as const;
export const PDFME_STATIC_LABEL_FIELD_KEYS = [
  'tel_label',
  'email_label',
  'att_label',
  'project_label',
  'site_label'
] as const;
export type TemplateSourceVariant = 'legacy' | 'product-v1';
export const SUPPORTED_TEMPLATE_MAPPING_SEEDS: TemplateMappingSeed[] = [
  {
    placeholderKey: 'customer_name',
    sourcePath: 'customer.name',
    dataType: 'scalar',
    sortOrder: 1
  },
  {
    placeholderKey: 'customer_addr',
    sourcePath: 'customer.address',
    dataType: 'multiline',
    sortOrder: 2
  },
  {
    placeholderKey: 'customer_tel',
    sourcePath: 'customer.phone',
    dataType: 'scalar',
    sortOrder: 3
  },
  {
    placeholderKey: 'customer_email',
    sourcePath: 'customer.email',
    dataType: 'scalar',
    sortOrder: 4
  },
  {
    placeholderKey: 'customer_att',
    sourcePath: 'quotation.attention',
    dataType: 'scalar',
    sortOrder: 5
  },
  {
    placeholderKey: 'project_name',
    sourcePath: 'quotation.projectName',
    dataType: 'scalar',
    sortOrder: 6
  },
  {
    placeholderKey: 'site_location',
    sourcePath: 'quotation.projectLocation',
    dataType: 'multiline',
    sortOrder: 7
  },
  {
    placeholderKey: 'quotation_date_data',
    sourcePath: 'pdfme.quotation_date',
    dataType: 'scalar',
    sortOrder: 8
  },
  {
    placeholderKey: 'quotation_code_data',
    sourcePath: 'quotation.code',
    dataType: 'scalar',
    sortOrder: 9
  },
  {
    placeholderKey: 'quotation_price_data',
    sourcePath: 'pdfme.quotation_price_data',
    dataType: 'table',
    defaultValue: JSON.stringify([[PRICE_TABLE_LABEL, '-', ' ', 'THB']]),
    sortOrder: 10
  },
  {
    placeholderKey: 'quotation_price',
    sourcePath: 'pdfme.quotation_price',
    dataType: 'scalar',
    sortOrder: 11
  },
  {
    placeholderKey: 'currency',
    sourcePath: 'quotation.currency',
    dataType: 'scalar',
    sortOrder: 12
  },
  {
    placeholderKey: 'exclusion_data',
    sourcePath: 'pdfme.exclusion_data',
    dataType: 'table',
    sortOrder: 13
  },
  {
    placeholderKey: 'tel_label',
    sourcePath: 'pdfme.labels.tel',
    dataType: 'scalar',
    defaultValue: PDFME_STATIC_LABELS.tel,
    sortOrder: 14
  },
  {
    placeholderKey: 'email_label',
    sourcePath: 'pdfme.labels.email',
    dataType: 'scalar',
    defaultValue: PDFME_STATIC_LABELS.email,
    sortOrder: 15
  },
  {
    placeholderKey: 'att_label',
    sourcePath: 'pdfme.labels.att',
    dataType: 'scalar',
    defaultValue: PDFME_STATIC_LABELS.att,
    sortOrder: 16
  },
  {
    placeholderKey: 'project_label',
    sourcePath: 'pdfme.labels.project',
    dataType: 'scalar',
    defaultValue: PDFME_STATIC_LABELS.project,
    sortOrder: 17
  },
  {
    placeholderKey: 'site_label',
    sourcePath: 'pdfme.labels.location',
    dataType: 'scalar',
    defaultValue: PDFME_STATIC_LABELS.location,
    sortOrder: 18
  },
  {
    placeholderKey: 'app1',
    sourcePath: 'signatures.preparedBy.name',
    dataType: 'scalar',
    defaultValue: '-',
    sortOrder: 19
  },
  {
    placeholderKey: 'app1_position',
    sourcePath: 'signatures.preparedBy.position',
    dataType: 'scalar',
    defaultValue: '-',
    sortOrder: 20
  },
  {
    placeholderKey: 'app2',
    sourcePath: 'signatures.approvedBy.name',
    dataType: 'scalar',
    defaultValue: '-',
    sortOrder: 21
  },
  {
    placeholderKey: 'app2_position',
    sourcePath: 'signatures.approvedBy.position',
    dataType: 'scalar',
    defaultValue: '-',
    sortOrder: 22
  },
  {
    placeholderKey: 'app3',
    sourcePath: 'signatures.authorizedBy.name',
    dataType: 'scalar',
    defaultValue: '-',
    sortOrder: 23
  },
  {
    placeholderKey: 'app3_position',
    sourcePath: 'signatures.authorizedBy.position',
    dataType: 'scalar',
    defaultValue: '-',
    sortOrder: 24
  },
  {
    placeholderKey: 'items_table',
    sourcePath: 'items',
    dataType: 'table',
    sortOrder: 25,
    columns: [
      { columnName: 'Item', sourceField: 'itemNumber', columnLetter: 'A', sortOrder: 1 },
      { columnName: 'Description', sourceField: 'description', columnLetter: 'B', sortOrder: 2 },
      { columnName: 'Qty', sourceField: 'quantity', columnLetter: 'C', sortOrder: 3 },
      { columnName: 'Unit', sourceField: 'unitLabel', columnLetter: 'D', sortOrder: 4 },
      {
        columnName: 'Unit Price',
        sourceField: 'unitPrice',
        columnLetter: 'E',
        sortOrder: 5,
        formatMask: 'currency'
      },
      {
        columnName: 'Discount',
        sourceField: 'discount',
        columnLetter: 'F',
        sortOrder: 6,
        formatMask: 'currency'
      },
      {
        columnName: 'Total',
        sourceField: 'totalPrice',
        columnLetter: 'G',
        sortOrder: 7,
        formatMask: 'currency'
      }
    ]
  }
] as const;
const SIGNATURE_ROLE_FALLBACK_LABELS: Record<string, string> = {
  sales: 'Sales Engineer',
  sales_support: 'Sales Support',
  sales_manager: 'Sales Manager',
  department_manager: 'Department Manager',
  top_manager: 'General Manager'
};

export function loadEnvFile(filePath: string) {
  if (!fs.existsSync(filePath)) {
    return;
  }

  const content = fs.readFileSync(filePath, 'utf8');

  for (const rawLine of content.split(/\r?\n/)) {
    const line = rawLine.trim();

    if (!line || line.startsWith('#')) {
      continue;
    }

    const separatorIndex = line.indexOf('=');

    if (separatorIndex === -1) {
      continue;
    }

    const key = line.slice(0, separatorIndex).trim();
    let value = line.slice(separatorIndex + 1).trim();

    if (
      (value.startsWith('"') && value.endsWith('"')) ||
      (value.startsWith("'") && value.endsWith("'"))
    ) {
      value = value.slice(1, -1);
    }

    if (!(key in process.env)) {
      process.env[key] = value;
    }
  }
}

export function loadLocalEnv() {
  loadEnvFile(path.resolve(/* turbopackIgnore: true */ process.cwd(), '.env.local'));
  loadEnvFile(path.resolve(/* turbopackIgnore: true */ process.cwd(), '.env'));
}

export function requireEnv(name: string) {
  const value = process.env[name]?.trim();

  if (!value) {
    throw new Error(`Missing required environment variable: ${name}`);
  }

  return value;
}

export function createSqlClient() {
  loadLocalEnv();
  return postgres(requireEnv('DATABASE_URL'), {
    prepare: false
  });
}

export async function ensureAuditArtifactsDir() {
  await mkdir(AUDIT_ARTIFACTS_DIR, { recursive: true });
}

export async function writeAuditArtifact(fileName: string, payload: unknown) {
  await ensureAuditArtifactsDir();
  const targetPath = path.resolve(AUDIT_ARTIFACTS_DIR, fileName);
  await writeFile(targetPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
  return targetPath;
}

export function computePayloadHash(value: unknown) {
  return createHash('sha256').update(JSON.stringify(value)).digest('hex');
}

export function extractTokensFromValue(value: unknown) {
  const tokens = new Set<string>();

  if (typeof value !== 'string') {
    return tokens;
  }

  for (const match of value.matchAll(/\{([^}]+)\}/g)) {
    tokens.add(match[1]);
  }

  try {
    const parsed = JSON.parse(value) as unknown;

    if (Array.isArray(parsed)) {
      for (const row of parsed) {
        if (!Array.isArray(row)) {
          continue;
        }

        for (const cell of row) {
          if (typeof cell !== 'string') {
            continue;
          }

          for (const match of cell.matchAll(/\{([^}]+)\}/g)) {
            tokens.add(match[1]);
          }
        }
      }
    }
  } catch {
    return tokens;
  }

  return tokens;
}

export function extractSinglePlaceholder(value: unknown) {
  if (typeof value !== 'string') {
    return null;
  }

  const matches = [...value.matchAll(/\{([^}]+)\}/g)].map((match) => match[1]);
  const uniqueMatches = [...new Set(matches)];

  return uniqueMatches.length === 1 ? uniqueMatches[0] : null;
}

function normalizeSchemaJson(schemaJson: unknown) {
  if (typeof schemaJson === 'string') {
    try {
      return JSON.parse(schemaJson) as unknown;
    } catch {
      return null;
    }
  }

  return schemaJson;
}

export function extractTemplateFields(schemaJson: unknown) {
  const fields: TemplateFieldRecord[] = [];
  const normalizedSchemaJson = normalizeSchemaJson(schemaJson);

  if (
    !normalizedSchemaJson ||
    typeof normalizedSchemaJson !== 'object' ||
    !('schemas' in normalizedSchemaJson)
  ) {
    return fields;
  }

  const pages = (normalizedSchemaJson as { schemas?: unknown }).schemas;

  if (!Array.isArray(pages)) {
    return fields;
  }

  for (const [pageIndex, page] of pages.entries()) {
    if (!Array.isArray(page)) {
      continue;
    }

    for (const [fieldIndex, field] of page.entries()) {
      if (!field || typeof field !== 'object') {
        continue;
      }

      const typedField = field as {
        name?: unknown;
        type?: unknown;
        content?: unknown;
        head?: unknown;
      };
      const tokens = new Set<string>();

      for (const token of extractTokensFromValue(typedField.content)) {
        tokens.add(token);
      }

      if (Array.isArray(typedField.head)) {
        for (const value of typedField.head) {
          for (const token of extractTokensFromValue(value)) {
            tokens.add(token);
          }
        }
      }

      fields.push({
        pageIndex,
        fieldIndex,
        name: typeof typedField.name === 'string' ? typedField.name : '',
        type: typeof typedField.type === 'string' ? typedField.type : 'unknown',
        tokens: [...tokens]
      });
    }
  }

  return fields;
}

export function getDuplicateFieldNames(fields: TemplateFieldRecord[]) {
  const fieldNameCounts = new Map<string, number>();

  for (const field of fields) {
    if (!field.name) {
      continue;
    }

    fieldNameCounts.set(field.name, (fieldNameCounts.get(field.name) ?? 0) + 1);
  }

  return [...fieldNameCounts.entries()]
    .filter(([, count]) => count > 1)
    .map(([name]) => name)
    .sort();
}

export function getUnknownFieldNames(
  fields: TemplateFieldRecord[],
  mappedKeys: Set<string> = new Set()
) {
  return fields
    .filter(
      (field) =>
        field.name && classifyTemplateField(field, mappedKeys) === 'unknown field'
    )
    .map((field) => field.name)
    .sort();
}

export function isDesignerLabelField(field: TemplateFieldRecord) {
  return (
    field.type === 'text' &&
    field.tokens.length === 0 &&
    DESIGNER_LABEL_NAME_PATTERNS.some((pattern) => pattern.test(field.name))
  );
}

export function classifyTemplateField(
  field: TemplateFieldRecord,
  mappedKeys: Set<string> = new Set()
): TemplateFieldClassification {
  if (!field.name) {
    return 'system field';
  }

  if (SYSTEM_FIELD_NAMES.has(field.name)) {
    return 'system field';
  }

  if (DYNAMIC_TOPIC_FIELD_NAMES.has(field.name)) {
    return 'dynamic topic template';
  }

  if (mappedKeys.has(field.name)) {
    return 'static input field';
  }

  if (isDesignerLabelField(field) || STATIC_VISUAL_FIELD_NAMES.has(field.name)) {
    return 'readonly/static visual field';
  }

  if (
    field.tokens.length === 0 &&
    ['text', 'line', 'rectangle', 'ellipse', 'image'].includes(field.type)
  ) {
    return 'readonly/static visual field';
  }

  return 'unknown field';
}

export function getEffectiveMappedKeys(
  fields: TemplateFieldRecord[],
  mappings: Array<{ placeholderKey: string }>
) {
  const mappedKeys = new Set(mappings.map((mapping) => mapping.placeholderKey));

  for (const field of fields) {
    if (!field.name || field.tokens.length !== 1) {
      continue;
    }

    const [token] = field.tokens;

    if (mappedKeys.has(field.name) && !mappedKeys.has(token)) {
      mappedKeys.add(token);
    }

    if (mappedKeys.has(token) && !mappedKeys.has(field.name)) {
      mappedKeys.add(field.name);
    }
  }

  return mappedKeys;
}

export function normalizeTemplateInputAliases(
  fields: TemplateFieldRecord[],
  templateInput: Record<string, unknown>
) {
  const normalized = { ...templateInput };

  for (const field of fields) {
    if (!field.name || field.tokens.length !== 1) {
      continue;
    }

    const [token] = field.tokens;

    if (!token || field.name === token) {
      continue;
    }

    if (normalized[field.name] !== undefined && normalized[token] === undefined) {
      normalized[token] = normalized[field.name];
    }

    if (normalized[token] !== undefined && normalized[field.name] === undefined) {
      normalized[field.name] = normalized[token];
    }
  }

  return normalized;
}

export function loadTemplateSourceByOrganizationName(
  organizationName: string,
  variant: TemplateSourceVariant = 'legacy',
) {
  const normalized = organizationName.toUpperCase();
  const fileName = normalized.includes('ONVALLA')
    ? (
      variant === 'product-v1'
        ? 'ONVALLA_template_pdfme_product_v1.json'
        : 'ONVALLA_template_pdfme_fainal3.json'
    )
    : normalized.includes('ALLA')
      ? (
        variant === 'product-v1'
          ? 'ALLA_template_pdfme_product_v1.json'
          : 'ALLA_template_pdfme_fainal3.json'
      )
      : null;

  if (!fileName) {
    return null;
  }

  const filePath = path.resolve(
    /* turbopackIgnore: true */ process.cwd(),
    'src',
    'pdfme_template',
    fileName,
  );
  return {
    fileName,
    relativePath: `src/pdfme_template/${fileName}`,
    absolutePath: filePath,
    schemaJson: JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown
  };
}

export function loadTemplateSourceByRelativePath(relativePath: string | null | undefined) {
  if (!relativePath) {
    return null;
  }

  const normalizedRelativePath = relativePath.replaceAll('\\', '/');
  const absolutePath = path.resolve(
    /* turbopackIgnore: true */ process.cwd(),
    normalizedRelativePath,
  );

  if (!fs.existsSync(absolutePath)) {
    return null;
  }

  return {
    fileName: path.basename(normalizedRelativePath),
    relativePath: normalizedRelativePath,
    absolutePath,
    schemaJson: JSON.parse(fs.readFileSync(absolutePath, 'utf8')) as unknown
  };
}

export function computeFieldDiff(
  currentFields: TemplateFieldRecord[],
  previousFields: TemplateFieldRecord[]
) {
  const currentFieldNames = new Set(currentFields.map((field) => field.name).filter(Boolean));
  const previousFieldNames = new Set(previousFields.map((field) => field.name).filter(Boolean));

  return {
    deletedFields: [...previousFieldNames].filter((name) => !currentFieldNames.has(name)).sort(),
    newFields: [...currentFieldNames].filter((name) => !previousFieldNames.has(name)).sort()
  };
}

export function buildSupportedMappingsForFieldNames(
  fieldNames: Set<string>,
  tokenNames: Set<string> = new Set()
) {
  return SUPPORTED_TEMPLATE_MAPPING_SEEDS.filter(
    (mapping) => fieldNames.has(mapping.placeholderKey) || tokenNames.has(mapping.placeholderKey)
  );
}

export function bumpMinorTemplateVersion(version: string) {
  const [majorPart, minorPart = '0'] = version.split('.');
  const major = Number(majorPart);
  const minor = Number(minorPart);

  if (!Number.isInteger(major) || !Number.isInteger(minor)) {
    return `${version}.1`;
  }

  return `${major}.${minor + 1}`;
}

function normalizeSnapshotPayload(snapshot: unknown) {
  if (!snapshot) {
    return null;
  }

  if (typeof snapshot === 'string') {
    try {
      const parsed = JSON.parse(snapshot) as unknown;
      return parsed && typeof parsed === 'object' ? (parsed as Record<string, unknown>) : null;
    } catch {
      return null;
    }
  }

  return snapshot && typeof snapshot === 'object' ? (snapshot as Record<string, unknown>) : null;
}

export async function getActiveTemplateVersions(sql: SqlClient): Promise<ActiveTemplateVersionRecord[]> {
  const rows = await sql`
select
  t.id as "templateId",
  t.template_name as "templateName",
  t.organization_id as "organizationId",
  t.document_type as "documentType",
  t.product_type as "productType",
  t.file_type as "fileType",
  v.id as "versionId",
  v.version,
  v.file_path as "filePath",
  v.schema_json as "schemaJson"
    from crm_document_templates t
    inner join crm_document_template_versions v
      on v.template_id = t.id
    where t.is_active = true
      and v.is_active = true
      and t.deleted_at is null
      and v.deleted_at is null
    order by t.organization_id asc, t.template_name asc, v.created_at asc
  `;

  return castRows<ActiveTemplateVersionRecord>(rows);
}

export async function getMappingsForVersion(sql: SqlClient, templateVersionId: string) {
  const [mappings, columns] = await Promise.all([
    sql`
      select
        id,
        organization_id as "organizationId",
        template_version_id as "templateVersionId",
        placeholder_key as "placeholderKey",
        source_path as "sourcePath",
        data_type as "dataType",
        default_value as "defaultValue",
        format_mask as "formatMask",
        sort_order as "sortOrder"
      from crm_document_template_mappings
      where template_version_id = ${templateVersionId}
        and deleted_at is null
      order by sort_order asc, created_at asc
    `,
    sql`
      select
        mapping_id as "mappingId",
        column_name as "columnName",
        source_field as "sourceField",
        sort_order as "sortOrder",
        format_mask as "formatMask"
      from crm_document_template_table_columns
      where deleted_at is null
      order by sort_order asc, created_at asc
    `
  ]);

  return {
    mappings: castRows<MappingRecord>(mappings),
    columns: castRows<MappingColumnRecord>(columns)
  };
}

export async function findAuditQuotation(sql: SqlClient) {
  const [fixtureQuotation] = await sql`
    select
      q.id,
      q.code,
      q.organization_id as "organizationId",
      q.customer_id as "customerId",
      q.contact_id as "contactId",
      q.quotation_date as "quotationDate",
      q.valid_until as "validUntil",
      q.project_name as "projectName",
      q.project_location as "projectLocation",
      q.attention,
      q.reference,
      q.currency,
      q.subtotal,
      q.total_amount as "totalAmount",
      currency_option.code as "currencyCode",
      q.approved_template_version_id as "approvedTemplateVersionId",
      q.salesman_id as "salesmanId",
      q.created_by as "createdBy",
      q.created_at as "createdAt",
      q.approved_snapshot as "approvedSnapshot",
      q.approved_pdf_url as "approvedPdfUrl"
    from crm_quotations q
    left join ms_options currency_option
      on currency_option.id = q.currency
    where q.code = ${AUDIT_QUOTATION_CODE}
      and q.deleted_at is null
    limit 1
  `;

  if (fixtureQuotation) {
    return fixtureQuotation;
  }

  const [fallbackQuotation] = await sql`
    select
      q.id,
      q.code,
      q.organization_id as "organizationId",
      q.customer_id as "customerId",
      q.contact_id as "contactId",
      q.quotation_date as "quotationDate",
      q.valid_until as "validUntil",
      q.project_name as "projectName",
      q.project_location as "projectLocation",
      q.attention,
      q.reference,
      q.currency,
      q.subtotal,
      q.total_amount as "totalAmount",
      currency_option.code as "currencyCode",
      q.salesman_id as "salesmanId",
      q.created_by as "createdBy",
      q.created_at as "createdAt",
      q.approved_snapshot as "approvedSnapshot",
      q.approved_pdf_url as "approvedPdfUrl"
    from crm_quotations q
    left join ms_options currency_option
      on currency_option.id = q.currency
    where q.deleted_at is null
    order by q.updated_at desc
    limit 1
  `;

  if (!fallbackQuotation) {
    throw new Error('No quotation found for PDF audit');
  }

  return fallbackQuotation;
}

function getValueByPath(source: unknown, valuePath: string): unknown {
  return valuePath.split('.').reduce<unknown>((current, segment) => {
    if (current === null || current === undefined) {
      return undefined;
    }

    if (Array.isArray(current) && /^\d+$/.test(segment)) {
      return current[Number(segment)];
    }

    if (typeof current === 'object' && current !== null && segment in current) {
      return (current as Record<string, unknown>)[segment];
    }

    return undefined;
  }, source);
}

function formatPdfDate(dateString: string | Date | null | undefined) {
  if (!dateString) {
    return '-';
  }

  const date = dateString instanceof Date ? dateString : new Date(dateString);

  if (Number.isNaN(date.getTime())) {
    return '-';
  }

  return date.toLocaleDateString('en-US', {
    day: 'numeric',
    month: 'short',
    year: 'numeric'
  });
}

function formatPdfCurrency(amount: number | string | null | undefined, currencyCode = 'THB') {
  if (amount === null || amount === undefined) {
    return '-';
  }

  const normalized = typeof amount === 'number' ? amount : Number(String(amount).replaceAll(',', ''));

  if (!Number.isFinite(normalized)) {
    return '-';
  }

  const symbols: Record<string, string> = {
    THB: 'THB ',
    USD: '$',
    EUR: 'EUR '
  };
  const prefix = symbols[currencyCode.toUpperCase()] ?? `${currencyCode.toUpperCase()} `;
  return `${prefix}${new Intl.NumberFormat('en-US', {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2
  }).format(normalized)}`.trim();
}

function formatPdfCurrencyAmount(amount: number | string | null | undefined, currencyCode = 'THB') {
  if (amount === null || amount === undefined) {
    return '-';
  }

  const normalized = typeof amount === 'number' ? amount : Number(String(amount).replaceAll(',', ''));

  if (!Number.isFinite(normalized)) {
    return '-';
  }

  const symbols: Record<string, string> = {
    THB: '฿',
    USD: '$',
    EUR: 'EUR '
  };
  const prefix = symbols[currencyCode.toUpperCase()] ?? `${currencyCode.toUpperCase()} `;
  return `${prefix}${new Intl.NumberFormat('en-US', {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2
  }).format(normalized)}`.trim();
}

function buildPdfPriceTableData(amount: number | string | null | undefined, currencyCode?: string | null) {
  const normalizedCurrency = currencyCode?.trim() || 'THB';

  return [[
    PRICE_TABLE_LABEL,
    formatPdfCurrencyAmount(amount, normalizedCurrency),
    ' ',
    normalizedCurrency
  ]];
}

function formatTopicItems(items: Array<{ content?: string | null; sortOrder?: number | null }> = []) {
  if (!items.length) {
    return [['-']];
  }

  return [...items]
    .sort((left, right) => (left.sortOrder ?? Number.MAX_SAFE_INTEGER) - (right.sortOrder ?? Number.MAX_SAFE_INTEGER))
    .map((item) => [item.content?.trim() || '-']);
}

function formatPdfDecimal(amount: number | string | null | undefined) {
  if (amount === null || amount === undefined) {
    return '-';
  }

  const normalized =
    typeof amount === 'number' ? amount : Number(String(amount).replaceAll(',', ''));

  if (!Number.isFinite(normalized)) {
    return '-';
  }

  return new Intl.NumberFormat('en-US', {
    minimumFractionDigits: 0,
    maximumFractionDigits: 3
  }).format(normalized);
}

function buildAuditItemsTable(
  items: Array<{
    itemNumber?: number | null;
    description?: string | null;
    quantity?: number | null;
    unitLabel?: string | null;
    unitPrice?: number | null;
    discount?: number | null;
    totalPrice?: number | null;
    notes?: string | null;
  }>,
  currencyCode?: string | null
) {
  return items.map((item, index) => {
    const description = [item.description?.trim(), item.notes?.trim()]
      .filter((value): value is string => Boolean(value))
      .join('\n')
      .trim();

    return [
      String(item.itemNumber ?? index + 1),
      description || '-',
      formatPdfDecimal(item.quantity ?? null),
      item.unitLabel?.trim() || '-',
      formatPdfCurrency(item.unitPrice ?? null, currencyCode?.trim() || 'THB'),
      formatPdfCurrency(item.discount ?? 0, currencyCode?.trim() || 'THB'),
      formatPdfCurrency(item.totalPrice ?? null, currencyCode?.trim() || 'THB')
    ];
  });
}

function applyFormatMask(value: unknown, formatMask?: string | null) {
  if (value === null || value === undefined) {
    return value;
  }

  if (formatMask === 'currency' || formatMask?.startsWith('currency_')) {
    const currencyCode = formatMask === 'currency' ? 'THB' : formatMask.replace('currency_', '');
    return formatPdfCurrency(value as number | string, currencyCode);
  }

  if (formatMask === 'date' || formatMask === 'date_en') {
    return formatPdfDate(value as string);
  }

  return value;
}

function isBlankString(value: unknown) {
  return typeof value === 'string' && value.trim().length === 0;
}

function resolveScalarMappingValue(
  rawValue: unknown,
  formattedValue: unknown,
  defaultValue?: string | null
) {
  if (formattedValue !== undefined && formattedValue !== null && !isBlankString(formattedValue)) {
    return formattedValue;
  }

  if (rawValue !== undefined && rawValue !== null && !isBlankString(rawValue)) {
    return rawValue;
  }

  if (defaultValue !== undefined && defaultValue !== null && defaultValue.trim().length > 0) {
    return defaultValue;
  }

  return '-';
}

function normalizePdfmeTable(
  value: unknown,
  columns?: Array<{ sourceField: string; formatMask?: string | null }>
) {
  if (!Array.isArray(value) || value.length === 0) {
    return [['-']];
  }

  if (value.every((row) => Array.isArray(row))) {
    return (value as unknown[][]).map((row) =>
      row.map((cell) => {
        if (typeof cell === 'string') {
          const trimmedValue = cell.trim();

          if (trimmedValue) {
            return trimmedValue;
          }

          return cell.length > 0 ? cell : '-';
        }

        return String(cell ?? '-').trim() || '-';
      })
    );
  }

  if (!columns?.length) {
    return value.map((row) => [String(row ?? '-').trim() || '-']);
  }

  return value.map((row) => {
    const rowRecord = row as Record<string, unknown>;
    return columns.map((column) => {
      const formatted = applyFormatMask(rowRecord[column.sourceField], column.formatMask);
      return String(formatted ?? '-').trim() || '-';
    });
  });
}

function mapDocumentDataToTemplateInput(
  documentData: Record<string, unknown>,
  mappings: MappingRecord[],
  columns: MappingColumnRecord[]
) {
  const result: Record<string, unknown> = {};
  const columnMap = new Map<string, MappingColumnRecord[]>();

  for (const column of columns) {
    const items = columnMap.get(column.mappingId) ?? [];
    items.push(column);
    columnMap.set(column.mappingId, items);
  }

  for (const mapping of mappings) {
    const rawValue = getValueByPath(documentData, mapping.sourcePath);

    if (mapping.dataType === 'table') {
      const mappingColumns = columnMap.get(mapping.id) ?? [];
      let tableValue = rawValue;

      if (!Array.isArray(tableValue) && mapping.defaultValue) {
        try {
          const parsedDefault = JSON.parse(mapping.defaultValue) as unknown;
          tableValue = Array.isArray(parsedDefault) ? parsedDefault : tableValue;
        } catch {
          // ignore invalid legacy table defaults
        }
      }

      result[mapping.placeholderKey] = normalizePdfmeTable(tableValue, mappingColumns);
      continue;
    }

    if (mapping.dataType === 'multiline') {
      if (Array.isArray(rawValue)) {
        result[mapping.placeholderKey] = rawValue
          .map((item) => String(item ?? '').trim())
          .filter(Boolean)
          .join('\n');
        continue;
      }

      result[mapping.placeholderKey] =
        typeof rawValue === 'string' && rawValue.trim().length > 0
          ? rawValue
          : mapping.defaultValue ?? '-';
      continue;
    }

    const formattedValue = applyFormatMask(rawValue, mapping.formatMask);
    result[mapping.placeholderKey] = resolveScalarMappingValue(
      rawValue,
      formattedValue,
      mapping.defaultValue
    );
  }

  return result;
}

function normalizeBusinessRoleLabel(businessRole: string | null | undefined) {
  if (!businessRole) {
    return '-';
  }

  return (
    SIGNATURE_ROLE_FALLBACK_LABELS[businessRole] ??
    businessRole
      .split('_')
      .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
      .join(' ')
  );
}

function buildTopicInputs(topics: Array<{ topicType: string; items: Array<{ content: string; sortOrder: number }> }>) {
  const sortedTopics = [...topics].sort((left, right) => left.topicType.localeCompare(right.topicType));
  const topicInputs: Record<string, string[][]> = {};

  for (const [index, topic] of sortedTopics.entries()) {
    topicInputs[`topic_1_${index}`] = [[topic.topicType || '-']];
    topicInputs[`item_topic_1_${index}`] = formatTopicItems(topic.items);
  }

  return topicInputs;
}

export async function buildAuditPayload(sql: SqlClient): Promise<AuditPayload> {
  const quotation = await findAuditQuotation(sql);
  const [organization, customer, contact, itemRows, topicRows, topicItemRows, partyRows, jobTitles] =
    await Promise.all([
      sql`
        select id, name
        from organizations
        where id = ${quotation.organizationId}
        limit 1
      `.then((rows) => rows[0] ?? null),
      sql`
        select id, name, address, phone, email
        from crm_customers
        where id = ${quotation.customerId}
        limit 1
      `.then((rows) => rows[0] ?? null),
      quotation.contactId
        ? sql`
            select id, name, email, mobile, position
            from crm_customer_contacts
            where id = ${quotation.contactId}
            limit 1
          `.then((rows) => rows[0] ?? null)
        : Promise.resolve(null),
      sql`
        select
          qi.id,
          qi.item_number as "itemNumber",
          qi.description,
          qi.quantity,
          qi.unit,
          qi.unit_price as "unitPrice",
          qi.discount,
          qi.total_price as "totalPrice",
          qi.sort_order as "sortOrder",
          u.label as "unitLabel"
        from crm_quotation_items qi
        left join ms_options u
          on u.id = qi.unit
        where qi.quotation_id = ${quotation.id}
          and qi.deleted_at is null
        order by qi.sort_order asc, qi.item_number asc
      `,
      sql`
        select
          qt.id,
          qt.title,
          qt.sort_order as "sortOrder",
          qt.topic_type as "topicTypeId",
          o.code as "topicCode",
          coalesce(o.label, qt.title) as "topicLabel"
        from crm_quotation_topics qt
        left join ms_options o
          on o.id = qt.topic_type
        where qt.quotation_id = ${quotation.id}
          and qt.deleted_at is null
        order by qt.sort_order asc, qt.created_at asc
      `,
      sql`
        select
          qti.id,
          qti.topic_id as "topicId",
          qti.content,
          qti.sort_order as "sortOrder"
        from crm_quotation_topic_items qti
        where qti.deleted_at is null
          and qti.topic_id in (
            select id
            from crm_quotation_topics
            where quotation_id = ${quotation.id}
              and deleted_at is null
          )
        order by qti.sort_order asc, qti.created_at asc
      `,
      sql`
        select
          qc.id,
          qc.customer_id as "customerId",
          qc.role,
          qc.is_primary as "isPrimary"
        from crm_quotation_customers qc
        where qc.quotation_id = ${quotation.id}
          and qc.deleted_at is null
      `,
      sql`
        select code, label
        from ms_options
        where organization_id = ${quotation.organizationId}
          and category = 'crm_job_title'
          and is_active = true
          and deleted_at is null
      `
    ]);

  if (!organization || !customer) {
    throw new Error('Audit quotation references missing organization or customer');
  }

  const activeTemplateVersions = await getActiveTemplateVersions(sql);
  const templateVersion = activeTemplateVersions.find(
    (item) => item.organizationId === quotation.organizationId && item.documentType === 'quotation'
  );

  if (!templateVersion) {
    throw new Error(`No active quotation template version found for organization ${quotation.organizationId}`);
  }

  const { mappings, columns } = await getMappingsForVersion(sql, templateVersion.versionId);
  const topicItemsByTopicId = new Map<string, Array<{ content: string; sortOrder: number }>>();

  for (const topicItemRow of castRows<{ topicId: string; content: string; sortOrder: number }>(
    topicItemRows
  )) {
    const items = topicItemsByTopicId.get(topicItemRow.topicId) ?? [];
    items.push({
      content: topicItemRow.content,
      sortOrder: topicItemRow.sortOrder
    });
    topicItemsByTopicId.set(topicItemRow.topicId, items);
  }

  const topics = (topicRows as Array<Record<string, unknown>>).map((topicRow) => ({
    id: String(topicRow.id),
    title: String(topicRow.title ?? topicRow.topicLabel ?? 'Topic'),
    topicCode: String(topicRow.topicCode ?? ''),
    topicType: String(topicRow.topicLabel ?? topicRow.title ?? 'Topic'),
    items: topicItemsByTopicId.get(String(topicRow.id)) ?? []
  }));
  const exclusionTopic = topics.find((topic) => topic.topicCode === 'exclusion') ?? topics[0] ?? null;
  const topicInputs = buildTopicInputs(
    topics.map((topic) => ({
      topicType: topic.topicType,
      items: topic.items
    }))
  );

  const [approvalActions, userRows, membershipRows] = await Promise.all([
    sql`
      select
        aa.acted_by as "actedBy",
        aa.acted_at as "actedAt",
        aa.action,
        aa.step_number as "stepNumber",
        aps.role_code as "roleCode",
        u.name as "actorName"
      from crm_approval_actions aa
      inner join crm_approval_requests ar
        on ar.id = aa.approval_request_id
      left join crm_approval_steps aps
        on aps.workflow_id = ar.workflow_id
       and aps.step_number = aa.step_number
       and aps.deleted_at is null
      left join users u
        on u.id = aa.acted_by
      where ar.entity_type = 'quotation'
        and ar.entity_id = ${quotation.id}
        and ar.organization_id = ${quotation.organizationId}
        and aa.deleted_at is null
      order by aa.acted_at asc, aa.created_at asc
    `,
    sql`
      select id, name
      from users
      where id in (${quotation.salesmanId ?? quotation.createdBy}, ${quotation.createdBy})
    `,
    sql`
      select user_id as "userId", business_role as "businessRole"
      from memberships
      where organization_id = ${quotation.organizationId}
    `
  ]);
  const userMap = new Map(
    castRows<{ id: string; name: string }>(userRows).map((row) => [row.id, row.name])
  );
  const membershipMap = new Map(
    castRows<{ userId: string; businessRole: string }>(membershipRows).map((row) => [
      row.userId,
      row.businessRole
    ])
  );
  const jobTitleMap = new Map(
    castRows<{ code: string; label: string }>(jobTitles).map((row) => [row.code, row.label])
  );
  const approvedActions = (approvalActions as Array<Record<string, unknown>>).filter(
    (row) => row.action === 'approve'
  );
  const finalApprovedAction = approvedActions.at(-1) ?? null;
  const approvedByAction =
    approvedActions.filter((row) => row.roleCode !== finalApprovedAction?.roleCode).at(-1) ?? null;
  const preparedByUserId = (quotation.salesmanId as string | null) ?? (quotation.createdBy as string);

  const resolvePosition = (userId: string | null | undefined, roleCode?: string | null) => {
    const businessRole = (userId ? membershipMap.get(userId) : null) ?? roleCode ?? null;
    return businessRole ? jobTitleMap.get(businessRole) ?? normalizeBusinessRoleLabel(businessRole) : '-';
  };
  const currencyCode =
    typeof quotation.currencyCode === 'string' && quotation.currencyCode.trim()
      ? quotation.currencyCode.trim()
      : 'THB';

  const documentData: Record<string, unknown> = {
    company: {
      id: organization.id,
      name: organization.name
    },
    customer: {
      id: customer.id,
      name: customer.name,
      address: customer.address,
      phone: customer.phone,
      email: customer.email
    },
    contact: contact
      ? {
          id: contact.id,
          name: contact.name,
          email: contact.email,
          mobile: contact.mobile,
          position: contact.position
        }
      : null,
    quotation: {
      id: quotation.id,
      code: quotation.code,
      quotationDate: quotation.quotationDate,
      validUntil: quotation.validUntil,
      projectName: quotation.projectName,
      projectLocation: quotation.projectLocation,
      attention: quotation.attention,
      reference: quotation.reference,
      currency: currencyCode,
      currencyCode,
      subtotal: quotation.subtotal,
      totalAmount: quotation.totalAmount
    },
    items: (itemRows as Array<Record<string, unknown>>).map((row) => ({
      id: row.id,
      itemNumber: row.itemNumber,
      description: row.description,
      quantity: row.quantity,
      unitLabel: row.unitLabel ?? row.unit ?? '-',
      unitPrice: row.unitPrice,
      discount: row.discount,
      totalPrice: row.totalPrice
    })),
    quotationCustomers: partyRows,
    topics: {
      all: topics,
      exclusion: exclusionTopic ? [exclusionTopic] : []
    },
    pdfme: {
      labels: PDFME_STATIC_LABELS,
      quotation_date: formatPdfDate(quotation.quotationDate as string),
      quotation_price: formatPdfCurrency(quotation.subtotal as number, currencyCode),
      quotation_price_data: buildPdfPriceTableData(quotation.subtotal as number, currencyCode),
      exclusion_data: formatTopicItems(exclusionTopic?.items ?? []),
      topic_inputs: topicInputs
    },
    signatures: {
      preparedBy: {
        name: userMap.get(preparedByUserId) ?? '-',
        position: resolvePosition(preparedByUserId),
        actedAt: new Date(String(quotation.createdAt)).toISOString()
      },
      approvedBy: {
        name: String(approvedByAction?.actorName ?? '-'),
        position: resolvePosition(
          (approvedByAction?.actedBy as string | null | undefined) ?? null,
          (approvedByAction?.roleCode as string | null | undefined) ?? null
        ),
        actedAt: approvedByAction?.actedAt ? new Date(String(approvedByAction.actedAt)).toISOString() : null
      },
      authorizedBy: {
        name: String(finalApprovedAction?.actorName ?? '-'),
        position: resolvePosition(
          (finalApprovedAction?.actedBy as string | null | undefined) ?? null,
          (finalApprovedAction?.roleCode as string | null | undefined) ?? null
        ),
        actedAt: finalApprovedAction?.actedAt
          ? new Date(String(finalApprovedAction.actedAt)).toISOString()
          : null
      }
    }
  };

  const templateInput = {
    ...mapDocumentDataToTemplateInput(documentData, mappings, columns),
    ...resolveQuotationStaticTemplateInputs(
      documentData as unknown as Parameters<
        typeof resolveQuotationStaticTemplateInputs
      >[0],
    ),
    ...topicInputs
  };
const templateFields = extractTemplateFields(templateVersion.schemaJson);
const hasItemsTableField = templateFields.some((field) => field.name === 'items_table');
const normalizedTemplateInput = normalizeTemplateInputAliases(
  templateFields,
  {
    ...templateInput,
    ...(hasItemsTableField
      ? {
          items_table: buildAuditItemsTable(
            (documentData as { items: Parameters<typeof buildAuditItemsTable>[0] }).items,
            (documentData as { quotation: { currencyCode?: string | null } }).quotation.currencyCode
          )
        }
      : {}),
  }
);

  return {
    organizationId: quotation.organizationId,
    quotationId: quotation.id,
    quotationCode: quotation.code,
    templateVersionId: templateVersion.versionId,
    approvedTemplateVersionId:
      typeof quotation.approvedTemplateVersionId === 'string'
        ? quotation.approvedTemplateVersionId
        : null,
    templateName: templateVersion.templateName,
    templateVersion: templateVersion.version,
    templateSchema: templateVersion.schemaJson,
    mappings,
    mappingColumns: columns,
    documentData,
    templateInput: normalizedTemplateInput,
    topicInputs,
    quotationMetrics: {
      topicCount: topics.length,
      itemCount: (itemRows as Array<unknown>).length,
      partyCount: (partyRows as Array<unknown>).length
    },
    approvedSnapshot: normalizeSnapshotPayload(quotation.approvedSnapshot),
    approvedArtifactReference:
      typeof quotation.approvedPdfUrl === 'string' ? quotation.approvedPdfUrl : null
  };
}

export function summarizeIssues(statuses: Array<'PASS' | 'WARNING' | 'FAIL'>) {
  if (statuses.includes('FAIL')) {
    return 'FAIL';
  }

  if (statuses.includes('WARNING')) {
    return 'WARNING';
  }

  return 'PASS';
}

function castRows<T>(rows: unknown): T[] {
  return rows as T[];
}

export interface ComputedSnapshotPayload {
  quotationId: string;
  approvedAt: string | null;
  documentData: Record<string, unknown>;
  templateVersionId: string;
  templateInput: Record<string, unknown>;
  generatedAt: string;
  generatedBy: string;
}

export function buildComputedSnapshotPayload(
  payload: AuditPayload,
  options?: { generatedAt?: string; generatedBy?: string }
): ComputedSnapshotPayload {
  return {
    quotationId: payload.quotationId,
    approvedAt:
      typeof (payload.documentData.approval as Record<string, unknown> | undefined)?.approvedAt ===
      'string'
        ? ((payload.documentData.approval as Record<string, unknown>).approvedAt as string)
        : null,
    documentData: payload.documentData,
    templateVersionId: payload.templateVersionId,
    templateInput: payload.templateInput,
    generatedAt: options?.generatedAt ?? 'audit-generated',
    generatedBy: options?.generatedBy ?? 'audit-suite'
  };
}

export function getSnapshotTemplateInput(snapshot: Record<string, unknown> | null) {
  if (!snapshot) {
    return null;
  }

  return snapshot.templateInput && typeof snapshot.templateInput === 'object'
    ? (snapshot.templateInput as Record<string, unknown>)
    : null;
}

export function getSnapshotDocumentData(snapshot: Record<string, unknown> | null) {
  if (!snapshot) {
    return null;
  }

  return snapshot.documentData && typeof snapshot.documentData === 'object'
    ? (snapshot.documentData as Record<string, unknown>)
    : null;
}
