/// LLRP inventory reader
///
/// Connects to an RFID reader, configures it, then streams tag reads
/// continuously until interrupted with Ctrl+C (SIGINT).  Each tag observation
/// triggers an immediate report.  On signal the reader is stopped, cleaned up,
/// and the program exits.
///
/// Usage:  reader [-v[-v]] READERHOSTNAME
///   -v    print one-line progress messages
///   -vv   also dump all LLRP messages as XML
const std = @import("std");
const llrp = @import("llrp.zig");
const tracker = @import("tracker.zig");
const ntp = @import("ntp.zig");

// ---------------------------------------------------------------------------
// Signal handling
// ---------------------------------------------------------------------------

/// Set to false by the SIGINT handler to stop the inventory loop.
var g_running = std.atomic.Value(bool).init(true);

fn sigintHandler(signo: c_int) callconv(.c) void {
    _ = signo;
    g_running.store(false, .release);
    _ = llrp.c.printf("\nINFO: SIGINT received, stopping inventory...\n");
}

// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------

pub fn main() void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const args = std.process.argsAlloc(allocator) catch {
        std.debug.print("ERROR: failed to allocate args\n", .{});
        std.process.exit(1);
    };
    defer std.process.argsFree(allocator, args);

    var verbose: c_int = 0;
    var hostname: ?[*:0]const u8 = null;

    for (args[1..]) |arg| {
        if (arg.len > 0 and (arg[0] == '-' or arg[0] == '/')) {
            for (arg[1..]) |ch| {
                switch (ch) {
                    'v', 'V' => verbose += 1,
                    else => {
                        std.debug.print("Usage: {s} [-v[-v]] READERHOSTNAME\n", .{args[0]});
                        std.process.exit(1);
                    },
                }
            }
        } else {
            hostname = arg.ptr;
        }
    }

    if (hostname == null) {
        std.debug.print("Usage: {s} [-v[-v]] READERHOSTNAME\n", .{args[0]});
        std.process.exit(1);
    }

    // Install SIGINT handler for graceful shutdown.
    const sa = std.posix.Sigaction{
        .handler = .{ .handler = sigintHandler },
        .mask = std.mem.zeroes(std.posix.sigset_t),
        .flags = 0,
    };
    std.posix.sigaction(std.posix.SIG.INT, &sa, null);

    run(allocator, hostname.?, verbose) catch |err| {
        std.debug.print("ERROR: {}\n", .{err});
        std.process.exit(2);
    };

    std.debug.print("INFO: Done\n", .{});
}

// ---------------------------------------------------------------------------
// Run loop
// ---------------------------------------------------------------------------

fn run(allocator: std.mem.Allocator, hostname: [*:0]const u8, verbose: c_int) !void {
    var t = tracker.Tracker.init(allocator, onTagEnter, onTagExit, .{});
    defer {
        t.flush();
        t.deinit();
    }

    // Query NTP once to get a correction offset for all reported timestamps.
    // A failure is non-fatal: we proceed with offset=0 and log a warning.
    const clock_offset_us = ntp.queryOffset(allocator, "pool.ntp.org") catch |err| blk: {
        _ = llrp.c.printf(
            "WARNING: NTP query failed (%s), timestamps may be inaccurate\n",
            @errorName(err).ptr,
        );
        break :blk @as(i64, 0);
    };
    if (verbose != 0)
        _ = llrp.c.printf("INFO: NTP clock offset: %lld µs\n", @as(c_longlong, clock_offset_us));

    var reader = try llrp.Reader.init(hostname, verbose);
    reader.clock_offset_us = clock_offset_us;
    defer reader.deinit();

    try reader.checkConnectionStatus();

    const info = try reader.queryReaderInfo();
    printReaderInfo(&info);

    if (verbose != 0) _ = llrp.c.printf("INFO: Scrubbing initial configuration...\n");
    try reader.scrubConfiguration();

    // Best-effort config cleanup on exit (deferred, errors ignored).
    defer {
        if (verbose != 0) _ = llrp.c.printf("INFO: Cleaning up reader configuration...\n");
        reader.scrubConfiguration() catch {};
    }

    try reader.enableImpinjExtensions();
    try reader.addROSpec();
    try reader.enableROSpec();

    _ = llrp.c.printf("INFO: Starting continuous inventory — press Ctrl+C to stop\n");

    // Start the ROSpec once; because the AISpec has a Null stop trigger the
    // reader inventories continuously.  Stop it explicitly before teardown.
    try reader.startROSpec();
    defer reader.stopROSpec() catch {};

    // Loop receiving one-tag reports until SIGINT.
    while (g_running.load(.acquire)) {
        reader.tryReceiveReport(500, .{
            .context = &t,
            .func = struct {
                fn shim(ctx: *anyopaque, obs: llrp.TagObservation) void {
                    const tp: *tracker.Tracker = @ptrCast(@alignCast(ctx));
                    tp.observe(obs) catch {};
                }
            }.shim,
        }, .{
            .context = undefined,
            .func = struct {
                fn shim(_: *anyopaque, event: llrp.GpiEvent) void {
                    onGpiEvent(event.port, event.state, event.timestamp_us);
                }
            }.shim,
        }) catch |err| {
            // A timeout just means no tag was seen in this window — keep going.
            if (err == error.Timeout) {
                t.checkExpired(now()) catch {};
                continue;
            }
            // Any other error during SIGINT shutdown is expected; exit cleanly.
            if (!g_running.load(.acquire)) break;
            return err;
        };
        t.checkExpired(now()) catch {};
    }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

fn now() u64 {
    return @intCast(std.time.microTimestamp());
}

fn printReaderInfo(info: *const llrp.ReaderInfo) void {
    _ = llrp.c.printf("INFO: Reader  manufacturer_pen=%u  model=%u  firmware=%s\n",
        info.manufacturer_pen, info.model, &info.firmware[0]);

    const r = info.reader_id;
    _ = llrp.c.printf(
        "INFO: Reader  id=%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X\n",
        r[0],  r[1],  r[2],  r[3],
        r[4],  r[5],
        r[6],  r[7],
        r[8],  r[9],
        r[10], r[11], r[12], r[13], r[14], r[15],
    );
}

/// Format raw EPC bytes as "XXXX-XXXX-..." into a stack buffer.
/// Returns a slice into `buf`.
fn fmtEpc(epc: []const u8, buf: []u8) [:0]const u8 {
    var written: usize = 0;
    for (epc, 0..) |byte, i| {
        if (i > 0 and i % 2 == 0) {
            buf[written] = '-';
            written += 1;
        }
        const pair = std.fmt.bufPrint(buf[written..], "{X:0>2}", .{byte}) catch break;
        written += pair.len;
    }
    buf[written] = 0;
    return buf[0..written :0];
}

// ---------------------------------------------------------------------------
// GPI lifecycle callback
// ---------------------------------------------------------------------------

fn onGpiEvent(port: u16, state: bool, timestamp_us: u64) void {
    _ = llrp.c.printf(
        "GPI    port=%u  state=%s  ts=%llu\n",
        port,
        if (state) @as([*:0]const u8, "high") else @as([*:0]const u8, "low"),
        @as(c_ulonglong, timestamp_us),
    );
}

// ---------------------------------------------------------------------------
// Tag lifecycle callbacks
// ---------------------------------------------------------------------------

fn onTagEnter(epc: []const u8, antenna: u16, timestamp_us: u64) void {
    var buf: [96]u8 = undefined;
    _ = llrp.c.printf(
        "ENTER  epc=%-32s  ant=%u  ts=%llu\n",
        (fmtEpc(epc, &buf).ptr),
        antenna,
        @as(c_ulonglong, timestamp_us),
    );
}

fn onTagExit(
    epc: []const u8,
    antenna: u16,
    first_seen_us: u64,
    samples: []const tracker.RssiSample,
) void {
    var buf: [96]u8 = undefined;
    const epc_str = fmtEpc(epc, &buf);
    const stats = rssiStats(samples);

    _ = llrp.c.printf(
        "EXIT   epc=%-32s  ant=%u  first_seen=%llu  samples=%zu" ++
            "  mean=%.1fdBm  peak=%ddBm  variance=%.2f  skew=%.3f  entropy=%.3f\n",
        epc_str.ptr,
        antenna,
        @as(c_ulonglong, first_seen_us),
        samples.len,
        stats.mean,
        stats.peak,
        stats.variance,
        stats.skew,
        stats.entropy,
    );
}

const RssiStats = struct {
    mean: f64,
    peak: i8,
    variance: f64,
    skew: f64,
    entropy: f64,
};

fn rssiStats(samples: []const tracker.RssiSample) RssiStats {
    if (samples.len == 0) return .{ .mean = 0, .peak = 0, .variance = 0, .skew = 0, .entropy = 0 };

    const n: f64 = @floatFromInt(samples.len);

    // --- mean and peak ---
    var sum: f64 = 0;
    var peak: i8 = samples[0].rssi_dbm;
    for (samples) |s| {
        sum += @floatFromInt(s.rssi_dbm);
        if (s.rssi_dbm > peak) peak = s.rssi_dbm;
    }
    const mean = sum / n;

    // --- variance (population) and third central moment for skewness ---
    var m2: f64 = 0; // sum of (x - mean)^2
    var m3: f64 = 0; // sum of (x - mean)^3
    for (samples) |s| {
        const d: f64 = @as(f64, @floatFromInt(s.rssi_dbm)) - mean;
        m2 += d * d;
        m3 += d * d * d;
    }
    const variance = m2 / n;

    // Pearson's moment coefficient of skewness; 0 if std dev is negligible.
    const std_dev = @sqrt(variance);
    const skew = if (std_dev > 1e-9) (m3 / n) / (std_dev * std_dev * std_dev) else 0.0;

    // --- Shannon entropy over the observed value distribution ---
    // RSSI is i8; use a 256-bucket counter array indexed by value + 128.
    var counts = std.mem.zeroes([256]u32);
    for (samples) |s| counts[@as(u8, @bitCast(s.rssi_dbm))] += 1;
    var entropy: f64 = 0;
    for (counts) |c| {
        if (c == 0) continue;
        const p: f64 = @as(f64, @floatFromInt(c)) / n;
        entropy -= p * @log2(p);
    }

    return .{ .mean = mean, .peak = peak, .variance = variance, .skew = skew, .entropy = entropy };
}
