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

pub const ParsedArgs = struct {
    symbol: []const u8,
};

pub const meta: framework.Meta = .{
    .name = "etf",
    .group = .symbol_lookup,
    .synopsis = "Show ETF profile (holdings, sectors, AUM, inception)",
    .uppercase_first_arg = true,
    .help =
    \\Usage: zfin etf <SYMBOL>
    \\
    \\Show the ETF profile for a fund symbol, assembled from
    \\public SEC EDGAR (NPORT-P holdings + sectors + AUM) and
    \\Wikidata (inception date + fund name). Cached for ~90 days.
    \\
    \\Several legacy fields (expense ratio, dividend yield,
    \\portfolio turnover, leveraged flag) come from a fund's
    \\prospectus and are not currently surfaced — those will
    \\appear once a prospectus parser lands.
    \\
    \\Examples:
    \\  zfin etf VTI         # broad market index
    \\  zfin etf SPY         # S&P 500 ETF
    \\
    ,
    .user_errors = error{ MissingSymbol, UnexpectedArg },
};

pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {
    if (cmd_args.len < 1) {
        cli.stderrPrint(ctx.io, "Error: 'etf' requires a symbol argument\n");
        return error.MissingSymbol;
    }
    if (cmd_args.len > 1) {
        cli.stderrPrint(ctx.io, "Error: 'etf' takes a single symbol argument\n");
        return error.UnexpectedArg;
    }
    return .{ .symbol = cmd_args[0] };
}

pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
    const svc = ctx.svc orelse return error.MissingDataService;
    const opts = cli.fetchOptionsFromPolicy(ctx.globals.refresh_policy);
    const result = svc.getEtfProfile(parsed.symbol, opts) catch |err| switch (err) {
        zfin.DataError.NoApiKey => {
            cli.stderrPrint(ctx.io, "Error: ZFIN_USER_EMAIL not set. Add it to .env (SEC EDGAR requires a contact email in the User-Agent header).\n");
            return;
        },
        zfin.DataError.NotFound => {
            cli.stderrPrint(ctx.io, "Error: symbol not found in EDGAR. Either it's not an ETF/fund, or the ticker map needs refreshing.\n");
            return;
        },
        else => {
            var buf: [128]u8 = undefined;
            const msg = std.fmt.bufPrint(&buf, "Error fetching ETF profile ({t}).\n", .{err}) catch "Error fetching ETF profile.\n";
            cli.stderrPrint(ctx.io, msg);
            return;
        },
    };

    const profile = result.data;
    defer result.deinit();

    if (result.source == .cached) cli.stderrPrint(ctx.io, "(using cached ETF profile)\n");

    try printProfile(profile, parsed.symbol, ctx.color, ctx.out);
}

pub fn printProfile(profile: zfin.EtfProfile, symbol: []const u8, color: bool, out: *std.Io.Writer) !void {
    try cli.printBold(out, color, "\nETF Profile: {s}\n", .{symbol});
    try out.print("========================================\n", .{});

    if (profile.expense_ratio) |er| {
        try out.print("  Expense Ratio:      {d:.2}%\n", .{er * 100.0});
    }
    if (profile.net_assets) |na| {
        try out.print("  Net Assets:         ${s}\n", .{std.mem.trimEnd(u8, &fmt.fmtLargeNum(na), &.{' '})});
    }
    if (profile.dividend_yield) |dy| {
        try out.print("  Dividend Yield:     {d:.2}%\n", .{dy * 100.0});
    }
    if (profile.portfolio_turnover) |pt| {
        try out.print("  Portfolio Turnover: {d:.1}%\n", .{pt * 100.0});
    }
    if (profile.inception_date) |d| {
        try out.print("  Inception Date:     {f}\n", .{d});
    }
    if (profile.leveraged) {
        try cli.printFg(out, color, cli.CLR_NEGATIVE, "  Leveraged:          YES\n", .{});
    }
    if (profile.total_holdings) |th| {
        try out.print("  Total Holdings:     {d}\n", .{th});
    }

    // Sectors
    if (profile.sectors) |sectors| {
        if (sectors.len > 0) {
            try cli.printBold(out, color, "\n  Sector Allocation:\n", .{});
            for (sectors) |sec| {
                var title_buf: [64]u8 = undefined;
                const name = fmt.toTitleCase(&title_buf, sec.name);
                try cli.printFg(out, color, cli.CLR_ACCENT, "    {d:>5.1}%", .{sec.weight * 100.0});
                try out.print("  {s}\n", .{name});
            }
        }
    }

    // Top holdings
    if (profile.holdings) |holdings| {
        if (holdings.len > 0) {
            try cli.printBold(out, color, "\n  Top Holdings:\n", .{});
            try cli.setFg(out, color, cli.CLR_MUTED);
            try out.print("  {s:>6} {s:>7} {s}\n", .{ "Symbol", "Weight", "Name" });
            try out.print("  {s:->6} {s:->7} {s:->30}\n", .{ "", "", "" });
            try cli.reset(out, color);
            for (holdings) |h| {
                if (h.symbol) |s| {
                    try cli.printFg(out, color, cli.CLR_ACCENT, "  {s:>6}", .{s});
                    try out.print(" {d:>6.2}% {s}\n", .{ h.weight * 100.0, h.name });
                } else {
                    try out.print("  {s:>6} {d:>6.2}% {s}\n", .{ "--", h.weight * 100.0, h.name });
                }
            }
        }
    }

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

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

test "parseArgs: accepts a single symbol" {
    var ctx: framework.RunCtx = undefined;
    ctx.io = std.testing.io;
    const args = [_][]const u8{"VTI"};
    const parsed = try parseArgs(&ctx, &args);
    try std.testing.expectEqualStrings("VTI", parsed.symbol);
}

test "parseArgs: missing symbol errors" {
    var ctx: framework.RunCtx = undefined;
    ctx.io = std.testing.io;
    const args = [_][]const u8{};
    try std.testing.expectError(error.MissingSymbol, parseArgs(&ctx, &args));
}

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

test "printProfile minimal ETF no color" {
    var buf: [4096]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);
    const profile: zfin.EtfProfile = .{
        .symbol = "VTI",
        .expense_ratio = 0.0003,
        .dividend_yield = 0.015,
        .total_holdings = 3500,
    };
    try printProfile(profile, "VTI", false, &w);
    const out = w.buffered();
    try std.testing.expect(std.mem.indexOf(u8, out, "VTI") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "Expense Ratio") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "0.03%") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "3500") != null);
    // No ANSI when color=false
    try std.testing.expect(std.mem.indexOf(u8, out, "\x1b[") == null);
}

test "printProfile leveraged ETF shows warning" {
    var buf: [4096]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);
    const profile: zfin.EtfProfile = .{
        .symbol = "TQQQ",
        .expense_ratio = 0.0095,
        .leveraged = true,
    };
    try printProfile(profile, "TQQQ", false, &w);
    const out = w.buffered();
    try std.testing.expect(std.mem.indexOf(u8, out, "Leveraged") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "YES") != null);
}
