/**
 * Import-mapping modal controller.
 *
 *   openImportMappingModal({ importId, filename, fc, onResult })
 *
 * Populates the modal from the parsed FeatureCollection, lets the user pick
 * a target type and adjust the field map, then calls onResult with one of:
 *
 *   { action: 'cancel' }              — keep as Other / view only
 *   { action: 'save',    targetType, mapping }
 *   { action: 'upload',  targetType, mapping }
 *
 * The caller is responsible for updating external_imports + the staged
 * features (this module knows nothing about the DB or the map).
 *
 * See LUPMIS2_Import_Upload_Design.docx §3.1.
 */

import { Modal } from 'bootstrap';
import {
  TARGET_TYPES,
  TARGET_FIELDS,
  detectTargetType,
  autoMapFields,
  listSourceFields,
  sampleSourceValues,
} from './import-detect.js';

const els = {};   // cached DOM lookups
let modal = null;
let state = null; // { importId, filename, fc, sourceFields, mapping, targetType, onResult }

function cacheEls() {
  if (els.root) return;
  els.root           = document.getElementById('importMappingModal');
  els.filename       = document.getElementById('import-modal-filename');
  els.summary        = document.getElementById('import-modal-summary');
  els.target         = document.getElementById('import-modal-target');
  els.targetHint     = document.getElementById('import-modal-target-hint');
  els.fieldsWrap     = document.getElementById('import-modal-fields-wrap');
  els.tbody          = document.getElementById('import-modal-fields-tbody');
  els.btnSave        = document.getElementById('import-modal-save');
  els.btnSaveUpload  = document.getElementById('import-modal-save-upload');
  els.btnCancel      = document.getElementById('import-modal-cancel');

  // Populate the target-type dropdown once.
  if (els.target && !els.target.dataset.populated) {
    els.target.innerHTML = TARGET_TYPES
      .map((t) => `<option value="${t.key}">${t.label}</option>`)
      .join('');
    els.target.dataset.populated = '1';
  }

  // Event wiring (idempotent).
  if (els.target && !els.target.dataset.wired) {
    els.target.dataset.wired = '1';
    els.target.addEventListener('change', onTargetChange);
  }
  if (els.btnSave && !els.btnSave.dataset.wired) {
    els.btnSave.dataset.wired = '1';
    els.btnSave.addEventListener('click', () => finish('save'));
  }
  if (els.btnSaveUpload && !els.btnSaveUpload.dataset.wired) {
    els.btnSaveUpload.dataset.wired = '1';
    els.btnSaveUpload.addEventListener('click', () => finish('upload'));
  }
  // Cancel uses Bootstrap's data-bs-dismiss; we hook the hidden event so
  // closing via × / ESC / Cancel all behave the same: a 'cancel' result.
  if (els.root && !els.root.dataset.wired) {
    els.root.dataset.wired = '1';
    els.root.addEventListener('hidden.bs.modal', () => {
      if (state?.onResult && !state._resolved) {
        state._resolved = true;
        state.onResult({ action: 'cancel' });
      }
      state = null;
    });
  }
}

/** Render the field-mapping table for the current targetType. */
function renderFieldsTable() {
  const targetType = state.targetType;
  const columns = TARGET_FIELDS[targetType] || [];

  // "Other (view only)" → no fields to map.
  if (targetType === 'other' || columns.length === 0) {
    els.fieldsWrap.style.display = 'none';
    return;
  }
  els.fieldsWrap.style.display = '';

  // Each dropdown option shows the source field name AND a one-feature
  // sample value so the user can recognise the attribute by its content,
  // not just its name. Sample is computed once in state.sourceSamples; see
  // sampleSourceValues() in import-detect.js.
  //
  // The field name is rendered with Unicode mathematical-bold characters
  // (U+1D400 / U+1D41A / U+1D7CE blocks) because HTML / CSS bold doesn't
  // render inside <option> elements — browsers strip markup and ignore
  // font-weight on options. Unicode bold works cross-browser without
  // HTML and gives an unmistakable visual distinction from the sample.
  const sampleSuffix = (s) => {
    const v = state.sourceSamples[s];
    return v ? ` — [${escapeHtml(v)}]` : ' — [(empty)]';
  };
  const optionsHtml = ['<option value="">(none)</option>']
    .concat(state.sourceFields.map((s) =>
      `<option value="${escapeAttr(s)}">${escapeHtml(toBoldUnicode(s))}${sampleSuffix(s)}</option>`))
    .join('');

  els.tbody.innerHTML = columns.map((col) => {
    const current = state.mapping[col] || '';
    // Mark the matching option as selected. The opening `<option value="…">`
    // is unique per field, so prefix-replace is safe regardless of label text.
    const select = optionsHtml.replace(
      `<option value="${escapeAttr(current)}">`,
      `<option value="${escapeAttr(current)}" selected>`
    );
    return `
      <tr>
        <td><code>${escapeHtml(col)}</code></td>
        <td>
          <select class="form-select form-select-sm import-field-map"
                  data-col="${escapeAttr(col)}">
            ${select}
          </select>
        </td>
      </tr>
    `;
  }).join('');

  // Update state.mapping when a row's source field changes.
  els.tbody.querySelectorAll('.import-field-map').forEach((sel) => {
    sel.addEventListener('change', (e) => {
      const col = e.target.dataset.col;
      state.mapping[col] = e.target.value || null;
    });
  });
}

/** Handler: target dropdown changed. */
function onTargetChange() {
  const newType = els.target.value;
  state.targetType = newType;
  state.mapping = autoMapFields(state.fc, newType);

  // Target-type hint: gentle reminder when "other" is chosen.
  if (newType === 'other') {
    els.targetHint.innerHTML =
      '<em>This dataset will be visible on the map but cannot be uploaded ' +
      'to the database. You can change the type later.</em>';
    els.btnSave.disabled = false;             // saves the "other" state
    els.btnSaveUpload.disabled = true;
  } else {
    els.targetHint.innerHTML =
      'Each LUPMIS2 column is matched to a source field where possible. ' +
      'You can override any choice below.';
    els.btnSave.disabled = false;
    els.btnSaveUpload.disabled = false;
  }
  renderFieldsTable();
}

/** Handler: Save or Save+Upload clicked. */
function finish(action) {
  if (!state || state._resolved) return;
  state._resolved = true;
  const { targetType, mapping, onResult } = state;
  modal.hide();
  if (onResult) {
    onResult({
      action,                                                    // 'save' | 'upload'
      targetType,
      mapping: targetType === 'other' ? null : { ...mapping },
    });
  }
  state = null;
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/**
 * Open the modal for a fresh import. Resolves via onResult callback.
 *
 * @param {Object} opts
 * @param {number} opts.importId    — staging row id (purely for the caller)
 * @param {string} opts.filename
 * @param {Object} opts.fc          — parsed FeatureCollection
 * @param {Function} opts.onResult  — see module header
 */
export function openImportMappingModal(opts) {
  cacheEls();
  if (!els.root) {
    console.warn('[ImportModal] Modal element missing — calling onResult with cancel');
    opts.onResult?.({ action: 'cancel' });
    return;
  }

  const fc = opts.fc;
  const featureCount = fc?.features?.length ?? 0;
  const targetType = detectTargetType(fc);

  state = {
    importId: opts.importId,
    filename: opts.filename,
    fc,
    sourceFields: listSourceFields(fc),
    sourceSamples: sampleSourceValues(fc),
    targetType,
    mapping: autoMapFields(fc, targetType),
    onResult: opts.onResult,
    _resolved: false,
  };

  // Header summary
  els.filename.textContent = opts.filename || 'imported dataset';
  els.summary.textContent  = `— ${featureCount} feature${featureCount === 1 ? '' : 's'}`;

  // Initial dropdown + hint + fields
  els.target.value = state.targetType;
  onTargetChange();

  modal = Modal.getOrCreateInstance(els.root);
  modal.show();
}

// ---------------------------------------------------------------------------
// HTML-escape helpers
// ---------------------------------------------------------------------------

function escapeHtml(s) {
  return String(s)
    .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
function escapeAttr(s) { return escapeHtml(s); }

/**
 * Map ASCII letters and digits to their Unicode mathematical-bold equivalents
 * (U+1D400 / U+1D41A / U+1D7CE blocks). Used to render source-attribute names
 * inside <option> labels with visible weight, since HTML/CSS bold is ignored
 * by browsers inside <select> dropdowns. Characters outside [A-Za-z0-9] pass
 * through unchanged so underscores, hyphens, and punctuation stay readable.
 */
function toBoldUnicode(s) {
  let out = '';
  for (const ch of String(s)) {
    const cp = ch.codePointAt(0);
    if (cp >= 0x61 && cp <= 0x7A) {           // a-z
      out += String.fromCodePoint(cp - 0x61 + 0x1D41A);
    } else if (cp >= 0x41 && cp <= 0x5A) {    // A-Z
      out += String.fromCodePoint(cp - 0x41 + 0x1D400);
    } else if (cp >= 0x30 && cp <= 0x39) {    // 0-9
      out += String.fromCodePoint(cp - 0x30 + 0x1D7CE);
    } else {
      out += ch;
    }
  }
  return out;
}
