import GObject from 'gi://GObject';
import St from 'gi://St';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Clutter from 'gi://Clutter';
import Soup from 'gi://Soup?version=3.0';
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
import { DataClient } from './dataClient.js';

const CACHE_TTL_MS = 300_000;
const TOP_ACTIVITIES = 10;
const CHART_HEIGHT = 52;
const BAR_TRACK_WIDTH = 240;

const PERIODS = [
  { id: 'today', label: 'Today' },
  { id: 'week', label: '7 Days' },
  { id: '30days', label: '30 Days' },
  { id: 'month', label: 'Month' },
  { id: 'all', label: '6 Months' },
];

const INSIGHTS = [
  { id: 'activity', label: 'Activity' },
  { id: 'trend', label: 'Trend' },
  { id: 'forecast', label: 'Forecast' },
  { id: 'pulse', label: 'Pulse' },
  { id: 'stats', label: 'Stats' },
];

const PROVIDERS = [
  { id: 'all', label: 'All' },
  { id: 'claude', label: 'Claude' },
  { id: 'codex', label: 'Codex' },
  { id: 'cursor', label: 'Cursor' },
  { id: 'copilot', label: 'Copilot' },
  { id: 'devin', label: 'Devin' },
  { id: 'opencode', label: 'OpenCode' },
  { id: 'pi', label: 'Pi' },
  { id: 'droid', label: 'Droid' },
  { id: 'gemini', label: 'Gemini' },
  { id: 'kilo-code', label: 'Kilo Code' },
  { id: 'kiro', label: 'Kiro' },
  { id: 'kimi', label: 'Kimi' },
  { id: 'roo-code', label: 'Roo Code' },
];

const CURRENCIES = [
  { code: 'USD', symbol: '$' },
  { code: 'EUR', symbol: '€' },
  { code: 'GBP', symbol: '£' },
  { code: 'CAD', symbol: 'C$' },
  { code: 'AUD', symbol: 'A$' },
  { code: 'JPY', symbol: '¥' },
  { code: 'INR', symbol: '₹' },
  { code: 'BRL', symbol: 'R$' },
  { code: 'CHF', symbol: 'CHF ' },
  { code: 'SEK', symbol: 'kr ' },
  { code: 'SGD', symbol: 'S$' },
  { code: 'HKD', symbol: 'HK$' },
  { code: 'KRW', symbol: '₩' },
  { code: 'MXN', symbol: 'MX$' },
  { code: 'ZAR', symbol: 'R ' },
  { code: 'DKK', symbol: 'kr ' },
  { code: 'RON', symbol: 'lei ' },
  { code: 'CNY', symbol: '¥' },
];

const PROVIDER_PATHS = {
  claude: '.claude/projects',
  codex: '.codex/sessions',
  cursor: '.config/Cursor/User/globalStorage/state.vscdb',
  copilot: '.copilot/session-state',
  devin: '.local/share/devin/cli',
  kimi: '.kimi/sessions',
  pi: '.pi/agent/sessions',
};

function formatCost(value, currency, rate = 1, exact = false) {
  const n = (Number(value) || 0) * (Number(rate) || 1);
  const abs = Math.abs(n);
  const symbol = currency?.symbol || '$';
  if (!exact && abs >= 1000) return `${symbol}${(n / 1000).toFixed(abs >= 10000 ? 0 : 1)}k`;
  const parts = n.toFixed(2).split('.');
  parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  return `${symbol}${parts.join('.')}`;
}

function formatTokensCompact(n) {
  const v = Number(n) || 0;
  if (v >= 1_000_000_000) return `${(v / 1_000_000_000).toFixed(1)}B`;
  if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;
  if (v >= 1000) return `${(v / 1000).toFixed(1)}k`;
  return String(v);
}

function formatTime(date) {
  if (!date || Number.isNaN(date.getTime())) return '';
  const now = new Date();
  const diffSec = Math.floor((now.getTime() - date.getTime()) / 1000);
  if (diffSec < 60) return 'just now';
  if (diffSec < 3600) return `${Math.floor(diffSec / 60)}m ago`;
  if (diffSec < 86400) return `${Math.floor(diffSec / 3600)}h ago`;
  return date.toLocaleDateString();
}

export const CodeBurnIndicator = GObject.registerClass(
class CodeBurnIndicator extends PanelMenu.Button {
  _init(extension) {
    super._init(0.0, 'CodeBurn');

    this._extension = extension;
    this._settings = extension.getSettings();
    this._dataClient = new DataClient(this._settings.get_string('codeburn-path'));
    this._settingsChangedIds = [];

    this._period = this._settings.get_string('default-period') || 'today';
    this._insight = 'activity';
    this._availableProviders = this._detectProviders();
    this._provider = this._availableProviders.length === 1 ? this._availableProviders[0] : 'all';

    this._currency = this._loadCurrency();
    this._exactCosts = this._settings.get_boolean('show-exact-costs');
    this._fxRate = 1;
    this._fxCache = { USD: 1 };
    this._soupSession = new Soup.Session();
    this._payload = null;
    this._payloadCache = new Map();
    this._inFlightKeys = new Set();
    this._refreshGen = 0;
    this._refreshSourceId = 0;
    this._chartSummaryText = '';
    this._destroyed = false;

    this._themeSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.interface' });
    this._themeSignal = this._themeSettings.connect('changed::color-scheme', () => this._applyThemeClass());
    this._applyThemeClass();
    this._updateFxRate();

    this._buildPanelButton();
    this._buildPopup();
    this._connectSettings();
    this._startRefreshLoop();
    this._refresh();
  }

  // -- Panel button --

  _buildPanelButton() {
    const box = new St.BoxLayout({ style_class: 'panel-status-menu-box codeburn-panel' });
    this._panelIcon = new St.Label({
      text: '🔥',
      y_align: Clutter.ActorAlign.CENTER,
      style_class: 'codeburn-flame',
    });
    this._panelLabel = new St.Label({
      text: '...',
      y_align: Clutter.ActorAlign.CENTER,
      style_class: 'codeburn-label',
    });
    box.add_child(this._panelIcon);
    box.add_child(this._panelLabel);
    this._panelLabel.visible = !this._settings.get_boolean('compact-mode');
    this.add_child(box);
  }

  // -- Popup --

  _buildPopup() {
    try {
      this.menu.box.add_style_class_name('codeburn-menu');
      this._popupHost = new PopupMenu.PopupBaseMenuItem({ reactive: false, can_focus: false });
      this._popupHost.add_style_class_name('codeburn-host');
      this.menu.addMenuItem(this._popupHost);

      this._root = new St.BoxLayout({ vertical: true, style_class: 'codeburn-root', x_expand: true });
      this._popupHost.add_child(this._root);

      this._buildBrandHeader();

      this._scrollView = new St.ScrollView({
        style_class: 'codeburn-scroll',
        hscrollbar_policy: St.PolicyType.NEVER,
        vscrollbar_policy: St.PolicyType.AUTOMATIC,
        y_expand: true,
      });
      this._scrollContent = new St.BoxLayout({ vertical: true, x_expand: true });
      this._scrollView.set_child(this._scrollContent);
      this._root.add_child(this._scrollView);

      this._buildAgentTabs();
      this._buildHero();
      this._buildPeriodTabs();
      this._buildInsightPills();
      this._buildTokenChart();
      this._buildLoadingIndicator();
      this._buildContentArea();
      this._buildBudgetAlert();
      this._buildFindingsSection();
      this._buildFooter();
    } catch (e) {
      log(`CodeBurn: popup build error: ${e.message}\n${e.stack}`);
    }
  }

  _buildBrandHeader() {
    const header = new St.BoxLayout({ vertical: true, style_class: 'codeburn-brand-header' });
    const title = new St.BoxLayout({ style_class: 'codeburn-brand-row' });
    title.add_child(new St.Label({ text: 'Code', style_class: 'codeburn-brand-primary' }));
    title.add_child(new St.Label({ text: 'Burn', style_class: 'codeburn-brand-accent' }));
    header.add_child(title);
    header.add_child(new St.Label({ text: 'AI Coding Cost Tracker', style_class: 'codeburn-brand-subhead' }));
    this._root.add_child(header);
  }

  _buildAgentTabs() {
    const detected = this._availableProviders;
    this._agentTabs = new Map();
    this._agentTabRow = null;
    if (detected.length === 0) return;

    const disabled = this._getDisabledProviders();
    const tabs = detected.length === 1
      ? PROVIDERS.filter(p => p.id === detected[0])
      : [PROVIDERS[0], ...PROVIDERS.slice(1).filter(p => detected.includes(p.id) && !disabled.has(p.id))];

    if (tabs.length === 1) {
      const badge = new St.Label({ text: tabs[0].label, style_class: 'codeburn-agent-badge' });
      const row = new St.BoxLayout({ style_class: 'codeburn-tab-row' });
      row.add_child(badge);
      this._scrollContent.add_child(row);
      return;
    }

    const useScroll = tabs.length > 5;
    this._agentTabRow = new St.BoxLayout({ style_class: 'codeburn-tab-row' });
    for (const p of tabs) {
      const btn = new St.Button({ label: p.label, style_class: 'codeburn-tab', can_focus: true, x_expand: !useScroll });
      btn.connect('clicked', () => {
        this._provider = p.id;
        this._updateAgentTabStyle();
        this._refresh();
      });
      this._agentTabRow.add_child(btn);
      this._agentTabs.set(p.id, btn);
    }
    if (useScroll) {
      const agentScroll = new St.ScrollView({
        style_class: 'codeburn-agent-scroll',
        hscrollbar_policy: St.PolicyType.AUTOMATIC,
        vscrollbar_policy: St.PolicyType.NEVER,
      });
      agentScroll.set_child(this._agentTabRow);
      this._scrollContent.add_child(agentScroll);
    } else {
      this._scrollContent.add_child(this._agentTabRow);
    }
    this._updateAgentTabStyle();
  }

  _updateAgentTabStyle() {
    for (const [id, btn] of this._agentTabs) {
      if (id === this._provider) btn.add_style_class_name('codeburn-tab-active');
      else btn.remove_style_class_name('codeburn-tab-active');
    }
  }

  _buildHero() {
    const hero = new St.BoxLayout({ vertical: true, style_class: 'codeburn-hero' });
    const topLine = new St.BoxLayout({ style_class: 'codeburn-hero-top' });
    this._heroDot = new St.Widget({ style_class: 'codeburn-hero-dot' });
    this._heroLabel = new St.Label({ text: 'Loading...', style_class: 'codeburn-hero-label' });
    topLine.add_child(this._heroDot);
    topLine.add_child(this._heroLabel);
    this._heroAmount = new St.Label({ text: '$0.00', style_class: 'codeburn-hero-amount' });
    this._heroMeta = new St.Label({ text: '', style_class: 'codeburn-hero-meta' });
    hero.add_child(topLine);
    hero.add_child(this._heroAmount);
    hero.add_child(this._heroMeta);
    this._scrollContent.add_child(hero);
  }

  _buildPeriodTabs() {
    const row = new St.BoxLayout({ style_class: 'codeburn-tab-row codeburn-period-row' });
    this._periodTabs = new Map();
    for (const p of PERIODS) {
      const btn = new St.Button({ label: p.label, style_class: 'codeburn-period', can_focus: true, x_expand: true });
      btn.connect('clicked', () => {
        this._period = p.id;
        this._updatePeriodTabStyle();
        this._refresh();
      });
      row.add_child(btn);
      this._periodTabs.set(p.id, btn);
    }
    this._scrollContent.add_child(row);
    this._updatePeriodTabStyle();
  }

  _updatePeriodTabStyle() {
    for (const [id, btn] of this._periodTabs) {
      if (id === this._period) btn.add_style_class_name('codeburn-period-active');
      else btn.remove_style_class_name('codeburn-period-active');
    }
  }

  _buildInsightPills() {
    const row = new St.BoxLayout({ style_class: 'codeburn-insight-row' });
    this._insightPills = new Map();
    for (const i of INSIGHTS) {
      const btn = new St.Button({ label: i.label, style_class: 'codeburn-insight-pill', can_focus: true, x_expand: true });
      btn.connect('clicked', () => {
        this._insight = i.id;
        this._updateInsightPillStyle();
        this._renderContent();
      });
      row.add_child(btn);
      this._insightPills.set(i.id, btn);
    }
    this._scrollContent.add_child(row);
    this._updateInsightPillStyle();
  }

  _updateInsightPillStyle() {
    for (const [id, btn] of this._insightPills) {
      if (id === this._insight) btn.add_style_class_name('codeburn-insight-pill-active');
      else btn.remove_style_class_name('codeburn-insight-pill-active');
    }
  }

  _buildTokenChart() {
    this._chartContainer = new St.BoxLayout({ vertical: true, style_class: 'codeburn-chart' });
    const header = new St.BoxLayout({ style_class: 'codeburn-chart-header' });
    this._chartLabel = new St.Label({ text: 'Tokens', style_class: 'codeburn-chart-label', x_expand: true });
    this._chartTotal = new St.Label({ text: '', style_class: 'codeburn-chart-total' });
    header.add_child(this._chartLabel);
    header.add_child(this._chartTotal);
    this._chartContainer.add_child(header);
    this._chartBars = new St.BoxLayout({ style_class: 'codeburn-chart-bars' });
    this._chartContainer.add_child(this._chartBars);
    this._scrollContent.add_child(this._chartContainer);
  }

  _buildContentArea() {
    this._scrollContent.add_child(new St.Widget({ style_class: 'codeburn-divider' }));
    this._contentArea = new St.BoxLayout({ vertical: true, style_class: 'codeburn-content' });
    this._scrollContent.add_child(this._contentArea);
  }

  _buildBudgetAlert() {
    this._budgetLabel = new St.Label({ text: '', style_class: 'codeburn-budget-warning', visible: false });
    this._scrollContent.add_child(this._budgetLabel);
  }

  _buildFindingsSection() {
    this._findingsBtn = new St.Button({ style_class: 'codeburn-findings', visible: false });
    const box = new St.BoxLayout({ style_class: 'codeburn-findings-inner' });
    this._findingsCount = new St.Label({ text: '', style_class: 'codeburn-findings-count' });
    this._findingsSavings = new St.Label({ text: '', style_class: 'codeburn-findings-savings' });
    box.add_child(this._findingsCount);
    box.add_child(this._findingsSavings);
    this._findingsBtn.set_child(box);
    this._findingsBtn.connect('clicked', () => this._spawnTerminal(['codeburn', 'optimize']));
    this._scrollContent.add_child(this._findingsBtn);
  }

  _buildLoadingIndicator() {
    this._loadingBox = new St.BoxLayout({ vertical: true, style_class: 'codeburn-loading', visible: false, x_expand: true });
    const widths = [0.85, 0.6, 0.92, 0.5, 0.75, 0.45];
    for (const w of widths) {
      const bar = new St.Widget({ style_class: 'codeburn-skeleton-bar', x_expand: false });
      bar.set_width(Math.round(308 * w));
      bar.set_height(10);
      this._loadingBox.add_child(bar);
    }
    this._scrollContent.add_child(this._loadingBox);
  }

  _showLoading() {
    if (!this._loadingBox) return;
    this._loadingBox.visible = true;
    this._loadingBox.get_children().forEach((bar, i) => {
      bar.opacity = 255;
      bar.ease({
        opacity: 60,
        duration: 900,
        delay: i * 120,
        mode: Clutter.AnimationMode.EASE_IN_OUT_SINE,
        repeatCount: -1,
        autoReverse: true,
      });
    });
  }

  _hideLoading() {
    if (!this._loadingBox) return;
    this._loadingBox.visible = false;
    this._loadingBox.get_children().forEach(bar => {
      bar.remove_all_transitions();
      bar.opacity = 255;
    });
  }

  _buildFooter() {
    this._currencyPicker = new St.ScrollView({
      style_class: 'codeburn-currency-picker',
      visible: false,
      hscrollbar_policy: St.PolicyType.NEVER,
      vscrollbar_policy: St.PolicyType.AUTOMATIC,
    });
    const pickerList = new St.BoxLayout({ vertical: true, style_class: 'codeburn-currency-list' });
    for (const c of CURRENCIES) {
      const item = new St.Button({ label: `${c.symbol} ${c.code}`, style_class: 'codeburn-currency-item', can_focus: true });
      if (c.code === this._currency.code) item.add_style_class_name('codeburn-currency-item-active');
      item.connect('clicked', () => {
        this._setCurrency(c.code);
        this._currencyPicker.hide();
        pickerList.get_children().forEach(ch => ch.remove_style_class_name('codeburn-currency-item-active'));
        item.add_style_class_name('codeburn-currency-item-active');
      });
      pickerList.add_child(item);
    }
    this._currencyPicker.set_child(pickerList);
    this._root.add_child(this._currencyPicker);

    const footer = new St.BoxLayout({ style_class: 'codeburn-footer' });

    this._currencyBtn = new St.Button({
      label: `${this._currency.code} ⌄`,
      style_class: 'codeburn-footer-btn codeburn-currency-btn',
      can_focus: true,
    });
    this._currencyBtn.connect('clicked', () => this._toggleCurrencyPicker());
    footer.add_child(this._currencyBtn);

    const refreshBtn = new St.Button({ label: 'Refresh', style_class: 'codeburn-footer-btn', can_focus: true, x_expand: true });
    refreshBtn.connect('clicked', () => this._refresh(true));
    footer.add_child(refreshBtn);

    const reportBtn = new St.Button({ label: 'Full Report', style_class: 'codeburn-footer-btn codeburn-footer-cta', can_focus: true, x_expand: true });
    reportBtn.connect('clicked', () => this._spawnTerminal(['codeburn', 'report', '--period', this._period, '--provider', this._provider]));
    footer.add_child(reportBtn);

    const prefsBtn = new St.Button({ label: '⚙', style_class: 'codeburn-footer-btn codeburn-prefs-btn', can_focus: true });
    prefsBtn.connect('clicked', () => {
      this._extension.openPreferences();
      this.menu.close();
    });
    footer.add_child(prefsBtn);

    this._root.add_child(footer);
    this._updatedLabel = new St.Label({ text: '', style_class: 'codeburn-updated' });
    this._root.add_child(this._updatedLabel);
  }

  // -- Settings --

  _connectSettings() {
    const watch = (key, cb) => {
      const id = this._settings.connect(`changed::${key}`, cb);
      this._settingsChangedIds.push(id);
    };
    watch('refresh-interval', () => this._restartRefreshLoop());
    watch('compact-mode', () => { this._panelLabel.visible = !this._settings.get_boolean('compact-mode'); });
    watch('codeburn-path', () => {
      this._dataClient.setCodeburnPath(this._settings.get_string('codeburn-path'));
      this._refresh(true);
    });
    watch('default-period', () => {
      this._period = this._settings.get_string('default-period');
      this._updatePeriodTabStyle();
      this._refresh();
    });
    watch('budget-threshold', () => this._updateBudget());
    watch('budget-alert-enabled', () => this._updateBudget());
    watch('force-dark-mode', () => this._applyThemeClass());
    watch('show-exact-costs', () => {
      this._exactCosts = this._settings.get_boolean('show-exact-costs');
      if (this._payload) this._render(this._payload);
    });
    watch('disabled-providers', () => {
      if (this._payload) this._render(this._payload);
    });
  }

  _getDisabledProviders() {
    return new Set(this._settings.get_strv('disabled-providers'));
  }

  // -- Provider detection --

  _detectProviders() {
    const home = GLib.get_home_dir();
    const xdgData = GLib.getenv('XDG_DATA_HOME') || `${home}/.local/share`;
    const checks = Object.fromEntries(
      Object.entries(PROVIDER_PATHS).map(([id, rel]) => [id, `${home}/${rel}`])
    );
    checks.opencode = `${xdgData}/opencode`;
    const out = [];
    for (const [id, path] of Object.entries(checks)) {
      if (Gio.File.new_for_path(path).query_exists(null)) out.push(id);
    }
    return out;
  }

  // -- Refresh loop --

  _startRefreshLoop() {
    const interval = this._settings.get_uint('refresh-interval') || 30;
    this._refreshSourceId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, interval, () => {
      this._refresh();
      return GLib.SOURCE_CONTINUE;
    });
  }

  _restartRefreshLoop() {
    if (this._refreshSourceId) {
      GLib.Source.remove(this._refreshSourceId);
      this._refreshSourceId = 0;
    }
    this._startRefreshLoop();
  }

  // -- Data fetching with cache --

  _cacheKey() {
    return `${this._period}|${this._provider}`;
  }

  async _refresh(force = false) {
    const key = this._cacheKey();
    const cached = this._payloadCache.get(key);
    const cacheAge = cached ? Date.now() - cached.fetchedAt : Infinity;

    if (!force && cached && cacheAge < CACHE_TTL_MS) {
      this._payload = cached.payload;
      this._render(this._payload);
      return;
    }

    if (this._inFlightKeys.has(key)) return;
    this._inFlightKeys.add(key);
    const gen = ++this._refreshGen;

    if (cached) {
      this._payload = cached.payload;
      this._render(this._payload);
    } else {
      this._showLoading();
      if (this._contentArea) this._contentArea.opacity = 120;
    }

    try {
      const payload = await this._dataClient.fetch(this._period, this._provider);
      this._inFlightKeys.delete(key);
      if (this._destroyed || gen !== this._refreshGen) return;
      this._payloadCache.set(key, { payload, fetchedAt: Date.now() });
      if (this._cacheKey() === key) {
        this._payload = payload;
        this._hideLoading();
        if (this._contentArea) this._contentArea.opacity = 255;
        this._render(this._payload);
      }
    } catch (e) {
      this._inFlightKeys.delete(key);
      if (this._destroyed) return;
      this._hideLoading();
      if (this._contentArea) this._contentArea.opacity = 255;
      if (gen !== this._refreshGen) return;
      if (e.message?.includes('cancelled')) return;
      log(`CodeBurn: refresh error: ${e.message}`);
      if (!this._payload) this._renderError(e.message);
    }
  }

  // -- Rendering --

  _render(payload) {
    const current = payload?.current ?? {};
    const cost = Number(current.cost ?? 0);
    const savings = Number(current?.localModelSavings?.totalUSD ?? 0);

    this._panelLabel.set_text(this._fmt(cost));
    this._heroLabel.set_text(current.label || '');
    this._heroAmount.set_text(this._fmt(cost));

    const calls = Number(current.calls ?? 0);
    const sessions = Number(current.sessions ?? 0);
    const metaParts = [`${calls.toLocaleString()} calls`, `${sessions} sessions`];
    if (savings > 0) metaParts.push(`saved ${this._fmt(savings)}`);
    this._heroMeta.set_text(metaParts.join('   '));

    this._renderChart(payload?.history?.daily ?? []);
    this._renderContent();
    this._renderFindings(payload?.optimize ?? {});
    this._updateBudget();

    const updated = payload?.generated ? formatTime(new Date(payload.generated)) : '';
    this._updatedLabel.set_text(updated ? `Updated ${updated}` : '');
  }

  _renderChart(daily) {
    this._chartBars.destroy_all_children();
    const days = Array.isArray(daily) ? daily.slice(-19) : [];
    if (days.length === 0) {
      this._chartContainer.visible = false;
      return;
    }
    const inTotals = days.map(d => Number(d?.inputTokens) || 0);
    const outTotals = days.map(d => Number(d?.outputTokens) || 0);
    const totals = inTotals.map((v, i) => v + outTotals[i]);
    let maxTotal = 1;
    let totalIn = 0;
    let totalOut = 0;
    let hasAnyTokens = false;
    for (let i = 0; i < days.length; i++) {
      if (totals[i] > maxTotal) maxTotal = totals[i];
      if (totals[i] > 0) hasAnyTokens = true;
      totalIn += inTotals[i];
      totalOut += outTotals[i];
    }
    if (!hasAnyTokens) {
      this._chartContainer.visible = false;
      return;
    }
    this._chartContainer.visible = true;
    const summaryText = `In: ${formatTokensCompact(totalIn)}  Out: ${formatTokensCompact(totalOut)}`;
    this._chartTotal.set_text(summaryText);
    this._chartSummaryText = summaryText;

    const chartWidth = 308;
    const gap = 2;
    const barW = Math.max(4, Math.floor((chartWidth - gap * (days.length - 1)) / days.length));

    for (let i = 0; i < days.length; i++) {
      const h = Math.max(2, Math.round((totals[i] / maxTotal) * CHART_HEIGHT));
      const col = new St.BoxLayout({ vertical: true, style_class: 'codeburn-chart-col', reactive: true });
      col.set_width(barW);
      col.set_height(CHART_HEIGHT);
      const spacer = new St.Widget({ style_class: 'codeburn-chart-spacer' });
      spacer.set_height(CHART_HEIGHT - h);
      const bar = new St.Widget({ style_class: 'codeburn-chart-bar' });
      bar.set_width(barW);
      bar.set_height(h);
      col.add_child(spacer);
      col.add_child(bar);

      const date = days[i]?.date || '';
      const inTok = formatTokensCompact(inTotals[i]);
      const outTok = formatTokensCompact(outTotals[i]);
      const cost = days[i]?.cost != null ? this._fmt(days[i].cost) : '';
      col.connect('enter-event', () => {
        this._chartTotal.set_text(`${date}  ${inTok}/${outTok}  ${cost}`);
        this._chartTotal.add_style_class_name('codeburn-chart-total-hover');
        bar.add_style_class_name('codeburn-chart-bar-hover');
        return Clutter.EVENT_PROPAGATE;
      });
      col.connect('leave-event', () => {
        this._chartTotal.set_text(this._chartSummaryText);
        this._chartTotal.remove_style_class_name('codeburn-chart-total-hover');
        bar.remove_style_class_name('codeburn-chart-bar-hover');
        return Clutter.EVENT_PROPAGATE;
      });

      this._chartBars.add_child(col);
    }
  }

  _renderContent() {
    this._contentArea.destroy_all_children();
    switch (this._insight) {
      case 'trend': return this._renderTrendView();
      case 'forecast': return this._renderForecastView();
      case 'pulse': return this._renderPulseView();
      case 'stats': return this._renderStatsView();
      default: return this._renderActivityView();
    }
  }

  _renderActivityView() {
    const current = this._payload?.current ?? {};
    this._contentArea.add_child(this._sectionTitle('Activity'));
    const actHeader = new St.BoxLayout({ style_class: 'codeburn-table-header' });
    actHeader.add_child(new St.Label({ text: 'Name', style_class: 'codeburn-th', x_expand: true }));
    actHeader.add_child(new St.Label({ text: 'Cost', style_class: 'codeburn-th codeburn-th-right codeburn-th-cost' }));
    actHeader.add_child(new St.Label({ text: 'Turns', style_class: 'codeburn-th codeburn-th-right codeburn-th-turns' }));
    actHeader.add_child(new St.Label({ text: '1-shot', style_class: 'codeburn-th codeburn-th-right codeburn-th-turns' }));
    this._contentArea.add_child(actHeader);
    const rows = new St.BoxLayout({ vertical: true, style_class: 'codeburn-activity-rows' });
    const activities = Array.isArray(current.topActivities) ? current.topActivities : [];
    if (!activities.length) {
      rows.add_child(new St.Label({ text: 'No activity for this period', style_class: 'codeburn-empty' }));
    } else {
      const maxCost = activities.reduce((m, a) => Math.max(m, Number(a.cost) || 0), 0) || 1;
      for (const a of activities.slice(0, TOP_ACTIVITIES)) {
        rows.add_child(this._buildActivityRow(a, maxCost));
      }
    }
    this._contentArea.add_child(rows);

    const models = Array.isArray(current.topModels) ? current.topModels : [];
    if (models.length) {
      this._contentArea.add_child(this._sectionTitle('Models'));
      const modHeader = new St.BoxLayout({ style_class: 'codeburn-table-header' });
      modHeader.add_child(new St.Label({ text: 'Model', style_class: 'codeburn-th', x_expand: true }));
      modHeader.add_child(new St.Label({ text: 'Cost', style_class: 'codeburn-th codeburn-th-right codeburn-th-cost' }));
      modHeader.add_child(new St.Label({ text: 'Calls', style_class: 'codeburn-th codeburn-th-right codeburn-th-calls' }));
      this._contentArea.add_child(modHeader);
      const mrows = new St.BoxLayout({ vertical: true, style_class: 'codeburn-models-rows' });
      for (const m of models.slice(0, 3)) mrows.add_child(this._buildModelRow(m));
      this._contentArea.add_child(mrows);
    }
  }

  _renderTrendView() {
    const daily = this._payload?.history?.daily ?? [];
    if (!daily.length) {
      this._contentArea.add_child(new St.Label({ text: 'Not enough history yet', style_class: 'codeburn-empty' }));
      return;
    }
    for (const d of daily.slice(-7).reverse()) {
      const row = new St.BoxLayout({ style_class: 'codeburn-trend-row' });
      row.add_child(new St.Label({ text: d.date, style_class: 'codeburn-trend-date', x_expand: true }));
      const costLabel = new St.Label({ text: this._fmt(d.cost), style_class: 'codeburn-trend-cost' });
      costLabel.clutter_text.x_align = Clutter.ActorAlign.END;
      row.add_child(costLabel);
      const callsLabel = new St.Label({ text: `${Number(d.calls).toLocaleString()} calls`, style_class: 'codeburn-trend-calls' });
      callsLabel.clutter_text.x_align = Clutter.ActorAlign.END;
      row.add_child(callsLabel);
      this._contentArea.add_child(row);
    }
  }

  _renderForecastView() {
    const daily = this._payload?.history?.daily ?? [];
    if (daily.length < 3) {
      this._contentArea.add_child(new St.Label({ text: 'Need at least 3 days of history', style_class: 'codeburn-empty' }));
      return;
    }
    const last7 = daily.slice(-7);
    const avg = last7.reduce((s, d) => s + Number(d.cost || 0), 0) / last7.length;
    const yesterday = daily.at(-2);
    const yestCost = Number(yesterday?.cost || 0);
    const todCost = Number(daily.at(-1)?.cost || 0);
    const dod = yestCost > 0 ? ((todCost - yestCost) / yestCost) * 100 : 0;
    const now = new Date();
    const dayOfMonth = now.getUTCDate();
    const daysInMonth = new Date(now.getUTCFullYear(), now.getUTCMonth() + 1, 0).getUTCDate();

    this._contentArea.add_child(this._kvRow('7-day avg', this._fmt(avg)));
    this._contentArea.add_child(this._kvRow('Yesterday', yesterday ? this._fmt(yestCost) : '-'));
    this._contentArea.add_child(this._kvRow('Day-over-day', `${dod > 0 ? '+' : ''}${dod.toFixed(1)}%`));
    this._contentArea.add_child(this._kvRow('Month projection', this._fmt(avg * daysInMonth)));
    this._contentArea.add_child(this._kvRow('Days elapsed', `${dayOfMonth} of ${daysInMonth}`));
  }

  _renderPulseView() {
    const current = this._payload?.current ?? {};
    const daily = this._payload?.history?.daily ?? [];
    this._contentArea.add_child(this._sectionTitle('Pulse'));
    const row = new St.BoxLayout({ style_class: 'codeburn-pulse-row' });
    row.add_child(this._pulseTile(this._fmt(current.cost), 'cost'));
    row.add_child(this._pulseTile(Number(current.calls || 0).toLocaleString(), 'calls'));
    row.add_child(this._pulseTile(`${Number(current.cacheHitPercent || 0).toFixed(0)}%`, 'cache hit'));
    this._contentArea.add_child(row);

    if (daily.length) {
      this._contentArea.add_child(this._sectionTitle('Last 7 days'));
      const last7 = daily.slice(-7);
      const sumCost = last7.reduce((s, d) => s + Number(d.cost || 0), 0);
      const sumCalls = last7.reduce((s, d) => s + Number(d.calls || 0), 0);
      const peakDay = last7.reduce((best, d) => Number(d.cost || 0) > Number(best.cost || 0) ? d : best, last7[0]);
      this._contentArea.add_child(this._kvRow('Total spend', this._fmt(sumCost)));
      this._contentArea.add_child(this._kvRow('Total calls', Number(sumCalls).toLocaleString()));
      this._contentArea.add_child(this._kvRow('Peak day', `${peakDay?.date || '-'}  ${this._fmt(peakDay?.cost)}`));
    }
  }

  _renderStatsView() {
    const current = this._payload?.current ?? {};
    const daily = this._payload?.history?.daily ?? [];
    this._contentArea.add_child(this._sectionTitle('Stats'));
    const models = Array.isArray(current.topModels) ? current.topModels : [];
    const favModel = models[0]?.name ?? '-';
    const activeDays = daily.filter(d => Number(d.cost || 0) > 0).length;
    const peakDay = daily.reduce((best, d) => Number(d.cost || 0) > Number((best || {}).cost || 0) ? d : best, null);
    let streak = 0;
    for (let i = daily.length - 1; i >= 0; i--) {
      if (Number(daily[i].cost || 0) > 0) streak++;
      else break;
    }
    this._contentArea.add_child(this._kvRow('Favorite model', favModel));
    this._contentArea.add_child(this._kvRow('Active days', `${activeDays}`));
    this._contentArea.add_child(this._kvRow('Current streak', `${streak} days`));
    if (peakDay) this._contentArea.add_child(this._kvRow('Peak day', `${peakDay.date}  ${this._fmt(peakDay.cost)}`));
  }

  _renderFindings(optimize) {
    const count = Number(optimize?.findingCount ?? 0);
    if (count === 0) {
      this._findingsBtn.hide();
      return;
    }
    const savings = Number(optimize?.savingsUSD ?? 0);
    this._findingsCount.set_text(`${count} optimize findings`);
    this._findingsSavings.set_text(`save ~${this._fmt(savings)}`);
    this._findingsBtn.show();
  }

  _renderError(message) {
    this._panelLabel.set_text('!');
    if (message?.includes('not found') || message?.includes('No such file')) {
      this._heroLabel.set_text('CodeBurn CLI not found');
      this._heroMeta.set_text('Install: npm i -g codeburn');
    } else {
      this._heroLabel.set_text('Error loading data');
      this._heroMeta.set_text(message?.substring(0, 80) || 'Unknown error');
    }
    this._heroAmount.set_text('');
    this._findingsBtn.hide();
  }

  // -- Budget --

  _updateBudget() {
    const enabled = this._settings.get_boolean('budget-alert-enabled');
    const threshold = this._settings.get_double('budget-threshold');
    if (!enabled || threshold <= 0 || !this._payload?.current) {
      this._budgetLabel.visible = false;
      return;
    }
    const cost = Number(this._payload.current.cost ?? 0) * this._fxRate;
    const thresholdConverted = threshold * this._fxRate;
    if (cost >= thresholdConverted) {
      this._budgetLabel.set_text(`Budget exceeded: ${this._fmt(cost)} / ${this._fmt(thresholdConverted)}`);
      this._budgetLabel.visible = true;
    } else {
      this._budgetLabel.visible = false;
    }
  }

  // -- Currency --

  _loadCurrency() {
    const configPath = GLib.build_filenamev([GLib.get_home_dir(), '.config', 'codeburn', 'config.json']);
    try {
      const [ok, contents] = GLib.file_get_contents(configPath);
      if (ok) {
        const config = JSON.parse(new TextDecoder().decode(contents));
        if (config.currency?.code) {
          const known = CURRENCIES.find(c => c.code === config.currency.code);
          if (known) return known;
          return { code: config.currency.code, symbol: config.currency.symbol || `${config.currency.code} ` };
        }
      }
    } catch (_) { /* default */ }
    return CURRENCIES[0];
  }

  _toggleCurrencyPicker() {
    this._currencyPicker.visible = !this._currencyPicker.visible;
  }

  _setCurrency(code) {
    try {
      Gio.Subprocess.new(['codeburn', 'currency', code], Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);
    } catch (_) { /* CLI missing */ }
    const known = CURRENCIES.find(c => c.code === code);
    this._currency = known || { code, symbol: `${code} ` };
    this._currencyBtn.set_label(`${this._currency.code} ⌄`);
    this._updateFxRate();
  }

  _updateFxRate() {
    const code = this._currency?.code || 'USD';
    if (this._fxCache[code] !== undefined) {
      this._fxRate = this._fxCache[code];
      if (this._payload) this._render(this._payload);
      return;
    }
    const url = `https://api.frankfurter.app/latest?from=USD&to=${code}`;
    const msg = Soup.Message.new('GET', url);
    this._soupSession.send_and_read_async(msg, GLib.PRIORITY_DEFAULT, null, (session, result) => {
      if (this._destroyed) return;
      try {
        const bytes = session.send_and_read_finish(result);
        if (!bytes) return;
        const json = JSON.parse(new TextDecoder().decode(bytes.get_data()));
        const rate = json?.rates?.[code];
        if (typeof rate === 'number' && rate > 0) {
          this._fxCache[code] = rate;
          this._fxRate = rate;
          if (this._payload) this._render(this._payload);
        }
      } catch (_) { /* FX fetch failed */ }
    });
  }

  _fmt(value) {
    return formatCost(value, this._currency, this._fxRate, this._exactCosts);
  }

  // -- UI helpers --

  _sectionTitle(text) {
    return new St.Label({ text, style_class: 'codeburn-section-title' });
  }

  _kvRow(label, value) {
    const row = new St.BoxLayout({ style_class: 'codeburn-kv-row' });
    row.add_child(new St.Label({ text: label, style_class: 'codeburn-kv-label', x_expand: true }));
    row.add_child(new St.Label({ text: String(value ?? '-'), style_class: 'codeburn-kv-value' }));
    return row;
  }

  _pulseTile(value, label) {
    const tile = new St.BoxLayout({ vertical: true, style_class: 'codeburn-pulse-tile', x_expand: true });
    tile.add_child(new St.Label({ text: value, style_class: 'codeburn-pulse-value' }));
    tile.add_child(new St.Label({ text: label, style_class: 'codeburn-pulse-label' }));
    return tile;
  }

  _buildActivityRow(activity, maxCost) {
    const row = new St.BoxLayout({ vertical: true, style_class: 'codeburn-activity-row' });
    const topLine = new St.BoxLayout({ style_class: 'codeburn-activity-top' });
    topLine.add_child(new St.Label({ text: activity.name, style_class: 'codeburn-activity-name', x_expand: true }));
    const costLabel = new St.Label({ text: this._fmt(activity.cost), style_class: 'codeburn-activity-cost' });
    costLabel.clutter_text.x_align = Clutter.ActorAlign.END;
    topLine.add_child(costLabel);
    const turnsLabel = new St.Label({ text: `${Number(activity.turns) || 0}`, style_class: 'codeburn-activity-turns' });
    turnsLabel.clutter_text.x_align = Clutter.ActorAlign.END;
    topLine.add_child(turnsLabel);
    const osText = activity.oneShotRate != null ? `${Math.round(Number(activity.oneShotRate) * 100)}%` : '--';
    const osLabel = new St.Label({ text: osText, style_class: 'codeburn-activity-oneshot' });
    osLabel.clutter_text.x_align = Clutter.ActorAlign.END;
    topLine.add_child(osLabel);
    row.add_child(topLine);

    const track = new St.BoxLayout({ style_class: 'codeburn-bar-track' });
    const pct = Math.max(0.02, Math.min(1, Number(activity.cost) / maxCost));
    const fill = new St.Widget({ style_class: 'codeburn-bar-fill' });
    fill.set_width(Math.round(BAR_TRACK_WIDTH * pct));
    track.add_child(fill);
    row.add_child(track);
    return row;
  }

  _buildModelRow(model) {
    const row = new St.BoxLayout({ style_class: 'codeburn-model-row' });
    row.add_child(new St.Label({ text: model.name, style_class: 'codeburn-model-name', x_expand: true }));
    const mc = new St.Label({ text: this._fmt(model.cost), style_class: 'codeburn-model-cost' });
    mc.clutter_text.x_align = Clutter.ActorAlign.END;
    row.add_child(mc);
    // Show saved counterfactual when this local model has a savings
    // mapping. Kept as a separate column so it never gets summed with
    // the actual cost on the left.
    const savings = Number(model.savingsUSD || 0);
    const savedLabel = new St.Label({
      text: savings > 0 ? this._fmt(savings) : '—',
      style_class: 'codeburn-model-saved',
    });
    savedLabel.clutter_text.x_align = Clutter.ActorAlign.END;
    row.add_child(savedLabel);
    const mcalls = new St.Label({ text: `${Number(model.calls || 0).toLocaleString()}`, style_class: 'codeburn-model-calls' });
    mcalls.clutter_text.x_align = Clutter.ActorAlign.END;
    row.add_child(mcalls);
    return row;
  }

  // -- Theme --

  _applyThemeClass() {
    const forceDark = this._settings.get_boolean('force-dark-mode');
    const scheme = this._themeSettings.get_string('color-scheme');
    const isDark = forceDark || scheme === 'prefer-dark';
    if (isDark) {
      this._root?.add_style_class_name('codeburn-dark');
      this._root?.remove_style_class_name('codeburn-light');
    } else {
      this._root?.add_style_class_name('codeburn-light');
      this._root?.remove_style_class_name('codeburn-dark');
    }
  }

  // -- Terminal spawning --

  _spawnTerminal(argv) {
    const command = `${argv.join(' ')}; echo; read -n 1 -s -r -p 'Press any key to close...'`;
    try {
      Gio.Subprocess.new(['gnome-terminal', '--', 'bash', '-lc', command], Gio.SubprocessFlags.NONE);
    } catch (e) {
      log(`CodeBurn: terminal spawn error: ${e.message}`);
    }
    this.menu.close();
  }

  // -- Cleanup --

  destroy() {
    this._destroyed = true;
    if (this._refreshSourceId) {
      GLib.Source.remove(this._refreshSourceId);
      this._refreshSourceId = 0;
    }
    if (this._themeSettings && this._themeSignal) {
      this._themeSettings.disconnect(this._themeSignal);
      this._themeSignal = null;
      this._themeSettings = null;
    }
    for (const id of this._settingsChangedIds) this._settings.disconnect(id);
    this._settingsChangedIds = [];
    this._dataClient?.destroy();
    if (this._soupSession) {
      this._soupSession.abort();
      this._soupSession = null;
    }
    super.destroy();
  }
});
