const std = @import("std");
const zfin = @import("../root.zig");
const fmt = @import("../format.zig");
const Money = @import("../Money.zig");
const theme = @import("theme.zig");
const tui = @import("../tui.zig");
const framework = @import("tab_framework.zig");

const App = tui.App;
const StyledLine = tui.StyledLine;

// ── Tab-local action enum ─────────────────────────────────────
//
// Performance tab is read-only — no tab-local keybinds. Empty
// enum is the explicit placeholder per the framework contract.

pub const Action = enum {};

// ── Tab-private state ─────────────────────────────────────────

pub const State = struct {
    /// Whether `activate` has populated `app.symbol_data` for the
    /// current symbol. Distinct from `app.symbol_data.candles !=
    /// null` because candle data is shared with the quote tab and
    /// might be populated by a different code path.
    loaded: bool = false,
};

// ── Tab framework contract ────────────────────────────────────

pub const meta: framework.TabMeta(Action) = .{
    .label = "Performance",
    .default_bindings = &.{},
    .action_labels = std.enums.EnumArray(Action, []const u8).initFill(""),
    .status_hints = &.{},
};

pub const tab = struct {
    pub const ActionT = Action;
    pub const StateT = State;

    pub fn init(state: *State, app: *App) !void {
        _ = app;
        state.* = .{};
    }

    /// State teardown. Owned data lives on `app.symbol_data`,
    /// which has its own deinit; nothing tab-local to free.
    pub fn deinit(state: *State, app: *App) void {
        _ = app;
        state.* = .{};
    }

    pub fn activate(state: *State, app: *App) !void {
        if (state.loaded) return;
        if (app.symbol.len == 0) return;
        loadData(state, app);
    }

    pub const deactivate = framework.noopDeactivate(State);

    /// Manual refresh (r/F5): the live header quote is re-fetched by
    /// the App's refresh handler (see `tui.zig`); here we just re-run
    /// `loadData` so trailing returns / the chart reflect any candle
    /// top-up. We do NOT invalidate candles or dividends — `r` is for
    /// live prices, not candle re-downloads. Candle history is
    /// maintained by the TTL/startup path, so `loadData`'s cache-
    /// respecting fetch only tops up when actually stale.
    ///
    /// Quote and performance share `app.symbol_data`; the quote tab
    /// piggybacks on this reload via its own delegating reload.
    pub fn reload(state: *State, app: *App) !void {
        // The chart is rendered by the quote tab but fed from
        // `app.symbol_data.candles` which performance owns. Drop the
        // indicator overlay cache (SMA/Bollinger/etc) and mark the
        // chart dirty so the next quote draw re-renders.
        app.states.quote.chart.dirty = true;
        app.states.quote.chart.freeCache(app.allocator);

        state.loaded = false;
        loadData(state, app);
    }

    pub const tick = framework.noopTick(State);

    pub fn handleAction(state: *State, app: *App, action: Action) void {
        _ = state;
        _ = app;
        switch (action) {}
    }

    pub fn isDisabled(app: *App) bool {
        _ = app;
        return false;
    }

    /// Symbol-change reset. Marks state as not-loaded so the next
    /// `activate` re-runs `loadData`. The performance tab's per-
    /// symbol fetched payload (candles, dividends, trailing returns)
    /// lives on `app.symbol_data` and is dropped centrally by the
    /// App when the symbol changes — this hook only owns the
    /// tab-local "have I run for this symbol yet?" flag.
    pub fn onSymbolChange(state: *State, app: *App) void {
        _ = app;
        state.loaded = false;
    }
};

// ── Data loading ──────────────────────────────────────────────

fn loadData(state: *State, app: *App) void {
    state.loaded = true;
    if (app.symbol_data.candles) |c| app.allocator.free(c);
    app.symbol_data.candles = null;
    if (app.symbol_data.dividends) |d| zfin.Dividend.freeSlice(app.allocator, d);
    app.symbol_data.dividends = null;
    app.symbol_data.trailing_price = null;
    app.symbol_data.trailing_total = null;
    app.symbol_data.trailing_me_price = null;
    app.symbol_data.trailing_me_total = null;

    const result = app.svc.getTrailingReturns(app.symbol, .{}) catch |err| {
        switch (err) {
            zfin.DataError.NoApiKey => app.setStatus("No API key. Set TIINGO_API_KEY"),
            zfin.DataError.FetchFailed => app.setStatus("Fetch failed (network error or rate limit)"),
            zfin.DataError.TransientError => app.setStatus("Provider temporarily unavailable — try again later"),
            zfin.DataError.AuthError => app.setStatus("API key auth failed — check TIINGO_API_KEY"),
            else => app.setStatus("Error loading data"),
        }
        return;
    };
    app.symbol_data.candles = result.candles;
    app.symbol_data.candle_timestamp = result.timestamp;

    const c = result.candles;
    if (c.len == 0) {
        app.setStatus("No data available for symbol");
        return;
    }
    // candle_count / candle_first_date / candle_last_date are derived
    // from `candles` via methods on SymbolData — no field assignments
    // needed here.

    app.symbol_data.trailing_price = result.asof_price;
    app.symbol_data.trailing_me_price = result.me_price;
    app.symbol_data.trailing_total = result.asof_total;
    app.symbol_data.trailing_me_total = result.me_total;

    if (result.dividends) |divs| {
        app.symbol_data.dividends = divs;
    }

    app.symbol_data.risk_metrics = zfin.risk.trailingRisk(c);

    // Try to load ETF profile (non-fatal, won't show for non-ETFs)
    if (!app.symbol_data.etf_loaded) {
        app.symbol_data.etf_loaded = true;
        if (app.svc.getEtfProfile(app.symbol, .{})) |etf_result| {
            if (etf_result.data.isEtf()) {
                // Take ownership of the EtfProfile data. We
                // deliberately don't call etf_result.deinit
                // here — the data fields (symbol, name,
                // holdings, sectors) are now owned by
                // symbol_data and will be freed by
                // symbol_data.clear() on next symbol change.
                // The FetchResult wrapper itself is a struct
                // with no separate heap allocation; dropping
                // it leaks nothing.
                app.symbol_data.etf_profile = etf_result.data;
            } else {
                // Non-ETF: the profile data was still allocated
                // by getEtfProfile (symbol dupe at minimum,
                // possibly name from Wikidata fallback). Free
                // it via the FetchResult contract.
                etf_result.deinit();
            }
        } else |_| {}
    }

    app.setStatus(if (result.source == .cached) "r/F5 to refresh" else "Fetched | r/F5 to refresh");
}

// ── Rendering ─────────────────────────────────────────────────

/// Format the performance tab's header line. When `as_of_close`
/// is non-null, includes the close-of-day date suffix; otherwise
/// renders just the title and symbol.
pub fn formatPerformanceHeader(
    arena: std.mem.Allocator,
    symbol: []const u8,
    as_of_close: ?zfin.Date,
) ![]const u8 {
    return if (as_of_close) |d|
        std.fmt.allocPrint(arena, "  Trailing Returns: {s}  (as of close on {f})", .{ symbol, d })
    else
        std.fmt.allocPrint(arena, "  Trailing Returns: {s}", .{symbol});
}

pub fn buildStyledLines(state: *State, app: *App, arena: std.mem.Allocator) ![]const StyledLine {
    _ = state;
    const th = app.theme;
    var lines: std.ArrayList(StyledLine) = .empty;

    try lines.append(arena, .{ .text = "", .style = th.contentStyle() });

    if (app.symbol.len == 0) {
        try lines.append(arena, .{ .text = "  No symbol selected. Press / to enter a symbol.", .style = th.mutedStyle() });
        return lines.toOwnedSlice(arena);
    }

    if (app.symbol_data.candleLastDate()) |d| {
        try lines.append(arena, .{ .text = try formatPerformanceHeader(arena, app.symbol, d), .style = th.headerStyle() });
    } else {
        try lines.append(arena, .{ .text = try formatPerformanceHeader(arena, app.symbol, null), .style = th.headerStyle() });
    }
    try lines.append(arena, .{ .text = "", .style = th.contentStyle() });

    if (app.symbol_data.trailing_price == null) {
        try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, "  No data. Run: zfin perf {s}", .{app.symbol}), .style = th.mutedStyle() });
        return lines.toOwnedSlice(arena);
    }

    if (app.symbol_data.candleCount() > 0) {
        if (app.symbol_data.candleFirstDate()) |first| {
            if (app.symbol_data.candleLastDate()) |last| {
                try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, "  Data: {d} points ({f} to {f})", .{
                    app.symbol_data.candleCount(), first, last,
                }), .style = th.mutedStyle() });
            }
        }
    }

    if (app.symbol_data.candles) |cc| {
        if (cc.len > 0) {
            try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, "  Latest close: {f}", .{Money.from(cc[cc.len - 1].close)}), .style = th.contentStyle() });
        }
    }

    const has_total = app.symbol_data.trailing_total != null;

    if (app.symbol_data.candleLastDate()) |last| {
        try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
        try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, "  As-of {f}:", .{last}), .style = th.headerStyle() });
    }
    try appendStyledReturnsTable(arena, &lines, app.symbol_data.trailing_price.?, if (has_total) app.symbol_data.trailing_total else null, th);

    {
        const today = app.today;
        const month_end = today.lastDayOfPriorMonth();
        try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
        try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, "  Month-end ({f}):", .{month_end}), .style = th.headerStyle() });
    }
    if (app.symbol_data.trailing_me_price) |me_price| {
        try appendStyledReturnsTable(arena, &lines, me_price, if (has_total) app.symbol_data.trailing_me_total else null, th);
    }

    if (!has_total) {
        try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
        try lines.append(arena, .{ .text = "  (Set POLYGON_API_KEY for total returns with dividends)", .style = th.dimStyle() });
    }

    if (app.symbol_data.risk_metrics) |tr| {
        try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
        try lines.append(arena, .{ .text = "  Risk Metrics (monthly returns):", .style = th.headerStyle() });
        try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, "  {s:<20} {s:>14} {s:>14} {s:>14}", .{ "", "Volatility", "Sharpe", "Max DD" }), .style = th.mutedStyle() });

        const risk_arr = [4]?zfin.risk.RiskMetrics{ tr.one_year, tr.three_year, tr.five_year, tr.ten_year };
        const risk_labels = [4][]const u8{ "1-Year:", "3-Year:", "5-Year:", "10-Year:" };

        for (0..4) |i| {
            // Format vol / Sharpe / MaxDD via the shared
            // `fmt.fmtPctOpt` / `fmt.fmtSharpeOpt` family. Width-pad
            // via display-column-aware `padLeftToCols` so the em-dash
            // sentinel for a missing window aligns the same way the
            // numeric branch does — `{d:>14}` byte-padding would
            // under-pad the 3-byte em-dash by 2 cols.
            const cell_width: usize = 13;
            const vol_v: ?f64 = if (risk_arr[i]) |rm| rm.volatility else null;
            const sharpe_v: ?f64 = if (risk_arr[i]) |rm| rm.sharpe else null;
            const dd_v: ?f64 = if (risk_arr[i]) |rm| rm.max_drawdown else null;
            var vol_buf: [16]u8 = undefined;
            var sharpe_buf: [16]u8 = undefined;
            var dd_buf: [16]u8 = undefined;
            var p1: [32]u8 = undefined;
            var p2: [32]u8 = undefined;
            var p3: [32]u8 = undefined;
            const vol_cell = fmt.padLeftToCols(&p1, fmt.fmtPctOpt(&vol_buf, vol_v, .{}), cell_width);
            const sharpe_cell = fmt.padLeftToCols(&p2, fmt.fmtSharpeOpt(&sharpe_buf, sharpe_v, .{}), cell_width);
            const dd_cell = fmt.padLeftToCols(&p3, fmt.fmtPctOpt(&dd_buf, dd_v, .{}), cell_width);
            const style = if (risk_arr[i] != null) th.contentStyle() else th.mutedStyle();
            try lines.append(arena, .{
                .text = try std.fmt.allocPrint(arena, "  {s:<20} {s} {s} {s}", .{
                    risk_labels[i], vol_cell, sharpe_cell, dd_cell,
                }),
                .style = style,
            });
        }
    }

    return lines.toOwnedSlice(arena);
}

fn appendStyledReturnsTable(
    arena: std.mem.Allocator,
    lines: *std.ArrayList(StyledLine),
    price: zfin.performance.TrailingReturns,
    total: ?zfin.performance.TrailingReturns,
    th: theme.Theme,
) !void {
    const has_total = total != null;
    if (has_total) {
        try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, "  {s:<20} {s:>14} {s:>14}", .{ "", "Price Only", "Total Return" }), .style = th.mutedStyle() });
    } else {
        try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, "  {s:<20} {s:>14}", .{ "", "Price Only" }), .style = th.mutedStyle() });
    }

    const price_arr = [4]?zfin.performance.PerformanceResult{ price.one_year, price.three_year, price.five_year, price.ten_year };
    const total_arr_vals: [4]?zfin.performance.PerformanceResult = if (total) |t|
        .{ t.one_year, t.three_year, t.five_year, t.ten_year }
    else
        .{ null, null, null, null };
    const labels = [4][]const u8{ "1-Year Return:", "3-Year Return:", "5-Year Return:", "10-Year Return:" };
    const annualize = [4]bool{ false, true, true, true };

    for (0..4) |i| {
        var price_buf: [32]u8 = undefined;
        var total_buf: [32]u8 = undefined;
        const row = fmt.fmtReturnsRow(
            &price_buf,
            &total_buf,
            price_arr[i],
            if (has_total) total_arr_vals[i] else null,
            annualize[i],
        );

        const row_style = if (price_arr[i] != null)
            (if (row.price_positive) th.positiveStyle() else th.negativeStyle())
        else
            th.mutedStyle();

        if (has_total) {
            try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, "  {s:<20} {s:>14} {s:>14}{s}", .{ labels[i], row.price_str, row.total_str orelse "N/A", row.suffix }), .style = row_style });
        } else {
            try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, "  {s:<20} {s:>14}{s}", .{ labels[i], row.price_str, row.suffix }), .style = row_style });
        }
    }
}

// ── Tests ─────────────────────────────────────────────────────

const testing = std.testing;
const Date = zfin.Date;

fn makeResult(ret: f64, ann: ?f64) zfin.performance.PerformanceResult {
    return .{
        .total_return = ret,
        .annualized_return = ann,
        .from = Date.fromYmd(2020, 1, 1),
        .to = Date.fromYmd(2024, 1, 1),
    };
}

test "appendStyledReturnsTable: price-only column shape" {
    var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena_state.deinit();
    const arena = arena_state.allocator();

    const price: zfin.performance.TrailingReturns = .{
        .one_year = makeResult(0.10, null),
        .three_year = makeResult(0.30, 0.0914),
        .five_year = makeResult(0.50, 0.0845),
        .ten_year = null,
    };

    var lines: std.ArrayList(StyledLine) = .empty;
    try appendStyledReturnsTable(arena, &lines, price, null, theme.default_theme);

    // Header + 4 rows = 5 lines.
    try testing.expectEqual(@as(usize, 5), lines.items.len);

    // Header has "Price Only" but no "Total Return".
    try testing.expect(std.mem.indexOf(u8, lines.items[0].text, "Price Only") != null);
    try testing.expect(std.mem.indexOf(u8, lines.items[0].text, "Total Return") == null);

    // Each row labels its period.
    try testing.expect(std.mem.indexOf(u8, lines.items[1].text, "1-Year Return:") != null);
    try testing.expect(std.mem.indexOf(u8, lines.items[2].text, "3-Year Return:") != null);
    try testing.expect(std.mem.indexOf(u8, lines.items[3].text, "5-Year Return:") != null);
    try testing.expect(std.mem.indexOf(u8, lines.items[4].text, "10-Year Return:") != null);
}

test "appendStyledReturnsTable: with total returns shows both columns" {
    var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena_state.deinit();
    const arena = arena_state.allocator();

    const price: zfin.performance.TrailingReturns = .{
        .one_year = makeResult(0.10, null),
        .three_year = null,
        .five_year = null,
        .ten_year = null,
    };
    const total: zfin.performance.TrailingReturns = .{
        .one_year = makeResult(0.12, null),
        .three_year = null,
        .five_year = null,
        .ten_year = null,
    };

    var lines: std.ArrayList(StyledLine) = .empty;
    try appendStyledReturnsTable(arena, &lines, price, total, theme.default_theme);

    try testing.expectEqual(@as(usize, 5), lines.items.len);

    // Header has both columns.
    try testing.expect(std.mem.indexOf(u8, lines.items[0].text, "Price Only") != null);
    try testing.expect(std.mem.indexOf(u8, lines.items[0].text, "Total Return") != null);
}

test "appendStyledReturnsTable: missing data renders N/A" {
    var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena_state.deinit();
    const arena = arena_state.allocator();

    const empty: zfin.performance.TrailingReturns = .{};

    var lines: std.ArrayList(StyledLine) = .empty;
    try appendStyledReturnsTable(arena, &lines, empty, null, theme.default_theme);

    try testing.expectEqual(@as(usize, 5), lines.items.len);
    // Each row should contain N/A or em-dash (depends on fmt impl).
    // At minimum the rows should render without crashing and each
    // includes its label.
    try testing.expect(std.mem.indexOf(u8, lines.items[1].text, "1-Year Return:") != null);
}

test "formatPerformanceHeader: with as-of date" {
    var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena_state.deinit();
    const arena = arena_state.allocator();
    const text = try formatPerformanceHeader(arena, "AAPL", Date.fromYmd(2024, 3, 15));
    try testing.expectEqualStrings("  Trailing Returns: AAPL  (as of close on 2024-03-15)", text);
}

test "formatPerformanceHeader: without as-of date" {
    var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena_state.deinit();
    const arena = arena_state.allocator();
    const text = try formatPerformanceHeader(arena, "VTI", null);
    try testing.expectEqualStrings("  Trailing Returns: VTI", text);
}
