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 {
    symbol: []const u8,
    /// `--ntm <N>`: show ±N strikes near the money on the auto-expanded
    /// expiration. Default 8.
    ntm: usize = 8,
};

pub const meta: framework.Meta = .{
    .name = "options",
    .group = .symbol_lookup,
    .synopsis = "Show options chain (all expirations) for a symbol",
    .uppercase_first_arg = true,
    .help =
    \\Usage: zfin options <SYMBOL> [--ntm <N>]
    \\
    \\Show the options chain (all expirations) for a symbol from
    \\CBOE. Cached for 1 hour. The nearest monthly expiration is
    \\auto-expanded with ±N strikes near the money; other
    \\expirations are listed collapsed.
    \\
    \\Options:
    \\  --ntm <N>     Show ±N strikes near the money (default: 8)
    \\
    \\Examples:
    \\  zfin options SPY
    \\  zfin options AAPL --ntm 12
    \\
    ,
    .user_errors = error{ MissingSymbol, UnexpectedArg, MissingFlagValue, InvalidFlagValue },
};

pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {
    if (cmd_args.len < 1) {
        cli.stderrPrint(ctx.io, "Error: 'options' requires a symbol argument\n");
        return error.MissingSymbol;
    }
    var parsed: ParsedArgs = .{ .symbol = cmd_args[0] };
    var i: usize = 1;
    while (i < cmd_args.len) : (i += 1) {
        const a = cmd_args[i];
        if (std.mem.eql(u8, a, "--ntm")) {
            if (i + 1 >= cmd_args.len) {
                cli.stderrPrint(ctx.io, "Error: --ntm requires a value\n");
                return error.MissingFlagValue;
            }
            parsed.ntm = std.fmt.parseInt(usize, cmd_args[i + 1], 10) catch {
                cli.stderrPrint(ctx.io, "Error: --ntm value must be a non-negative integer\n");
                return error.InvalidFlagValue;
            };
            i += 1;
        } else {
            cli.stderrPrint(ctx.io, "Error: unexpected argument to 'options': ");
            cli.stderrPrint(ctx.io, a);
            cli.stderrPrint(ctx.io, "\n");
            return error.UnexpectedArg;
        }
    }
    return parsed;
}

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.getOptions(parsed.symbol, opts) catch |err| switch (err) {
        zfin.DataError.FetchFailed => {
            cli.stderrPrint(ctx.io, "Error fetching options data from CBOE.\n");
            return;
        },
        else => {
            cli.stderrPrint(ctx.io, "Error loading options data.\n");
            return;
        },
    };
    const ch = result.data;
    defer result.deinit();

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

    if (ch.len == 0) {
        cli.stderrPrint(ctx.io, "No options data found.\n");
        return;
    }

    try display(ctx.out, ch, parsed.symbol, parsed.ntm, ctx.color);
}

pub fn display(out: *std.Io.Writer, chains: []const zfin.OptionsChain, symbol: []const u8, ntm: usize, color: bool) !void {
    if (chains.len == 0) return;

    try cli.printBold(out, color, "\nOptions Chain for {s}\n", .{symbol});
    try out.print("========================================\n", .{});
    if (chains[0].underlying_price) |price| {
        try out.print("Underlying: {f}    {d} expiration(s)    +/- {d} strikes NTM\n", .{ Money.from(price), chains.len, ntm });
    } else {
        try out.print("{d} expiration(s) available\n", .{chains.len});
    }

    // Find nearest monthly expiration to auto-expand
    var auto_expand_idx: ?usize = null;
    for (chains, 0..) |chain, ci| {
        if (fmt.isMonthlyExpiration(chain.expiration)) {
            auto_expand_idx = ci;
            break;
        }
    }
    // If no monthly found, expand the first one
    if (auto_expand_idx == null and chains.len > 0) auto_expand_idx = 0;

    const atm_price = if (chains[0].underlying_price) |p| p else @as(f64, 0);

    // List all expirations, expanding the nearest monthly
    for (chains, 0..) |chain, ci| {
        const is_monthly = fmt.isMonthlyExpiration(chain.expiration);
        const is_expanded = auto_expand_idx != null and ci == auto_expand_idx.?;

        try out.print("\n", .{});
        if (is_expanded) {
            try cli.setBold(out, color);
            try out.print("{f}  ({d} calls, {d} puts)", .{
                chain.expiration, chain.calls.len, chain.puts.len,
            });
            if (is_monthly) try out.print("  [monthly]", .{});
            try cli.reset(out, color);
            try out.print("\n", .{});

            // Print calls
            try printSection(out, "CALLS", chain.calls, atm_price, ntm, true, color);
            try out.print("\n", .{});
            // Print puts
            try printSection(out, "PUTS", chain.puts, atm_price, ntm, false, color);
        } else {
            try cli.setFg(out, color, if (is_monthly) cli.CLR_HEADER else cli.CLR_MUTED);
            try out.print("{f}  ({d} calls, {d} puts)", .{
                chain.expiration, chain.calls.len, chain.puts.len,
            });
            if (is_monthly) try out.print("  [monthly]", .{});
            try cli.reset(out, color);
            try out.print("\n", .{});
        }
    }

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

pub fn printSection(
    out: *std.Io.Writer,
    label: []const u8,
    contracts: []const zfin.OptionContract,
    atm_price: f64,
    ntm: usize,
    is_calls: bool,
    color: bool,
) !void {
    try cli.printBold(out, color, "    {s}\n", .{label});
    try cli.setFg(out, color, cli.CLR_MUTED);
    try out.print("    {s:>10} {s:>10} {s:>10} {s:>10} {s:>10} {s:>8} {s:>8}\n", .{
        "Strike", "Last", "Bid", "Ask", "Volume", "OI", "IV",
    });
    try out.print("    {s:->10} {s:->10} {s:->10} {s:->10} {s:->10} {s:->8} {s:->8}\n", .{
        "", "", "", "", "", "", "",
    });
    try cli.reset(out, color);

    const filtered = fmt.filterNearMoney(contracts, atm_price, ntm);
    for (filtered) |c| {
        const itm = if (is_calls) c.strike <= atm_price else c.strike >= atm_price;
        const prefix: []const u8 = if (itm) "    |" else "     ";
        var contract_buf: [128]u8 = undefined;
        const line = fmt.fmtContractLine(&contract_buf, prefix, c);
        try out.print("{s}\n", .{line});
    }
}

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

test "parseArgs: accepts a symbol with default ntm" {
    var ctx: framework.RunCtx = undefined;
    ctx.io = std.testing.io;
    const args = [_][]const u8{"SPY"};
    const parsed = try parseArgs(&ctx, &args);
    try std.testing.expectEqualStrings("SPY", parsed.symbol);
    try std.testing.expectEqual(@as(usize, 8), parsed.ntm);
}

test "parseArgs: --ntm overrides default" {
    var ctx: framework.RunCtx = undefined;
    ctx.io = std.testing.io;
    const args = [_][]const u8{ "SPY", "--ntm", "12" };
    const parsed = try parseArgs(&ctx, &args);
    try std.testing.expectEqual(@as(usize, 12), parsed.ntm);
}

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: --ntm without value errors" {
    var ctx: framework.RunCtx = undefined;
    ctx.io = std.testing.io;
    const args = [_][]const u8{ "SPY", "--ntm" };
    try std.testing.expectError(error.MissingFlagValue, parseArgs(&ctx, &args));
}

test "parseArgs: --ntm with non-numeric value errors" {
    var ctx: framework.RunCtx = undefined;
    ctx.io = std.testing.io;
    const args = [_][]const u8{ "SPY", "--ntm", "abc" };
    try std.testing.expectError(error.InvalidFlagValue, parseArgs(&ctx, &args));
}

test "parseArgs: unknown flag errors" {
    var ctx: framework.RunCtx = undefined;
    ctx.io = std.testing.io;
    const args = [_][]const u8{ "SPY", "--bogus" };
    try std.testing.expectError(error.UnexpectedArg, parseArgs(&ctx, &args));
}

test "printSection shows header and contracts" {
    var buf: [8192]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);
    const calls = [_]zfin.OptionContract{
        .{ .contract_type = .call, .strike = 150.0, .expiration = .{ .days = 20100 }, .bid = 5.0, .ask = 5.50, .last_price = 5.25 },
        .{ .contract_type = .call, .strike = 155.0, .expiration = .{ .days = 20100 }, .bid = 2.0, .ask = 2.50, .last_price = 2.25 },
    };
    try printSection(&w, "CALLS", &calls, 152.0, 8, true, false);
    const out = w.buffered();
    try std.testing.expect(std.mem.indexOf(u8, out, "CALLS") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "Strike") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "\x1b[") == null);
}

test "display shows chain header no color" {
    var buf: [8192]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);
    const calls = [_]zfin.OptionContract{};
    const puts = [_]zfin.OptionContract{};
    const chains = [_]zfin.OptionsChain{
        .{ .underlying_symbol = "SPY", .underlying_price = 500.0, .expiration = .{ .days = 20100 }, .calls = &calls, .puts = &puts },
    };
    try display(&w, &chains, "SPY", 8, false);
    const out = w.buffered();
    try std.testing.expect(std.mem.indexOf(u8, out, "Options Chain for SPY") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "1 expiration(s)") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "\x1b[") == null);
}
