const std = @import("std");
const testing = std.testing;
const Date = @import("../Date.zig");

/// A single OHLCV bar, normalized from any provider.
pub const Candle = struct {
    date: Date,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    /// Close price adjusted for splits and dividends (for total return calculations).
    /// If the provider does not supply this, it equals `close`.
    adj_close: f64,
    volume: u64,

    /// Close value to use when rendering charts. Returns `adj_close`
    /// if it is non-zero, otherwise falls back to raw `close`.
    ///
    /// Why: charts should render split-adjusted prices so the line
    /// doesn't show artificial cliffs at split dates (e.g. SOXX
    /// 3-for-1 on 2024-03-07, NVDA 10-for-1 on 2024-06-10). Some
    /// older cached candles or providers that don't populate
    /// `adj_close` leave it at 0; in that case the raw close is
    /// the best we can do.
    ///
    /// The zero fallback matters beyond charts: Yahoo's parser yields
    /// 0 for a JSON `null` element, and a risk series that divides by
    /// it drops the month entirely. `analytics/portfolio_risk.zig`
    /// reuses this for that reason - "adjusted close, or raw close
    /// when the provider left it unusable" is the same question there.
    pub fn chartClose(self: Candle) f64 {
        return if (self.adj_close != 0) self.adj_close else self.close;
    }
};

/// Return the prefix of `candles` whose dates are `<= as_of`.
///
/// When `as_of` is null, returns the full slice unchanged (live mode
/// pass-through). When set, binary-searches for the first index
/// strictly after `as_of` and slices up to it. Zero-length slice
/// when `as_of` precedes all cached candles.
///
/// Candles are assumed sorted by date ascending. Truncating the tail is
/// how "compute as of date D" is expressed throughout the analytics
/// layer: every window that derives its endpoint from the newest bar
/// (trailing returns, week change, month-end snaps) then lands on D
/// without needing its own as-of parameter.
pub fn sliceCandlesAsOf(candles: []const Candle, as_of: ?Date) []const Candle {
    const d = as_of orelse return candles;
    if (candles.len == 0) return candles;
    var lo: usize = 0;
    var hi: usize = candles.len;
    while (lo < hi) {
        const mid = lo + (hi - lo) / 2;
        const cd = candles[mid].date;
        if (cd.lessThan(d) or cd.eql(d)) {
            lo = mid + 1;
        } else {
            hi = mid;
        }
    }
    return candles[0..lo];
}

test "chartClose prefers adj_close when populated" {
    const c = Candle{
        .date = Date.fromYmd(2024, 3, 7),
        .open = 700.0,
        .high = 700.0,
        .low = 230.0,
        .close = 237.75, // raw post-split
        .adj_close = 234.0, // split-adjusted
        .volume = 0,
    };
    try std.testing.expectEqual(@as(f64, 234.0), c.chartClose());
}

test "chartClose falls back to close when adj_close is zero" {
    const c = Candle{
        .date = Date.fromYmd(2024, 1, 1),
        .open = 100.0,
        .high = 100.0,
        .low = 100.0,
        .close = 100.0,
        .adj_close = 0,
        .volume = 0,
    };
    try std.testing.expectEqual(@as(f64, 100.0), c.chartClose());
}

test "chartClose synthetic split has continuous chart values" {
    // Pre-split: close=300, adj_close=100 (after a 3:1 split)
    // Post-split: close=100, adj_close=100
    // Chart should see [100, 100] not [300, 100].
    const pre = Candle{
        .date = Date.fromYmd(2024, 3, 6),
        .open = 300,
        .high = 300,
        .low = 300,
        .close = 300,
        .adj_close = 100,
        .volume = 0,
    };
    const post = Candle{
        .date = Date.fromYmd(2024, 3, 7),
        .open = 100,
        .high = 100,
        .low = 100,
        .close = 100,
        .adj_close = 100,
        .volume = 0,
    };
    try std.testing.expectEqual(pre.chartClose(), post.chartClose());
}

// ── sliceCandlesAsOf tests ────────────────────────────────────

fn makeTestCandle(y: i16, m: u8, d: u8, close: f64) Candle {
    return .{
        .date = Date.fromYmd(y, m, d),
        .open = close,
        .high = close,
        .low = close,
        .close = close,
        .adj_close = close,
        .volume = 0,
    };
}

test "sliceCandlesAsOf: null as_of returns everything" {
    const candles = [_]Candle{
        makeTestCandle(2024, 1, 1, 100),
        makeTestCandle(2024, 1, 2, 101),
    };
    const sliced = sliceCandlesAsOf(&candles, null);
    try testing.expectEqual(@as(usize, 2), sliced.len);
}

test "sliceCandlesAsOf: empty input" {
    const candles = [_]Candle{};
    const sliced = sliceCandlesAsOf(&candles, Date.fromYmd(2024, 1, 1));
    try testing.expectEqual(@as(usize, 0), sliced.len);
}

test "sliceCandlesAsOf: empty input with null as_of" {
    const candles = [_]Candle{};
    const sliced = sliceCandlesAsOf(&candles, null);
    try testing.expectEqual(@as(usize, 0), sliced.len);
}

test "sliceCandlesAsOf: exact date match included" {
    const candles = [_]Candle{
        makeTestCandle(2024, 1, 1, 100),
        makeTestCandle(2024, 1, 2, 101),
        makeTestCandle(2024, 1, 3, 102),
        makeTestCandle(2024, 1, 4, 103),
    };
    const sliced = sliceCandlesAsOf(&candles, Date.fromYmd(2024, 1, 2));
    try testing.expectEqual(@as(usize, 2), sliced.len);
    try testing.expectApproxEqAbs(@as(f64, 101), sliced[sliced.len - 1].close, 0.001);
}

test "sliceCandlesAsOf: no exact match snaps to earlier" {
    const candles = [_]Candle{
        makeTestCandle(2024, 1, 1, 100),
        makeTestCandle(2024, 1, 3, 102), // gap - no candle on the 2nd
        makeTestCandle(2024, 1, 4, 103),
    };
    // Asking for Jan 2 returns everything through Jan 1 (nothing at/after Jan 2).
    const sliced = sliceCandlesAsOf(&candles, Date.fromYmd(2024, 1, 2));
    try testing.expectEqual(@as(usize, 1), sliced.len);
    try testing.expectApproxEqAbs(@as(f64, 100), sliced[0].close, 0.001);
}

test "sliceCandlesAsOf: as_of before all candles returns empty" {
    const candles = [_]Candle{
        makeTestCandle(2024, 1, 1, 100),
        makeTestCandle(2024, 1, 2, 101),
    };
    const sliced = sliceCandlesAsOf(&candles, Date.fromYmd(2023, 12, 31));
    try testing.expectEqual(@as(usize, 0), sliced.len);
}

test "sliceCandlesAsOf: as_of after all candles returns everything" {
    const candles = [_]Candle{
        makeTestCandle(2024, 1, 1, 100),
        makeTestCandle(2024, 1, 2, 101),
    };
    const sliced = sliceCandlesAsOf(&candles, Date.fromYmd(2026, 1, 1));
    try testing.expectEqual(@as(usize, 2), sliced.len);
}
