//! Yahoo Finance provider -- free, no API key required.
//!
//! Uses the unofficial v8 chart API for both candles and quotes.
//! No rate limiting is applied since there's no documented limit,
//! but this API is unofficial and can break without notice.
//!
//! Yahoo has broader mutual fund coverage than TwelveData/Polygon,
//! making it useful as a fallback for symbols like VTTHX.

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

const base_url = "https://query1.finance.yahoo.com/v8/finance/chart";

pub const Yahoo = struct {
    client: http.Client,
    allocator: std.mem.Allocator,

    pub fn init(io: std.Io, allocator: std.mem.Allocator) Yahoo {
        return .{
            .client = http.Client.init(io, allocator),
            .allocator = allocator,
        };
    }

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

    /// Fetch daily candles for a symbol between two dates.
    /// Returns candles sorted oldest-first.
    pub fn fetchCandles(
        self: *Yahoo,
        allocator: std.mem.Allocator,
        symbol: []const u8,
        from: Date,
        to: Date,
    ) ![]Candle {
        const period1 = from.toEpoch();
        const period2 = to.toEpoch() + std.time.s_per_day; // inclusive end

        var p1_buf: [20]u8 = undefined;
        var p2_buf: [20]u8 = undefined;
        const p1_str = std.fmt.bufPrint(&p1_buf, "{d}", .{period1}) catch return error.ParseError;
        const p2_str = std.fmt.bufPrint(&p2_buf, "{d}", .{period2}) catch return error.ParseError;

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

        const url = try http.buildUrl(allocator, symbol_url, &.{
            .{ "interval", "1d" },
            .{ "period1", p1_str },
            .{ "period2", p2_str },
        });
        defer allocator.free(url);

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

        return parseChartCandles(allocator, response.body);
    }

    /// Fetch a quote snapshot for a symbol.
    /// Uses the chart API meta fields (regularMarketPrice, etc.).
    pub fn fetchQuote(
        self: *Yahoo,
        allocator: std.mem.Allocator,
        symbol: []const u8,
    ) !Quote {
        // Fetch just 1 day of data to get the meta block with quote info
        const symbol_url = try std.fmt.allocPrint(allocator, base_url ++ "/{s}", .{symbol});
        defer allocator.free(symbol_url);

        const url = try http.buildUrl(allocator, symbol_url, &.{
            .{ "interval", "1d" },
            .{ "range", "5d" },
        });
        defer allocator.free(url);

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

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

// -- JSON parsing --

/// Parse the chart API result array, returning the first result's chart data.
fn getChartResult(allocator: std.mem.Allocator, body: []const u8) !struct { parsed: std.json.Parsed(std.json.Value), result: std.json.ObjectMap } {
    const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
        return error.ParseError;
    errdefer parsed.deinit();

    const chart = parsed.value.object.get("chart") orelse return error.ParseError;
    const chart_obj = switch (chart) {
        .object => |o| o,
        else => return error.ParseError,
    };

    // Check for error
    if (chart_obj.get("error")) |err_val| {
        if (err_val != .null) return error.RequestFailed;
    }

    const result_arr = chart_obj.get("result") orelse return error.ParseError;
    const results = switch (result_arr) {
        .array => |a| a.items,
        else => return error.ParseError,
    };

    if (results.len == 0) return error.ParseError;

    const result = switch (results[0]) {
        .object => |o| o,
        else => return error.ParseError,
    };

    return .{ .parsed = parsed, .result = result };
}

fn parseChartCandles(allocator: std.mem.Allocator, body: []const u8) ![]Candle {
    const chart = try getChartResult(allocator, body);
    const parsed = chart.parsed;
    defer parsed.deinit();
    const result = chart.result;

    // Get timestamps
    const ts_json = result.get("timestamp") orelse return error.ParseError;
    const timestamps = switch (ts_json) {
        .array => |a| a.items,
        else => return error.ParseError,
    };

    // Get quote data (open/high/low/close/volume arrays)
    const indicators = result.get("indicators") orelse return error.ParseError;
    const indicators_obj = switch (indicators) {
        .object => |o| o,
        else => return error.ParseError,
    };
    const quote_arr = indicators_obj.get("quote") orelse return error.ParseError;
    const quotes = switch (quote_arr) {
        .array => |a| a.items,
        else => return error.ParseError,
    };
    if (quotes.len == 0) return error.ParseError;
    const q = switch (quotes[0]) {
        .object => |o| o,
        else => return error.ParseError,
    };

    const opens = getFloatArray(q.get("open")) orelse return error.ParseError;
    const highs = getFloatArray(q.get("high")) orelse return error.ParseError;
    const lows = getFloatArray(q.get("low")) orelse return error.ParseError;
    const closes = getFloatArray(q.get("close")) orelse return error.ParseError;
    const volumes = getFloatArray(q.get("volume")) orelse return error.ParseError;

    // Get adjclose if available
    const adjcloses = blk: {
        const adjclose_section = indicators_obj.get("adjclose") orelse break :blk null;
        const adj_arr = switch (adjclose_section) {
            .array => |a| a.items,
            else => break :blk null,
        };
        if (adj_arr.len == 0) break :blk null;
        const adj_obj = switch (adj_arr[0]) {
            .object => |o| o,
            else => break :blk null,
        };
        break :blk getFloatArray(adj_obj.get("adjclose"));
    };

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

    // Yahoo returns oldest-first, which is what we want
    for (timestamps, 0..) |ts, i| {
        if (i >= opens.len or i >= closes.len) break;

        // Skip null entries (incomplete trading day)
        const close_val = closes[i];
        if (close_val == .null) continue;

        const epoch: i64 = switch (ts) {
            .integer => |v| v,
            else => continue,
        };

        const date = Date.fromEpoch(epoch);

        const close = parseJsonFloat(close_val);
        const adj = if (adjcloses) |ac| (if (i < ac.len) parseJsonFloat(ac[i]) else close) else close;

        try candles.append(allocator, .{
            .date = date,
            .open = parseJsonFloat(opens[i]),
            .high = parseJsonFloat(highs[i]),
            .low = parseJsonFloat(lows[i]),
            .close = close,
            .adj_close = adj,
            .volume = @intFromFloat(@max(0, parseJsonFloat(volumes[i]))),
        });
    }

    return candles.toOwnedSlice(allocator);
}

fn parseChartQuote(allocator: std.mem.Allocator, body: []const u8, symbol: []const u8) !Quote {
    const chart = try getChartResult(allocator, body);
    const parsed = chart.parsed;
    defer parsed.deinit();
    const result = chart.result;

    const meta = result.get("meta") orelse return error.ParseError;
    const m = switch (meta) {
        .object => |o| o,
        else => return error.ParseError,
    };

    const price = parseJsonFloat(m.get("regularMarketPrice"));
    const prev_close = parseJsonFloat(m.get("chartPreviousClose"));
    const change = price - prev_close;
    const pct = if (prev_close != 0) (change / prev_close) * 100.0 else 0;

    return .{
        .symbol = symbol,
        .name = symbol,
        .exchange = "",
        .datetime = "",
        .close = price,
        .open = price, // meta doesn't have open
        .high = parseJsonFloat(m.get("fiftyTwoWeekHigh")),
        .low = parseJsonFloat(m.get("fiftyTwoWeekLow")),
        .volume = 0,
        .previous_close = prev_close,
        .change = change,
        .percent_change = pct,
        .average_volume = 0,
        .fifty_two_week_low = parseJsonFloat(m.get("fiftyTwoWeekLow")),
        .fifty_two_week_high = parseJsonFloat(m.get("fiftyTwoWeekHigh")),
    };
}

fn getFloatArray(val: ?std.json.Value) ?[]const std.json.Value {
    const v = val orelse return null;
    return switch (v) {
        .array => |a| a.items,
        else => null,
    };
}

// -- Tests --

test "parseChartCandles basic" {
    const body =
        \\{
        \\  "chart": {
        \\    "result": [{
        \\      "timestamp": [1704067800, 1704154200],
        \\      "indicators": {
        \\        "quote": [{
        \\          "open": [185.0, 187.15],
        \\          "high": [186.1, 188.44],
        \\          "low": [184.0, 183.89],
        \\          "close": [185.5, 184.25],
        \\          "volume": [42000000, 58414460]
        \\        }],
        \\        "adjclose": [{
        \\          "adjclose": [185.5, 184.25]
        \\        }]
        \\      }
        \\    }],
        \\    "error": null
        \\  }
        \\}
    ;

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

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

    // Oldest first (Yahoo returns chronological order)
    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);

    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);
}

test "parseChartCandles skips null entries" {
    const body =
        \\{
        \\  "chart": {
        \\    "result": [{
        \\      "timestamp": [1704067800, 1704154200],
        \\      "indicators": {
        \\        "quote": [{
        \\          "open": [185.0, null],
        \\          "high": [186.1, null],
        \\          "low": [184.0, null],
        \\          "close": [185.5, null],
        \\          "volume": [42000000, null]
        \\        }]
        \\      }
        \\    }],
        \\    "error": null
        \\  }
        \\}
    ;

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

    // Should only have 1 candle (null entry skipped)
    try std.testing.expectEqual(@as(usize, 1), candles.len);
    try std.testing.expectApproxEqAbs(@as(f64, 185.5), candles[0].close, 0.01);
}

test "parseChartCandles error response" {
    const body =
        \\{
        \\  "chart": {
        \\    "result": null,
        \\    "error": {"code": "Not Found", "description": "No data found"}
        \\  }
        \\}
    ;

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

test "parseChartQuote basic" {
    const body =
        \\{
        \\  "chart": {
        \\    "result": [{
        \\      "meta": {
        \\        "symbol": "VTTHX",
        \\        "regularMarketPrice": 27.78,
        \\        "chartPreviousClose": 28.06,
        \\        "fiftyTwoWeekHigh": 28.59,
        \\        "fiftyTwoWeekLow": 22.21
        \\      },
        \\      "timestamp": [1704067800],
        \\      "indicators": {"quote": [{"open": [27.78], "high": [27.78], "low": [27.78], "close": [27.78], "volume": [0]}]}
        \\    }],
        \\    "error": null
        \\  }
        \\}
    ;

    const allocator = std.testing.allocator;
    const quote = try parseChartQuote(allocator, body, "VTTHX");

    try std.testing.expectApproxEqAbs(@as(f64, 27.78), quote.close, 0.01);
    try std.testing.expectApproxEqAbs(@as(f64, 28.06), quote.previous_close, 0.01);
    try std.testing.expectApproxEqAbs(@as(f64, 28.59), quote.fifty_two_week_high, 0.01);
    try std.testing.expectApproxEqAbs(@as(f64, 22.21), quote.fifty_two_week_low, 0.01);
    // change = 27.78 - 28.06 = -0.28
    try std.testing.expectApproxEqAbs(@as(f64, -0.28), quote.change, 0.01);
    // percent_change = (-0.28 / 28.06) * 100 ≈ -0.998
    try std.testing.expectApproxEqAbs(@as(f64, -0.998), quote.percent_change, 0.01);
}
