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

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

pub const meta: framework.Meta = .{
    .name = "lookup",
    .group = .hygiene,
    .synopsis = "Look up a CUSIP to ticker via OpenFIGI",
    .uppercase_first_arg = true,
    .help =
    \\Usage: zfin lookup <CUSIP>
    \\
    \\Look up a CUSIP (9-char alphanumeric security identifier) to
    \\its ticker via the OpenFIGI API. Successful results are cached
    \\indefinitely in `cusip_tickers.srf`. OPENFIGI_API_KEY raises
    \\the rate limit but the unauthenticated tier works for low
    \\volume.
    \\
    \\Mutual funds frequently have no OpenFIGI coverage; the
    \\command surfaces that and suggests a manual portfolio entry.
    \\
    \\Examples:
    \\  zfin lookup 037833100   # → AAPL
    \\
    ,
    .user_errors = error{ MissingCusip, UnexpectedArg },
};

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

pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
    const svc = ctx.svc orelse return error.MissingDataService;
    const out = ctx.out;
    const color = ctx.color;
    const allocator = ctx.allocator;
    if (!isCusipLike(parsed.cusip)) {
        try cli.printFg(out, color, cli.CLR_MUTED, "Note: '{s}' doesn't look like a CUSIP (expected 9 alphanumeric chars with digits)\n", .{parsed.cusip});
    }

    // L1: check the local cache before any network call. Hits are
    // permanent (CUSIP->ticker mappings don't change), so a cached
    // answer is authoritative and skips OpenFIGI entirely.
    var cache_map = svc.loadCusipTickerMap(allocator);
    defer cache_map.deinit();
    if (cache_map.get(parsed.cusip)) |ticker| {
        const cached: zfin.CusipResult = .{
            .ticker = ticker,
            .name = null,
            .security_type = null,
            .found = true,
        };
        try display(cached, parsed.cusip, color, out);
        try cli.printFg(out, color, cli.CLR_MUTED, "  (from local cache)\n", .{});
        return;
    }

    cli.stderrPrint(ctx.io, "Looking up via OpenFIGI...\n");

    // Try full batch lookup for richer output
    const results = svc.lookupCusips(&.{parsed.cusip}) catch {
        cli.stderrPrint(ctx.io, "Error: OpenFIGI request failed (network error)\n");
        return;
    };
    defer {
        for (results) |r| {
            if (r.ticker) |t| allocator.free(t);
            if (r.name) |n| allocator.free(n);
            if (r.security_type) |s| allocator.free(s);
        }
        allocator.free(results);
    }

    if (results.len == 0 or !results[0].found) {
        try out.print("No result from OpenFIGI for '{s}'\n", .{parsed.cusip});
        return;
    }

    try display(results[0], parsed.cusip, color, out);

    // Also cache it
    if (results[0].ticker) |ticker| {
        svc.cacheCusipTicker(parsed.cusip, ticker);
    }
}

pub fn display(result: zfin.CusipResult, cusip: []const u8, color: bool, out: *std.Io.Writer) !void {
    if (result.ticker) |ticker| {
        try cli.printBold(out, color, "{s}", .{cusip});
        try out.print(" -> ", .{});
        try cli.printFg(out, color, cli.CLR_ACCENT, "{s}\n", .{ticker});

        if (result.name) |name| {
            try cli.printFg(out, color, cli.CLR_MUTED, "  Name: {s}\n", .{name});
        }
        if (result.security_type) |st| {
            try cli.printFg(out, color, cli.CLR_MUTED, "  Type: {s}\n", .{st});
        }

        try out.print("\n  To use in portfolio: ticker::{s}\n", .{ticker});
    } else {
        try out.print("No ticker found for CUSIP '{s}'\n", .{cusip});
        if (result.name) |name| {
            try cli.printFg(out, color, cli.CLR_MUTED, "  Name: {s}\n", .{name});
        }
        try out.print("\n  Tip: For mutual funds, OpenFIGI often has no coverage.\n", .{});
        try out.print("  Add manually: symbol::{s},ticker::XXXX,...\n", .{cusip});
    }
}

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

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

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

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

test "display shows ticker mapping" {
    var buf: [4096]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);
    const result: zfin.CusipResult = .{
        .ticker = "AAPL",
        .name = "Apple Inc",
        .security_type = "Common Stock",
        .found = true,
    };
    try display(result, "037833100", false, &w);
    const out = w.buffered();
    try std.testing.expect(std.mem.indexOf(u8, out, "037833100") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "AAPL") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "Apple Inc") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "Common Stock") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "ticker::AAPL") != null);
}

test "display shows no-ticker message" {
    var buf: [4096]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);
    const result: zfin.CusipResult = .{
        .ticker = null,
        .name = "Some Fund",
        .security_type = null,
        .found = true,
    };
    try display(result, "123456789", false, &w);
    const out = w.buffered();
    try std.testing.expect(std.mem.indexOf(u8, out, "No ticker found") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "Some Fund") != null);
    try std.testing.expect(std.mem.indexOf(u8, out, "mutual funds") != null);
}

test "display no ANSI without color" {
    var buf: [4096]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);
    const result: zfin.CusipResult = .{
        .ticker = "MSFT",
        .name = null,
        .security_type = null,
        .found = true,
    };
    try display(result, "594918104", false, &w);
    const out = w.buffered();
    try std.testing.expect(std.mem.indexOf(u8, out, "\x1b[") == null);
}
