//! OpenFIGI API provider -- maps CUSIPs (and other identifiers) to ticker symbols.
//! API docs: https://www.openfigi.com/api/documentation
//!
//! Free tier: 25 requests/minute with API key (each request can contain up to 100 jobs).
//! No API key required for basic use (lower rate limits).
//!
//! Note: OpenFIGI does NOT cover most mutual funds (especially institutional/401(k) share
//! classes). For those, use the `ticker::` alias field in the portfolio SRF file.

const std = @import("std");
const http = @import("../net/http.zig");

const api_url = "https://api.openfigi.com/v3/mapping";

/// Result of a CUSIP lookup.
pub const FigiResult = struct {
    ticker: ?[]const u8,
    name: ?[]const u8,
    security_type: ?[]const u8,
    /// Whether the API returned a valid response (even if no match was found)
    found: bool,
};

/// Look up multiple CUSIPs in a single batch request. Caller owns all returned slices.
/// Results array is parallel to the input cusips array (same length, same order).
pub fn lookupCusips(
    io: std.Io,
    allocator: std.mem.Allocator,
    cusips: []const []const u8,
    api_key: ?[]const u8,
) ![]FigiResult {
    if (cusips.len == 0) return try allocator.alloc(FigiResult, 0);

    const Job = struct { idType: []const u8, idValue: []const u8 };

    // Build jobs array
    var jobs = try allocator.alloc(Job, cusips.len);
    defer allocator.free(jobs);
    for (cusips, 0..) |cusip, i| {
        jobs[i] = .{ .idType = "ID_CUSIP", .idValue = cusip };
    }

    // Serialize to JSON
    const body = try std.fmt.allocPrint(allocator, "{f}", .{std.json.fmt(jobs, .{})});
    defer allocator.free(body);

    // Build headers
    var headers_buf: [2]std.http.Header = undefined;
    var n_headers: usize = 0;
    headers_buf[n_headers] = .{ .name = "Content-Type", .value = "application/json" };
    n_headers += 1;
    if (api_key) |key| {
        headers_buf[n_headers] = .{ .name = "X-OPENFIGI-APIKEY", .value = key };
        n_headers += 1;
    }

    var client = http.Client.init(io, allocator);
    defer client.deinit();

    var response = try client.post(api_url, body, headers_buf[0..n_headers]);
    defer response.deinit();

    return parseResponse(allocator, response.body, cusips.len);
}

fn parseResponse(allocator: std.mem.Allocator, body: []const u8, expected_count: usize) ![]FigiResult {
    const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
        return error.ParseError;
    defer parsed.deinit();

    const root = parsed.value;
    const arr = switch (root) {
        .array => |a| a,
        else => return error.ParseError,
    };

    var results = try allocator.alloc(FigiResult, expected_count);
    for (results) |*r| r.* = .{ .ticker = null, .name = null, .security_type = null, .found = false };

    for (arr.items, 0..) |item, i| {
        if (i >= expected_count) break;

        const obj = switch (item) {
            .object => |o| o,
            else => continue,
        };

        // Check for error/warning (no match)
        if (obj.get("warning") != null or obj.get("error") != null) {
            results[i].found = true; // API responded, just no match
            continue;
        }

        // Get the data array
        const data = switch (obj.get("data") orelse continue) {
            .array => |a| a,
            else => continue,
        };

        if (data.items.len == 0) {
            results[i].found = true;
            continue;
        }

        // Use the first result, preferring exchCode "US" if available
        var best: ?std.json.ObjectMap = null;
        for (data.items) |entry| {
            const entry_obj = switch (entry) {
                .object => |o| o,
                else => continue,
            };
            if (best == null) best = entry_obj;
            // Prefer US exchange
            if (entry_obj.get("exchCode")) |ec| {
                if (ec == .string and std.mem.eql(u8, ec.string, "US")) {
                    best = entry_obj;
                    break;
                }
            }
        }

        if (best) |b| {
            results[i].found = true;
            if (b.get("ticker")) |t| {
                if (t == .string and t.string.len > 0) {
                    results[i].ticker = try allocator.dupe(u8, t.string);
                }
            }
            if (b.get("name")) |n| {
                if (n == .string and n.string.len > 0) {
                    results[i].name = try allocator.dupe(u8, n.string);
                }
            }
            if (b.get("securityType")) |st| {
                if (st == .string and st.string.len > 0) {
                    results[i].security_type = try allocator.dupe(u8, st.string);
                }
            }
        }
    }

    return results;
}

// -- Tests --

test "parseResponse basic single CUSIP" {
    const allocator = std.testing.allocator;

    const body =
        \\[
        \\  {
        \\    "data": [
        \\      {"ticker": "AAPL", "name": "APPLE INC", "securityType": "Common Stock", "exchCode": "US"}
        \\    ]
        \\  }
        \\]
    ;

    const results = try parseResponse(allocator, body, 1);
    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);
    }

    try std.testing.expectEqual(@as(usize, 1), results.len);
    try std.testing.expect(results[0].found);
    try std.testing.expectEqualStrings("AAPL", results[0].ticker.?);
    try std.testing.expectEqualStrings("APPLE INC", results[0].name.?);
    try std.testing.expectEqualStrings("Common Stock", results[0].security_type.?);
}

test "parseResponse prefers US exchange" {
    const allocator = std.testing.allocator;

    const body =
        \\[
        \\  {
        \\    "data": [
        \\      {"ticker": "AAPL-DE", "name": "APPLE INC", "securityType": "Common Stock", "exchCode": "GY"},
        \\      {"ticker": "AAPL", "name": "APPLE INC", "securityType": "Common Stock", "exchCode": "US"}
        \\    ]
        \\  }
        \\]
    ;

    const results = try parseResponse(allocator, body, 1);
    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);
    }

    try std.testing.expectEqualStrings("AAPL", results[0].ticker.?);
}

test "parseResponse warning (no match)" {
    const allocator = std.testing.allocator;

    const body =
        \\[
        \\  {
        \\    "warning": "No identifier found."
        \\  }
        \\]
    ;

    const results = try parseResponse(allocator, body, 1);
    defer allocator.free(results);

    try std.testing.expectEqual(@as(usize, 1), results.len);
    try std.testing.expect(results[0].found); // API responded, just no match
    try std.testing.expect(results[0].ticker == null);
}

test "parseResponse multiple CUSIPs" {
    const allocator = std.testing.allocator;

    const body =
        \\[
        \\  {
        \\    "data": [
        \\      {"ticker": "AAPL", "name": "APPLE INC", "securityType": "Common Stock", "exchCode": "US"}
        \\    ]
        \\  },
        \\  {
        \\    "warning": "No identifier found."
        \\  },
        \\  {
        \\    "data": [
        \\      {"ticker": "MSFT", "name": "MICROSOFT CORP", "securityType": "Common Stock", "exchCode": "US"}
        \\    ]
        \\  }
        \\]
    ;

    const results = try parseResponse(allocator, body, 3);
    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);
    }

    try std.testing.expectEqual(@as(usize, 3), results.len);

    // First: AAPL
    try std.testing.expect(results[0].found);
    try std.testing.expectEqualStrings("AAPL", results[0].ticker.?);

    // Second: no match
    try std.testing.expect(results[1].found);
    try std.testing.expect(results[1].ticker == null);

    // Third: MSFT
    try std.testing.expect(results[2].found);
    try std.testing.expectEqualStrings("MSFT", results[2].ticker.?);
}

test "parseResponse empty data array" {
    const allocator = std.testing.allocator;

    const body =
        \\[
        \\  {
        \\    "data": []
        \\  }
        \\]
    ;

    const results = try parseResponse(allocator, body, 1);
    defer allocator.free(results);

    try std.testing.expectEqual(@as(usize, 1), results.len);
    try std.testing.expect(results[0].found);
    try std.testing.expect(results[0].ticker == null);
}
