//! Twelve Data API provider -- primary source for historical price data.
//! API docs: https://twelvedata.com/docs
//!
//! Free tier: 800 requests/day, 8 credits/min, all US market data.
//!
//! Note: Twelve Data returns split-adjusted prices but NOT dividend-adjusted.
//! The `adj_close` field is set equal to `close` here. For true total-return
//! calculations, use Polygon dividend data with the manual reinvestment method.

const std = @import("std");
const http = @import("../net/http.zig");
const RateLimiter = @import("../net/RateLimiter.zig");
const Date = @import("../Date.zig");
const Candle = @import("../models/candle.zig").Candle;
const Quote = @import("../models/quote.zig").Quote;
const json_utils = @import("json_utils.zig");
const parseJsonFloat = json_utils.parseJsonFloat;

const base_url = "https://api.twelvedata.com";

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

    pub fn init(io: std.Io, allocator: std.mem.Allocator, api_key: []const u8) TwelveData {
        return .{
            .api_key = api_key,
            .client = http.Client.init(io, allocator),
            // Provider is 8/min, but we seem to be bumping against it, so we
            // will be a bit more conservative here. Slow and steady
            .rate_limiter = RateLimiter.perMinute(io, 7),
            .allocator = allocator,
        };
    }

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

    /// Fetch daily candles for a symbol between two dates.
    /// Returns candles sorted oldest-first.
    pub fn fetchCandles(
        self: *TwelveData,
        allocator: std.mem.Allocator,
        symbol: []const u8,
        from: Date,
        to: Date,
    ) ![]Candle {
        self.rate_limiter.acquire();

        var from_buf: [10]u8 = undefined;
        var to_buf: [10]u8 = undefined;
        // Date's `{f}` output is exactly 10 bytes (YYYY-MM-DD), so
        // these cannot fail in practice; `try` instead of
        // `catch unreachable` to keep the error path honest.
        const from_str = try std.fmt.bufPrint(&from_buf, "{f}", .{from});
        const to_str = try std.fmt.bufPrint(&to_buf, "{f}", .{to});

        // TwelveData's max outputsize is 5000 data points per request.
        // For daily candles this covers ~20 years of trading days (~252/year),
        // well beyond our typical 10-year lookback. No pagination needed.
        const url = try http.buildUrl(allocator, base_url ++ "/time_series", &.{
            .{ "symbol", symbol },
            .{ "interval", "1day" },
            .{ "start_date", from_str },
            .{ "end_date", to_str },
            .{ "outputsize", "5000" },
            .{ "apikey", self.api_key },
        });
        defer allocator.free(url);

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

        return parseTimeSeriesResponse(allocator, response.body);
    }

    pub fn fetchQuote(
        self: *TwelveData,
        allocator: std.mem.Allocator,
        symbol: []const u8,
    ) !Quote {
        self.rate_limiter.acquire();

        const url = try http.buildUrl(allocator, base_url ++ "/quote", &.{
            .{ "symbol", symbol },
            .{ "apikey", self.api_key },
        });
        defer allocator.free(url);

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

        return parseQuoteResponse(allocator, response.body, symbol);
    }
};

// -- JSON parsing --

fn parseTimeSeriesResponse(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 root = parsed.value;

    // Check for API error
    if (root.object.get("status")) |status| {
        if (status == .string) {
            if (std.mem.eql(u8, status.string, "error")) {
                // Check error code
                if (root.object.get("code")) |code| {
                    if (code == .integer and code.integer == 429) return error.RateLimited;
                    if (code == .integer and code.integer == 401) return error.Unauthorized;
                }
                return error.RequestFailed;
            }
        }
    }

    const values_json = root.object.get("values") orelse return error.ParseError;
    const values = switch (values_json) {
        .array => |a| a.items,
        else => return error.ParseError,
    };

    // Twelve Data returns newest first. We'll parse into a list and reverse.
    var candles: std.ArrayList(Candle) = .empty;
    errdefer candles.deinit(allocator);

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

        const date = blk: {
            const dt = obj.get("datetime") orelse continue;
            const dt_str = switch (dt) {
                .string => |s| s,
                else => continue,
            };
            // datetime can be "YYYY-MM-DD" or "YYYY-MM-DD HH:MM:SS"
            const date_part = if (dt_str.len >= 10) dt_str[0..10] else continue;
            break :blk Date.parse(date_part) catch continue;
        };

        try candles.append(allocator, .{
            .date = date,
            .open = parseJsonFloat(obj.get("open")),
            .high = parseJsonFloat(obj.get("high")),
            .low = parseJsonFloat(obj.get("low")),
            .close = parseJsonFloat(obj.get("close")),
            // Twelve Data close is split-adjusted only, not dividend-adjusted
            .adj_close = parseJsonFloat(obj.get("close")),
            .volume = @intFromFloat(parseJsonFloat(obj.get("volume"))),
        });
    }

    // Reverse to get oldest-first ordering
    const slice = try candles.toOwnedSlice(allocator);
    std.mem.reverse(Candle, slice);
    return slice;
}

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

    const root = parsed.value.object;

    // Check for error
    if (root.get("status")) |status| {
        if (status == .string and std.mem.eql(u8, status.string, "error")) {
            if (root.get("code")) |code| {
                if (code == .integer and code.integer == 429) return error.RateLimited;
            }
            return error.RequestFailed;
        }
    }

    const ftw = root.get("fifty_two_week");

    return .{
        .symbol = symbol,
        .name = symbol,
        .exchange = "",
        .datetime = "",
        .close = parseJsonFloat(root.get("close")),
        .open = parseJsonFloat(root.get("open")),
        .high = parseJsonFloat(root.get("high")),
        .low = parseJsonFloat(root.get("low")),
        .volume = @intFromFloat(parseJsonFloat(root.get("volume"))),
        .previous_close = parseJsonFloat(root.get("previous_close")),
        .change = parseJsonFloat(root.get("change")),
        .percent_change = parseJsonFloat(root.get("percent_change")),
        .average_volume = @intFromFloat(parseJsonFloat(root.get("average_volume"))),
        .fifty_two_week_low = if (ftw) |f| switch (f) {
            .object => |o| parseJsonFloat(o.get("low")),
            else => 0,
        } else 0,
        .fifty_two_week_high = if (ftw) |f| switch (f) {
            .object => |o| parseJsonFloat(o.get("high")),
            else => 0,
        } else 0,
    };
}

// -- Tests --

test "parseTimeSeriesResponse basic" {
    const body =
        \\{
        \\  "meta": {"symbol": "AAPL"},
        \\  "values": [
        \\    {"datetime": "2024-01-03", "open": "187.15", "high": "188.44", "low": "183.89", "close": "184.25", "volume": "58414460"},
        \\    {"datetime": "2024-01-02", "open": "185.00", "high": "186.10", "low": "184.00", "close": "185.50", "volume": "42000000"}
        \\  ],
        \\  "status": "ok"
        \\}
    ;

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

    // Should reverse to oldest-first
    try std.testing.expectEqual(@as(usize, 2), candles.len);

    // First candle should be 2024-01-02 (oldest)
    try std.testing.expectEqual(@as(i16, 2024), candles[0].date.year());
    try std.testing.expectEqual(@as(u8, 1), candles[0].date.month());
    try std.testing.expectEqual(@as(u8, 2), candles[0].date.day());
    try std.testing.expectApproxEqAbs(@as(f64, 185.0), candles[0].open, 0.01);
    try std.testing.expectApproxEqAbs(@as(f64, 185.5), candles[0].close, 0.01);
    try std.testing.expectEqual(@as(u64, 42000000), candles[0].volume);

    // Second candle should be 2024-01-03 (newest)
    try std.testing.expectEqual(@as(u8, 3), candles[1].date.day());
    try std.testing.expectApproxEqAbs(@as(f64, 187.15), candles[1].open, 0.01);
    try std.testing.expectApproxEqAbs(@as(f64, 184.25), candles[1].close, 0.01);

    // adj_close should equal close (split-adjusted only)
    try std.testing.expectApproxEqAbs(candles[0].close, candles[0].adj_close, 0.001);
}

test "parseTimeSeriesResponse with datetime timestamps" {
    // Twelve Data can return "YYYY-MM-DD HH:MM:SS" format
    const body =
        \\{
        \\  "values": [
        \\    {"datetime": "2024-06-15 15:30:00", "open": "100.0", "high": "101.0", "low": "99.0", "close": "100.5", "volume": "1000000"}
        \\  ],
        \\  "status": "ok"
        \\}
    ;

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

    try std.testing.expectEqual(@as(usize, 1), candles.len);
    try std.testing.expectEqual(@as(i16, 2024), candles[0].date.year());
    try std.testing.expectEqual(@as(u8, 6), candles[0].date.month());
    try std.testing.expectEqual(@as(u8, 15), candles[0].date.day());
}

test "parseTimeSeriesResponse error response" {
    const body =
        \\{
        \\  "status": "error",
        \\  "code": 400,
        \\  "message": "Invalid symbol"
        \\}
    ;

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

test "parseTimeSeriesResponse rate limited" {
    const body =
        \\{
        \\  "status": "error",
        \\  "code": 429,
        \\  "message": "Too many requests"
        \\}
    ;

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

test "parseTimeSeriesResponse empty values" {
    const body =
        \\{
        \\  "values": [],
        \\  "status": "ok"
        \\}
    ;

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

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

test "parseQuoteResponse basic" {
    const body =
        \\{
        \\  "symbol": "AAPL",
        \\  "name": "Apple Inc",
        \\  "exchange": "NASDAQ",
        \\  "datetime": "2024-01-15",
        \\  "open": "182.15",
        \\  "high": "185.00",
        \\  "low": "181.50",
        \\  "close": "183.63",
        \\  "volume": "65000000",
        \\  "previous_close": "181.18",
        \\  "change": "2.45",
        \\  "percent_change": "1.35",
        \\  "average_volume": "55000000",
        \\  "fifty_two_week": {
        \\    "low": "140.00",
        \\    "high": "200.00"
        \\  }
        \\}
    ;

    const allocator = std.testing.allocator;
    const quote = try parseQuoteResponse(allocator, body, "AAPL");

    try std.testing.expectApproxEqAbs(@as(f64, 183.63), quote.close, 0.01);
    try std.testing.expectApproxEqAbs(@as(f64, 182.15), quote.open, 0.01);
    try std.testing.expectApproxEqAbs(@as(f64, 185.0), quote.high, 0.01);
    try std.testing.expectApproxEqAbs(@as(f64, 181.5), quote.low, 0.01);
    try std.testing.expectEqual(@as(u64, 65000000), quote.volume);
    try std.testing.expectApproxEqAbs(@as(f64, 181.18), quote.previous_close, 0.01);
    try std.testing.expectApproxEqAbs(@as(f64, 2.45), quote.change, 0.01);
    try std.testing.expectApproxEqAbs(@as(f64, 1.35), quote.percent_change, 0.01);
    try std.testing.expectEqual(@as(u64, 55000000), quote.average_volume);
    try std.testing.expectApproxEqAbs(@as(f64, 140.0), quote.fifty_two_week_low, 0.01);
    try std.testing.expectApproxEqAbs(@as(f64, 200.0), quote.fifty_two_week_high, 0.01);
}

test "parseQuoteResponse error response" {
    const body =
        \\{
        \\  "status": "error",
        \\  "code": 404,
        \\  "message": "Symbol not found"
        \\}
    ;

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

test "parseJsonFloat various formats" {
    // String number
    const str_val: std.json.Value = .{ .string = "42.5" };
    try std.testing.expectApproxEqAbs(@as(f64, 42.5), parseJsonFloat(str_val), 0.001);

    // Float
    const float_val: std.json.Value = .{ .float = 3.14 };
    try std.testing.expectApproxEqAbs(@as(f64, 3.14), parseJsonFloat(float_val), 0.001);

    // Integer
    const int_val: std.json.Value = .{ .integer = 100 };
    try std.testing.expectApproxEqAbs(@as(f64, 100.0), parseJsonFloat(int_val), 0.001);

    // Null
    try std.testing.expectApproxEqAbs(@as(f64, 0.0), parseJsonFloat(null), 0.001);

    // Invalid string
    const bad_str: std.json.Value = .{ .string = "not_a_number" };
    try std.testing.expectApproxEqAbs(@as(f64, 0.0), parseJsonFloat(bad_str), 0.001);
}
