/* ==========================================================================
   BOTSOL dashboard — Surface B controller.

   Vanilla JS, no framework, no build step. One state object, one render path
   (§8.3). Tab switching never tears down in-flight order polling — the poll
   belongs to app state, not to the tab's DOM.

   All server-provided strings go in via textContent, never innerHTML (§8.1).
   Nothing sensitive is written to the console (§8.1, FE-C8).
   ========================================================================== */

'use strict';

(function () {
  var STATUS_POLL_MS = 2000;
  var ORDER_POLL_MS = 10000;
  var TIMEOUT_MS = 10000;
  var LOG_LIMIT = 500;

  var app = {
    status: null,
    licenseBase: null,       // from GET /api/dashboard-config
    licenseVarName: 'LICENSE_SERVER_BASE_URL',
    remoteConfig: null,
    selectedChain: null,
    order: null,
    licenseKey: null,
    keyRevealed: false,
    orderPollTimer: null,
    logFilter: 'all',
    logPinned: true,         // auto-scroll unless the user scrolled up
    lastSeenUpdate: 0,
  };

  var $ = function (id) { return document.getElementById(id); };

  function setText(id, value) {
    var el = $(id);
    if (el) el.textContent = value == null ? '' : String(value);
  }

  function announce(message) {
    var el = $('announcer');
    if (el) el.textContent = message;
  }

  function toast(message) {
    var region = $('toasts');
    if (!region) return;
    var el = document.createElement('div');
    el.className = 'toast';
    el.textContent = message;
    region.appendChild(el);
    setTimeout(function () { el.remove(); }, 4000);
  }

  /* ---------------------------------------------------------------------- */
  /* Fetch — every call times out so the UI can never stick on loading.       */
  /* ---------------------------------------------------------------------- */

  function request(url, options) {
    options = options || {};
    var controller = new AbortController();
    var timer = setTimeout(function () { controller.abort(); }, TIMEOUT_MS);
    return fetch(url, {
      method: options.method || 'GET',
      headers: options.body ? { 'Content-Type': 'application/json' } : undefined,
      body: options.body ? JSON.stringify(options.body) : undefined,
      signal: controller.signal,
      credentials: 'omit',
    })
      .then(function (res) {
        return res.text().then(function (text) {
          var data = null;
          try { data = JSON.parse(text); } catch (e) {}
          if (!res.ok) {
            var err = new Error((data && data.error) || 'Request failed.');
            err.code = data && data.code;
            err.status = res.status;
            err.kind = 'rejected';
            throw err;
          }
          return data;
        });
      })
      .catch(function (err) {
        if (err.name === 'AbortError') {
          var t = new Error('The request timed out.');
          t.kind = 'unreachable';
          throw t;
        }
        if (!err.kind) {
          var n = new Error('Could not reach the server.');
          n.kind = 'unreachable';
          throw n;
        }
        throw err;
      })
      .finally(function () { clearTimeout(timer); });
  }

  /* ---------------------------------------------------------------------- */
  /* Copy — always the complete value (FE-7).                                 */
  /* ---------------------------------------------------------------------- */

  document.addEventListener('click', function (event) {
    var btn = event.target.closest('[data-copy-target]');
    if (!btn) return;
    var source = $(btn.getAttribute('data-copy-target'));
    if (!source) return;
    var value = source.dataset.full != null ? source.dataset.full : source.textContent;
    navigator.clipboard.writeText(value).then(function () {
      var previous = btn.textContent;
      btn.textContent = 'Copied';
      setTimeout(function () { btn.textContent = previous; }, 1500);
    });
  });

  /* ---------------------------------------------------------------------- */
  /* Tabs — ARIA pattern with arrow-key navigation (FE-N4).                   */
  /* ---------------------------------------------------------------------- */

  var tabs = [
    { tab: 'tab-status', panel: 'panel-status' },
    { tab: 'tab-buy', panel: 'panel-buy' },
    { tab: 'tab-builder', panel: 'panel-builder' },
  ];

  function selectTab(index, focus) {
    tabs.forEach(function (entry, i) {
      var isActive = i === index;
      var tabEl = $(entry.tab);
      tabEl.setAttribute('aria-selected', isActive ? 'true' : 'false');
      tabEl.tabIndex = isActive ? 0 : -1;
      $(entry.panel).hidden = !isActive;
      if (isActive && focus) tabEl.focus();
    });
    try { sessionStorage.setItem('botsol.tab', String(index)); } catch (e) {}

    // Remote tabs load lazily, but polling started elsewhere is never torn down.
    if (index === 1) initBuyTab();
    if (index === 2) initBuilderTab();
  }

  tabs.forEach(function (entry, i) {
    $(entry.tab).addEventListener('click', function () { selectTab(i); });
    $(entry.tab).addEventListener('keydown', function (event) {
      var next = null;
      if (event.key === 'ArrowRight') next = (i + 1) % tabs.length;
      else if (event.key === 'ArrowLeft') next = (i - 1 + tabs.length) % tabs.length;
      else if (event.key === 'Home') next = 0;
      else if (event.key === 'End') next = tabs.length - 1;
      if (next !== null) { event.preventDefault(); selectTab(next, true); }
    });
  });

  /* ---------------------------------------------------------------------- */
  /* Status tab                                                              */
  /* ---------------------------------------------------------------------- */

  var BOT_STATE_VARIANTS = {
    running:  { cls: 'badge-success', label: 'Running' },
    paused:   { cls: 'badge-warn',    label: 'Paused' },
    stopped:  { cls: 'badge-neutral', label: 'Stopped' },
    starting: { cls: 'badge-neutral', label: 'Starting' },
    error:    { cls: 'badge-danger',  label: 'Error' },
  };

  function pollStatus() {
    request('/api/status')
      .then(function (status) {
        app.status = status;
        renderStatus(status);
      })
      .catch(function () {
        // The bot process is the server. If it is gone, say so rather than
        // continuing to display numbers that look live.
        markStale('Not receiving updates.');
      });
  }

  function markStale() {
    document.body.setAttribute('data-stale', 'true');
    $('stale-banner').hidden = false;
    var badge = $('bot-state');
    badge.className = 'badge badge-danger dash-state';
    badge.textContent = 'Not responding';
  }

  function clearStale() {
    document.body.removeAttribute('data-stale');
    $('stale-banner').hidden = true;
  }

  function renderStatus(s) {
    setText('version', 'v' + s.version);

    // FE-S3: if no update has arrived for more than 3x the expected refresh
    // interval, degrade the page rather than showing stale numbers as live.
    var age = Date.now() - s.lastUpdated;
    var pollMs = (s.config && s.config.pollIntervalMs) || 3000;
    if (age > Math.max(pollMs * 3, STATUS_POLL_MS * 3)) {
      markStale();
    } else {
      clearStale();
      var variant = BOT_STATE_VARIANTS[s.bot.status] || BOT_STATE_VARIANTS.stopped;
      var badge = $('bot-state');
      if (badge.textContent !== variant.label) announce('Bot ' + variant.label);
      badge.className = 'badge ' + variant.cls + ' dash-state';
      badge.textContent = variant.label;
    }

    setText('freshness', 'Updated ' + relativeTime(s.lastUpdated));

    // Licence badge (FE-S2).
    var lic = $('licence-badge');
    if (!s.license.configured) {
      lic.className = 'badge badge-warn dash-licence';
      lic.textContent = 'Unlicensed';
      lic.dataset.actionable = 'true';
    } else if (s.license.enforced) {
      lic.className = 'badge badge-success dash-licence';
      lic.textContent = 'Licensed ····' + (s.license.keyMasked || '');
      lic.dataset.actionable = 'false';
    } else {
      lic.className = 'badge badge-warn dash-licence';
      lic.textContent = 'Not verified';
      lic.dataset.actionable = 'true';
    }

    // Pause / resume reflects real state.
    var toggle = $('pause-toggle');
    toggle.disabled = false;
    toggle.textContent = s.bot.status === 'paused' ? 'Resume' : 'Pause';

    // P&L (FE-S5).
    setText('pnl-value', s.pnl.realizedSol.toFixed(6) + ' SOL');
    setText('pnl-period', s.pnl.periodLabel);
    setText('pnl-trades', s.pnl.completedRoundTrips + ' completed round trip(s)');
    var mode = $('pnl-mode');
    mode.className = 'badge ' + (s.pnl.dryRun ? 'badge-warn' : 'badge-neutral');
    mode.textContent = s.pnl.dryRun ? 'Dry run' : 'Live';
    $('pnl-note').hidden = !s.pnl.dryRun;
    var pnlValue = $('pnl-value');
    pnlValue.className = 'stat-value' +
      (s.pnl.realizedSol > 0 ? ' is-profit' : s.pnl.realizedSol < 0 ? ' is-loss' : '');

    // Wallet — public address only (FE-S6).
    var addr = $('wallet-address');
    addr.textContent = s.wallet.address || 'Unknown';
    if (s.wallet.address) addr.dataset.full = s.wallet.address;
    setText(
      'wallet-balance',
      s.wallet.solBalance == null ? 'Unknown' : s.wallet.solBalance.toFixed(6) + ' SOL'
    );
    setText(
      'wallet-balance-age',
      s.wallet.balanceUpdatedAt ? 'as of ' + relativeTime(s.wallet.balanceUpdatedAt) : ''
    );

    renderPairs(s);
    renderRouting(s);
    renderConfig(s);
    renderSkips(s);
    renderLog(s);
  }

  function renderPairs(s) {
    setText('pairs-count', String(s.pairs.length));
    var list = $('pairs-list');
    list.textContent = '';
    s.pairs.forEach(function (pair) {
      var li = document.createElement('li');
      var name = document.createElement('span');
      name.textContent = pair.symbol;
      li.appendChild(name);
      list.appendChild(li);
    });

    // Skipped symbols surface here as visible rows, not only in the log (FE-S7).
    var warnHost = $('pair-warnings');
    warnHost.textContent = '';
    s.pairWarnings.forEach(function (warning) {
      var box = document.createElement('div');
      box.className = 'notice notice-warn';
      var strong = document.createElement('strong');
      strong.textContent = warning.entry + ' skipped. ';
      box.appendChild(strong);
      box.appendChild(document.createTextNode(warning.reason));
      warnHost.appendChild(box);
    });
  }

  function renderRouting(s) {
    var badge = $('routing-badge');
    badge.className = 'badge ' + (s.routing.jitoEnabled ? 'badge-warn' : 'badge-neutral');
    badge.textContent = s.routing.jitoEnabled ? 'Jito bundles' : 'Plain RPC';

    if (s.routing.jitoEnabled) {
      setText(
        'routing-text',
        'Trades are submitted as Jito bundles. Tip ' + s.routing.tipLamports +
          ' lamports (' + s.routing.tipSol + ' SOL) per attempt.'
      );
      $('jito-cost').hidden = false;
    } else {
      setText(
        'routing-text',
        "Execution goes through your own RPC endpoint. Jito bundles are off."
      );
      $('jito-cost').hidden = true;
    }
  }

  var CONFIG_LABELS = {
    minProfitThresholdPct: ['Min profit threshold', '%'],
    maxTradeSizeSol: ['Max trade size', ' SOL'],
    pollIntervalMs: ['Poll interval', ' ms'],
    slippageBps: ['Slippage', ' bps'],
    maxPriceImpactPct: ['Max price impact', '%'],
    maxProfitDegradationPct: ['Max profit degradation', ' pts'],
  };

  function renderConfig(s) {
    var body = $('config-rows');
    body.textContent = '';
    Object.keys(CONFIG_LABELS).forEach(function (key) {
      if (s.config[key] == null) return;
      var meta = CONFIG_LABELS[key];
      var tr = document.createElement('tr');
      var th = document.createElement('td');
      th.textContent = meta[0];
      var td = document.createElement('td');
      td.className = 'num';
      td.textContent = String(s.config[key]) + meta[1];
      tr.appendChild(th);
      tr.appendChild(td);
      body.appendChild(tr);
    });
  }

  var SKIP_LABELS = {
    spread_closed: 'Spread no longer clears threshold',
    price_impact_too_high: 'Price impact too high',
    profit_degraded: 'Profit degraded beyond tolerance',
    price_impact_unknown: 'Price impact not reported',
    quote_failed: 'Could not re-quote',
  };

  function renderSkips(s) {
    setText('skips-total', String(s.skips.total));
    var body = $('skip-rows');
    body.textContent = '';
    var reasons = Object.keys(s.skips.byReason);
    if (!reasons.length) {
      var tr = document.createElement('tr');
      var td = document.createElement('td');
      td.className = 'field-hint';
      td.textContent = 'No skips yet.';
      tr.appendChild(td);
      body.appendChild(tr);
      return;
    }
    reasons.forEach(function (reason) {
      var row = document.createElement('tr');
      var label = document.createElement('td');
      label.textContent = SKIP_LABELS[reason] || reason;
      var count = document.createElement('td');
      count.className = 'num';
      count.textContent = String(s.skips.byReason[reason]);
      row.appendChild(label);
      row.appendChild(count);
      body.appendChild(row);
    });
  }

  function renderLog(s) {
    var host = $('log');
    var entries = s.log.filter(function (entry) {
      return app.logFilter === 'all' || entry.level === app.logFilter;
    });
    var trimmed = entries.slice(-LOG_LIMIT);

    host.textContent = '';
    if (!trimmed.length) {
      var empty = document.createElement('p');
      empty.className = 'log-empty';
      empty.textContent = 'Nothing logged yet.';
      host.appendChild(empty);
      return;
    }

    trimmed.forEach(function (entry) {
      var line = document.createElement('div');
      line.className = 'log-line';
      line.setAttribute('data-level', entry.level);

      var time = document.createElement('span');
      time.className = 'log-time';
      time.textContent = new Date(entry.ts).toLocaleTimeString();

      var level = document.createElement('span');
      level.className = 'log-level';
      level.textContent = entry.level;

      var message = document.createElement('span');
      message.className = 'log-msg';
      message.textContent = entry.message;

      line.appendChild(time);
      line.appendChild(level);
      line.appendChild(message);
      host.appendChild(line);
    });

    // Auto-scroll pauses when the user scrolls up (FE-S11).
    if (app.logPinned) host.scrollTop = host.scrollHeight;
    $('log-jump').hidden = app.logPinned;
  }

  $('log').addEventListener('scroll', function () {
    var el = $('log');
    var atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 24;
    app.logPinned = atBottom;
    $('log-jump').hidden = atBottom;
  });

  $('log-jump').addEventListener('click', function () {
    app.logPinned = true;
    var el = $('log');
    el.scrollTop = el.scrollHeight;
    $('log-jump').hidden = true;
  });

  $('log-filter').addEventListener('change', function (event) {
    app.logFilter = event.target.value;
    if (app.status) renderLog(app.status);
  });

  $('log-copy').addEventListener('click', function () {
    if (!app.status) return;
    var text = app.status.log
      .filter(function (e) { return app.logFilter === 'all' || e.level === app.logFilter; })
      .map(function (e) {
        return new Date(e.ts).toISOString() + '  ' + e.level.toUpperCase() + '  ' + e.message;
      })
      .join('\n');
    navigator.clipboard.writeText(text).then(function () { toast('Log copied.'); });
  });

  // Pause / resume — disable, call, re-render from the response (FE-S4).
  $('pause-toggle').addEventListener('click', function () {
    var button = $('pause-toggle');
    var wantPause = button.textContent === 'Pause';
    button.disabled = true;
    request(wantPause ? '/api/pause' : '/api/resume', { method: 'POST' })
      .then(function (result) {
        button.textContent = result.status === 'paused' ? 'Resume' : 'Pause';
        announce('Bot ' + result.status);
        pollStatus();
      })
      .catch(function () {
        toast('Could not change bot state.');
      })
      .finally(function () { button.disabled = false; });
  });

  $('licence-badge').addEventListener('click', function () {
    if (this.dataset.actionable === 'true') selectTab(1);
  });

  function relativeTime(ts) {
    var seconds = Math.round((Date.now() - ts) / 1000);
    if (seconds < 2) return 'just now';
    if (seconds < 60) return seconds + 's ago';
    var minutes = Math.round(seconds / 60);
    if (minutes < 60) return minutes + 'm ago';
    return Math.round(minutes / 60) + 'h ago';
  }

  /* ---------------------------------------------------------------------- */
  /* Remote tabs — Buy Licence and Builder Pro                               */
  /* ---------------------------------------------------------------------- */

  var buyInitialised = false;
  var builderInitialised = false;

  function loadDashboardConfig() {
    return request('/api/dashboard-config').then(function (config) {
      app.licenseBase = config.licenseServerBaseUrl;
      app.licenseVarName = config.variableName || 'LICENSE_SERVER_BASE_URL';
      return config;
    });
  }

  function remote(path, options) {
    return request(app.licenseBase + path, options);
  }

  function initBuyTab() {
    if (buyInitialised) return;
    buyInitialised = true;

    // FE-B1: unconfigured state comes first, and names the variable.
    if (!app.licenseBase) {
      setText('buy-var', app.licenseVarName);
      $('buy-unconfigured').hidden = false;
      return;
    }

    // FE-B11: do not ask someone to buy what they already own.
    if (app.status && app.status.license.enforced) {
      $('buy-licensed').hidden = false;
      return;
    }

    loadBuyPricing();
  }

  function loadBuyPricing() {
    $('buy-unreachable').hidden = true;
    $('buy-flow').hidden = false;

    remote('/api/config')
      .then(function (config) {
        app.remoteConfig = config;

        var block = $('buy-price-block');
        block.textContent = '';
        if (config.discountActive) {
          var was = document.createElement('div');
          was.className = 'price-was num';
          was.textContent = config.basePriceUsdt + ' ' + config.currency;
          block.appendChild(was);
        }
        var now = document.createElement('div');
        now.className = 'stat-value';
        now.textContent = config.priceUsdt + ' ' + config.currency;
        block.appendChild(now);
        if (config.discountActive && config.discountEndsAt) {
          var until = document.createElement('p');
          until.className = 'stat-meta';
          until.textContent = 'Discount ends ' + new Date(config.discountEndsAt).toLocaleString();
          block.appendChild(until);
        }
        setText('buy-term', config.licenseTerm === 'perpetual' ? 'Never expires' : '');

        var host = $('buy-chains');
        host.textContent = '';
        (config.chains || []).forEach(function (chain, index) {
          if (!chain.available) return;
          var input = document.createElement('input');
          input.type = 'radio';
          input.name = 'buychain';
          input.id = 'buychain-' + chain.id;
          input.value = chain.id;
          if (index === 0) { input.checked = true; app.selectedChain = chain.id; }
          input.addEventListener('change', function () { app.selectedChain = chain.id; });

          var label = document.createElement('label');
          label.setAttribute('for', input.id);
          var name = document.createElement('strong');
          name.textContent = chain.label;
          label.appendChild(name);
          var token = document.createElement('div');
          token.className = 'field-hint';
          token.textContent = 'Token: ' + chain.token;
          label.appendChild(token);

          host.appendChild(input);
          host.appendChild(label);
        });
      })
      .catch(function (err) {
        // Three distinct failures, never collapsed into one message (§8.1).
        $('buy-flow').hidden = true;
        $('buy-unreachable').hidden = false;
        setText(
          'buy-unreachable-detail',
          err.kind === 'unreachable'
            ? 'Could not reach the licence server at ' + app.licenseBase + '.'
            : err.message
        );
      });
  }

  $('buy-retry').addEventListener('click', loadBuyPricing);
  $('buy-another').addEventListener('click', function () {
    $('buy-licensed').hidden = true;
    loadBuyPricing();
  });

  var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

  $('buy-form').addEventListener('submit', function (event) {
    event.preventDefault();
    setText('buy-err-email', '');
    setText('buy-err-order', '');

    var email = $('buy-email').value.trim();
    if (!EMAIL_RE.test(email)) {
      setText('buy-err-email', 'Enter a valid email address.');
      return;
    }
    var button = $('buy-submit');
    button.disabled = true;
    button.textContent = 'Creating…';

    remote('/api/orders/create', {
      method: 'POST',
      body: { email: email, chain: app.selectedChain },
    })
      .then(adoptOrder)
      .catch(function (err) { setText('buy-err-order', err.message); })
      .finally(function () {
        button.disabled = false;
        button.textContent = 'Create order';
      });
  });

  function adoptOrder(order) {
    app.order = order;
    try { localStorage.setItem('botsol.dash.order', order.order_id); } catch (e) {}

    setText('buy-amount', order.expected_amount_usdt);
    setText('buy-address', order.pay_to_address);
    setText('buy-order-id', order.order_id);
    var acceptedTokens = (order.accepted_tokens || [])
      .map(function (t) { return t.symbol + ' (' + t.contract + ')'; })
      .join('  ·  ');
    setText('buy-token', (order.token || '') + (acceptedTokens ? '  —  ' + acceptedTokens : ''));

    if (order.status === 'paid' && order.license_key) {
      showKey(order.license_key);
    } else {
      showBuyStep(2);
      startOrderPoll();
    }
  }

  function showBuyStep(n) {
    $('buy-panel-order').hidden = n !== 1;
    $('buy-panel-pay').hidden = n !== 2;
    $('buy-panel-key').hidden = n !== 3;
    var steps = document.querySelectorAll('#buy-steps .step');
    for (var i = 0; i < steps.length; i++) {
      var num = Number(steps[i].getAttribute('data-step'));
      steps[i].setAttribute('data-state', num < n ? 'done' : num === n ? 'current' : 'todo');
    }
  }

  // The poll lives in app state, so switching tabs never tears it down (§8.3).
  function startOrderPoll() {
    stopOrderPoll();
    var failures = 0;
    app.orderPollTimer = setInterval(function () {
      if (!app.order) return stopOrderPoll();
      remote('/api/orders/' + encodeURIComponent(app.order.order_id))
        .then(function (order) {
          failures = 0;
          app.order = order;
          if (order.status === 'paid' && order.license_key) {
            stopOrderPoll();
            showKey(order.license_key);
          }
        })
        .catch(function () {
          failures += 1;
          if (failures >= 5) {
            stopOrderPoll();
            setText('buy-err-claim', 'Automatic checking stopped. Use "Check my payment".');
          }
        });
    }, ORDER_POLL_MS);
  }

  function stopOrderPoll() {
    if (app.orderPollTimer) {
      clearInterval(app.orderPollTimer);
      app.orderPollTimer = null;
    }
  }

  $('buy-claim').addEventListener('click', function () {
    setText('buy-err-claim', '');
    var tx = $('buy-tx').value.trim();
    if (!tx) { setText('buy-err-claim', 'Paste the transaction hash.'); return; }
    var button = $('buy-claim');
    button.disabled = true;
    remote('/api/orders/claim', {
      method: 'POST',
      body: { orderId: app.order.order_id, txHash: tx },
    })
      .then(function (result) {
        if (result.status === 'paid' && result.license_key) {
          stopOrderPoll();
          showKey(result.license_key);
        }
      })
      .catch(function (err) {
        var message = err.message;
        if (err.code === 'tx_hash_already_used') {
          message = 'That transaction has already been used for another order.';
        } else if (err.code === 'insufficient_confirmations') {
          message = 'Payment seen — waiting for confirmations.';
        }
        setText('buy-err-claim', message);
      })
      .finally(function () { button.disabled = false; });
  });

  function showKey(key) {
    app.licenseKey = key;
    app.keyRevealed = false;
    var value = $('buy-key');
    value.textContent = '';
    var mask = document.createElement('span');
    mask.className = 'secret-mask';
    mask.textContent = '••••••••••••••••';
    value.appendChild(mask);
    value.dataset.full = key;
    showBuyStep(3);
    toast('Payment confirmed — licence key ready.');
  }

  $('buy-key-reveal').addEventListener('click', function () {
    var value = $('buy-key');
    if (!app.keyRevealed) {
      value.textContent = app.licenseKey;
      app.keyRevealed = true;
      this.textContent = 'Hide';
    } else {
      value.textContent = '';
      var mask = document.createElement('span');
      mask.className = 'secret-mask';
      mask.textContent = '••••••••••••••••';
      value.appendChild(mask);
      app.keyRevealed = false;
      this.textContent = 'Reveal';
    }
    value.dataset.full = app.licenseKey;
  });

  $('buy-resume').addEventListener('click', function () {
    setText('buy-err-resume', '');
    var id = $('buy-resume-id').value.trim();
    if (!id) { setText('buy-err-resume', 'Paste your order ID.'); return; }
    remote('/api/orders/' + encodeURIComponent(id))
      .then(adoptOrder)
      .catch(function (err) { setText('buy-err-resume', err.message); });
  });

  /* ---------------------------------------------------------------------- */
  /* Builder Pro                                                             */
  /* ---------------------------------------------------------------------- */

  function initBuilderTab() {
    if (builderInitialised) return;
    builderInitialised = true;

    if (!app.licenseBase) {
      $('bp-unconfigured').hidden = false;
      return;
    }
    $('bp-content').hidden = false;

    // Commission comes from the server, never hardcoded in the page (FE-R1).
    if (app.remoteConfig && app.remoteConfig.referralCommissionPct != null) {
      setText('bp-rate', app.remoteConfig.referralCommissionPct + '%');
    } else {
      remote('/api/config')
        .then(function (config) {
          app.remoteConfig = config;
          if (config.referralCommissionPct != null) {
            setText('bp-rate', config.referralCommissionPct + '%');
          }
        })
        .catch(function () { /* leave the neutral wording in place */ });
    }
  }

  $('bp-form').addEventListener('submit', function (event) {
    event.preventDefault();
    setText('bp-err-telegram', '');
    setText('bp-err-wallet', '');
    setText('bp-err', '');

    var telegram = $('bp-telegram').value.trim();
    var wallet = $('bp-wallet').value.trim();
    if (!telegram) { setText('bp-err-telegram', 'Enter your Telegram user ID.'); return; }
    if (!wallet) { setText('bp-err-wallet', 'Enter your payout wallet address.'); return; }

    var button = $('bp-submit');
    button.disabled = true;
    remote('/api/referrers/register', {
      method: 'POST',
      body: { telegramUserId: telegram, walletAddress: wallet },
    })
      .then(function (result) {
        var host = $('bp-result');
        host.hidden = false;
        host.textContent = '';
        var notice = document.createElement('div');
        notice.className = 'notice notice-accent';
        var strong = document.createElement('strong');
        strong.textContent = 'Application received — pending approval. ';
        notice.appendChild(strong);
        notice.appendChild(
          document.createTextNode('Save this code; it activates only once approved.')
        );
        host.appendChild(notice);

        var wrap = document.createElement('div');
        wrap.className = 'copyable';
        var val = document.createElement('div');
        val.className = 'copyable-value';
        val.id = 'bp-code-value';
        val.textContent = result.referral_code;
        var copy = document.createElement('button');
        copy.type = 'button';
        copy.className = 'copyable-btn';
        copy.setAttribute('data-copy-target', 'bp-code-value');
        copy.textContent = 'Copy';
        wrap.appendChild(val);
        wrap.appendChild(copy);
        host.appendChild(wrap);
        $('bp-form').reset();
      })
      .catch(function (err) { setText('bp-err', err.message); })
      .finally(function () { button.disabled = false; });
  });

  $('bp-check').addEventListener('click', function () {
    setText('bp-err-code', '');
    var code = $('bp-code').value.trim();
    if (!code) { setText('bp-err-code', 'Enter a code.'); return; }

    remote('/api/referrers/status/' + encodeURIComponent(code))
      .then(function (result) {
        var host = $('bp-status');
        host.hidden = false;
        host.textContent = '';

        // Pending must not look like failure; rejected is never dressed up
        // as pending (FE-R3).
        var variants = {
          pending: { cls: 'badge-warn', label: 'Pending review',
            text: 'Waiting for manual review. This code does not earn commission yet.' },
          approved: { cls: 'badge-success', label: 'Approved',
            text: 'This code is active and earns commission on referred purchases.' },
          rejected: { cls: 'badge-danger', label: 'Rejected',
            text: 'This application was not approved.' },
        };
        var v = variants[result.status] || { cls: 'badge-neutral', label: result.status, text: '' };

        var badge = document.createElement('span');
        badge.className = 'badge ' + v.cls;
        badge.textContent = v.label;
        host.appendChild(badge);

        var explain = document.createElement('p');
        explain.className = 'field-hint';
        explain.textContent = v.text;
        host.appendChild(explain);

        // Only show earnings the server actually returned (FE-R4).
        if (result.status === 'approved' && result.referred_count != null) {
          var stats = document.createElement('p');
          stats.className = 'mono';
          stats.textContent = result.referred_count + ' referred · ' +
            result.total_commission_usdt + ' USDT total commission';
          host.appendChild(stats);
        }
      })
      .catch(function (err) { setText('bp-err-code', err.message); });
  });

  /* ---------------------------------------------------------------------- */
  /* Boot                                                                    */
  /* ---------------------------------------------------------------------- */

  loadDashboardConfig().catch(function () { app.licenseBase = null; });

  pollStatus();
  setInterval(pollStatus, STATUS_POLL_MS);

  var storedTab = 0;
  try { storedTab = Number(sessionStorage.getItem('botsol.tab')) || 0; } catch (e) {}
  selectTab(storedTab);
})();
