// src/screens/backtesting/BacktestingScreen_Results.cpp
//
// Result rendering: summary cards, metrics table, details table, raw JSON
// pane, and the per-command formatting in display_result(). Also owns the
// service-callback handlers that route results into the UI.
//
// Part of the partial-class split of BacktestingScreen.cpp; helpers shared
// across split files live in BacktestingScreen_internal.h.

#include "core/logging/Logger.h"
#include "screens/backtesting/BacktestingScreen.h"
#include "screens/backtesting/BacktestingScreen_internal.h"
#include "services/backtesting/BacktestingService.h"
#include "ui/theme/Theme.h"

#include <QGridLayout>
#include <QHeaderView>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QRegularExpression>
#include <QtCharts/QChart>
#include <QtCharts/QChartView>
#include <QtCharts/QDateTimeAxis>
#include <QtCharts/QLineSeries>
#include <QtCharts/QValueAxis>

namespace fincept::screens {

using namespace fincept::services::backtest;
using fincept::screens::backtesting_internal::clear_layout;
using fincept::screens::backtesting_internal::fmt_metric;

// ── Result display ───────────────────────────────────────────────────────────

void BacktestingScreen::clear_results() {
    clear_layout(summary_layout_);
    metrics_table_->setRowCount(0);
    trades_table_->setRowCount(0);
    trades_table_->setColumnCount(0);
    raw_json_edit_->clear();
    if (equity_chart_tab_ && equity_chart_tab_->layout()) {
        auto* layout = equity_chart_tab_->layout();
        while (layout->count() > 0) {
            auto* item = layout->takeAt(0);
            if (item->widget())
                item->widget()->deleteLater();
            delete item;
        }
    }
}

void BacktestingScreen::display_result(const QString& command, const QJsonObject& payload) {
    clear_results();

    const auto accent = providers_[active_provider_].color.name();

    // ── Local helpers (capture summary_container_ / metrics_table_ / trades_table_) ──

    auto add_section_header = [&](const QString& text, const QString& color = QString()) {
        auto* lbl = new QLabel(text, summary_container_);
        lbl->setStyleSheet(QString("color:%1; font-size:%2px; font-weight:700; font-family:%3; "
                                   "letter-spacing:1px; padding-top:4px;")
                               .arg(color.isEmpty() ? accent : color)
                               .arg(ui::fonts::TINY)
                               .arg(ui::fonts::DATA_FAMILY));
        summary_layout_->addWidget(lbl);
    };

    auto humanize = [](QString key) {
        key.replace('_', ' ');
        key.replace(QRegularExpression("([a-z])([A-Z])"), "\\1 \\2");
        return key.toUpper();
    };

    auto add_cards_from = [&](const QJsonObject& obj, const QStringList& ordered_keys, int cols_per_row = 3) {
        if (obj.isEmpty())
            return;
        auto* cards = new QWidget(summary_container_);
        auto* gl = new QGridLayout(cards);
        gl->setContentsMargins(0, 0, 0, 0);
        gl->setSpacing(8);

        // Build the display order: explicit keys first, then any remaining scalars.
        QStringList order = ordered_keys;
        if (order.isEmpty()) {
            for (auto it = obj.begin(); it != obj.end(); ++it)
                if (!it.value().isObject() && !it.value().isArray())
                    order.append(it.key());
        }

        int col = 0, row = 0;
        QStringList shown;
        for (const auto& key : order) {
            if (!obj.contains(key))
                continue;
            const auto& val = obj[key];
            if (val.isObject() || val.isArray() || val.isNull())
                continue;
            auto canon = key;
            canon.replace(QRegularExpression("([a-z])([A-Z])"), "\\1_\\2");
            canon = canon.toLower();
            if (shown.contains(canon))
                continue;
            shown.append(canon);

            auto* card = new QWidget(cards);
            card->setStyleSheet(
                QString("background:%1; border:1px solid %2;").arg(ui::colors::BG_RAISED(), ui::colors::BORDER_DIM()));
            auto* cvl = new QVBoxLayout(card);
            cvl->setContentsMargins(10, 8, 10, 8);
            cvl->setSpacing(2);
            auto* lbl = new QLabel(humanize(key), card);
            lbl->setStyleSheet(QString("color:%1; font-size:%2px; font-family:%3;")
                                   .arg(ui::colors::TEXT_TERTIARY())
                                   .arg(ui::fonts::TINY)
                                   .arg(ui::fonts::DATA_FAMILY));
            auto* vlbl = new QLabel(fmt_metric(key, val), card);
            vlbl->setStyleSheet(QString("color:%1; font-size:%2px; font-weight:700; font-family:%3;")
                                    .arg(accent)
                                    .arg(ui::fonts::DATA)
                                    .arg(ui::fonts::DATA_FAMILY));
            cvl->addWidget(lbl);
            cvl->addWidget(vlbl);
            gl->addWidget(card, row, col);
            if (++col >= cols_per_row) {
                col = 0;
                row++;
            }
        }
        if (row > 0 || col > 0)
            summary_layout_->addWidget(cards);
        else
            cards->deleteLater();
    };

    auto fill_kv_table = [&](const QJsonObject& obj) {
        QStringList keys;
        for (auto it = obj.begin(); it != obj.end(); ++it)
            if (!it.value().isObject() && !it.value().isArray() && !it.value().isNull())
                keys.append(it.key());
        metrics_table_->setRowCount(keys.size());
        for (int r = 0; r < keys.size(); ++r) {
            auto* name = new QTableWidgetItem(humanize(keys[r]));
            metrics_table_->setItem(r, 0, name);
            auto* val = new QTableWidgetItem(fmt_metric(keys[r], obj[keys[r]]));
            val->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
            metrics_table_->setItem(r, 1, val);
        }
        metrics_table_->resizeColumnsToContents();
    };

    // Row-cap bookkeeping — the DETAILS table is capped at kMaxDetailRows so a
    // 100k-trade backtest doesn't allocate 100k QTableWidgetItems. The cap must
    // be VISIBLE (tab label + banner) or a trader silently reads a 200-trade
    // sample as the whole trade list.
    constexpr int kMaxDetailRows = 200;
    int details_total = 0;
    int details_shown = 0;

    auto fill_details_table = [&](const QJsonArray& arr, const QStringList& preferred_cols = {}) {
        if (arr.isEmpty() || !arr[0].isObject())
            return;
        auto first = arr[0].toObject();
        QStringList cols = preferred_cols;
        if (cols.isEmpty()) {
            for (auto it = first.begin(); it != first.end(); ++it)
                if (!it.value().isObject() && !it.value().isArray())
                    cols.append(it.key());
        }
        const int rows = qMin(static_cast<int>(arr.size()), kMaxDetailRows);
        details_total = static_cast<int>(arr.size());
        details_shown = rows;
        trades_table_->setColumnCount(cols.size());
        trades_table_->setHorizontalHeaderLabels(cols);
        trades_table_->setRowCount(rows);
        for (int r = 0; r < rows; ++r) {
            auto rec = arr[r].toObject();
            for (int c = 0; c < cols.size(); ++c) {
                auto* item = new QTableWidgetItem(fmt_metric(cols[c], rec[cols[c]]));
                item->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
                trades_table_->setItem(r, c, item);
            }
        }
        trades_table_->resizeColumnsToContents();
    };

    // ── Header (per-command title) ──
    QString cmd_label = command.toUpper();
    for (const auto& c : commands_)
        if (c.id == command) {
            cmd_label = c.label.toUpper();
            break;
        }
    add_section_header(tr("%1 RESULTS").arg(cmd_label));

    // ── Synthetic-data warning (Python sets this when yfinance is unavailable) ──
    if (payload.value("usingSyntheticData").toBool()) {
        auto* warn =
            new QLabel(tr("Using synthetic price data (yfinance unavailable). Results do not reflect real markets."),
                       summary_container_);
        warn->setWordWrap(true);
        warn->setStyleSheet(QString("color:%1; font-size:%2px; font-family:%3; padding:8px 10px; "
                                    "background:rgba(245,158,11,0.10); border:1px solid %4;")
                                .arg(ui::colors::WARNING())
                                .arg(ui::fonts::SMALL)
                                .arg(ui::fonts::DATA_FAMILY)
                                .arg(ui::colors::WARNING()));
        summary_layout_->addWidget(warn);
    }

    // ── Per-command rendering ──

    if (command == "backtest") {
        auto perf = payload.value("performance").toObject();

        // ── Cost disclosure ──
        // Results are only achievable net of the costs that were actually sent
        // to the provider. State them next to the headline numbers so a reader
        // never mistakes a gross curve for a tradeable one.
        {
            const double comm = commission_spin_ ? commission_spin_->value() : 0.0;
            const double slip = slippage_spin_ ? slippage_spin_->value() : 0.0;
            const bool costless = (comm <= 0.0 && slip <= 0.0);
            QString cost_text;
            QString cost_fg;
            QString cost_bg;
            QString cost_border;
            if (costless) {
                cost_text = tr("NO COSTS MODELLED — commission and slippage are both 0%. "
                               "These returns are gross and are not achievable.");
                cost_fg = ui::colors::WARNING();
                cost_bg = QStringLiteral("rgba(245,158,11,0.10)");
                cost_border = ui::colors::WARNING();
            } else {
                cost_text = tr("Costs applied: commission %1% per trade, slippage %2% per fill.")
                                .arg(comm, 0, 'f', 3)
                                .arg(slip, 0, 'f', 4);
                cost_fg = ui::colors::TEXT_TERTIARY();
                cost_bg = ui::colors::BG_RAISED();
                cost_border = ui::colors::BORDER_DIM();
            }
            auto* costs = new QLabel(cost_text, summary_container_);
            costs->setWordWrap(true);
            costs->setStyleSheet(QString("color:%1; font-size:%2px; font-family:%3; padding:6px 10px; "
                                         "background:%4; border:1px solid %5;")
                                     .arg(cost_fg)
                                     .arg(ui::fonts::TINY)
                                     .arg(ui::fonts::DATA_FAMILY)
                                     .arg(cost_bg, cost_border));
            summary_layout_->addWidget(costs);
        }

        // ── No-trade guard ──
        // A run that never traded reports 0% drawdown / 0 volatility, which reads
        // like a flawless strategy on the summary cards. Say so explicitly.
        const int n_trades = perf.value("totalTrades").toInt(perf.value("total_trades").toInt(-1));
        if (n_trades == 0) {
            auto* nt = new QLabel(tr("This strategy produced ZERO trades over the selected range. "
                                     "Every performance figure below is meaningless — widen the date range, "
                                     "loosen the entry rules, or check the symbol/interval."),
                                  summary_container_);
            nt->setWordWrap(true);
            nt->setStyleSheet(QString("color:%1; font-size:%2px; font-family:%3; padding:8px 10px; "
                                      "background:rgba(220,38,38,0.08); border:1px solid %1;")
                                  .arg(ui::colors::NEGATIVE())
                                  .arg(ui::fonts::SMALL)
                                  .arg(ui::fonts::DATA_FAMILY));
            summary_layout_->addWidget(nt);
        }

        add_cards_from(perf, {"totalReturn", "annualizedReturn", "sharpeRatio", "sortinoRatio", "maxDrawdown",
                              "winRate", "profitFactor", "calmarRatio", "totalTrades", "volatility"});
        if (payload.contains("status")) {
            auto status = payload["status"].toString();
            auto* sl = new QLabel(tr("Status: %1").arg(status), summary_container_);
            sl->setStyleSheet(QString("color:%1; font-size:%2px; font-family:%3; padding:8px;")
                                  .arg(status == "success" ? ui::colors::POSITIVE() : ui::colors::WARNING())
                                  .arg(ui::fonts::SMALL)
                                  .arg(ui::fonts::DATA_FAMILY));
            summary_layout_->addWidget(sl);
        }
        fill_kv_table(perf);
        auto trades = payload.value("trades").toArray();
        fill_details_table(trades, {"symbol", "entryDate", "exitDate", "side", "entryPrice", "exitPrice", "quantity",
                                    "pnl", "pnlPercent", "holdingPeriod", "exitReason", "commission"});
    } else if (command == "optimize") {
        QJsonObject summary{
            {"iterations", payload.value("iterations")},
            {"totalCombinations", payload.value("totalCombinations")},
            {"method", payload.value("method")},
            {"objective", payload.value("objective")},
            {"bestObjectiveValue", payload.value("bestObjectiveValue")},
        };
        add_cards_from(summary, {"bestObjectiveValue", "iterations", "totalCombinations", "method", "objective"});

        auto best_params = payload.value("bestParameters").toObject();
        if (!best_params.isEmpty()) {
            add_section_header(tr("BEST PARAMETERS"));
            add_cards_from(best_params, {}, 4);
        }
        auto best_perf = payload.value("bestPerformance").toObject();
        if (!best_perf.isEmpty()) {
            add_section_header(tr("BEST PERFORMANCE"));
            add_cards_from(best_perf,
                           {"totalReturn", "sharpeRatio", "maxDrawdown", "winRate", "profitFactor", "totalTrades"});
            fill_kv_table(best_perf);
        }
        // Details: top results — flatten params into the row
        auto all_results = payload.value("allResults").toArray();
        QJsonArray flat;
        for (const auto& v : all_results) {
            auto o = v.toObject();
            QJsonObject row;
            auto params = o.value("parameters").toObject();
            for (auto it = params.begin(); it != params.end(); ++it)
                row[it.key()] = it.value();
            row["objective"] = o.value("objective_value");
            auto perf = o.value("performance").toObject();
            for (const auto& k : QStringList{"totalReturn", "sharpeRatio", "maxDrawdown", "winRate"}) {
                if (perf.contains(k))
                    row[k] = perf.value(k);
            }
            flat.append(row);
        }
        fill_details_table(flat);
    } else if (command == "walk_forward") {
        QJsonObject summary{
            {"nWindows", payload.value("nWindows")},
            {"avgOosReturn", payload.value("avgOosReturn")},
            {"avgOosSharpe", payload.value("avgOosSharpe")},
            {"oosReturnStd", payload.value("oosReturnStd")},
            {"robustnessScore", payload.value("robustnessScore")},
            {"degradationRatio", payload.value("degradationRatio")},
        };
        add_cards_from(summary, {"nWindows", "avgOosReturn", "avgOosSharpe", "robustnessScore", "degradationRatio",
                                 "oosReturnStd"});
        fill_kv_table(summary);
        auto windows = payload.value("windows").toArray();
        fill_details_table(windows, {"window", "trainStart", "trainEnd", "testStart", "testEnd", "inSampleReturn",
                                     "inSampleSharpe", "outOfSampleReturn", "outOfSampleSharpe"});
    } else if (command == "indicator") {
        QJsonObject summary{
            {"indicator", payload.value("indicator")},
            {"symbol", payload.value("symbol")},
            {"period", payload.value("period")},
            {"totalBars", payload.value("totalBars")},
        };
        add_cards_from(summary, {"indicator", "symbol", "period", "totalBars"}, 4);
        auto stats = payload.value("stats").toObject();
        // Multi-output indicators have stats per component — flatten one level.
        if (!stats.isEmpty()) {
            QJsonObject flat;
            bool nested = false;
            for (auto it = stats.begin(); it != stats.end(); ++it) {
                if (it.value().isObject()) {
                    nested = true;
                    break;
                }
            }
            if (nested) {
                for (auto it = stats.begin(); it != stats.end(); ++it) {
                    auto sub = it.value().toObject();
                    for (auto sit = sub.begin(); sit != sub.end(); ++sit)
                        flat[it.key() + "." + sit.key()] = sit.value();
                }
            } else {
                flat = stats;
            }
            add_section_header(tr("STATISTICS"));
            add_cards_from(flat, {"last", "mean", "std", "min", "max"});
            fill_kv_table(flat);
        }
        // Details: series sample. For multi-output use the first non-empty component.
        auto series_v = payload.value("series");
        QJsonArray sample;
        if (series_v.isArray()) {
            sample = series_v.toArray();
        } else if (series_v.isObject()) {
            auto so = series_v.toObject();
            for (auto it = so.begin(); it != so.end(); ++it) {
                if (it.value().isArray() && !it.value().toArray().isEmpty()) {
                    sample = it.value().toArray();
                    break;
                }
            }
        }
        fill_details_table(sample, {"date", "value"});
    } else if (command == "indicator_signals") {
        add_cards_from(payload, {"totalSignals", "winRate", "profitFactor", "avgReturn", "medianReturn", "bestTrade",
                                 "worstTrade", "avgHoldingPeriod", "signalDensity", "entryCount", "exitCount",
                                 "originalEntryCount", "filteredEntryCount"});
        fill_kv_table(payload);
        // Combine entry & exit signals into a single details table with a Type column.
        auto entries = payload.value("entrySignals").toArray();
        auto exits = payload.value("exitSignals").toArray();
        QJsonArray combined;
        for (const auto& v : entries) {
            auto o = v.toObject();
            o["type"] = "ENTRY";
            combined.append(o);
        }
        for (const auto& v : exits) {
            auto o = v.toObject();
            o["type"] = "EXIT";
            combined.append(o);
        }
        if (combined.isEmpty()) {
            // Some modes only emit plain entries[]/exits[] string arrays
            QJsonArray plain;
            for (const auto& v : payload.value("entries").toArray()) {
                QJsonObject o;
                o["type"] = "ENTRY";
                o["date"] = v.toString();
                plain.append(o);
            }
            for (const auto& v : payload.value("exits").toArray()) {
                QJsonObject o;
                o["type"] = "EXIT";
                o["date"] = v.toString();
                plain.append(o);
            }
            combined = plain;
        }
        fill_details_table(combined);
    } else if (command == "labels") {
        QJsonObject summary{
            {"labelType", payload.value("labelType")},
            {"totalBars", payload.value("totalBars")},
            {"labeledBars", payload.value("labeledBars")},
        };
        add_cards_from(summary, {"labelType", "totalBars", "labeledBars"}, 3);

        auto dist = payload.value("distribution").toObject();
        if (!dist.isEmpty()) {
            add_section_header(tr("CLASS DISTRIBUTION"));
            // Re-key so cards show "Class -1 / Class 0 / Class 1"
            QJsonObject relabeled;
            QStringList ordered;
            QStringList raw_keys = dist.keys();
            std::sort(raw_keys.begin(), raw_keys.end(),
                      [](const QString& a, const QString& b) { return a.toInt() < b.toInt(); });
            for (const auto& k : raw_keys) {
                QString rk = tr("Class %1").arg(k);
                relabeled[rk] = dist.value(k);
                ordered.append(rk);
            }
            add_cards_from(relabeled, ordered, 5);
            fill_kv_table(relabeled);
        }
        fill_details_table(payload.value("sampleLabels").toArray(), {"date", "label"});
    } else if (command == "splits") {
        QJsonObject summary{
            {"splitterType", payload.value("splitterType")}, {"nSplits", payload.value("nSplits")},
            {"totalBars", payload.value("totalBars")},       {"indexStart", payload.value("indexStart")},
            {"indexEnd", payload.value("indexEnd")},
        };
        add_cards_from(summary, {"splitterType", "nSplits", "totalBars", "indexStart", "indexEnd"}, 3);
        fill_kv_table(summary);
        fill_details_table(payload.value("splits").toArray(),
                           {"fold", "trainStart", "trainEnd", "trainSize", "testStart", "testEnd", "testSize"});
    } else if (command == "returns") {
        QJsonObject summary{
            {"analysisType", payload.value("analysisType")},
            {"totalBars", payload.value("totalBars")},
            {"returnBars", payload.value("returnBars")},
        };
        add_cards_from(summary, {"analysisType", "totalBars", "returnBars"}, 3);

        // The interesting numbers live in payload.stats (returns_stats / drawdowns / ranges)
        // OR scattered at the top level for other shapes.
        auto stats = payload.value("stats").toObject();
        if (!stats.isEmpty()) {
            add_section_header(tr("STATISTICS"));
            add_cards_from(stats, {"Total Return", "Annualized Return", "Sharpe Ratio", "Sortino Ratio", "Calmar Ratio",
                                   "Max Drawdown", "Annualized Volatility", "Downside Risk"});
            fill_kv_table(stats);
        } else {
            // rolling-style payloads expose top-level fields
            QJsonObject topnum;
            for (const auto& k : QStringList{"metric", "window", "dataPoints", "maxDrawdown", "avgDrawdown",
                                             "totalDrawdowns", "activeDrawdown", "activeDuration", "totalRanges",
                                             "avgDuration", "maxDuration", "coverage"}) {
                if (payload.contains(k))
                    topnum[k] = payload.value(k);
            }
            if (!topnum.isEmpty()) {
                add_cards_from(topnum, {});
                fill_kv_table(topnum);
            }
        }

        // Details: the series / records that ship with the chosen analysis type
        QJsonArray detail;
        for (const auto& k : QStringList{"records", "cumulativeReturns", "drawdownSeries", "series"}) {
            if (payload.value(k).isArray()) {
                auto a = payload.value(k).toArray();
                if (!a.isEmpty()) {
                    detail = a;
                    break;
                }
            }
        }
        fill_details_table(detail);
    } else if (command == "signals") {
        QJsonObject summary{
            {"generatorType", payload.value("generatorType")},
            {"totalBars", payload.value("totalBars")},
            {"entryCount", payload.value("entryCount")},
            {"exitCount", payload.value("exitCount")},
        };
        add_cards_from(summary, {"generatorType", "entryCount", "exitCount", "totalBars"}, 4);
        fill_kv_table(summary);

        QJsonArray combined;
        for (const auto& v : payload.value("entries").toArray()) {
            QJsonObject o;
            o["type"] = "ENTRY";
            o["date"] = v.toString();
            combined.append(o);
        }
        for (const auto& v : payload.value("exits").toArray()) {
            QJsonObject o;
            o["type"] = "EXIT";
            o["date"] = v.toString();
            combined.append(o);
        }
        fill_details_table(combined, {"type", "date"});
    } else if (command == "labels_to_signals") {
        QJsonObject summary{
            {"labelType", payload.value("labelType")}, {"entryLabel", payload.value("entryLabel")},
            {"exitLabel", payload.value("exitLabel")}, {"entryCount", payload.value("entryCount")},
            {"exitCount", payload.value("exitCount")}, {"totalBars", payload.value("totalBars")},
        };
        add_cards_from(summary, {"labelType", "entryCount", "exitCount", "totalBars", "entryLabel", "exitLabel"}, 3);
        fill_kv_table(summary);
        QJsonArray combined;
        for (const auto& v : payload.value("entries").toArray()) {
            QJsonObject o;
            o["type"] = "ENTRY";
            o["date"] = v.toString();
            combined.append(o);
        }
        for (const auto& v : payload.value("exits").toArray()) {
            QJsonObject o;
            o["type"] = "EXIT";
            o["date"] = v.toString();
            combined.append(o);
        }
        fill_details_table(combined, {"type", "date"});
    } else if (command == "indicator_sweep") {
        QJsonObject summary{
            {"indicator", payload.value("indicator")},
            {"totalCombinations", payload.value("totalCombinations")},
        };
        add_cards_from(summary, {"indicator", "totalCombinations"}, 2);
        // Flatten each result row: params + scalar stats
        auto results = payload.value("results").toArray();
        QJsonArray flat;
        for (const auto& v : results) {
            auto o = v.toObject();
            QJsonObject row;
            auto params = o.value("params").toObject();
            for (auto it = params.begin(); it != params.end(); ++it)
                row[it.key()] = it.value();
            for (const auto& k : QStringList{"mean", "std", "min", "max", "last"})
                if (o.contains(k))
                    row[k] = o.value(k);
            flat.append(row);
        }
        fill_details_table(flat);
    } else {
        // Unknown command — best-effort: show all scalar fields as cards.
        add_cards_from(payload, {});
        fill_kv_table(payload);
    }

    // ── Row-cap disclosure ──
    // Make the DETAILS truncation impossible to miss: label the tab with the
    // real count and drop a banner on the summary.
    if (result_tabs_ && result_tabs_->count() > 3) {
        result_tabs_->setTabText(3, details_total > details_shown
                                        ? tr("DETAILS (%1 of %2)").arg(details_shown).arg(details_total)
                                        : tr("DETAILS"));
    }
    if (details_total > details_shown) {
        auto* trunc = new QLabel(tr("Showing the first %1 of %2 rows. Use EXPORT JSON for the complete set.")
                                     .arg(details_shown)
                                     .arg(details_total),
                                 summary_container_);
        trunc->setWordWrap(true);
        trunc->setStyleSheet(QString("color:%1; font-size:%2px; font-family:%3; padding:6px 10px; "
                                     "background:rgba(245,158,11,0.10); border:1px solid %1;")
                                 .arg(ui::colors::WARNING())
                                 .arg(ui::fonts::TINY)
                                 .arg(ui::fonts::DATA_FAMILY));
        summary_layout_->addWidget(trunc);
    }

    summary_layout_->addStretch();

    // ── Equity Curve chart ──
    auto equity_arr = payload.value("equity").toArray();
    if (!equity_arr.isEmpty() && equity_chart_tab_ && equity_chart_tab_->layout()) {
        auto* eq_series = new QLineSeries;
        eq_series->setPen(QPen(QColor(accent), 1.5));
        auto* dd_series = new QLineSeries;
        dd_series->setPen(QPen(QColor(ui::colors::NEGATIVE()), 1.0));

        double y_min = 1e18, y_max = -1e18;
        double dd_min = 0;

        for (const auto& pt : equity_arr) {
            auto obj = pt.toObject();
            auto date_str = obj.value("date").toString();
            auto equity_val = obj.value("equity").toDouble();
            auto dd_val = obj.value("drawdown").toDouble();
            auto dt = QDateTime::fromString(date_str, "yyyy-MM-dd");
            if (!dt.isValid())
                dt = QDateTime::fromString(date_str, Qt::ISODate);
            if (!dt.isValid())
                continue;
            qint64 ms = dt.toMSecsSinceEpoch();
            eq_series->append(ms, equity_val);
            dd_series->append(ms, dd_val * 100.0);
            y_min = qMin(y_min, equity_val);
            y_max = qMax(y_max, equity_val);
            dd_min = qMin(dd_min, dd_val * 100.0);
        }

        if (eq_series->count() > 1) {
            auto* chart = new QChart;
            chart->addSeries(eq_series);
            chart->legend()->setVisible(false);
            chart->setMargins(QMargins(0, 0, 0, 0));
            chart->setBackgroundBrush(QBrush(QColor(ui::colors::BG_BASE())));
            chart->setPlotAreaBackgroundBrush(QBrush(QColor(ui::colors::BG_SURFACE())));
            chart->setPlotAreaBackgroundVisible(true);

            auto* x_axis = new QDateTimeAxis;
            x_axis->setFormat("MMM yyyy");
            x_axis->setLabelsColor(QColor(ui::colors::TEXT_TERTIARY()));
            x_axis->setGridLineColor(QColor(ui::colors::BORDER_DIM()));
            chart->addAxis(x_axis, Qt::AlignBottom);
            eq_series->attachAxis(x_axis);

            auto* y_axis = new QValueAxis;
            double margin = (y_max - y_min) * 0.05;
            y_axis->setRange(y_min - margin, y_max + margin);
            y_axis->setLabelFormat(cur::symbol() + "%.0f");
            y_axis->setLabelsColor(QColor(ui::colors::TEXT_TERTIARY()));
            y_axis->setGridLineColor(QColor(ui::colors::BORDER_DIM()));
            chart->addAxis(y_axis, Qt::AlignLeft);
            eq_series->attachAxis(y_axis);

            // Drawdown on right axis
            if (dd_min < -0.01) {
                chart->addSeries(dd_series);
                auto* dd_axis = new QValueAxis;
                dd_axis->setRange(dd_min * 1.2, 0);
                dd_axis->setLabelFormat("%.1f%%");
                dd_axis->setLabelsColor(QColor(ui::colors::NEGATIVE()));
                dd_axis->setGridLineVisible(false);
                chart->addAxis(dd_axis, Qt::AlignRight);
                dd_series->attachAxis(x_axis);
                dd_series->attachAxis(dd_axis);
            } else {
                delete dd_series;
            }

            auto* view = new QChartView(chart, equity_chart_tab_);
            view->setRenderHint(QPainter::Antialiasing);
            equity_chart_tab_->layout()->addWidget(view);
        } else {
            delete eq_series;
            delete dd_series;
        }
    }

    // ── RAW JSON tab (always populated) ──
    raw_json_edit_->setPlainText(QJsonDocument(payload).toJson(QJsonDocument::Indented));

    // Switch to summary tab
    result_tabs_->setCurrentIndex(0);
}

void BacktestingScreen::display_error(const QString& msg) {
    clear_results();

    auto* err = new QLabel(msg, summary_container_);
    err->setWordWrap(true);
    err->setStyleSheet(QString("color:%1; font-size:%2px; font-family:%3; padding:12px;"
                               "background:rgba(220,38,38,0.08); border:1px solid rgba(220,38,38,0.3);")
                           .arg(ui::colors::NEGATIVE())
                           .arg(ui::fonts::SMALL)
                           .arg(ui::fonts::DATA_FAMILY));
    summary_layout_->addWidget(err);
    summary_layout_->addStretch();

    // Also put error in raw JSON tab for debugging
    raw_json_edit_->setPlainText(msg);

    result_tabs_->setCurrentIndex(0);
}

// ── Signal handlers ──────────────────────────────────────────────────────────

void BacktestingScreen::on_result(const QString& provider, const QString& command, const QJsonObject& payload) {
    // Route background metadata commands — don't touch run state or results UI
    if (command == "get_strategies") {
        // Only apply if result is for the currently-active provider
        if (provider != providers_[active_provider_].slug)
            return;
        strategies_ = services::backtest::strategies_from_json(payload);
        auto cats = services::backtest::categories_from_strategies(strategies_);
        strategy_category_combo_->blockSignals(true);
        strategy_category_combo_->clear();
        for (const auto& cat : cats)
            strategy_category_combo_->addItem(cat);
        strategy_category_combo_->blockSignals(false);
        populate_strategies();
        LOG_INFO("Backtesting", QString("[%1] Loaded %2 strategies").arg(provider).arg(strategies_.size()));
        // A backtest auto-run was requested (e.g. from Equity Research) before
        // strategies were ready — now that a strategy is selected, fire it.
        if (pending_auto_run_) {
            pending_auto_run_ = false;
            trigger_auto_run();
        }
        return;
    }
    if (command == "get_indicators") {
        if (provider != providers_[active_provider_].slug)
            return;
        // Populate all three indicator combos from {indicators:{Cat:[{id,name}]}}.
        // sweep_indicator_combo_ may be null if a future refactor strips the
        // sweep page — guard so we don't crash older builds during migration.
        auto ind_obj = payload.value("indicators").toObject();
        indicator_combo_->clear();
        ind_signal_indicator_combo_->clear();
        if (sweep_indicator_combo_)
            sweep_indicator_combo_->clear();
        for (const auto& cat : ind_obj.keys()) {
            for (const auto& iv : ind_obj.value(cat).toArray()) {
                auto o = iv.toObject();
                auto id = o.value("id").toString();
                auto name = o.value("name").toString(id);
                indicator_combo_->addItem(name, id);
                ind_signal_indicator_combo_->addItem(name, id);
                if (sweep_indicator_combo_)
                    sweep_indicator_combo_->addItem(name, id);
            }
        }
        return;
    }

    // Regular command result — update run state and display
    is_running_ = false;
    stop_run_ticker();
    run_button_->setEnabled(true);
    set_status_state(tr("READY"), ui::colors::POSITIVE, "rgba(22,163,74,0.08)");
    display_result(command, payload);
    LOG_INFO("Backtesting", QString("[%1/%2] Complete").arg(provider, command));
}

void BacktestingScreen::on_command_options_loaded(const QString& provider, const QJsonObject& options) {
    if (provider != providers_[active_provider_].slug)
        return;

    auto repopulate = [](QComboBox* combo, const QJsonArray& arr) {
        if (arr.isEmpty())
            return;
        combo->clear();
        for (const auto& v : arr)
            combo->addItem(v.toString());
        combo->setCurrentIndex(0);
    };

    // Providers using json_response() emit camelCase keys; fincept_provider
    // uses raw json.dumps so its keys stay snake_case. Try camelCase first
    // (the dominant form across vectorbt/bt/backtestingpy/fasttrade/zipline),
    // then fall back to snake_case for fincept.
    auto pick = [&](const char* camel, const char* snake) -> QJsonArray {
        auto a = options.value(camel).toArray();
        return a.isEmpty() ? options.value(snake).toArray() : a;
    };

    repopulate(pos_sizing_combo_, pick("positionSizingMethods", "position_sizing_methods"));
    repopulate(opt_objective_combo_, pick("optimizeObjectives", "optimize_objectives"));
    repopulate(opt_method_combo_, pick("optimizeMethods", "optimize_methods"));
    repopulate(labels_type_combo_, pick("labelTypes", "label_types"));
    repopulate(splitter_type_combo_, pick("splitterTypes", "splitter_types"));
    repopulate(signal_gen_combo_, pick("signalGenerators", "signal_generators"));
    repopulate(ind_signal_mode_combo_, pick("indicatorSignalModes", "indicator_signal_modes"));
    repopulate(returns_type_combo_, pick("returnsAnalysisTypes", "returns_analysis_types"));

    LOG_INFO("Backtesting", QString("[%1] Command options loaded").arg(provider));
}

void BacktestingScreen::on_error(const QString& context, const QString& message) {
    is_running_ = false;
    stop_run_ticker();
    run_button_->setEnabled(true);
    set_status_state(tr("ERROR"), ui::colors::NEGATIVE, "rgba(220,38,38,0.08)");
    display_error(QString("[%1] %2").arg(context, message));
}

} // namespace fincept::screens
