// Nordbank — Batch CSV builder for sherpa-api automated policy import.
// =============================================================================
//
// PURPOSE
// -------
// Convert the locally-stored Nordbank batch application rows (see
// dbListBatchApplications) into a CSV file that the sherpa-api batch import
// endpoint can consume to create equivalent loan_protection_insurance policies.
//
// CSV SCHEMA RATIONALE
// --------------------
// The column set mirrors two things:
//
//   1. sherpa-api's CSVRow interface (see
//      /sherpa-api/src/services/automatedPolicyService.ts, around lines 17–30).
//      That interface requires `externalClientId` and recognizes a fixed set
//      of customer columns (firstName, lastName, email, phoneNumber, country,
//      address, city, postalCode, …). It also accepts any additional columns
//      as dotted paths that map onto PolicyParameter.name / quote parameters
//      (the `[key: string]: string | undefined` index signature).
//
//   2. Nordbank's existing live integration (see buildSherpaPayload() in
//      sherpa.jsx). The dotted column names — for example
//      `insuranceCoverage.totalSumInsured` and `insuranceCoverage.repaymentTerm`
//      — match the policy/quote parameter paths Nordbank already writes when
//      calling /v1/policy/embed-checkout. The CSV import resolves those dotted
//      names by writing into the nested `param.path` structure, so a batch
//      import produces policies that are field-for-field equivalent to the
//      live checkout flow.
//
// FORMAT
// ------
// - RFC 4180: fields are quoted only when they contain `"`, `,`, `\n`, or
//   `\r`; internal `"` is doubled to `""`. Line ending is `\r\n` (RFC 4180 +
//   Excel-friendly).
// - A UTF-8 BOM is prepended so Excel opens accented characters (æ, ø, å, …)
//   correctly without manual encoding selection.
//
// ROBUSTNESS
// ----------
// Each row is converted inside a try/catch. A malformed application row
// (missing `data`, JSON-parse failure upstream, etc.) is logged with
// console.warn and emitted as a partial row containing whatever fields could
// be derived — the batch import will reject incomplete rows rather than us
// throwing and aborting the whole export.
//
// USAGE
// -----
//   const csv = buildBatchCsv(applications);
//   downloadBatchCsv(batchCsvFilename(), csv);
//
// EXPORTS (attached to window)
//   - buildBatchCsv(applications) -> string
//   - downloadBatchCsv(filename, csv) -> void
//   - batchCsvFilename(prefix?)    -> string
//
// No imports, no module system. Loaded via <script type="text/babel"> in
// Nordbank.html. Pure functions; no React.
// =============================================================================

// ---------------------------------------------------------------------------
// Column order — exported to sherpa-api batch import.
// Order matters: humans read the file in Excel, and stable order makes diffs
// reviewable. The set matches sherpa-api CSVRow + Nordbank PolicyParameter
// paths used by buildSherpaPayload().
// ---------------------------------------------------------------------------
// Sherpa-api's automatedPolicyService (mapCsvToParameters) only reads CSV
// columns that correspond to:
//   - the Customer model's standard fields (firstName, lastName, email, …)
//   - PolicyParameter.name rows configured on the product's active Policy
//
// It ALWAYS recomputes the premium via calculatePriceforQuote() from the
// product's Pricing rules — so any premium/tax columns in the CSV are silently
// ignored, and createPolicyOrderFromPremium re-validates the computed value
// against itself. We therefore emit ONLY the columns sherpa-api actually
// reads: customer identity + the two quote parameters Nordbank's
// loan_protection_insurance product uses to price the policy
// (insuranceCoverage.totalSumInsured + insuranceCoverage.repaymentTerm in
// months — exact same pair scenario 1 sends to /v1/policy/embed-checkout).
const BATCH_CSV_COLUMNS = [
  'externalClientId',
  'externalTransactionId',
  'firstName',
  'lastName',
  'email',
  'mobilePhone',
  'birthDate',
  'idNumber',
  'address',
  'city',
  'postalCode',
  'country',
  'insuranceCoverage.totalSumInsured',
  'insuranceCoverage.repaymentTerm',
];

// Country dial codes for E.164 normalization. Mirrors COUNTRY_DIAL_CODE in
// sherpa.jsx — kept inline because this file is standalone (no imports).
const BATCH_CSV_COUNTRY_DIAL_CODE = { NO: '47', DK: '45', SE: '46', FI: '358' };

// ---------------------------------------------------------------------------
// E.164 phone normalizer.
// Reimplemented from toE164() in sherpa.jsx — kept inline so this file has no
// cross-file dependencies. Accepts: "+47 900 00 000", "47 900 00 000",
// "900 00 000", "00 47 900 00 000". Returns "" when input is empty/invalid.
// ---------------------------------------------------------------------------
function _batchCsvToE164(raw, countryCode) {
  if (!raw) return '';
  let s = String(raw).replace(/[\s().-]/g, '');
  if (s.startsWith('+')) {
    return '+' + s.slice(1).replace(/\D/g, '');
  }
  s = s.replace(/\D/g, '');
  if (s.startsWith('00')) s = s.slice(2);
  const dial = BATCH_CSV_COUNTRY_DIAL_CODE[(countryCode || '').toUpperCase()];
  if (dial && s.startsWith('0')) s = dial + s.slice(1);
  return s ? '+' + s : '';
}

// ---------------------------------------------------------------------------
// Nordbank externalClientId — must match nordbankExternalClientId() in
// sherpa.jsx so the customer portal correlates batch-imported policies with
// the same customer as live-checkout policies.
// ---------------------------------------------------------------------------
function _batchCsvExternalClientId(email) {
  if (!email) return '';
  return 'nb-' + String(email).trim().toLowerCase();
}

// ---------------------------------------------------------------------------
// RFC 4180 cell escape: quote only when needed; double internal quotes.
// ---------------------------------------------------------------------------
function _batchCsvEscapeCell(value) {
  if (value === null || value === undefined) return '';
  const s = String(value);
  if (s === '') return '';
  if (/[",\r\n]/.test(s)) {
    return '"' + s.replace(/"/g, '""') + '"';
  }
  return s;
}

// ---------------------------------------------------------------------------
// Round to 2 decimals — used for premium amounts. Returns a number; the
// caller stringifies via _batchCsvEscapeCell. NaN/non-finite returns ''.
// ---------------------------------------------------------------------------
function _batchCsvRound2(n) {
  const num = Number(n);
  if (!Number.isFinite(num)) return '';
  return Math.round(num * 100) / 100;
}

// ---------------------------------------------------------------------------
// Split a full name into first / last. First whitespace-delimited token is
// firstName; the remainder is lastName. Empty input yields ['', ''].
// ---------------------------------------------------------------------------
function _batchCsvSplitName(fullName) {
  const trimmed = String(fullName || '').trim();
  if (!trimmed) return ['', ''];
  const parts = trimmed.split(/\s+/);
  const firstName = parts.shift() || '';
  const lastName = parts.join(' ');
  return [firstName, lastName];
}

// ---------------------------------------------------------------------------
// Safe ISO timestamp for signed_at (unix ms). Returns '' on invalid input
// rather than throwing or emitting "Invalid Date".
// ---------------------------------------------------------------------------
function _batchCsvIsoFromMs(ms) {
  if (ms === null || ms === undefined || ms === '') return '';
  const n = Number(ms);
  if (!Number.isFinite(n)) return '';
  const d = new Date(n);
  if (Number.isNaN(d.getTime())) return '';
  return d.toISOString();
}

// ---------------------------------------------------------------------------
// Build a single CSV row (array, column-aligned) from an application object.
// Never throws — returns whatever could be derived; missing fields become ''.
// ---------------------------------------------------------------------------
function _batchCsvRowFromApplication(app) {
  // Defensive: `data` is the JSON snapshot at sign time and is the primary
  // source of personal/loan details. Top-level row fields (monthly, etc.) are
  // mirrored from the DB row.
  const data = (app && app.data) || {};
  const personal = (data && data.personal) || {};

  const country = String(data.country || app.country || '').toUpperCase();
  const currency = String(data.currency || app.currency || '').toLowerCase();

  const [firstName, lastName] = _batchCsvSplitName(personal.name);
  const email = String(personal.email || '').toLowerCase();

  const amount = (data.amount !== undefined && data.amount !== null)
    ? data.amount
    : (app.amount !== undefined && app.amount !== null ? app.amount : '');

  const termYears = (data.termYears !== undefined && data.termYears !== null)
    ? data.termYears
    : (app.term_years !== undefined && app.term_years !== null ? app.term_years : '');

  // repaymentTerm is months; sherpa.jsx uses Math.round(termYears * 12).
  let repaymentTermMonths = '';
  const termYearsNum = Number(termYears);
  if (Number.isFinite(termYearsNum)) {
    repaymentTermMonths = String(Math.round(termYearsNum * 12));
  }

  // Per spec: confirmationNumber from row (data.confirmationNumber is a
  // mirror, but the DB column is canonical).
  const externalTransactionId = app.confirmation_number != null
    ? String(app.confirmation_number)
    : (data.confirmationNumber != null ? String(data.confirmationNumber) : '');

  // Suppress unused-var lint: termYears is intentionally only used to compute
  // repaymentTermMonths above; the raw years value is not a sherpa-api column.
  void termYears;

  // Map by column name so reordering BATCH_CSV_COLUMNS doesn't desync values.
  const values = {
    externalClientId: _batchCsvExternalClientId(email),
    externalTransactionId,
    firstName,
    lastName,
    email,
    mobilePhone: _batchCsvToE164(personal.phone, country),
    birthDate: personal.dob || '',
    idNumber: personal.ssn || '',
    address: personal.address || '',
    city: personal.city || '',
    postalCode: personal.postcode || '',
    country,
    'insuranceCoverage.totalSumInsured': amount === '' ? '' : String(amount),
    'insuranceCoverage.repaymentTerm': repaymentTermMonths,
  };

  return BATCH_CSV_COLUMNS.map((col) => values[col] !== undefined ? values[col] : '');
}

// ---------------------------------------------------------------------------
// PUBLIC: buildBatchCsv(applications) -> string
// ---------------------------------------------------------------------------
function buildBatchCsv(applications) {
  const rows = Array.isArray(applications) ? applications : [];

  const headerLine = BATCH_CSV_COLUMNS.map(_batchCsvEscapeCell).join(',');

  const dataLines = [];
  for (let i = 0; i < rows.length; i++) {
    const app = rows[i];
    try {
      const cells = _batchCsvRowFromApplication(app || {});
      dataLines.push(cells.map(_batchCsvEscapeCell).join(','));
    } catch (err) {
      // Never throw mid-row — emit a best-effort partial row using only the
      // fields safe to read directly from the top-level row, and warn loudly
      // so the operator can fix the source data.
      console.warn(
        '[lib-batch-csv] Failed to build CSV row for application id=' +
          (app && app.id != null ? app.id : '<unknown>') +
          '; emitting partial row.',
        err
      );
      const fallback = {
        externalTransactionId: app && app.confirmation_number != null
          ? String(app.confirmation_number)
          : '',
        country: app && app.country ? String(app.country).toUpperCase() : '',
      };
      const cells = BATCH_CSV_COLUMNS.map((col) =>
        fallback[col] !== undefined ? fallback[col] : ''
      );
      dataLines.push(cells.map(_batchCsvEscapeCell).join(','));
    }
  }

  // UTF-8 BOM (U+FEFF) so Excel auto-detects UTF-8 and renders æ/ø/å/ä/ö.
  const BOM = '﻿';
  const body = [headerLine].concat(dataLines).join('\r\n');
  return BOM + body + '\r\n';
}

// ---------------------------------------------------------------------------
// PUBLIC: downloadBatchCsv(filename, csv) -> void
// Browser-only. No-op when csv is empty/falsy.
// ---------------------------------------------------------------------------
function downloadBatchCsv(filename, csv) {
  if (!csv) return;
  if (typeof document === 'undefined' || typeof URL === 'undefined' || typeof Blob === 'undefined') {
    console.warn('[lib-batch-csv] downloadBatchCsv called in non-browser environment; no-op.');
    return;
  }

  let url;
  let anchor;
  try {
    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
    url = URL.createObjectURL(blob);
    anchor = document.createElement('a');
    anchor.href = url;
    anchor.download = filename || 'batch.csv';
    // Hidden from layout; appending is required in some browsers for click().
    anchor.style.display = 'none';
    document.body.appendChild(anchor);
    anchor.click();
  } catch (err) {
    console.warn('[lib-batch-csv] downloadBatchCsv failed:', err);
  } finally {
    if (anchor && anchor.parentNode) {
      anchor.parentNode.removeChild(anchor);
    }
    if (url) {
      try { URL.revokeObjectURL(url); } catch (_e) { /* ignore */ }
    }
  }
}

// ---------------------------------------------------------------------------
// PUBLIC: batchCsvFilename(prefix='nordbank-batch') -> string
// Example: nordbank-batch-2026-05-27T14-32-05-123.csv
// Timestamp uses ISO with `:` and `.` replaced by `-` (filesystem-safe) and
// the trailing `Z` stripped (the timezone is implicit / UTC).
// ---------------------------------------------------------------------------
function batchCsvFilename(prefix) {
  const safePrefix = prefix && String(prefix).trim() ? String(prefix).trim() : 'nordbank-batch';
  const ts = new Date().toISOString().replace(/[:.]/g, '-').replace(/Z$/, '');
  return safePrefix + '-' + ts + '.csv';
}

// ---------------------------------------------------------------------------
// Export to window — no module system in this app.
// ---------------------------------------------------------------------------
Object.assign(window, {
  buildBatchCsv,
  downloadBatchCsv,
  batchCsvFilename,
});
