//! Google Antigravity usage feed implementation.
//!
//! Mirrors prime-agent:packages/ai/src/utils/usage/providers/antigravity.ts field for field.
const std = @import("std");
const usage = @import("../usage.zig");
const iso8601 = @import("../iso8601.zig");

pub const ANTIGRAVITY_USAGE_URL = "https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota";
pub const ANTIGRAVITY_USER_AGENT = "antigravity/hub/2.8.0 (aidev_client; os_type=darwin; arch=arm64; cl=963137146)";

const FamilyInfo = struct {
    min_remaining: ?f64 = null,
    reset_time: ?[]const u8 = null,
    present: bool = false,
};

fn modelFamily(alloc: std.mem.Allocator, model_id: []const u8) !?[]const u8 {
    const lower = try alloc.alloc(u8, model_id.len);
    _ = std.ascii.lowerString(lower, model_id);
    if (std.mem.startsWith(u8, lower, "chat_") or std.mem.startsWith(u8, lower, "tab_")) {
        return null;
    }
    if (std.mem.indexOf(u8, lower, "gemini") != null) return "Gemini";
    if (std.mem.indexOf(u8, lower, "claude") != null) return "Claude";
    if (std.mem.indexOf(u8, lower, "gpt") != null) return "GPT-OSS";
    return null;
}

pub fn parseAntigravityUsage(alloc: std.mem.Allocator, payload: ?std.json.Value, now_ms: i64) ![]const usage.UsageLimit {
    _ = now_ms;
    const root = usage.asObject(payload);
    const buckets = if (root) |r| usage.asArray(r.get("buckets")) else null;

    var gemini = FamilyInfo{};
    var claude = FamilyInfo{};
    var gpt = FamilyInfo{};

    if (buckets) |arr| {
        for (arr.items) |entry| {
            const bucket = usage.asObject(entry) orelse continue;
            const model_id = usage.asString(bucket.get("modelId")) orelse "";
            const family = try modelFamily(alloc, model_id) orelse continue;

            const remaining_fraction = usage.asNumber(bucket.get("remainingFraction"));
            const reset_time = usage.asString(bucket.get("resetTime"));

            const target: *FamilyInfo = if (std.mem.eql(u8, family, "Gemini"))
                &gemini
            else if (std.mem.eql(u8, family, "Claude"))
                &claude
            else
                &gpt;

            if (!target.present) {
                target.present = true;
                target.min_remaining = remaining_fraction;
                target.reset_time = reset_time;
            } else {
                if (remaining_fraction != null and (target.min_remaining == null or remaining_fraction.? < target.min_remaining.?)) {
                    target.min_remaining = remaining_fraction;
                    target.reset_time = reset_time;
                } else if (target.reset_time == null and reset_time != null) {
                    target.reset_time = reset_time;
                }
            }
        }
    }

    var limits = std.ArrayList(usage.UsageLimit).empty;
    const families = [_]struct { id: []const u8, label: []const u8, info: *const FamilyInfo }{
        .{ .id = "gemini", .label = "Gemini", .info = &gemini },
        .{ .id = "claude", .label = "Claude", .info = &claude },
        .{ .id = "gpt-oss", .label = "GPT-OSS", .info = &gpt },
    };

    for (families) |f| {
        if (!f.info.present) continue;
        const rem = f.info.min_remaining;

        const used_percent: ?f64 = if (rem) |r|
            @max(0.0, @min(100.0, @round((1.0 - r) * 1000.0) / 10.0))
        else
            null;

        const resets_at: ?i64 = if (f.info.reset_time) |rt| iso8601.parseIso8601(rt) else null;
        // The feed states when a family resets, never how long its window is, so there is
        // no honest label: `epochToWindowLabel` would print the time left, which reads as
        // "3h window" and then "1h window" for one fixed five-hour window.

        const detail: ?[]const u8 = if (rem) |r| blk: {
            const free_pct = usage.roundToInt(i64, r * 100.0) orelse break :blk null;
            break :blk try std.fmt.allocPrint(alloc, "{d}% free", .{free_pct});
        } else null;

        const status = if (used_percent) |up| usage.statusFor(up) else "unknown";

        // Each family's buckets are its own quota, so the family is also the scope.
        try limits.append(alloc, .{
            .id = f.id,
            .label = f.label,
            .usedPercent = used_percent,
            .resetsAt = resets_at,
            .scope = f.label,
            .detail = detail,
            .status = status,
        });
    }

    return limits.items;
}

pub fn advance(
    alloc: std.mem.Allocator,
    credential_json: []const u8,
    responses_json: []const u8,
    now_ms: i64,
) !usage.Outcome {
    const cred_parsed = std.json.parseFromSlice(std.json.Value, alloc, credential_json, .{}) catch {
        return .{ .report = .{ .error_message = "invalid credential JSON" } };
    };
    const cred_root = usage.asObject(cred_parsed.value) orelse {
        return .{ .report = .{ .error_message = "invalid credential JSON" } };
    };
    const kind = usage.asString(cred_root.get("kind")) orelse "";
    if (!std.mem.eql(u8, kind, "oauth")) {
        return .{ .report = .{ .error_message = "google-antigravity needs an OAuth account" } };
    }

    const access = usage.asString(cred_root.get("access")) orelse "";
    const extra_obj = usage.asObject(cred_root.get("extra"));
    const project_id = if (extra_obj) |eo| usage.asString(eo.get("projectId")) else null;
    const cred_account_id = usage.asString(cred_root.get("accountId"));
    const account_id = cred_account_id orelse project_id;
    const email = usage.asString(cred_root.get("email"));

    const resp_parsed = std.json.parseFromSlice(std.json.Value, alloc, responses_json, .{}) catch {
        return .{ .report = .{ .error_message = "invalid responses JSON" } };
    };
    const resp_array = usage.asArray(resp_parsed.value) orelse {
        return .{ .report = .{ .error_message = "invalid responses JSON" } };
    };

    if (resp_array.items.len == 0) {
        var headers = std.ArrayList([2][]const u8).empty;
        const auth_hdr = try std.fmt.allocPrint(alloc, "Bearer {s}", .{access});
        try headers.append(alloc, .{ "Authorization", auth_hdr });
        try headers.append(alloc, .{ "Content-Type", "application/json" });
        try headers.append(alloc, .{ "User-Agent", ANTIGRAVITY_USER_AGENT });

        const requests = try alloc.alloc(usage.UsageRequest, 1);
        requests[0] = .{
            .method = "POST",
            .url = ANTIGRAVITY_USAGE_URL,
            .headers = headers.items,
            .body = "{}",
        };
        return .{ .requests = requests };
    }

    const first = switch (try usage.parseFirstResponseEx(alloc, resp_array, "usage body is not JSON", true)) {
        .ok => |f| f,
        .err => |msg| return .{ .report = .{ .error_message = msg } },
    };
    const body_parsed = first.parsed;

    const limits = try parseAntigravityUsage(alloc, body_parsed.value, now_ms);

    const notes: []const []const u8 = if (project_id) |pid| blk: {
        const arr = try alloc.alloc([]const u8, 1);
        arr[0] = try std.fmt.allocPrint(alloc, "Project: {s}", .{pid});
        break :blk arr;
    } else &[_][]const u8{};

    return .{
        .report = .{
            .planType = "Cloud Code",
            .accountId = account_id,
            .email = email,
            .limits = limits,
            .notes = notes,
            .error_message = null,
        },
    };
}
