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

pub const HYPER_TEAMS_URL = "https://hyper.charm.land/v1/teams";

pub const ParseBillingResult = struct {
    planType: ?[]const u8 = null,
    limits: []const usage.UsageLimit = &[_]usage.UsageLimit{},
    balance: ?[]const u8 = null,
};

pub fn parseHyperBilling(
    alloc: std.mem.Allocator,
    payload: ?std.json.Value,
    now_ms: i64,
) !ParseBillingResult {
    const root = usage.asObject(payload);
    const plan = if (root) |r| usage.asObject(r.get("plan")) else null;
    const balances = if (root) |r| usage.asObject(r.get("balances")) else null;
    const plan_balances = if (balances) |b| usage.asObject(b.get("plan")) else null;
    const total_balances = if (balances) |b| usage.asObject(b.get("total")) else null;
    const refresh = if (root) |r| usage.asObject(r.get("hypercredit_refresh")) else null;

    const plan_name = if (plan) |p| usage.asString(p.get("name")) else null;
    const plan_type_raw = if (plan) |p| usage.asString(p.get("type")) else null;

    const plan_type: ?[]const u8 = if (plan_name) |pn| blk: {
        const trimmed = std.mem.trim(u8, pn, " \t\r\n");
        if (trimmed.len == 0) break :blk null;
        const lower = try alloc.alloc(u8, trimmed.len);
        break :blk std.ascii.lowerString(lower, trimmed);
    } else if (plan_type_raw) |ptr| blk: {
        const trimmed = std.mem.trim(u8, ptr, " \t\r\n");
        if (trimmed.len == 0) break :blk null;
        const lower = try alloc.alloc(u8, trimmed.len);
        break :blk std.ascii.lowerString(lower, trimmed);
    } else null;

    const limit = if (plan) |p| usage.asNumber(p.get("hypercredits")) else null;
    const remaining = if (plan_balances) |pb| usage.asNumber(pb.get("hypercredits")) else null;

    var used_percent: ?f64 = null;
    if (limit != null and limit.? > 0.0 and remaining != null) {
        const used = @max(0.0, limit.? - remaining.?);
        used_percent = @max(0.0, @min(100.0, @round((used / limit.?) * 1000.0) / 10.0));
    } else if (remaining != null and remaining.? <= 0.0) {
        used_percent = 100.0;
    }

    const resets_at: ?i64 = if (refresh) |rf| (if (usage.asString(rf.get("next_at"))) |s| iso8601.parseIso8601(s) else null) else null;
    const last_at: ?i64 = if (refresh) |rf| (if (usage.asString(rf.get("last_at"))) |s| iso8601.parseIso8601(s) else null) else null;

    var window_label: ?[]const u8 = null;
    var window_seconds: ?i64 = null;
    if (resets_at != null and last_at != null) {
        const diff_ms = resets_at.? - last_at.?;
        if (diff_ms > 0) {
            const diff_sec = usage.roundToInt(i64, @as(f64, @floatFromInt(diff_ms)) / 1000.0) orelse 0;
            // The refresh period is measured from its own endpoints, so it is the window.
            if (diff_sec > 0) window_seconds = diff_sec;
            if (diff_sec >= 86_400) {
                const days = usage.roundToInt(i64, @as(f64, @floatFromInt(diff_sec)) / 86_400.0) orelse 1;
                window_label = if (days == 1) "24h window" else try std.fmt.allocPrint(alloc, "{d}d window", .{days});
            } else if (diff_sec > 0) {
                const hours = @max(1, usage.roundToInt(i64, @as(f64, @floatFromInt(diff_sec)) / 3600.0) orelse 1);
                window_label = try std.fmt.allocPrint(alloc, "{d}h window", .{hours});
            }
        }
    } else {
        window_label = try usage.epochToWindowLabel(alloc, resets_at, now_ms);
    }

    const root_status = if (root) |r| usage.asString(r.get("status")) else null;
    const status: []const u8 = if (root_status == null or !std.mem.eql(u8, root_status.?, "active"))
        "unknown"
    else if (used_percent == null)
        "unknown"
    else if (used_percent.? >= 100.0)
        "exhausted"
    else if (used_percent.? >= 90.0)
        "warning"
    else
        "ok";

    const detail: ?[]const u8 = if (remaining != null and limit != null) blk: {
        const rem = usage.toInt(i64, remaining.?) orelse break :blk null;
        const lim = usage.toInt(i64, limit.?) orelse break :blk null;
        break :blk try std.fmt.allocPrint(alloc, "{d} of {d} credits", .{ rem, lim });
    } else null;

    var limits = std.ArrayList(usage.UsageLimit).empty;
    try limits.append(alloc, .{
        .id = "hypercredits",
        .label = "Primary",
        .usedPercent = used_percent,
        .windowLabel = window_label,
        .windowSeconds = window_seconds,
        .resetsAt = resets_at,
        .status = status,
        .detail = detail,
    });

    const balance: ?[]const u8 = if (total_balances) |tb| blk: {
        if (usage.asString(tb.get("usd"))) |u| {
            const trimmed = std.mem.trim(u8, u, " \t\r\n");
            if (trimmed.len > 0) break :blk trimmed;
        }
        if (usage.asNumber(tb.get("usd"))) |n| {
            break :blk try std.fmt.allocPrint(alloc, "{d:.2}", .{n});
        }
        break :blk null;
    } else null;

    return ParseBillingResult{
        .planType = plan_type,
        .limits = limits.items,
        .balance = balance,
    };
}

pub fn advance(
    alloc: std.mem.Allocator,
    state: ?*usage.State,
    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 "";
    const is_oauth = std.mem.eql(u8, kind, "oauth");
    const is_api_key = std.mem.eql(u8, kind, "apiKey");

    if (!is_oauth and !is_api_key) {
        return .{ .report = .{ .error_message = "hyper needs an apiKey or OAuth credential" } };
    }

    const token = if (is_oauth)
        usage.asString(cred_root.get("access")) orelse ""
    else
        usage.asString(cred_root.get("apiKey")) orelse "";

    const cred_account_id = usage.stringOf(cred_root.get("accountId"));
    const cred_email = usage.stringOf(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" } };
    };

    var auth_hdr_list = std.ArrayList([2][]const u8).empty;
    const auth_value = try std.fmt.allocPrint(alloc, "Bearer {s}", .{token});
    try auth_hdr_list.append(alloc, .{ "Authorization", auth_value });

    // Round 1: Fetch teams
    if (resp_array.items.len == 0) {
        const requests = try alloc.alloc(usage.UsageRequest, 1);
        requests[0] = .{
            .method = "GET",
            .url = HYPER_TEAMS_URL,
            .headers = auth_hdr_list.items,
        };
        return .{ .requests = requests };
    }

    // Round 2: the answer is either the teams list or already a billing body. The reference
    // (hyper.ts) distinguishes them by shape, not by round number, so a caller that hands the
    // billing body in as the only response gets a report instead of "no teams found".
    const first_payload = switch (try usage.parseFirstResponse(alloc, resp_array, "teams body is not JSON")) {
        .ok => |f| f,
        .err => |msg| return .{ .report = .{ .error_message = msg } },
    };
    const first_object = usage.asObject(first_payload.parsed.value);
    const is_billing_response = first_object != null and
        (first_object.?.get("plan") != null or first_object.?.get("balances") != null);

    if (!is_billing_response and resp_array.items.len == 1) {
        const teams_root = first_object;
        const items = if (teams_root) |tr| usage.asArray(tr.get("items")) else null;
        if (items == null or items.?.items.len == 0) {
            return .{ .report = .{ .error_message = "no hyper teams found" } };
        }

        var selected_team: ?std.json.ObjectMap = null;
        if (cred_account_id) |caid| {
            for (items.?.items) |it| {
                const o = usage.asObject(it) orelse continue;
                if (usage.asString(o.get("id"))) |id| {
                    if (std.mem.eql(u8, id, caid)) {
                        selected_team = o;
                        break;
                    }
                }
            }
        }
        if (selected_team == null) {
            selected_team = usage.asObject(items.?.items[0]);
        }

        const team_id = if (selected_team) |st| usage.asString(st.get("id")) else null;
        const team_name = if (selected_team) |st| usage.asString(st.get("name")) else null;

        if (team_id == null or team_id.?.len == 0) {
            return .{ .report = .{ .error_message = "invalid team in hyper response" } };
        }

        if (state) |s| {
            try s.setHyperTeam(team_id.?, team_name);
        }

        const billing_url = try std.fmt.allocPrint(alloc, "{s}/{s}/billing", .{ HYPER_TEAMS_URL, team_id.? });
        const members_url = try std.fmt.allocPrint(alloc, "{s}/{s}/members", .{ HYPER_TEAMS_URL, team_id.? });

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

    // Round 3: Billing & members responses handling. The billing body is whichever response the
    // shaping above identified, so it is already parsed.
    const parsed_billing = try parseHyperBilling(alloc, first_payload.parsed.value, now_ms);

    var member_email: ?[]const u8 = null;
    if (resp_array.items.len >= 2) {
        const resp1_obj = usage.asObject(resp_array.items[1]);
        if (resp1_obj) |r1| {
            const st1 = usage.toInt(i64, usage.asNumber(r1.get("status")));
            if (st1 != null and st1.? == 200) {
                if (usage.asString(r1.get("body"))) |b1_str| {
                    if (std.json.parseFromSlice(std.json.Value, alloc, b1_str, .{})) |b1_parsed| {
                        if (usage.asObject(b1_parsed.value)) |b1_obj| {
                            if (usage.asArray(b1_obj.get("items"))) |mitems| {
                                if (mitems.items.len > 0) {
                                    if (usage.asObject(mitems.items[0])) |m0| {
                                        if (usage.asString(m0.get("email"))) |em| {
                                            const trimmed = std.mem.trim(u8, em, " \t\r\n");
                                            if (trimmed.len > 0) member_email = trimmed;
                                        }
                                    }
                                }
                            }
                        }
                    } else |_| {}
                }
            }
        }
    }

    const cached_id = if (state) |s| s.hyper_team_id else null;
    const cached_name = if (state) |s| s.hyper_team_name else null;
    const account_id = cred_account_id orelse cached_id;
    const email = member_email orelse cred_email;
    const notes: []const []const u8 = if (cached_name) |tn| blk: {
        const arr = try alloc.alloc([]const u8, 1);
        arr[0] = try std.fmt.allocPrint(alloc, "Team: {s}", .{tn});
        break :blk arr;
    } else &[_][]const u8{};

    return .{
        .report = .{
            .planType = parsed_billing.planType,
            .accountId = account_id,
            .email = email,
            .limits = parsed_billing.limits,
            .balance = parsed_billing.balance,
            .notes = notes,
            .error_message = null,
        },
    };
}
