//! Minimal SNTP client (RFC 4330).
//!
//! Performs a single round-trip to an NTP server and computes the offset
//! between the local system clock and UTC so that reported timestamps can
//! be corrected before being sent to a server.
//!
//! Typical usage:
//!   const offset = ntp.queryOffset(allocator, "pool.ntp.org") catch |err| blk: {
//!       std.log.warn("NTP query failed ({s}), timestamps may be inaccurate", .{@errorName(err)});
//!       break :blk 0;
//!   };
//!   // corrected_us = std.time.microTimestamp() + offset

const std = @import("std");

const ntp_port: u16 = 123;

/// NTP epoch is 1 Jan 1900; Unix epoch is 1 Jan 1970.
/// Difference = 70 years = 2 208 988 800 seconds.
const ntp_to_unix_s: i64 = 2_208_988_800;

/// Decode a 64-bit NTP timestamp starting at `buf[off]` into microseconds
/// since the Unix epoch.
fn ntpToUs(buf: []const u8, off: usize) i64 {
    const secs = std.mem.readInt(u32, buf[off..][0..4], .big);
    const frac = std.mem.readInt(u32, buf[off + 4 ..][0..4], .big);
    const unix_s: i64 = @as(i64, secs) - ntp_to_unix_s;
    // frac / 2^32 * 1_000_000 µs  (no overflow: frac < 2^32, result < 1_000_000)
    const us_frac: i64 = @intCast((@as(u64, frac) * 1_000_000) >> 32);
    return unix_s * 1_000_000 + us_frac;
}

/// Query `server` (hostname or IP) via SNTP and return the estimated clock
/// offset in microseconds.
///
/// Return value semantics:
///   offset_us = ntp_time_us - local_time_us
///
/// To get a corrected UTC timestamp:
///   corrected_us = std.time.microTimestamp() + offset_us
///
/// Uses the standard SNTP offset formula:
///   offset = ((T2 - T1) + (T3 - T4)) / 2
/// where T1 = local send time, T2 = server receive, T3 = server transmit,
/// T4 = local receive time.
pub fn queryOffset(allocator: std.mem.Allocator, server: []const u8) !i64 {
    // Resolve hostname.
    const addr_list = try std.net.getAddressList(allocator, server, ntp_port);
    defer addr_list.deinit();
    if (addr_list.addrs.len == 0) return error.NoAddress;
    const addr = addr_list.addrs[0];

    // Open a UDP socket.
    const sock = try std.posix.socket(
        addr.any.family,
        std.posix.SOCK.DGRAM,
        std.posix.IPPROTO.UDP,
    );
    defer std.posix.close(sock);

    // 2 second receive timeout.
    const tv = std.posix.timeval{ .sec = 2, .usec = 0 };
    try std.posix.setsockopt(
        sock,
        std.posix.SOL.SOCKET,
        std.posix.SO.RCVTIMEO,
        std.mem.asBytes(&tv),
    );

    // 48-byte SNTP request: only the flags byte is non-zero.
    //   LI = 0 (no leap second warning)
    //   VN = 4 (NTP version 4)
    //   Mode = 3 (client)
    //   Packed: 0b00_100_011 = 0x23
    var req = std.mem.zeroes([48]u8);
    req[0] = 0x23;

    // T1: local clock immediately before send.
    const t1: i64 = std.time.microTimestamp();

    const sent = try std.posix.sendto(
        sock,
        &req,
        0,
        &addr.any,
        addr.getOsSockLen(),
    );
    if (sent != 48) return error.SendFailed;

    var resp: [48]u8 = undefined;
    const n = try std.posix.recvfrom(sock, &resp, 0, null, null);
    if (n < 48) return error.ShortResponse;

    // T4: local clock immediately after receive.
    const t4: i64 = std.time.microTimestamp();

    // T2 = server receive timestamp  (bytes 32–39)
    // T3 = server transmit timestamp (bytes 40–47)
    const t2 = ntpToUs(&resp, 32);
    const t3 = ntpToUs(&resp, 40);

    return @divTrunc((t2 - t1) + (t3 - t4), 2);
}
