const std = @import("std");
const vaxis = @import("vaxis");
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 ─────────────────────────────────────
//
// Analysis has no tab-local keybinds today — it's a read-only
// breakdown view. Refresh is global. Empty enum is the explicit
// placeholder.

pub const Action = enum {};

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

pub const State = struct {
    /// Whether `init`/`activate` has populated `result`. Internal
    /// short-circuit for `activate`; App never reads this.
    loaded: bool = false,
    /// Computed analysis output. Owned by State; freed in
    /// `deinit` and `reload`.
    result: ?zfin.analysis.AnalysisResult = null,
    /// Per-portfolio classification metadata (`metadata.srf`).
    /// Used only by analysis today; lives here because no other
    /// tab consumes it. Loaded lazily on first activation; freed
    /// in `deinit`.
    classification_map: ?zfin.classification.ClassificationMap = null,
};

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

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

    /// Display name for the tab bar.
    pub const label: []const u8 = "Analysis";

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

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

    pub fn deinit(state: *State, app: *App) void {
        if (state.result) |*ar| ar.deinit(app.allocator);
        if (state.classification_map) |*cm| cm.deinit();
        state.* = .{};
    }

    pub fn activate(state: *State, app: *App) !void {
        if (tab.isDisabled(app)) return;
        if (state.loaded) return;
        loadData(state, app);
    }

    pub const deactivate = framework.noopDeactivate(State);

    /// Force re-fetch on user request. Frees the analysis result
    /// AND the shared `account_map` on App (analysis's refresh
    /// also re-reads accounts.srf). The classification_map persists
    /// — it's per-portfolio, not per-symbol or per-refresh.
    pub fn reload(state: *State, app: *App) !void {
        if (state.result) |*ar| ar.deinit(app.allocator);
        state.result = null;
        state.loaded = false;
        // Refresh-analysis intentionally drops the shared account
        // map so the next load re-reads `accounts.srf` from disk
        // (the user may have edited it).
        if (app.portfolio.account_map) |*am| am.deinit();
        app.portfolio.account_map = null;
        loadData(state, app);
    }

    pub const tick = framework.noopTick(State);

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

    /// Analysis requires a loaded portfolio file (the breakdown
    /// is computed from `app.portfolio.summary.allocations`).
    /// Derived directly from `app.portfolio.file` rather than
    /// stored as a field, so it can't go stale and App never has
    /// to reach across into the tab's State to set it.
    pub fn isDisabled(app: *App) bool {
        return app.portfolio.file == null;
    }
};

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

fn loadData(state: *State, app: *App) void {
    state.loaded = true;

    // Ensure portfolio is loaded first
    app.ensurePortfolioDataLoaded();
    const pf = app.portfolio.file orelse return;
    const summary = app.portfolio.summary orelse return;

    // Load classification metadata file
    if (state.classification_map == null) {
        // Look for metadata.srf next to the portfolio file
        if (app.portfolio_path) |ppath| {
            // Derive metadata path: same directory as portfolio, named "metadata.srf"
            const dir_end = if (std.mem.lastIndexOfScalar(u8, ppath, std.fs.path.sep)) |idx| idx + 1 else 0;
            const meta_path = std.fmt.allocPrint(app.allocator, "{s}metadata.srf", .{ppath[0..dir_end]}) catch return;
            defer app.allocator.free(meta_path);

            const file_data = std.Io.Dir.cwd().readFileAlloc(app.io, meta_path, app.allocator, .limited(1024 * 1024)) catch {
                app.setStatus("No metadata.srf found. Run: zfin enrich <portfolio.srf> > metadata.srf");
                return;
            };
            defer app.allocator.free(file_data);

            state.classification_map = zfin.classification.parseClassificationFile(app.allocator, file_data) catch {
                app.setStatus("Error parsing metadata.srf");
                return;
            };
        }
    }

    // Load account tax type metadata file (optional)
    app.ensureAccountMap();

    loadDataFinish(state, app, pf, summary);
}

fn loadDataFinish(state: *State, app: *App, pf: zfin.Portfolio, summary: zfin.valuation.PortfolioSummary) void {
    const cm = state.classification_map orelse {
        app.setStatus("No classification data. Run: zfin enrich <portfolio.srf> > metadata.srf");
        return;
    };

    // Free previous result
    if (state.result) |*ar| ar.deinit(app.allocator);

    state.result = zfin.analysis.analyzePortfolio(
        app.allocator,
        summary.allocations,
        cm,
        pf,
        summary.total_value,
        app.portfolio.account_map,
        app.today, // live mode in TUI → resolves to app.today
    ) catch {
        app.setStatus("Error computing analysis");
        return;
    };
}

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

pub fn buildStyledLines(state: *State, app: *App, arena: std.mem.Allocator) ![]const StyledLine {
    // Compute equity/fixed split from classification + portfolio
    var stock_pct: f64 = 0;
    var bond_pct: f64 = 0;
    var total_value: f64 = 0;
    if (app.portfolio.summary) |summary| {
        total_value = summary.total_value;
        if (app.portfolio.file) |pf| {
            const benchmark = @import("../analytics/benchmark.zig");
            const cm_entries = if (state.classification_map) |cm| cm.entries else &.{};
            const split = benchmark.deriveAllocationSplit(
                summary.allocations,
                cm_entries,
                summary.total_value,
                pf.totalCash(app.today),
                pf.totalCdFaceValue(app.today),
            );
            stock_pct = split.stock_pct;
            bond_pct = split.bond_pct;
        }
    }
    return renderAnalysisLines(arena, app.theme, state.result, stock_pct, bond_pct, total_value);
}

/// Render analysis tab content. Pure function — no App dependency.
pub fn renderAnalysisLines(
    arena: std.mem.Allocator,
    th: theme.Theme,
    analysis_result: ?zfin.analysis.AnalysisResult,
    stock_pct: f64,
    bond_pct: f64,
    total_value: f64,
) ![]const StyledLine {
    var lines: std.ArrayList(StyledLine) = .empty;

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

    const result = analysis_result orelse {
        try lines.append(arena, .{ .text = "  No analysis data. Ensure metadata.srf exists alongside portfolio.", .style = th.mutedStyle() });
        try lines.append(arena, .{ .text = "  Run: zfin enrich <portfolio.srf> > metadata.srf", .style = th.mutedStyle() });
        return lines.toOwnedSlice(arena);
    };

    // Equities vs Fixed Income summary
    if (stock_pct > 0 or bond_pct > 0) {
        try lines.append(arena, .{
            .text = try std.fmt.allocPrint(arena, "  Equities {d:.1}% ({f})  /  Fixed Income {d:.1}% ({f})", .{
                stock_pct * 100,
                Money.from(stock_pct * total_value),
                bond_pct * 100,
                Money.from(bond_pct * total_value),
            }),
            .style = th.mutedStyle(),
        });
        try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
    }

    const bar_width: usize = 30;
    const label_width: usize = 24;

    const sections = [_]struct { items: []const zfin.analysis.BreakdownItem, title: []const u8 }{
        .{ .items = result.asset_class, .title = "  Asset Class" },
        .{ .items = result.sector, .title = "  Sector (Equities)" },
        .{ .items = result.geo, .title = "  Geographic" },
        .{ .items = result.account, .title = "  By Account" },
        .{ .items = result.tax_type, .title = "  By Tax Type" },
    };

    for (sections, 0..) |sec, si| {
        if (si > 0 and sec.items.len == 0) continue;
        if (si > 0) try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
        try lines.append(arena, .{ .text = sec.title, .style = th.headerStyle() });
        try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
        for (sec.items) |item| {
            const text = try fmtBreakdownLine(arena, item, bar_width, label_width);
            try lines.append(arena, .{ .text = text, .style = th.contentStyle(), .alt_text = null, .alt_style = th.barFillStyle(), .alt_start = 2 + label_width + 1, .alt_end = 2 + label_width + 1 + bar_width });
        }
    }

    if (result.unclassified.len > 0) {
        try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
        try lines.append(arena, .{ .text = "  Unclassified (not in metadata.srf)", .style = th.warningStyle() });
        for (result.unclassified) |sym| {
            const text = try std.fmt.allocPrint(arena, "    {s}", .{sym});
            try lines.append(arena, .{ .text = text, .style = th.mutedStyle() });
        }
    }

    return lines.toOwnedSlice(arena);
}

pub fn fmtBreakdownLine(arena: std.mem.Allocator, item: zfin.analysis.BreakdownItem, bar_width: usize, label_width: usize) ![]const u8 {
    const pct = item.weight * 100.0;
    const bar = try buildBlockBar(arena, item.weight, bar_width);
    // Build label padded to label_width
    const lbl = item.label;
    const lbl_len = @min(lbl.len, label_width);
    const padded_label = try arena.alloc(u8, label_width);
    @memcpy(padded_label[0..lbl_len], lbl[0..lbl_len]);
    if (lbl_len < label_width) @memset(padded_label[lbl_len..], ' ');
    return std.fmt.allocPrint(arena, "  {s} {s} {d:>5.1}%  {f}", .{
        padded_label, bar, pct, Money.from(item.value),
    });
}

/// Build a bar using Unicode block elements for sub-character precision.
/// Wraps fmt.buildBlockBar into arena-allocated memory.
pub fn buildBlockBar(arena: std.mem.Allocator, weight: f64, total_chars: usize) ![]const u8 {
    var buf: [256]u8 = undefined;
    const result = fmt.buildBlockBar(&buf, weight, total_chars);
    return arena.dupe(u8, result);
}

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

const testing = std.testing;

test "buildBlockBar empty" {
    const bar = try buildBlockBar(testing.allocator, 0, 10);
    defer testing.allocator.free(bar);
    // All spaces
    try testing.expectEqual(@as(usize, 10), bar.len);
    try testing.expectEqualStrings("          ", bar);
}

test "buildBlockBar full" {
    const bar = try buildBlockBar(testing.allocator, 1.0, 5);
    defer testing.allocator.free(bar);
    // 5 full blocks, each 3 bytes UTF-8 (█ = E2 96 88)
    try testing.expectEqual(@as(usize, 15), bar.len);
    // Verify first block is █
    try testing.expectEqualStrings("\xe2\x96\x88", bar[0..3]);
}

test "buildBlockBar partial" {
    const bar = try buildBlockBar(testing.allocator, 0.5, 10);
    defer testing.allocator.free(bar);
    // 50% of 10 chars = 5 full blocks (no partial)
    // 5 full blocks (15 bytes) + 5 spaces = 20 bytes
    try testing.expectEqual(@as(usize, 20), bar.len);
}

test "fmtBreakdownLine formats correctly" {
    var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena_state.deinit();
    const arena = arena_state.allocator();

    const item = zfin.analysis.BreakdownItem{
        .label = "US Stock",
        .weight = 0.65,
        .value = 130000,
    };
    const line = try fmtBreakdownLine(arena, item, 10, 12);
    // Should contain the label, percentage, and dollar amount
    try testing.expect(std.mem.indexOf(u8, line, "US Stock") != null);
    try testing.expect(std.mem.indexOf(u8, line, "65.0%") != null);
    try testing.expect(std.mem.indexOf(u8, line, "$130,000") != null);
}

test "renderAnalysisLines with data" {
    var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena_state.deinit();
    const arena = arena_state.allocator();
    const th = theme.default_theme;

    var asset_class = [_]zfin.analysis.BreakdownItem{
        .{ .label = "US Stock", .weight = 0.60, .value = 120000 },
        .{ .label = "Int'l Stock", .weight = 0.40, .value = 80000 },
    };
    const result = zfin.analysis.AnalysisResult{
        .asset_class = &asset_class,
        .sector = &.{},
        .geo = &.{},
        .account = &.{},
        .tax_type = &.{},
        .unclassified = &.{},
        .total_value = 200000,
    };
    const lines = try renderAnalysisLines(arena, th, result, 0.80, 0.20, 200000);
    // Should have header section + asset class items
    try testing.expect(lines.len >= 5);
    // Find "Portfolio Analysis" header
    var found_header = false;
    for (lines) |l| {
        if (std.mem.indexOf(u8, l.text, "Portfolio Analysis") != null) found_header = true;
    }
    try testing.expect(found_header);
    // Find asset class data
    var found_us = false;
    for (lines) |l| {
        if (std.mem.indexOf(u8, l.text, "US Stock") != null) found_us = true;
    }
    try testing.expect(found_us);
}

test "renderAnalysisLines no data" {
    var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
    defer arena_state.deinit();
    const arena = arena_state.allocator();
    const th = theme.default_theme;

    const lines = try renderAnalysisLines(arena, th, null, 0, 0, 0);
    try testing.expectEqual(@as(usize, 5), lines.len);
    try testing.expect(std.mem.indexOf(u8, lines[3].text, "No analysis data") != null);
}

test "tab.init produces zero-defaulted state" {
    var state: State = undefined;
    var dummy_app: tui.App = undefined; // intentionally undefined: init
    // for analysis doesn't touch app fields.

    try tab.init(&state, &dummy_app);
    try testing.expectEqual(false, state.loaded);
    try testing.expect(state.result == null);
    try testing.expect(state.classification_map == null);
}
