// Harnex Visual Runtime — bridge protocol version 1.
// Loaded by visual_runtime.html. Receives a Visual Package via the
// HarnexVisualBoot call below (invoked from Flutter after the WebView
// posts ready). Sets up the ctx object, monkey-patches dangerous APIs
// before the package's JS runs, and bridges height/event/error/log
// messages back to Flutter.

(function () {
  'use strict';

  // === Defensive patches — installed before any package code runs. ===
  // Non-configurable / non-writable so the package cannot restore them.
  function block(name, holder) {
    try {
      Object.defineProperty(holder, name, {
        configurable: false,
        get: function () {
          throw new Error('harnex-visual: ' + name + ' is disabled');
        },
      });
    } catch (e) { /* already non-configurable on some hosts; ignore */ }
  }
  block('fetch', window);
  block('XMLHttpRequest', window);
  block('WebSocket', window);
  block('localStorage', window);
  block('sessionStorage', window);
  block('indexedDB', window);
  block('open', window);              // window.open
  // document.cookie — must override via property
  try {
    Object.defineProperty(document, 'cookie', {
      configurable: false,
      get: function () { return ''; },
      set: function () { /* swallow */ },
    });
  } catch (e) { /* ignore */ }
  // eval / Function constructor are accessible via globalThis; can't fully
  // remove without breaking the runtime itself. Provide a final-stage
  // veto: replace the bindings on window. (Note: the package author can
  // still use Function() via reflection — defense in depth, not absolute.)
  try { window.eval = function () { throw new Error('eval disabled'); }; } catch (e) {}
  try { window.Function = function () { throw new Error('Function disabled'); }; } catch (e) {}

  // === Bridge helpers ===
  var pendingVisualId = null;

  function post(message) {
    if (window.flutter_inappwebview && window.flutter_inappwebview.callHandler) {
      window.flutter_inappwebview.callHandler('harnex_visual', message);
    }
  }

  function postError(message, stack) {
    post({
      visualId: pendingVisualId,
      type: 'error',
      message: String(message || ''),
      stack: stack ? String(stack) : undefined,
    });
  }

  window.addEventListener('error', function (e) {
    postError(e.message, e.error && e.error.stack);
  });
  window.addEventListener('unhandledrejection', function (e) {
    postError('unhandled rejection: ' + (e.reason && e.reason.message));
  });

  // === Vertical scroll passthrough ===
  // The figures never scroll vertically, but the native WebView swallows the
  // wheel — so on the desktop app the chat history could not scroll while the
  // pointer sat over a figure. Flutter's framework never sees these events
  // (the platform WebView consumes them before they reach an ancestor
  // Listener), so the page's own JS is the only reliable place to catch them.
  // Forward the vertical delta over the bridge; the Flutter host re-drives the
  // chat ScrollPosition. Passive so we never block the page (it has nothing to
  // scroll anyway).
  window.addEventListener('wheel', function (e) {
    if (pendingVisualId === null) return;
    post({ visualId: pendingVisualId, type: 'scroll', deltaY: e.deltaY });
  }, { passive: true });

  // === ResizeObserver — debounced height reporting ===
  var lastHeight = -1;
  var heightTimer = null;
  var resizeRoot = null;
  function scheduleHeight() {
    if (heightTimer) return;
    heightTimer = setTimeout(function () {
      heightTimer = null;
      if (!resizeRoot) return;
      var h = resizeRoot.scrollHeight;
      if (Math.abs(h - lastHeight) >= 2) {
        lastHeight = h;
        post({
          visualId: pendingVisualId,
          type: 'height',
          value: h,
        });
      }
    }, 50);
  }

  // === Package mounter — invoked from Flutter after the WebView is ready ===
  // Signature: window.HarnexVisualBoot(packageJson) — packageJson must be a
  // LocalVisualPackage JSON object (kind: visual_package). For
  // remote_artifact the runtime page isn't used; the WebView loads the URL
  // directly and the remote page emits height/error via the same bridge.
  window.HarnexVisualBoot = function (packageJson) {
    pendingVisualId = packageJson.visualId || 'unknown';
    if (packageJson.kind === 'echarts_spec') {
      return bootEChartsSpec(packageJson);
    }
    return bootCustomPackage(packageJson);
  };

  function bootCustomPackage(packageJson) {
    var root = document.getElementById('harnex-visual-root');
    if (!root) {
      postError('runtime: root node missing');
      return;
    }

    pendingVisualId = packageJson.visualId || 'unknown';

    // Inject CSS
    if (packageJson.css) {
      var style = document.createElement('style');
      style.textContent = String(packageJson.css);
      document.head.appendChild(style);
    }

    // Inject HTML body fragment
    if (packageJson.html) {
      root.innerHTML = String(packageJson.html);
    }

    // Build ctx
    var declaredInteractions = new Set(packageJson.interactions || []);
    var ctx = {
      root: root,
      data: packageJson.data || {},
      theme: packageJson.theme || {},
      emit: function (name, payload) {
        if (!declaredInteractions.has(name)) {
          post({ visualId: pendingVisualId, type: 'log', level: 'warn',
                 message: 'rejected undeclared interaction: ' + name });
          return;
        }
        post({
          visualId: pendingVisualId,
          type: 'event',
          name: String(name),
          payload: payload || {},
        });
      },
      requestResize: scheduleHeight,
      log: function (level, msg) {
        post({
          visualId: pendingVisualId,
          type: 'log',
          level: String(level || 'info'),
          message: String(msg || ''),
        });
      },
    };

    // Install ResizeObserver
    try {
      resizeRoot = document.documentElement;
      if (typeof ResizeObserver !== 'undefined') {
        var ro = new ResizeObserver(scheduleHeight);
        ro.observe(resizeRoot);
      }
    } catch (e) {
      postError('ResizeObserver setup failed: ' + e.message);
    }

    // Evaluate package JS — this populates window.HarnexVisualPackage
    if (packageJson.js) {
      var script = document.createElement('script');
      script.textContent = String(packageJson.js);
      try {
        document.head.appendChild(script);
      } catch (e) {
        postError('package js insertion failed: ' + e.message);
        return;
      }
    }

    if (!window.HarnexVisualPackage || typeof window.HarnexVisualPackage.mount !== 'function') {
      postError('package did not export HarnexVisualPackage.mount');
      return;
    }

    try {
      var handle = window.HarnexVisualPackage.mount(ctx);
      window.__harnexVisualHandle = handle || {};
      scheduleHeight();
      post({ visualId: pendingVisualId, type: 'ready' });
    } catch (e) {
      postError('mount threw: ' + e.message, e.stack);
    }
  }

  // The single shared chart theme — the "template" all charts inherit, so
  // per-chart options carry only structure + data, never repeated styling.
  // Grounded in the Harnex dark tokens (canvasTop #0A0D15, brandInk #F3F6FC,
  // mutedInk #B4BBCC, glassBorder, successInk/warningInk/errorInk).
  var _harnexThemeRegistered = false;
  function ensureHarnexTheme() {
    if (_harnexThemeRegistered || typeof echarts === 'undefined') return;
    var faintLine = 'rgba(244,247,255,0.07)';
    var axisLine = 'rgba(244,247,255,0.18)';
    echarts.registerTheme('harnex', {
      // Match the app canvas (harnex canvasTop #0A0D15) explicitly. Was
      // 'transparent', which looked right on iOS (WebView transparency lets
      // the dark chat show through) but rendered on a WHITE block on macOS
      // (flutter_inappwebview_macos ignores WebView transparency). A solid
      // canvas-matched fill is consistent everywhere.
      backgroundColor: '#0A0D15',
      color: [
        '#6C8CFF', '#4ADE80', '#FBBF24', '#22D3EE',
        '#A78BFA', '#FCA5A5', '#34D399', '#F472B6',
      ],
      textStyle: { color: '#B4BBCC' },
      title: {
        left: 'center',
        textStyle: { color: '#F3F6FC', fontWeight: '600', fontSize: 14 },
      },
      legend: { textStyle: { color: '#B4BBCC' }, inactiveColor: '#4B5366' },
      line: { symbol: 'circle', symbolSize: 4, smooth: true },
      categoryAxis: {
        axisLine: { show: true, lineStyle: { color: axisLine } },
        axisTick: { show: false },
        axisLabel: { color: '#8A93A6' },
        splitLine: { show: false, lineStyle: { color: faintLine } },
        splitArea: { show: false },
      },
      valueAxis: {
        axisLine: { show: false },
        axisTick: { show: false },
        axisLabel: { color: '#8A93A6' },
        splitLine: { show: true, lineStyle: { color: faintLine } },
        splitArea: { show: false },
      },
      tooltip: {
        backgroundColor: 'rgba(20,24,39,0.96)',
        borderColor: 'rgba(244,247,255,0.12)',
        borderWidth: 1,
        textStyle: { color: '#F3F6FC' },
      },
      // Styling only — applies to a dataZoom slider IF an option declares
      // one (long-series charts); does not create a dataZoom on its own.
      dataZoom: [{
        textStyle: { color: '#8A93A6' },
        borderColor: 'rgba(244,247,255,0.10)',
        fillerColor: 'rgba(108,140,255,0.18)',
        handleStyle: { color: '#6C8CFF', borderColor: '#6C8CFF' },
        moveHandleStyle: { color: '#6C8CFF' },
        dataBackground: {
          lineStyle: { color: 'rgba(244,247,255,0.18)' },
          areaStyle: { color: 'rgba(244,247,255,0.06)' },
        },
        selectedDataBackground: {
          lineStyle: { color: '#6C8CFF' },
          areaStyle: { color: 'rgba(108,140,255,0.12)' },
        },
      }],
    });
    _harnexThemeRegistered = true;
  }

  function bootEChartsSpec(pkg) {
    var root = document.getElementById('harnex-visual-root');
    if (!root) {
      postError('runtime: root node missing');
      return;
    }
    if (typeof echarts === 'undefined') {
      postError('echarts library not loaded');
      return;
    }
    ensureHarnexTheme();
    // Strip the runtime's default padding for clean chart edges.
    root.style.padding = '0';
    var chart;
    try {
      // Pass an explicit height so the canvas is sized deterministically
      // even if the CSS height chain ever breaks. ECharts reads the
      // container size at init; a 0-height container renders an invisible
      // chart. Width stays auto (fills the container's clientWidth).
      chart = echarts.init(root, 'harnex', {
        renderer: 'canvas',
        height: pkg.estimatedHeight || 320,
      });
      chart.setOption(pkg.option || {});
    } catch (e) {
      postError('echarts setOption threw: ' + e.message, e.stack);
      return;
    }
    function safeResize() { try { chart.resize(); } catch (e) {} }
    window.__harnexVisualHandle = {
      update: function (next) {
        if (next && next.option) {
          try { chart.setOption(next.option, false); }
          catch (e) { postError('setOption(update) threw: ' + e.message); }
        }
      },
      resize: safeResize,
      dispose: function () {
        try { chart.dispose(); } catch (e) {}
      },
    };
    // Re-render on container resize / late layout. On a (re)mount the
    // PlatformView surface can be zero-width or not yet composited when
    // echarts.init read the size, so the one-shot draw would land on an
    // invisible canvas with no recovery (the blank-on-scroll symptom). A
    // ResizeObserver redraws the chart once the surface gets a real size.
    if (typeof ResizeObserver !== 'undefined') {
      try { new ResizeObserver(safeResize).observe(root); } catch (e) {}
    }
    // Height reporting: ECharts uses container size, so we report what
    // the package asked for as estimatedHeight. The InteractiveVisualInline
    // SizedBox listens for BridgeHeight and resizes the chat row.
    post({
      visualId: pendingVisualId,
      type: 'height',
      value: pkg.estimatedHeight || 320,
    });
    // Gate `ready` on a real container width so the host does not drop its
    // spinner onto an unpainted canvas. Poll a few frames for a non-zero width,
    // resize once sized, then post ready. The frame cap guarantees we never hang
    // if the container genuinely has no size.
    var sizeTries = 0;
    function postReadyWhenSized() {
      if (root.clientWidth > 0 || sizeTries >= 30) {
        safeResize();
        post({ visualId: pendingVisualId, type: 'ready' });
        return;
      }
      sizeTries += 1;
      if (typeof requestAnimationFrame !== 'undefined') {
        requestAnimationFrame(postReadyWhenSized);
      } else {
        post({ visualId: pendingVisualId, type: 'ready' });
      }
    }
    postReadyWhenSized();
  }

  // === Flutter → Web command dispatcher ===
  window.HarnexVisualCommand = function (command) {
    var handle = window.__harnexVisualHandle || {};
    switch (command.type) {
      case 'updateData':
        if (typeof handle.update === 'function') {
          try { handle.update(command.data || {}); }
          catch (e) { postError('update threw: ' + e.message, e.stack); }
        }
        scheduleHeight();
        break;
      case 'setTheme':
        // No standard application path — packages access ctx.theme at mount
        // time. setTheme is plumbed but does not force a rebuild in the
        // initial release. Future runtime versions may pass into handle.
        break;
      case 'pause':
      case 'resume':
        // No-op for initial release; future runtime can forward to handle.
        break;
      case 'dispose':
        if (typeof handle.dispose === 'function') {
          try { handle.dispose(); }
          catch (e) { /* swallow */ }
        }
        break;
    }
  };
})();
