//! Tiingo provider -- official REST API for end-of-day prices.
//!
//! Free tier: 1,000 requests/day, no per-minute restriction.
//! Covers stocks, ETFs, and mutual funds with same-day NAV updates
//! (mutual fund NAVs available after midnight ET).
//!
//! API docs: https://www.tiingo.com/documentation/end-of-day

const std = @import("std");
const http = @import("../net/http.zig");
const Date = @import("../Date.zig");
const Candle = @import("../models/candle.zig").Candle;
const json_utils = @import("json_utils.zig");
const optFloat = json_utils.optFloat;
const jsonStr = json_utils.jsonStr;

const base_url = "https://api.tiingo.com/tiingo/daily";

pub const Tiingo = struct {
    client: http.Client,
    allocator: std.mem.Allocator,
    api_key: []const u8,

    pub fn init(io: std.Io, allocator: std.mem.Allocator, api_key: []const u8) Tiingo {
        return .{
            .client = http.Client.init(io, allocator),
            .allocator = allocator,
            .api_key = api_key,
        };
    }

    pub fn deinit(self: *Tiingo) void {
        self.client.deinit();
    }

    /// Fetch daily candles for a symbol between two dates (inclusive).
    /// Returns candles sorted oldest-first.
    pub fn fetchCandles(
        self: *Tiingo,
        allocator: std.mem.Allocator,
        symbol: []const u8,
        from: Date,
        to: Date,
    ) ![]Candle {
        var from_buf: [10]u8 = undefined;
        var to_buf: [10]u8 = undefined;
        const from_str = std.fmt.bufPrint(&from_buf, "{f}", .{from}) catch unreachable;
        const to_str = std.fmt.bufPrint(&to_buf, "{f}", .{to}) catch unreachable;

        const symbol_url = try std.fmt.allocPrint(allocator, base_url ++ "/{s}/prices", .{symbol});
        defer allocator.free(symbol_url);

        const url = try http.buildUrl(allocator, symbol_url, &.{
            .{ "startDate", from_str },
            .{ "endDate", to_str },
            .{ "token", self.api_key },
        });
        defer allocator.free(url);

        var response = try self.client.get(url);
        defer response.deinit();

        return parseCandles(allocator, response.body);
    }
};

/// Parse Tiingo's JSON array of price objects into Candles.
/// Tiingo returns oldest-first, which matches our convention.
fn parseCandles(allocator: std.mem.Allocator, body: []const u8) ![]Candle {
    const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
        return error.ParseError;
    defer parsed.deinit();

    const items = switch (parsed.value) {
        .array => |a| a.items,
        // Tiingo returns an object with "detail" on errors (bad symbol, auth, etc.)
        else => return error.RequestFailed,
    };

    var candles: std.ArrayList(Candle) = .empty;
    errdefer {
        candles.deinit(allocator);
    }

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

        const date = parseDate(obj.get("date")) orelse continue;
        const close = optFloat(obj.get("close")) orelse continue;

        try candles.append(allocator, .{
            .date = date,
            .open = optFloat(obj.get("open")) orelse close,
            .high = optFloat(obj.get("high")) orelse close,
            .low = optFloat(obj.get("low")) orelse close,
            .close = close,
            .adj_close = optFloat(obj.get("adjClose")) orelse close,
            .volume = blk: {
                const v = optFloat(obj.get("volume")) orelse break :blk 0;
                break :blk @intFromFloat(@max(0, v));
            },
        });
    }

    return candles.toOwnedSlice(allocator);
}

/// Parse a Tiingo date string (e.g. "2026-03-16T00:00:00.000Z") into a Date.
fn parseDate(val: ?std.json.Value) ?Date {
    const s = jsonStr(val) orelse return null;
    if (s.len < 10) return null;
    return Date.parse(s[0..10]) catch null;
}

// -- Tests --

test "parseCandles basic" {
    const body =
        \\[
        \\  {
        \\    "date": "2026-03-13T00:00:00.000Z",
        \\    "close": 42.41, "high": 42.41, "low": 42.41, "open": 42.41,
        \\    "volume": 0, "adjClose": 42.41, "adjHigh": 42.41,
        \\    "adjLow": 42.41, "adjOpen": 42.41, "adjVolume": 0,
        \\    "divCash": 0.0, "splitFactor": 1.0
        \\  },
        \\  {
        \\    "date": "2026-03-16T00:00:00.000Z",
        \\    "close": 42.74, "high": 42.74, "low": 42.74, "open": 42.74,
        \\    "volume": 0, "adjClose": 42.74, "adjHigh": 42.74,
        \\    "adjLow": 42.74, "adjOpen": 42.74, "adjVolume": 0,
        \\    "divCash": 0.0, "splitFactor": 1.0
        \\  }
        \\]
    ;

    const allocator = std.testing.allocator;
    const candles = try parseCandles(allocator, body);
    defer allocator.free(candles);

    try std.testing.expectEqual(@as(usize, 2), candles.len);

    // Oldest first
    try std.testing.expectEqual(@as(i16, 2026), candles[0].date.year());
    try std.testing.expectEqual(@as(u8, 3), candles[0].date.month());
    try std.testing.expectEqual(@as(u8, 13), candles[0].date.day());
    try std.testing.expectApproxEqAbs(@as(f64, 42.41), candles[0].close, 0.01);

    try std.testing.expectEqual(@as(u8, 16), candles[1].date.day());
    try std.testing.expectApproxEqAbs(@as(f64, 42.74), candles[1].close, 0.01);
    try std.testing.expectApproxEqAbs(@as(f64, 42.74), candles[1].adj_close, 0.01);
}

test "parseCandles stock with volume" {
    const body =
        \\[
        \\  {
        \\    "date": "2026-03-16T00:00:00.000Z",
        \\    "close": 183.22, "high": 185.10, "low": 180.50, "open": 181.00,
        \\    "volume": 217307380, "adjClose": 183.22, "adjHigh": 185.10,
        \\    "adjLow": 180.50, "adjOpen": 181.00, "adjVolume": 217307380,
        \\    "divCash": 0.0, "splitFactor": 1.0
        \\  }
        \\]
    ;

    const allocator = std.testing.allocator;
    const candles = try parseCandles(allocator, body);
    defer allocator.free(candles);

    try std.testing.expectEqual(@as(usize, 1), candles.len);
    try std.testing.expectApproxEqAbs(@as(f64, 181.00), candles[0].open, 0.01);
    try std.testing.expectApproxEqAbs(@as(f64, 185.10), candles[0].high, 0.01);
    try std.testing.expectApproxEqAbs(@as(f64, 180.50), candles[0].low, 0.01);
    try std.testing.expectApproxEqAbs(@as(f64, 183.22), candles[0].close, 0.01);
    try std.testing.expectEqual(@as(u64, 217307380), candles[0].volume);
}

test "parseCandles error response" {
    const body =
        \\{"detail": "Not found."}
    ;

    const allocator = std.testing.allocator;
    const result = parseCandles(allocator, body);
    try std.testing.expectError(error.RequestFailed, result);
}

test "parseCandles empty array" {
    const body = "[]";
    const allocator = std.testing.allocator;
    const candles = try parseCandles(allocator, body);
    defer allocator.free(candles);
    try std.testing.expectEqual(@as(usize, 0), candles.len);
}
