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

pub const ANTHROPIC_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
pub const ANTHROPIC_PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
const OAUTH_BETA = "oauth-2025-04-20";

pub const WindowInfo = struct {
    label: []const u8,
    windowLabel: ?[]const u8 = null,
    windowSeconds: ?i64 = null,
    scope: ?[]const u8 = null,
};

const five_hours = 5 * 3600;
const one_day = 86_400;
const one_week = 7 * one_day;

/// The part of a kind that names what the window is restricted to; `all` restricts nothing.
fn kindScope(scope: []const u8) ?[]const u8 {
    return if (scope.len == 0 or std.mem.eql(u8, scope, "all")) null else scope;
}

fn titleCase(alloc: std.mem.Allocator, value: []const u8) ![]const u8 {
    var parts = std.mem.splitAny(u8, value, "_-");
    var res = std.ArrayList(u8).empty;
    var first = true;
    while (parts.next()) |part| {
        if (part.len == 0) continue;
        if (!first) try res.append(alloc, ' ');
        first = false;
        try res.append(alloc, std.ascii.toUpper(part[0]));
        if (part.len > 1) {
            try res.appendSlice(alloc, part[1..]);
        }
    }
    return res.items;
}

pub fn windowForKind(alloc: std.mem.Allocator, kind: []const u8) !WindowInfo {
    if (std.mem.eql(u8, kind, "session")) {
        return .{ .label = "Session", .windowLabel = "5h window", .windowSeconds = five_hours };
    }
    if (std.mem.endsWith(u8, kind, "_session")) {
        const scope = kindScope(kind[0 .. kind.len - "_session".len]);
        return .{ .label = "Session", .windowLabel = "5h window", .windowSeconds = five_hours, .scope = scope };
    }
    if (std.mem.startsWith(u8, kind, "weekly_")) {
        const scope = kind["weekly_".len..];
        if (std.mem.eql(u8, scope, "all")) {
            return .{ .label = "Weekly", .windowLabel = "7d window", .windowSeconds = one_week };
        }
        const tc = try titleCase(alloc, scope);
        const label = try std.fmt.allocPrint(alloc, "Weekly ({s})", .{tc});
        return .{ .label = label, .windowLabel = "7d window", .windowSeconds = one_week, .scope = kindScope(scope) };
    }
    if (std.mem.eql(u8, kind, "weekly")) {
        return .{ .label = "Weekly", .windowLabel = "7d window", .windowSeconds = one_week };
    }
    if (std.mem.startsWith(u8, kind, "daily_")) {
        const scope = kind["daily_".len..];
        const tc = try titleCase(alloc, scope);
        const label = try std.fmt.allocPrint(alloc, "Daily ({s})", .{tc});
        return .{ .label = label, .windowLabel = "24h window", .windowSeconds = one_day, .scope = kindScope(scope) };
    }
    const label = try titleCase(alloc, kind);
    return .{ .label = label, .windowLabel = null };
}

pub const ParseUsageResult = struct {
    limits: []const usage.UsageLimit,
    notes: []const []const u8,
};

pub fn parseAnthropicUsage(alloc: std.mem.Allocator, payload: ?std.json.Value, _now_ms: i64) !ParseUsageResult {
    _ = _now_ms;
    const root = usage.asObject(payload);
    var notes = std.ArrayList([]const u8).empty;
    var limits = std.ArrayList(usage.UsageLimit).empty;

    const entries = if (root) |r| usage.asArray(r.get("limits")) else null;
    if (entries) |arr| {
        for (arr.items) |entry| {
            const limit = usage.asObject(entry) orelse continue;
            const kind = usage.asString(limit.get("kind")) orelse "unknown";
            const used_percent = usage.asNumber(limit.get("percent"));
            const win = try windowForKind(alloc, kind);
            const severity = usage.asString(limit.get("severity"));

            const status = if (used_percent == null)
                "unknown"
            else if (severity != null and std.mem.eql(u8, severity.?, "critical"))
                "warning"
            else
                usage.statusFor(used_percent);

            const final_status = if (used_percent != null and used_percent.? >= 100.0)
                "exhausted"
            else
                status;

            const resets_at_str = usage.asString(limit.get("resets_at"));
            const resets_at_ms: ?i64 = if (resets_at_str) |s| iso8601.parseIso8601(s) else null;

            try limits.append(alloc, .{
                .id = kind,
                .label = win.label,
                .usedPercent = used_percent,
                .windowLabel = win.windowLabel,
                .windowSeconds = win.windowSeconds,
                .resetsAt = resets_at_ms,
                // The body's own scope wins over the one read from the kind.
                .scope = usage.stringOf(limit.get("scope")) orelse win.scope,
                .status = final_status,
            });
        }
    }

    const extra = if (root) |r| usage.asObject(r.get("extra_usage")) else null;
    if (extra) |ex| {
        const is_enabled = ex.get("is_enabled");
        const enabled_bool = if (is_enabled) |b| switch (b) {
            .bool => |v| v,
            else => false,
        } else false;

        if (enabled_bool) {
            const monthly_limit = usage.asNumber(ex.get("monthly_limit"));
            const used_credits = usage.asNumber(ex.get("used_credits"));
            const percent: ?f64 = if (monthly_limit == null or monthly_limit.? == 0.0 or used_credits == null)
                null
            else
                (used_credits.? / monthly_limit.?) * 100.0;

            const detail: ?[]const u8 = if (monthly_limit) |ml| blk: {
                const uc_int = usage.toInt(i64, used_credits orelse 0.0) orelse break :blk null;
                const ml_int = usage.toInt(i64, ml) orelse break :blk null;
                break :blk try std.fmt.allocPrint(alloc, "{d} of {d} credits", .{ uc_int, ml_int });
            } else null;

            try limits.append(alloc, .{
                .id = "extra_usage",
                .label = "Extra usage",
                .usedPercent = percent,
                .resetsAt = null,
                .status = usage.statusFor(percent),
                .detail = detail,
            });
        } else {
            const credits_ever_enabled = ex.get("credits_ever_enabled");
            const ever_bool = if (credits_ever_enabled) |b| switch (b) {
                .bool => |v| v,
                else => false,
            } else false;
            if (ever_bool) {
                try notes.append(alloc, "Extra usage is off");
            }
        }
    }

    return ParseUsageResult{
        .limits = limits.items,
        .notes = notes.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 = "anthropic needs an OAuth account" } };
    }
    const access = usage.asString(cred_root.get("access")) orelse "";
    const cred_email = usage.stringOf(cred_root.get("email"));
    const cred_account_id = usage.stringOf(cred_root.get("accountId"));

    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, .{ "anthropic-beta", OAUTH_BETA });
        try headers.append(alloc, .{ "User-Agent", "prime-agent" });

        const requests = try alloc.alloc(usage.UsageRequest, 2);
        requests[0] = .{
            .method = "GET",
            .url = ANTHROPIC_USAGE_URL,
            .headers = headers.items,
        };
        requests[1] = .{
            .method = "GET",
            .url = ANTHROPIC_PROFILE_URL,
            .headers = headers.items,
        };
        return .{ .requests = requests };
    }

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

    // Profile response (optional / responses[1])
    var profile_obj: ?std.json.ObjectMap = null;
    if (resp_array.items.len > 1) {
        const resp1_obj = usage.asObject(resp_array.items[1]);
        if (resp1_obj) |r1| {
            if (usage.asString(r1.get("body"))) |pbody| {
                if (std.json.parseFromSlice(std.json.Value, alloc, pbody, .{})) |p_parsed| {
                    profile_obj = usage.asObject(p_parsed.value);
                } else |_| {}
            }
        }
    }

    const org_obj = if (profile_obj) |p| usage.asObject(p.get("organization")) else null;
    const acct_obj = if (profile_obj) |p| usage.asObject(p.get("account")) else null;

    const parsed_usage = try parseAnthropicUsage(alloc, body_parsed.value, now_ms);

    const plan_type = if (org_obj) |o|
        (usage.stringOf(o.get("seat_tier")) orelse usage.stringOf(o.get("rate_limit_tier")))
    else
        null;

    const account_email = if (acct_obj) |a| usage.stringOf(a.get("email")) else null;
    const final_email: ?[]const u8 = if (account_email) |em| blk: {
        const lowered = try alloc.alloc(u8, em.len);
        break :blk std.ascii.lowerString(lowered, em);
    } else cred_email;

    const final_account_id = if (cred_account_id) |cid|
        cid
    else if (acct_obj) |a|
        usage.stringOf(a.get("uuid"))
    else
        null;

    return .{
        .report = .{
            .planType = plan_type,
            .email = final_email,
            .accountId = final_account_id,
            .limits = parsed_usage.limits,
            .notes = parsed_usage.notes,
            .error_message = null,
        },
    };
}
