//! ISO-8601 parser for usage feed reset timestamps.
//!
//! Handles full ISO-8601 strings, e.g.:
//! - 2026-09-16T08:00:00.037593+00:00
//! - 2026-09-15T23:46:00Z
//! - 2026-06-09T11:28:39.481128Z
//! - 2026-01-01T00:00:00+05:30
//! - 2026-01-01T00:00:00-08:00
//!
//! Returns epoch milliseconds (UTC), matching Date.parse in TypeScript.
const std = @import("std");

fn isLeapYear(year: i64) bool {
    if (@mod(year, 4) != 0) return false;
    if (@mod(year, 100) != 0) return true;
    return @mod(year, 400) == 0;
}

const days_before_month_common = [_]i64{ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 };
const days_before_month_leap = [_]i64{ 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335 };

pub fn daysFromCivil(year: i64, month: u8, day: u8) i64 {
    // Days from 1970-01-01 to start of `year`
    var leaps: i64 = 0;
    if (year >= 1970) {
        const y_prev = year - 1;
        leaps = @divFloor(y_prev, 4) - @divFloor(y_prev, 100) + @divFloor(y_prev, 400) -
            (@divFloor(1969, 4) - @divFloor(1969, 100) + @divFloor(1969, 400));
    } else {
        const y_curr = year;
        leaps = @divFloor(1969, 4) - @divFloor(1969, 100) + @divFloor(1969, 400) -
            (@divFloor(y_curr - 1, 4) - @divFloor(y_curr - 1, 100) + @divFloor(y_curr - 1, 400));
        leaps = -leaps;
    }
    const days_in_years = (year - 1970) * 365 + leaps;
    const m_days = if (isLeapYear(year)) days_before_month_leap[month - 1] else days_before_month_common[month - 1];
    return days_in_years + m_days + (day - 1);
}

pub fn parseIso8601(str: []const u8) ?i64 {
    const trimmed = std.mem.trim(u8, str, " \t\r\n");
    if (trimmed.len < 19) return null; // YYYY-MM-DDTHH:MM:SS
    if (trimmed[4] != '-' or trimmed[7] != '-' or (trimmed[10] != 'T' and trimmed[10] != 't' and trimmed[10] != ' ')) return null;
    if (trimmed[13] != ':' or trimmed[16] != ':') return null;

    const year = std.fmt.parseInt(i64, trimmed[0..4], 10) catch return null;
    const month = std.fmt.parseInt(u8, trimmed[5..7], 10) catch return null;
    const day = std.fmt.parseInt(u8, trimmed[8..10], 10) catch return null;
    if (month < 1 or month > 12) return null;
    if (day < 1 or day > 31) return null;

    const hour = std.fmt.parseInt(i64, trimmed[11..13], 10) catch return null;
    const minute = std.fmt.parseInt(i64, trimmed[14..16], 10) catch return null;
    const second = std.fmt.parseInt(i64, trimmed[17..19], 10) catch return null;
    if (hour < 0 or hour > 23 or minute < 0 or minute > 59 or second < 0 or second > 60) return null;

    var idx: usize = 19;
    var millis: i64 = 0;
    if (idx < trimmed.len and trimmed[idx] == '.') {
        idx += 1;
        var frac_digits: usize = 0;
        var frac_val: i64 = 0;
        while (idx < trimmed.len and std.ascii.isDigit(trimmed[idx])) : (idx += 1) {
            if (frac_digits < 3) {
                frac_val = frac_val * 10 + (trimmed[idx] - '0');
                frac_digits += 1;
            }
        }
        while (frac_digits < 3) : (frac_digits += 1) {
            frac_val *= 10;
        }
        millis = frac_val;
    }

    var offset_sec: i64 = 0;
    if (idx < trimmed.len) {
        if (trimmed[idx] == 'Z' or trimmed[idx] == 'z') {
            idx += 1;
        } else if (trimmed[idx] == '+' or trimmed[idx] == '-') {
            const sign: i64 = if (trimmed[idx] == '+') 1 else -1;
            idx += 1;
            if (idx + 2 <= trimmed.len) {
                const off_h = std.fmt.parseInt(i64, trimmed[idx .. idx + 2], 10) catch return null;
                idx += 2;
                var off_m: i64 = 0;
                if (idx < trimmed.len and trimmed[idx] == ':') idx += 1;
                if (idx + 2 <= trimmed.len) {
                    off_m = std.fmt.parseInt(i64, trimmed[idx .. idx + 2], 10) catch return null;
                    idx += 2;
                }
                offset_sec = sign * (off_h * 3600 + off_m * 60);
            } else {
                return null;
            }
        } else {
            return null;
        }
    }
    if (idx != trimmed.len) return null; // trailing unparsed garbage

    const days = daysFromCivil(year, month, day);
    const total_sec = days * 86400 + hour * 3600 + minute * 60 + second - offset_sec;
    return total_sec * 1000 + millis;
}

test "parseIso8601 with various formats" {
    const cases = [_]struct { s: []const u8, expected: i64 }{
        .{ .s = "2026-09-16T08:00:00.037593+00:00", .expected = 1789545600037 },
        .{ .s = "2026-09-15T23:46:00Z", .expected = 1789515960000 },
        .{ .s = "2026-06-09T11:28:39.481128Z", .expected = 1781004519481 },
        .{ .s = "2026-01-01T00:00:00+05:30", .expected = 1767205800000 },
        .{ .s = "2026-01-01T00:00:00-08:00", .expected = 1767254400000 },
        .{ .s = "2026-01-01T00:00:00.5Z", .expected = 1767225600500 },
        .{ .s = "2026-01-01T00:00:00.12Z", .expected = 1767225600120 },
        .{ .s = "2026-01-01T00:00:00.123Z", .expected = 1767225600123 },
        .{ .s = "2026-01-01T00:00:00.1234Z", .expected = 1767225600123 },
    };

    for (cases) |c| {
        const got = parseIso8601(c.s);
        try std.testing.expectEqual(c.expected, got.?);
    }

    try std.testing.expect(parseIso8601("") == null);
    try std.testing.expect(parseIso8601("not-a-date") == null);
    try std.testing.expect(parseIso8601("2026-09-16") == null);
}
