//! C ABI over the pure core.
//!
//! One rule for every export: return a JSON string the caller frees with `usage_free`,
//! and report failure *inside* that JSON. A host never has to tell "no error" apart from
//! "no data", and NULL means only that allocation failed.
//!
//! The core owns no cross-call state; the host owns it via usage_state and passes it back.
const std = @import("std");
const usage = @import("usage.zig");

const alloc = std.heap.c_allocator;

/// Opaque cross-round state struct owned by the host.
pub const usage_state = struct {
    inner: usage.State,
};

/// C callers (Swift included) pass NULL for an absent string; treat it as empty rather
/// than dereferencing a null pointer.
fn slice(ptr: ?[*]const u8, len: usize) []const u8 {
    const p = ptr orelse return "";
    return p[0..len];
}

fn finish(w: *usage.Writer) ?[*:0]u8 {
    return alloc.dupeZ(u8, w.items()) catch null;
}

fn errorResult(a: std.mem.Allocator, text: []const u8) ?[*:0]u8 {
    var w = usage.Writer.init(a);
    w.raw("{\"error\":") catch return null;
    w.string(text) catch return null;
    w.raw("}") catch return null;
    return finish(&w);
}

fn message(error_code: anyerror) []const u8 {
    return switch (error_code) {
        error.OutOfMemory => "out of memory",
        else => "usage query failed",
    };
}

export fn usage_abi_version() u32 {
    return usage.abi_version;
}

export fn usage_free(ptr: ?[*:0]u8) void {
    if (ptr) |p| alloc.free(std.mem.span(p));
}

export fn usage_state_new() ?*usage_state {
    const s = alloc.create(usage_state) catch return null;
    s.inner = usage.State.init(alloc);
    return s;
}

export fn usage_state_free(state: ?*usage_state) void {
    if (state) |s| {
        s.inner.deinit();
        alloc.destroy(s);
    }
}

export fn usage_providers() ?[*:0]u8 {
    var arena = std.heap.ArenaAllocator.init(alloc);
    defer arena.deinit();
    const a = arena.allocator();

    var w = usage.Writer.init(a);
    usage.writeProviders(&w) catch return null;
    return finish(&w);
}

export fn usage_advance(
    state: ?*usage_state,
    provider: ?[*]const u8,
    provider_len: usize,
    credential_json: ?[*]const u8,
    credential_len: usize,
    responses_json: ?[*]const u8,
    responses_len: usize,
    now_ms: i64,
) ?[*:0]u8 {
    var arena = std.heap.ArenaAllocator.init(alloc);
    defer arena.deinit();
    const a = arena.allocator();

    const prov_slice = slice(provider, provider_len);
    const cred_slice = slice(credential_json, credential_len);
    const resp_slice = slice(responses_json, responses_len);
    const state_ptr: ?*usage.State = if (state) |s| &s.inner else null;

    const outcome = usage.advance(a, state_ptr, prov_slice, cred_slice, resp_slice, now_ms) catch |e| {
        return errorResult(a, message(e));
    };

    var w = usage.Writer.init(a);
    usage.writeStep(&w, outcome) catch |e| {
        return errorResult(a, message(e));
    };
    return finish(&w);
}

export fn usage_credentials_from_access_token(jwt: ?[*]const u8, jwt_len: usize) ?[*:0]u8 {
    var arena = std.heap.ArenaAllocator.init(alloc);
    defer arena.deinit();
    const a = arena.allocator();

    var w = usage.Writer.init(a);
    const outcome = usage.claimsFromAccessToken(a, slice(jwt, jwt_len), &w) catch |e| {
        return errorResult(a, message(e));
    };
    switch (outcome) {
        .ok => return finish(&w),
        .err => |text| return errorResult(a, text),
    }
}
