/**
 * Analysis panel controller — Stage 1 of the GIS Analytical Tools concept.
 *
 *   openAnalysisModal(mapView)
 *
 * Drives two families of client-side analysis:
 *   • Overlays          — intersect / clip / difference / union between layers
 *   • Zonal statistics   — summarise features (or a numeric field) per zone
 *
 * Both run entirely in the browser via Turf.js, so they work offline. Results
 * are added to the map as a new styled layer in an "Analysis" group, from
 * where they can be exported with the existing GIS export tools.
 *
 * Same Bootstrap modal pattern as src/export-gis-modal.js and src/import-modal.js.
 */

import { Modal } from 'bootstrap';
import { runOverlay, OVERLAY_OPS } from './analysis/overlay.js';
import { zonalStatistics, listNumericFields, STATS, MEMBERSHIP_MODES } from './analysis/zonal.js';
import { showToast } from './toast.js';

const ANALYSIS_GROUP_ID = 9001;
const ANALYSIS_GROUP_TITLE = 'Analysis';

const els = {};
let modal = null;
let mapView = null;
let layerChoices = [];   // [{ layer, title, count }]
let drawnScope = null;   // { features: Set, label } when opened from a Circle/Area popup
let lastZonal = null;    // { rows, params } — the most recent zonal run, for export

// ---------------------------------------------------------------------------
// Element wiring
// ---------------------------------------------------------------------------

function cacheEls() {
  if (els.root) return;
  els.root        = document.getElementById('analysisModal');
  els.mode        = Array.from(document.querySelectorAll('input[name="analysis-mode"]'));
  els.overlayBox  = document.getElementById('analysis-overlay-box');
  els.zonalBox    = document.getElementById('analysis-zonal-box');
  els.op          = document.getElementById('analysis-op');
  els.opHint      = document.getElementById('analysis-op-hint');
  els.layerA      = document.getElementById('analysis-layer-a');
  els.layerB      = document.getElementById('analysis-layer-b');
  els.layerBWrap  = document.getElementById('analysis-layer-b-wrap');
  els.zoneLayer   = document.getElementById('analysis-zone-layer');
  els.inputLayer  = document.getElementById('analysis-input-layer');
  els.membership  = document.getElementById('analysis-membership');
  els.memberHint  = document.getElementById('analysis-membership-hint');
  els.field       = document.getElementById('analysis-field');
  els.statsBox    = document.getElementById('analysis-stats-box');
  els.outName     = document.getElementById('analysis-output-name');
  els.result      = document.getElementById('analysis-result');
  els.btnRun      = document.getElementById('analysis-run');
  els.btnExport   = document.getElementById('analysis-export-table');
  els.scope       = document.getElementById('analysis-scope');
  els.scopeHint   = document.getElementById('analysis-scope-hint');

  if (!els.root.dataset.wired) {
    els.root.dataset.wired = '1';
    els.mode.forEach((r) => r.addEventListener('change', onModeChange));
    els.op.addEventListener('change', onOpChange);
    els.membership.addEventListener('change', onMembershipChange);
    els.inputLayer.addEventListener('change', refreshFieldOptions);
    els.scope.addEventListener('change', onScopeChange);
    els.btnRun.addEventListener('click', onRun);
    els.btnExport.addEventListener('click', onExportTable);
  }
}

// ---------------------------------------------------------------------------
// Population
// ---------------------------------------------------------------------------

function layerOptionsHtml(selectedIdx = null) {
  return layerChoices.map((c, i) =>
    `<option value="${i}"${i === selectedIdx ? ' selected' : ''}>` +
    `${escapeHtml(c.title)} (${c.count.toLocaleString()})</option>`
  ).join('');
}

function populateOps() {
  els.op.innerHTML = OVERLAY_OPS
    .map((o) => `<option value="${o.key}">${escapeHtml(o.label)}</option>`).join('');
  els.membership.innerHTML = MEMBERSHIP_MODES
    .map((m) => `<option value="${m.key}">${escapeHtml(m.label)}</option>`).join('');
  els.statsBox.innerHTML = STATS.map((s) => `
    <div class="form-check form-check-inline">
      <input class="form-check-input" type="checkbox" value="${s.key}"
             id="analysis-stat-${s.key}"${s.key === 'count' ? ' checked' : ''}>
      <label class="form-check-label" for="analysis-stat-${s.key}">${escapeHtml(s.label)}</label>
    </div>`).join('');
}

function refreshFieldOptions() {
  const choice = layerChoices[Number(els.inputLayer.value)];
  const feats = choice ? choice.layer.getSource().getFeatures() : [];
  const fields = listNumericFields(feats);
  els.field.innerHTML = '<option value="">(none — count/area only)</option>' +
    fields.map((f) => `<option value="${escapeHtml(f)}">${escapeHtml(f)}</option>`).join('');
  els.field.disabled = fields.length === 0;
}

// ---------------------------------------------------------------------------
// UI reactions
// ---------------------------------------------------------------------------

function currentMode() {
  return (els.mode.find((r) => r.checked) || {}).value || 'overlay';
}

function onModeChange() {
  // A previous zonal table doesn't belong to a new overlay run.
  lastZonal = null; syncExportButton();
  const overlay = currentMode() === 'overlay';
  els.overlayBox.classList.toggle('d-none', !overlay);
  els.zonalBox.classList.toggle('d-none', overlay);
  suggestName();
}

function onOpChange() {
  const op = OVERLAY_OPS.find((o) => o.key === els.op.value);
  els.opHint.textContent = op ? op.hint : '';
  // Union can run on a single layer; the others need a second one.
  const needsB = op ? op.needsB : true;
  els.layerBWrap.querySelector('label').textContent =
    needsB ? 'Layer B' : 'Layer B (optional)';
  suggestName();
}

function onMembershipChange() {
  const m = MEMBERSHIP_MODES.find((x) => x.key === els.membership.value);
  els.memberHint.textContent = m ? m.hint : '';
}

function suggestName() {
  if (els.outName.dataset.touched === '1') return;
  if (currentMode() === 'overlay') {
    const op = els.op.value || 'overlay';
    els.outName.value = `${op}_result`;
  } else {
    els.outName.value = 'zonal_stats';
  }
}

// ---------------------------------------------------------------------------
// Run
// ---------------------------------------------------------------------------

/**
 * Features of the chosen layer, narrowed by the "Apply to" scope.
 * 'view' uses the source's spatial index, so it stays fast on large layers.
 */
function featuresOf(select) {
  const choice = layerChoices[Number(select.value)];
  if (!choice) return [];
  const source = choice.layer.getSource();
  const scope = els.scope ? els.scope.value : 'all';

  if (scope === 'view') {
    const map = mapView.getMap();
    const extent = map.getView().calculateExtent(map.getSize());
    return source.getFeaturesInExtent(extent);
  }
  if (scope === 'selection') {
    const selected = new Set(mapView.getSelectedFeatures());
    return source.getFeatures().filter((f) => selected.has(f));
  }
  if (scope === 'drawn' && drawnScope) {
    return source.getFeatures().filter((f) => drawnScope.features.has(f));
  }
  return source.getFeatures();
}

function onScopeChange() {
  const scope = els.scope.value;
  if (scope === 'drawn' && drawnScope) {
    els.scopeHint.textContent =
      `${drawnScope.features.size.toLocaleString()} feature(s) caught by the ${drawnScope.label}.`;
  } else if (scope === 'selection') {
    const n = mapView ? mapView.getSelectedFeatures().length : 0;
    els.scopeHint.textContent = n === 0
      ? 'Nothing selected yet — use the Digitise tool: shift-click features, or hold Ctrl/Cmd and drag a box.'
      : `${n.toLocaleString()} feature(s) currently selected.`;
  } else if (scope === 'view') {
    els.scopeHint.textContent = 'Only features inside the current map view are analysed — zoom to the area of interest first.';
  } else {
    els.scopeHint.textContent = 'Every feature in the chosen layer. Narrow this if a large layer is slow.';
  }
}

function ensureAnalysisGroup() {
  let group = mapView.getLayerGroupByTitle(ANALYSIS_GROUP_TITLE);
  if (!group) {
    mapView.addLayerGroup(ANALYSIS_GROUP_ID, ANALYSIS_GROUP_TITLE,
      'Results of client-side spatial analysis');
    group = mapView.getLayerGroupByTitle(ANALYSIS_GROUP_TITLE);
  }
  return group;
}

function onRun() {
  els.result.innerHTML = '';
  els.btnRun.disabled = true;
  // Yield a frame so the button state paints before a heavy synchronous run.
  setTimeout(() => {
    try {
      const mode = currentMode();
      const name = (els.outName.value || '').trim() ||
        (mode === 'overlay' ? 'overlay_result' : 'zonal_stats');

      let geojson, summary;
      let zonalWarnings = [];
      if (mode === 'overlay') {
        lastZonal = null;
        const op = els.op.value;
        const a = featuresOf(els.layerA);
        const b = featuresOf(els.layerB);
        const needsB = (OVERLAY_OPS.find((o) => o.key === op) || {}).needsB;
        if (a.length === 0) throw new Error('Layer A has no features.');
        if (needsB && b.length === 0) throw new Error('This operation needs a second layer with features.');
        geojson = runOverlay(op, a, b);
        summary = `${geojson.features.length.toLocaleString()} feature(s) produced.`;
      } else {
        const zones = featuresOf(els.zoneLayer);
        const inputs = featuresOf(els.inputLayer);
        const stats = Array.from(els.statsBox.querySelectorAll('input:checked')).map((i) => i.value);
        const field = els.field.value || null;
        if (stats.length === 0) throw new Error('Select at least one statistic.');
        const needsField = stats.some((s) => (STATS.find((x) => x.key === s) || {}).needsField);
        if (needsField && !field) throw new Error('Sum / mean / min / max need a numeric field.');

        const res = zonalStatistics({
          zoneFeatures: zones, inputFeatures: inputs,
          stats, field, membership: els.membership.value,
        });
        geojson = res.geojson;
        zonalWarnings = res.warnings || [];
        summary = renderRows(res.rows);

        // Keep the table AND the settings that produced it, so the Excel export
        // is self-describing — a table of numbers with no record of how it was
        // produced can't be checked or reproduced later.
        const membershipLabel = (MEMBERSHIP_MODES.find((m) => m.key === els.membership.value) || {}).label;
        lastZonal = {
          rows: res.rows,
          warnings: res.warnings || [],
          params: {
            'Analysis': 'Zonal statistics',
            'Zone layer': labelOfSelect(els.zoneLayer),
            'Features summarised': labelOfSelect(els.inputLayer),
            'Applied to': scopeLabel(),
            'Zones analysed': zones.length,
            'Features analysed': inputs.length,
            'Counted when': membershipLabel || els.membership.value,
            'Statistics': stats.map((k) => (STATS.find((x) => x.key === k) || {}).label || k).join(', '),
            'Numeric field': field || '(none)',
            'Result layer': name,
          },
        };
      }

      if (!geojson.features.length) {
        els.result.innerHTML =
          `<div class="alert alert-warning py-2 px-3 mb-0">No result features — the layers may not overlap.</div>`;
        return;
      }

      const group = ensureAnalysisGroup();
      mapView.addGeoJSONLayer(geojson, name, {
        strokeColor: '#d97706', strokeWidth: 2, fillColor: 'rgba(217,119,6,0.18)',
      }, group);

      syncExportButton();
      const warnHtml = zonalWarnings.length
        ? `<div class="alert alert-warning py-2 px-3 mb-2" style="font-size:0.85rem;">` +
          zonalWarnings.map((w) => `<div>${escapeHtml(w)}</div>`).join('') + `</div>`
        : '';
      els.result.innerHTML =
        `<div class="alert alert-success py-2 px-3 mb-2">Added layer “${escapeHtml(name)}”.</div>` +
        warnHtml +
        (typeof summary === 'string' && summary.startsWith('<') ? summary : `<div class="small text-muted">${escapeHtml(summary)}</div>`);
      showToast(`Analysis complete — “${name}” added to the Analysis group.`, 'success', 4000);
    } catch (err) {
      console.error('[Analysis] run failed:', err);
      els.result.innerHTML =
        `<div class="alert alert-danger py-2 px-3 mb-0">${escapeHtml(err.message || String(err))}</div>`;
    } finally {
      els.btnRun.disabled = false;
    }
  }, 20);
}

/** Human label of a layer <select>, e.g. "Parcels (25,004)". */
function labelOfSelect(select) {
  const opt = select.options[select.selectedIndex];
  return opt ? opt.textContent : '(none)';
}

/** Human label of the current "Apply to" scope. */
function scopeLabel() {
  const opt = els.scope.options[els.scope.selectedIndex];
  return opt ? opt.textContent : 'Whole layer';
}

/**
 * Export the most recent zonal table to .xlsx — two sheets, the figures plus
 * the parameters that produced them. The writer is loaded on demand.
 */
async function onExportTable() {
  if (!lastZonal) return;
  els.btnExport.disabled = true;
  try {
    const { exportXlsx } = await import('./analysis/xlsx.js');

    const { rows, params, warnings } = lastZonal;
    const keys = Object.keys(rows[0] || {}).filter((k) => k !== 'zone');
    const resultRows = [
      { cells: ['Zone', ...keys], bold: true },
      ...rows.map((r) => ['zone' in r ? r.zone : '', ...keys.map((k) => (r[k] === null || r[k] === undefined ? '' : r[k]))]),
    ];

    const paramRows = [
      { cells: ['LUPMIS2 — Zonal statistics'], bold: true },
      [],
      { cells: ['Parameter', 'Value'], bold: true },
      ...Object.entries(params).map(([k, v]) => [k, v]),
      ['Exported', new Date().toLocaleString()],
      // Carry any caveats into the workbook, so a figure cannot be read out of
      // context once it leaves the application.
      ...((warnings && warnings.length)
        ? [[], { cells: ['Notes'], bold: true }, ...warnings.map((w) => ['', w])]
        : []),
    ];

    const base = (params['Result layer'] || 'zonal_stats')
      .toString().trim().replace(/[^A-Za-z0-9_\-]+/g, '_') || 'zonal_stats';
    exportXlsx(
      [{ name: 'Parameters', rows: paramRows }, { name: 'Results', rows: resultRows }],
      `${base}.xlsx`
    );
    showToast('Table exported to Excel.', 'success', 3000);
  } catch (err) {
    console.error('[Analysis] table export failed:', err);
    showToast(`Could not export the table: ${err.message}`, 'error', 6000);
  } finally {
    els.btnExport.disabled = false;
  }
}

/** Show or hide the export button depending on whether a table exists. */
function syncExportButton() {
  if (!els.btnExport) return;
  els.btnExport.classList.toggle('d-none', !lastZonal);
}

function renderRows(rows) {
  if (!rows || !rows.length) return '';
  const keys = Object.keys(rows[0]).filter((k) => k !== 'zone');
  const head = ['Zone', ...keys.map((k) => k.replace(/^zs_/, ''))]
    .map((h) => `<th class="text-nowrap">${escapeHtml(h)}</th>`).join('');
  const body = rows.map((r) =>
    `<tr><td class="text-nowrap">${escapeHtml(String(r.zone))}</td>` +
    keys.map((k) => `<td>${r[k] === null || r[k] === undefined ? '—' : escapeHtml(String(r[k]))}</td>`).join('') +
    '</tr>').join('');
  return `<div class="table-responsive" style="max-height:220px;overflow:auto;">
    <table class="table table-sm table-striped mb-0" style="font-size:0.85rem;">
      <thead class="table-light" style="position:sticky;top:0;"><tr>${head}</tr></thead>
      <tbody>${body}</tbody>
    </table></div>`;
}

// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------

/**
 * Open the Analysis panel.
 * @param {import('./components/MapView.js').default} view
 */
export function openAnalysisModal(view, opts = {}) {
  mapView = view;
  cacheEls();

  // When launched from a Circle/Area analysis popup, offer that catch as a
  // scope and pre-select it — the whole point of starting from a drawn shape.
  drawnScope = (opts.scopeFeatures && opts.scopeFeatures.length)
    ? { features: new Set(opts.scopeFeatures), label: opts.scopeLabel || 'drawn area' }
    : null;
  syncDrawnScopeOption();

  layerChoices = mapView.listVectorLayers({ nonEmptyOnly: true });
  if (layerChoices.length === 0) {
    showToast('No vector layers with features are loaded — nothing to analyse yet.', 'warning', 5000);
    return;
  }

  populateOps();
  const optsA = layerOptionsHtml(0);
  const optsB = layerOptionsHtml(layerChoices.length > 1 ? 1 : 0);
  els.layerA.innerHTML = optsA;
  els.layerB.innerHTML = optsB;
  els.zoneLayer.innerHTML = optsA;
  els.inputLayer.innerHTML = optsB;

  els.outName.dataset.touched = '';
  els.outName.addEventListener('input', () => { els.outName.dataset.touched = '1'; }, { once: true });
  els.result.innerHTML = '';

  lastZonal = null;
  els.scope.value = drawnScope ? 'drawn' : 'all';
  refreshFieldOptions();
  onOpChange();
  onMembershipChange();
  onScopeChange();
  onModeChange();

  modal = modal || new Modal(els.root);
  modal.show();
}

/**
 * Add or remove the "From drawn area" scope option depending on whether the
 * panel was opened from a Circle/Area popup.
 * @private
 */
function syncDrawnScopeOption() {
  const existing = els.scope.querySelector('option[value="drawn"]');
  if (!drawnScope) {
    if (existing) existing.remove();
    return;
  }
  const label = `From ${drawnScope.label} (${drawnScope.features.size.toLocaleString()} features)`;
  if (existing) {
    existing.textContent = label;
  } else {
    const opt = document.createElement('option');
    opt.value = 'drawn';
    opt.textContent = label;
    els.scope.insertBefore(opt, els.scope.firstChild);
  }
}

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