// src/editorBridge.js
import JSONEditor from 'jsoneditor';
import 'jsoneditor/dist/jsoneditor.css';

import ace from 'ace-builds/src-noconflict/ace'
import 'ace-builds/src-noconflict/ext-searchbox'
import 'ace-builds/src-noconflict/mode-json'
import 'ace-builds/src-noconflict/worker-json'
// themes
import 'ace-builds/src-noconflict/theme-textmate'
import 'ace-builds/src-noconflict/theme-tomorrow_night_eighties'
import 'ace-builds/src-noconflict/theme-monokai';
import 'ace-builds/src-noconflict/theme-github';

import Ajv2020 from 'ajv/dist/2020';
import Ajv2019 from 'ajv/dist/2019';
import Ajv from 'ajv';                // draft-07
import draft4 from 'ajv-draft-04';    // patch for draft-04
import addFormats from 'ajv-formats';
import addErrors from 'ajv-errors';

// globals
let editor = null
let currentSchema = null
let currentData = null
let externalAjv = null
let externalValidate = null
let menuObserver = null;

// --- AUTOSAVE STATE ---
let lastSentHash = null;
let autosaveTimer = null;


// APPLY QGIS THEME
// ---- editorBridge.js (top-level) ----
window.applyTheme = function (t) {
  const root = document.documentElement;
  const vars = {
    '--qgis-bg': t?.bg || '#1e1e1e',
    '--qgis-fg': t?.fg || '#e6e6e6',
    '--qgis-panel': t?.panel || '#242526',
    '--qgis-panel-text': t?.panelText || '#e6e6e6',
    '--qgis-border': t?.border || 'rgba(255,255,255,.08)',
    '--qgis-accent': t?.accent || '#4d94ff',
    '--qgis-accent-text': t?.accentText || '#ffffff',
    '--qgis-muted': t?.muted || '#9aa0a6',
  };
  for (const [k, v] of Object.entries(vars)) {
    document.documentElement.style.setProperty(k, v, 'important');
  }

  // Optional: drive Ace theme off isDark hint
  const isDark = !!t?.isDark;
  if (window.editor && editor.aceEditor && typeof desiredAceTheme === 'function') {
    editor.aceEditor.setTheme(desiredAceTheme(isDark));
  }
};

function makeAjvForSchema(schema) {
  const url = (schema && schema.$schema) || ''
  const base = { allErrors: true, strict: false, allowUnionTypes: true, verbose: true }
  let ajv
  if (/2020-12/.test(url)) ajv = new Ajv2020(base)
  else if (/2019-09/.test(url)) ajv = new Ajv2019(base)
  else if (/draft-04|draft\/4/.test(url)) { ajv = new Ajv({ ...base, schemaId: 'auto' }); draft4(ajv) }
  else ajv = new Ajv(base) // draft-07 default
  try { addFormats(ajv) } catch { }
  try { addErrors(ajv) } catch { }
  return ajv
}

// AUTOSAVE methods
// stable stringify (order keys) to detect real content changes
function stableStringify(obj) {
  const seen = new WeakSet();
  const sort = (v) => {
    if (!v || typeof v !== 'object') return v;
    if (seen.has(v)) return null; // break cycles just in case
    seen.add(v);
    if (Array.isArray(v)) return v.map(sort);
    const out = {};
    Object.keys(v).sort().forEach(k => { out[k] = sort(v[k]); });
    return out;
  };
  try { return JSON.stringify(sort(obj)); } catch { return ''; }
}

// send immediately (called by debounced scheduler)
function autosaveSendNow(payload) {
  if (!window.backend || typeof window.backend.onSave !== 'function') {
    statusSet('Save unavailable: no backend bridge', '', '#f5b342');
    return;
  }
  try {
    window.backend.onSave(JSON.stringify(payload), 'jsoneditor');
    lastSentHash = stableStringify(payload);
    statusSet('Synced ✔', '');
  } catch (e) {
    statusSet('Sync failed: ' + (e?.message || e), '#ff5a5a');
  }
}

// schedule debounce (call this whenever external validation is OK)
function scheduleAutosaveIfChanged(payload, delayMs = 1000) {
  const h = stableStringify(payload);
  if (!h || h === lastSentHash) return;      // nothing new
  if (autosaveTimer) clearTimeout(autosaveTimer);
  autosaveTimer = setTimeout(() => autosaveSendNow(payload), delayMs);
}

// STATUSBAR methods

function getStatusEl() {
  return document.getElementById('status');
}

function statusToggle() {
  const el = getStatusEl();
  if (!el) return;

  // Only toggle when there are details
  if (el.getAttribute('data-has-details') !== '1') return;

  const expanded = el.classList.toggle('expanded');
  const summary = el.dataset.summary || '';
  const details = el.dataset.details || '';

  el.textContent = expanded ? (details || summary) : (summary || details);
}

function statusBindClick() {
  const el = getStatusEl();
  if (!el) return;
  // Remove any old handler (builds/hot-reloads can duplicate)
  el.onclick = null;
  el.addEventListener('click', statusToggle);
}

export function statusSet(summary, details, color) {
  const el = getStatusEl();
  if (!el) return;

  // Store both forms for toggle
  el.dataset.summary = summary || '';
  el.dataset.details = details || '';
  if (details) {
    el.setAttribute('data-has-details', '1');
  } else {
    el.removeAttribute('data-has-details');
  }

  // Reset collapsed view on every new message
  el.classList.remove('expanded');
  el.textContent = summary || details || '';
  el.style.color = color || '#f5b342';

  // (Re)bind the click every time to be safe
  statusBindClick();
}



function isDark() {
  // simple heuristic; works in QGIS dock
  try {
    const bg = getComputedStyle(document.body).backgroundColor
    if (!bg) return false
    const m = bg.match(/\d+/g)
    if (!m) return false
    const [r, g, b] = m.slice(0, 3).map(Number)
    const L = 0.2126 * r + 0.7152 * g + 0.0722 * b
    return L < 128
  } catch { return false }
}

function desiredAceTheme() {
  return isDark() ? 'ace/theme/tomorrow_night_eighties' : 'ace/theme/github'
}

// SAVE utility methods

function canSaveNow() {
  // if you track external Ajv: don’t allow save when invalid
  try {
    const errs = (typeof externalValidate === 'function' && currentData != null)
      ? (externalValidate(currentData) ? [] : externalValidate.errors || [])
      : [];
    return errs.length === 0;
  } catch { return true; }
}

function doSave() {
  if (!editor) return;
  // Re-validate once more, show errors and bail if invalid
  if (typeof externalValidate === 'function') {
    const ok = externalValidate(editor.get());
    if (!ok) {
      const { summary, details } = formatAjvErrors(externalValidate.errors || []);
      statusSet(summary || 'Fix validation errors before saving', details || '');
      return;
    }
  }
  // collect data from the editor (you mounted it on config_obj)
  const data = editor.get();
  // send to Python (Qt WebChannel)
  if (window.backend && typeof window.backend.onSave === 'function') {
    try {
      window.backend.onSave(JSON.stringify(data), 'jsoneditor');
      statusSet('Saved ✔', '');
    } catch (e) {
      statusSet('Save failed: ' + (e?.message || e), '#ff5a5a');
    }
  } else {
    statusSet('Save unavailable: no QWC bridge', '#f5b342');
  }
}

// UI setup and status messages

function ensureUI() {
  const root = document.getElementById('app');
  if (!root) return;
  if (!document.getElementById('status')) {
    const s = document.createElement('div');
    s.id = 'status';
    s.style.cssText = 'font:12px sans-serif;color:#999;margin:.25rem 0;';
    root.prepend(s);
  }
  if (!document.getElementById('editor')) {
    const pane = document.createElement('div');
    pane.id = 'editor';
    // pane.style.cssText = 'height: calc(100vh - 16px);';
    root.appendChild(pane);
  }
}

function setStatus(msg, color) {
  const el = document.getElementById('status');
  if (!el) return;
  el.textContent = msg || '';
  el.style.color = color || '#999';
  el.style.fontWeight = '600';   // <-- make it bold/semibold
}

function formatAjvErrors(errs) {
  if (!errs || !errs.length) return { summary: '', details: '' }
  const path = (e) => {
    const p = e.instancePath || e.dataPath || ''
    return p ? p.replace(/\//g, '.').replace(/^\./, '') : '(root)'
  }
  const msg = (e) => {
    const k = e.keyword || ''
    let m = e.message || ''
    if (!m) return k || 'invalid'
    if (k === 'enum' && e.params && e.params.allowedValues) m += ' ' + JSON.stringify(e.params.allowedValues)
    else if (k === 'type' && e.params && e.params.type) m += ` (${e.params.type})`
    else if ((k === 'minimum' || k === 'maximum') && e.params && 'limit' in e.params) m += ` (${e.params.limit})`
    return m
  }
  const lines = []
  const seen = new Set()
  for (let i = 0; i < errs.length && lines.length < 12; i++) {
    const line = `• ${path(errs[i])}: ${msg(errs[i])}`
    if (seen.has(line)) continue
    seen.add(line)
    lines.push(line)
  }
  const summary = lines.length > 1 ? `${lines[0]}  … +${lines.length - 1} more` : (lines[0] || '')
  return { summary, details: lines.join('\n') }
}


function buildValidationTexts(errors) {
  // Return { summary, details } strings for the status bar.

  if (!errors || !errors.length) return { summary: '', details: '' };

  // Helpers
  const path = (e) => {
    const p = e.instancePath || e.dataPath || '';
    return p ? p.replace(/\//g, '.').replace(/^\./, '') : '(root)';
  };
  const msg = (e) => {
    const k = e.keyword || '';
    let m = e.message || '';
    if (!m) return k || 'validation error';
    if (k === 'enum' && e.params && e.params.allowedValues) {
      m += ' ' + JSON.stringify(e.params.allowedValues);
    } else if (k === 'type' && e.params && e.params.type) {
      m += ` (${e.params.type})`;
    } else if ((k === 'minimum' || k === 'maximum') && e.params && 'limit' in e.params) {
      m += ` (${e.params.limit})`;
    }
    return m;
  };

  // If ALL errors are at root, show a short summary; rely on inline markers for detail.
  const allRoot = errors.every((e) => path(e) === '(root)');
  if (allRoot) {
    // Try to show option names for anyOf/oneOf
    const labels = (function labelsFor(k) {
      try {
        const arr = currentSchema && currentSchema[k];
        if (!Array.isArray(arr)) return null;
        return arr.map((s, i) => (s && (s.title || s.description)) ? (s.title || s.description)
          : `option ${i + 1}`);
      } catch { return null; }
    })('oneOf') || (function () {
      try {
        const arr = currentSchema && currentSchema.anyOf;
        if (!Array.isArray(arr)) return null;
        return arr.map((s, i) => (s && (s.title || s.description)) ? (s.title || s.description)
          : `option ${i + 1}`);
      } catch { return null; }
    })();

    const summary = labels && labels.length
      ? `Schema mismatch: choose one of → ${labels.slice(0, 4).join(' | ')}${labels.length > 4 ? ` … +${labels.length - 4}` : ''}`
      : 'Schema mismatch at root. Expand highlighted fields for details.';
    // No details body for pure root errors (avoids ugly “(root)” spam)
    return { summary, details: '' };
  }

  // Otherwise, produce a de-duplicated list of field errors
  const seen = new Set();
  const lines = [];
  for (let i = 0; i < errors.length; i++) {
    const line = `• ${path(errors[i])}: ${msg(errors[i])}`;
    if (seen.has(line)) continue;
    seen.add(line);
    lines.push(line);
  }

  // Summary = first line (+count); Details = full list (multi-line)
  const summary = lines.length > 1 ? `${lines[0]}  … +${lines.length - 1} more` : (lines[0] || '');
  const details = lines.join('\n');
  return { summary, details };
}


function mountEditor(schemaObj, dataObj) {
  try { editor && editor.destroy && editor.destroy() } catch { }

  currentSchema = schemaObj || null
  currentData = dataObj || null

  // Build external Ajv validator (do NOT pass it into JSONEditor)
  try {
    externalAjv = makeAjvForSchema(currentSchema || {})
    externalValidate = currentSchema ? externalAjv.compile(currentSchema) : null
  } catch {
    externalAjv = null
    externalValidate = null
  }

  const opts = {
    mode: 'tree',
    modes: ['tree', 'code', 'text', 'view'],
    mainMenuBar: true,
    statusBar: false,
    navigationBar: false,
    schema: schemaObj || undefined,
    ace: ace,

    // Recreate the save button every time
    onModeChange: function (newMode) {
      // re-add immediately (toolbar just got rebuilt)
      if (newMode === 'code' && editor && editor.aceEditor && typeof desiredAceTheme === 'function') {
        editor.aceEditor.setTheme(desiredAceTheme())
      };
      // addSaveButton();
      // updateSaveButtonEnabled();
      // theme for ace (if you use code mode)
      if (newMode === 'code' && editor?.aceEditor && typeof desiredAceTheme === 'function') {
        editor.aceEditor.setTheme(desiredAceTheme());
      }
    },
    // Save to the layer functionality
    // onCreateMenu: function (items /*, node */) {
    //   // push a separator before our button (optional)
    //   items.push({ type: 'separator' });

    //   // push the Save button
    //   items.push({
    //     type: 'button',
    //     text: 'Save',
    //     title: 'Save to layer (Ctrl+S)',
    //     className: 'jsoneditor-save',
    //     onClick: () => doSave(),
    //     // JSONEditor will read disabled; we’ll update it in onChange below
    //     disabled: !canSaveNow(),
    //   });

    //   return items;
    // },


    // keep JSONEditor’s own inline validation; we drive the status via external Ajv:
    onValidationError: function () {
      // no-op here; status comes from external Ajv run below
    },

    onChange: function () {
      // (run the external Ajv + status)
      try { currentData = editor.get(); } catch { currentData = null; }

      if (externalValidate) {
        const ok = externalValidate(currentData);

        if (ok) {
          statusSet('', '');                // clear statusbar
          scheduleAutosaveIfChanged(currentData);   // ← autosync when valid
        } else {
          const { summary, details } = formatAjvErrors(externalValidate.errors || []);
          statusSet(summary, details, '#b96a25ff');
          if (autosaveTimer) { clearTimeout(autosaveTimer); autosaveTimer = null; } // stop pending sync
        }
      } else {
        // no validator configured: treat as OK (optional)
        scheduleAutosaveIfChanged(currentData);
      }
    }
  }

  editor = new JSONEditor(document.getElementById('editor'), opts)



  // Ace code editor and status
  if (editor && editor.aceEditor && typeof desiredAceTheme === 'function') {
    editor.aceEditor.setTheme(desiredAceTheme())
  }
  if (dataObj != null) editor.set(dataObj)

  // initial external validation
  if (externalValidate) {
    const ok = externalValidate(dataObj)
    if (ok) statusSet('', '')
    else {
      const { summary, details } = formatAjvErrors(externalValidate.errors || [])
      statusSet(summary, details)
    }
  } else {
    statusSet('', '', '#6a6a6aff')
  }

  // bind ctrl s to save back to layer
  (function bindCtrlSOnce() {
    if (window.__jsoneditor_save_bound) return;
    window.__jsoneditor_save_bound = true;
    window.addEventListener('keydown', (ev) => {
      const isMac = navigator.platform.toLowerCase().includes('mac');
      const hit = (isMac ? ev.metaKey : ev.ctrlKey) && (ev.key === 's' || ev.key === 'S');
      if (!hit) return;
      ev.preventDefault();
      doSave();
    }, true);
  })();
}



function pickVersionFromData(dataObj, fallback) {
  if (dataObj?.config_obj && Number.isInteger(dataObj.config_obj.version)) return dataObj.config_obj.version;
  if (Number.isInteger(dataObj?.version)) return dataObj.version;
  if (Number.isInteger(dataObj?.config_revision)) return dataObj.config_revision;
  return fallback ?? null;
}

export function installEditorBridge() {
  if (window.__editor_bridge_installed) return; // idempotent
  window.__editor_bridge_installed = true;

  ensureUI();

  // Python pushes exact schema:
  window.editorLoad = function (dataObj, schemaObj, meta) {
    mountEditor(schemaObj, dataObj);
    setStatus(`Editing ${meta?.app_name ?? ''}${meta?.version != null ? ' (schema v' + meta.version + ')' : ''}`);
  };

  // Python pushes {version: schema} map; JS picks by feature's version

  window.editorLoadAuto = function (dataObj, versionsByVer, meta) {
    // normalize keys to integers without Object.entries / at
    var byVer = {};
    for (var k in (versionsByVer || {})) {
      if (Object.prototype.hasOwnProperty.call(versionsByVer, k)) {
        var ik = parseInt(k, 10);
        byVer[ik] = versionsByVer[k];
      }
    }

    // collect and sort versions
    var vers = [];
    for (var vk in byVer) {
      if (Object.prototype.hasOwnProperty.call(byVer, vk)) {
        vers.push(parseInt(vk, 10));
      }
    }
    vers.sort(function (a, b) { return a - b; });

    // pick version from dataObj
    function pickVersionFromData(obj, fallback) {
      if (obj && obj.config_obj && typeof obj.config_obj.version === 'number') return obj.config_obj.version;
      if (obj && typeof obj.version === 'number') return obj.version;
      if (obj && typeof obj.config_revision === 'number') return obj.config_revision;
      return fallback;
    }

    var fallback = vers.length ? vers[vers.length - 1] : null;
    var chosen = pickVersionFromData(dataObj, fallback);
    var schema = (chosen !== null && byVer[chosen]) ? byVer[chosen] : (fallback !== null ? byVer[fallback] : {});

    // mountEditor & setStatus should already be defined in this file
    mountEditor(schema, dataObj);

    var appName = (meta && meta.app_name) ? meta.app_name : '';
    var verLabel = (chosen !== null && chosen !== undefined) ? (' (schema v' + chosen + ')') : '';
    setStatus('Editing ' + appName + verLabel);
  };

  window.editorSetStatus = (msg) => setStatus(msg);
  window.editorShowWarning = (msg) => setStatus(msg, '#f5b342');
  window.editorShowError = (msg) => setStatus(msg, '#ff5a5a');

  window.editorTriggerSave = function () {
    try {
      const json = editor.get();
      if (window.backend && window.backend.onSave) {
        window.backend.onSave(JSON.stringify(json), 'jsoneditor');
        setStatus('Saved.');
      } else {
        setStatus('No backend.onSave connected', '#ff5a5a');
      }
    } catch (e) {
      setStatus(`Invalid JSON: ${e}`, '#ff5a5a');
    }
  };
}
