const std = @import("std");
const zfin = @import("../root.zig");
const cli = @import("common.zig");
const framework = @import("framework.zig");
const fmt = cli.fmt;
const Money = @import("../Money.zig");

pub const ParsedArgs = struct {};

pub const meta: framework.Meta = .{
    .name = "analysis",
    .group = .portfolio,
    .synopsis = "Show portfolio breakdowns by asset class, sector, geo, account, tax type",
    .help =
    \\Usage: zfin analysis
    \\
    \\Show portfolio analysis: equities/fixed-income split, plus
    \\block-bar breakdowns by asset class, sector, geographic
    \\region, account, and tax type. Reads classifications from
    \\`metadata.srf` and account tax types from `accounts.srf`
    \\(both in the same directory as the portfolio file).
    \\
    \\Run `zfin enrich <portfolio.srf> > metadata.srf` to bootstrap
    \\classifications, then edit by hand.
    \\
    ,
    .uppercase_first_arg = false,
};

comptime {
    framework.validateCommandModule(@This());
}

pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {
    if (cmd_args.len > 0) {
        try cli.stderrPrint(ctx.io, "Error: 'analysis' takes no arguments\n");
        return error.UnexpectedArg;
    }
    return .{};
}

/// CLI `analysis` command: show portfolio analysis breakdowns.
pub fn run(ctx: *framework.RunCtx, _: ParsedArgs) !void {
    const svc = ctx.svc orelse return error.MissingDataService;
    const io = ctx.io;
    const allocator = ctx.allocator;
    const out = ctx.out;
    const color = ctx.color;
    const as_of = ctx.today;

    const pf = ctx.resolvePortfolioPath();
    defer pf.deinit(allocator);
    const file_path = pf.path;

    var loaded = cli.loadPortfolio(io, allocator, file_path, as_of) orelse return;
    defer loaded.deinit(allocator);

    const portfolio = loaded.portfolio;
    const positions = loaded.positions;
    const syms = loaded.syms;

    // Refresh per-symbol prices via the parallel loader so analysis
    // works on TTL-fresh data by default. Previously this read
    // `getCachedCandles` directly, which silently used stale data
    // after long weekends or when the cache hadn't been refreshed.
    // The loader emits a stderr summary line ("Loaded N symbols
    // (X cached, Y server, Z provider)").
    var prices = std.StringHashMap(f64).init(allocator);
    defer prices.deinit();
    if (syms.len > 0) {
        var load_result = cli.loadPortfolioPrices(io, svc, syms, &.{}, ctx.globals.refresh_policy, color);
        defer load_result.deinit();
        var it = load_result.prices.iterator();
        while (it.next()) |entry| {
            try prices.put(entry.key_ptr.*, entry.value_ptr.*);
        }
    }

    // Build summary via shared pipeline
    var pf_data = cli.buildPortfolioData(allocator, portfolio, positions, syms, &prices, svc, as_of) catch |err| switch (err) {
        error.NoAllocations, error.SummaryFailed => {
            try cli.stderrPrint(io, "Error computing portfolio summary.\n");
            return;
        },
        else => return err,
    };
    defer pf_data.deinit(allocator);

    // Load classification metadata
    const dir_end = if (std.mem.lastIndexOfScalar(u8, file_path, std.fs.path.sep)) |idx| idx + 1 else 0;
    const meta_path = std.fmt.allocPrint(allocator, "{s}metadata.srf", .{file_path[0..dir_end]}) catch return;
    defer allocator.free(meta_path);

    const meta_data = std.Io.Dir.cwd().readFileAlloc(io, meta_path, allocator, .limited(1024 * 1024)) catch {
        try cli.stderrPrint(io, "Error: No metadata.srf found. Run: zfin enrich <portfolio.srf> > metadata.srf\n");
        return;
    };
    defer allocator.free(meta_data);

    var cm = zfin.classification.parseClassificationFile(allocator, meta_data) catch {
        try cli.stderrPrint(io, "Error: Cannot parse metadata.srf\n");
        return;
    };
    defer cm.deinit();

    // Load account tax type metadata (optional)
    var acct_map_opt: ?zfin.analysis.AccountMap = svc.loadAccountMap(file_path);
    defer if (acct_map_opt) |*am| am.deinit();

    var result = zfin.analysis.analyzePortfolio(
        allocator,
        pf_data.summary.allocations,
        cm,
        portfolio,
        pf_data.summary.total_value,
        acct_map_opt,
        as_of,
    ) catch {
        try cli.stderrPrint(io, "Error computing analysis.\n");
        return;
    };
    defer result.deinit(allocator);

    const benchmark = @import("../analytics/benchmark.zig");
    const split = benchmark.deriveAllocationSplit(
        pf_data.summary.allocations,
        cm.entries,
        pf_data.summary.total_value,
        portfolio.totalCash(as_of),
        portfolio.totalCdFaceValue(as_of),
    );

    try display(result, split.stock_pct, split.bond_pct, pf_data.summary.total_value, file_path, color, out);
}

pub fn display(result: zfin.analysis.AnalysisResult, stock_pct: f64, bond_pct: f64, total_value: f64, file_path: []const u8, color: bool, out: *std.Io.Writer) !void {
    const label_width = fmt.analysis_label_width;
    const bar_width = fmt.analysis_bar_width;

    try cli.printBold(out, color, "\nPortfolio Analysis ({s})\n", .{file_path});
    try out.print("========================================\n\n", .{});

    // Equities vs Fixed Income summary
    {
        try cli.printFg(out, color, cli.CLR_MUTED, "  Equities {d:.1}% ({f})  /  Fixed Income {d:.1}% ({f})\n\n", .{ stock_pct * 100, Money.from(stock_pct * total_value), bond_pct * 100, Money.from(bond_pct * total_value) });
    }

    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 out.print("\n", .{});
        // Bold + header color — reset at end of printFg clears both.
        try cli.setBold(out, color);
        try cli.printFg(out, color, cli.CLR_HEADER, "{s}\n", .{sec.title});
        try printBreakdownSection(out, sec.items, label_width, bar_width, color);
    }

    // Unclassified
    if (result.unclassified.len > 0) {
        try out.print("\n", .{});
        try cli.printFg(out, color, cli.CLR_WARNING, "  Unclassified (not in metadata.srf)\n", .{});
        for (result.unclassified) |sym| {
            try cli.printFg(out, color, cli.CLR_MUTED, "    {s}\n", .{sym});
        }
    }

    try out.print("\n", .{});
}

/// Print a breakdown section with block-element bar charts to the CLI output.
pub fn printBreakdownSection(out: *std.Io.Writer, items: []const zfin.analysis.BreakdownItem, label_width: usize, bar_width: usize, color: bool) !void {
    for (items) |item| {
        const pct = item.weight * 100.0;

        // Build bar using shared function
        var bar_buf: [256]u8 = undefined;
        const bar = fmt.buildBlockBar(&bar_buf, item.weight, bar_width);

        // Padded label
        const lbl_len = @min(item.label.len, label_width);
        try out.print("  ", .{});
        try out.writeAll(item.label[0..lbl_len]);
        if (lbl_len < label_width) {
            for (0..label_width - lbl_len) |_| try out.writeAll(" ");
        }
        try out.writeAll(" ");
        if (color) try fmt.ansiSetFg(out, cli.CLR_ACCENT[0], cli.CLR_ACCENT[1], cli.CLR_ACCENT[2]);
        try out.writeAll(bar);
        if (color) try fmt.ansiReset(out);
        try out.print(" {d:>5.1}%  {f}\n", .{ pct, Money.from(item.value) });
    }
}

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

test "parseArgs: no args produces empty ParsedArgs" {
    var ctx: framework.RunCtx = undefined;
    ctx.io = std.testing.io;
    const args = [_][]const u8{};
    _ = try parseArgs(&ctx, &args);
}

test "parseArgs: any positional is rejected" {
    var ctx: framework.RunCtx = undefined;
    ctx.io = std.testing.io;
    const args = [_][]const u8{"unexpected"};
    try std.testing.expectError(error.UnexpectedArg, parseArgs(&ctx, &args));
}

test "printBreakdownSection single item no color" {
    var buf: [4096]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);
    const items = [_]zfin.analysis.BreakdownItem{
        .{ .label = "US Large Cap", .weight = 0.60, .value = 60000.0 },
    };
    try printBreakdownSection(&w, &items, 24, 30, false);
    const out = w.buffered();
    try std.testing.expect(std.mem.indexOf(u8, out, "US Large Cap") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "60.0%") != null);
    // No ANSI when color=false
    try std.testing.expect(std.mem.indexOf(u8, out, "\x1b[") == null);
}

test "printBreakdownSection multiple items" {
    var buf: [4096]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);
    const items = [_]zfin.analysis.BreakdownItem{
        .{ .label = "Stocks", .weight = 0.70, .value = 70000.0 },
        .{ .label = "Bonds", .weight = 0.20, .value = 20000.0 },
        .{ .label = "Cash", .weight = 0.10, .value = 10000.0 },
    };
    try printBreakdownSection(&w, &items, 24, 30, false);
    const out = w.buffered();
    try std.testing.expect(std.mem.indexOf(u8, out, "Stocks") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "Bonds") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "Cash") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "70.0%") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "10.0%") != null);
}

test "printBreakdownSection zero weight" {
    var buf: [4096]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);
    const items = [_]zfin.analysis.BreakdownItem{
        .{ .label = "Empty", .weight = 0.0, .value = 0.0 },
    };
    try printBreakdownSection(&w, &items, 24, 30, false);
    const out = w.buffered();
    try std.testing.expect(std.mem.indexOf(u8, out, "0.0%") != null);
}

test "printBreakdownSection with color emits ANSI" {
    var buf: [4096]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);
    const items = [_]zfin.analysis.BreakdownItem{
        .{ .label = "Test", .weight = 0.50, .value = 50000.0 },
    };
    try printBreakdownSection(&w, &items, 24, 30, true);
    const out = w.buffered();
    // Should contain ANSI escape for bar color
    try std.testing.expect(std.mem.indexOf(u8, out, "\x1b[") != null);
}

test "display shows all sections" {
    var buf: [8192]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);
    const asset_class = [_]zfin.analysis.BreakdownItem{
        .{ .label = "US Large Cap", .weight = 0.60, .value = 60000.0 },
        .{ .label = "International", .weight = 0.40, .value = 40000.0 },
    };
    const sector = [_]zfin.analysis.BreakdownItem{
        .{ .label = "Technology", .weight = 0.35, .value = 35000.0 },
    };
    const geo = [_]zfin.analysis.BreakdownItem{
        .{ .label = "US", .weight = 0.80, .value = 80000.0 },
    };
    const empty = [_]zfin.analysis.BreakdownItem{};
    const unclassified = [_][]const u8{"WEIRD"};
    const result: zfin.analysis.AnalysisResult = .{
        .asset_class = @constCast(&asset_class),
        .sector = @constCast(&sector),
        .geo = @constCast(&geo),
        .account = @constCast(&empty),
        .tax_type = @constCast(&empty),
        .unclassified = @constCast(&unclassified),
        .total_value = 100000.0,
    };
    try display(result, 0.80, 0.20, 100000.0, "test.srf", false, &w);
    const out = w.buffered();
    try std.testing.expect(std.mem.indexOf(u8, out, "Portfolio Analysis") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "Asset Class") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "US Large Cap") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "Sector") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "Technology") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "Geographic") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "Unclassified") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "WEIRD") != null);
    // No ANSI when color=false
    try std.testing.expect(std.mem.indexOf(u8, out, "\x1b[") == null);
}
