/**
 * Usage Bar Extension - Shows AI provider usage stats like CodexBar
 * Run /usage to see usage for Claude, Copilot, Gemini, and Codex
 *
 * Features:
 * - Usage stats with progress bars
 * - Provider status (outages/incidents)
 * - Reset countdowns
 */
import { execSync, spawnSync } from "node:child_process";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { snapshotGeminiCliAccount } from "@singularity-forge/google-gemini-cli-provider";
import { visibleWidth } from "@singularity-forge/tui";
import { sfHome } from "../sf-home.js";

// ============================================================================
// Auth helper
// ============================================================================
function loadAuthJson() {
	const sfAuthPath = path.join(sfHome(), "agent", "auth.json");
	try {
		if (fs.existsSync(sfAuthPath)) {
			return JSON.parse(fs.readFileSync(sfAuthPath, "utf-8"));
		}
	} catch {} // file missing or invalid → try PI path
	const piAuthPath = path.join(os.homedir(), ".pi", "agent", "auth.json");
	try {
		if (fs.existsSync(piAuthPath)) {
			return JSON.parse(fs.readFileSync(piAuthPath, "utf-8"));
		}
	} catch {} // file missing or invalid → return undefined
}
// ============================================================================
// Status Polling
// ============================================================================
const STATUS_URLS = {
	anthropic: "https://status.anthropic.com/api/v2/status.json",
	codex: "https://status.openai.com/api/v2/status.json",
};
async function fetchProviderStatus(provider) {
	const url = STATUS_URLS[provider];
	if (!url) return { indicator: "none" };
	try {
		const controller = new AbortController();
		setTimeout(() => controller.abort(), 5000);
		const res = await fetch(url, { signal: controller.signal });
		if (!res.ok) return { indicator: "unknown" };
		const data = await res.json();
		const indicator = data.status?.indicator || "none";
		const description = data.status?.description;
		return {
			indicator: indicator,
			description,
		};
	} catch {
		return { indicator: "unknown" };
	}
}
async function fetchGeminiStatus() {
	try {
		const controller = new AbortController();
		setTimeout(() => controller.abort(), 5000);
		const res = await fetch(
			"https://www.google.com/appsstatus/dashboard/incidents.json",
			{
				signal: controller.signal,
			},
		);
		if (!res.ok) return { indicator: "unknown" };
		const incidents = await res.json();
		// Look for active Gemini incidents (product ID: npdyhgECDJ6tB66MxXyo)
		const geminiProductId = "npdyhgECDJ6tB66MxXyo";
		const activeIncidents = incidents.filter((inc) => {
			if (inc.end) return false; // Not active
			const affected =
				inc.currently_affected_products || inc.affected_products || [];
			return affected.some((p) => p.id === geminiProductId);
		});
		if (activeIncidents.length === 0) {
			return { indicator: "none" };
		}
		// Find most severe
		let worstIndicator = "minor";
		let description;
		for (const inc of activeIncidents) {
			const status = inc.most_recent_update?.status || inc.status_impact;
			if (status === "SERVICE_OUTAGE") {
				worstIndicator = "critical";
				description = inc.external_desc;
			} else if (
				status === "SERVICE_DISRUPTION" &&
				worstIndicator !== "critical"
			) {
				worstIndicator = "major";
				description = inc.external_desc;
			}
		}
		return { indicator: worstIndicator, description };
	} catch {
		return { indicator: "unknown" };
	}
}
// ============================================================================
// Claude Usage
// ============================================================================
function loadClaudeToken() {
	// Try sf's auth.json first (has user:profile scope), fallback to pi's
	const data = loadAuthJson();
	if (data?.anthropic?.access) return data.anthropic.access;
	// Fallback to Claude CLI keychain (macOS)
	try {
		const keychainData = execSync(
			'security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null',
			{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] },
		).trim();
		if (keychainData) {
			const parsed = JSON.parse(keychainData);
			const scopes = parsed.claudeAiOauth?.scopes || [];
			if (
				scopes.includes("user:profile") &&
				parsed.claudeAiOauth?.accessToken
			) {
				return parsed.claudeAiOauth.accessToken;
			}
		}
	} catch {}
	return undefined;
}
async function fetchClaudeUsage() {
	const token = loadClaudeToken();
	if (!token) {
		return {
			provider: "anthropic",
			displayName: "Claude",
			windows: [],
			error: "No credentials",
		};
	}
	try {
		const controller = new AbortController();
		setTimeout(() => controller.abort(), 5000);
		const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
			headers: {
				Authorization: `Bearer ${token}`,
				"anthropic-beta": "oauth-2025-04-20",
			},
			signal: controller.signal,
		});
		if (!res.ok) {
			return {
				provider: "anthropic",
				displayName: "Claude",
				windows: [],
				error: `HTTP ${res.status}`,
			};
		}
		const data = await res.json();
		const windows = [];
		if (data.five_hour?.utilization !== undefined) {
			windows.push({
				label: "5h",
				usedPercent: data.five_hour.utilization,
				resetDescription: data.five_hour.resets_at
					? formatReset(new Date(data.five_hour.resets_at))
					: undefined,
			});
		}
		if (data.seven_day?.utilization !== undefined) {
			windows.push({
				label: "Week",
				usedPercent: data.seven_day.utilization,
				resetDescription: data.seven_day.resets_at
					? formatReset(new Date(data.seven_day.resets_at))
					: undefined,
			});
		}
		const modelWindow = data.seven_day_sonnet || data.seven_day_opus;
		if (modelWindow?.utilization !== undefined) {
			windows.push({
				label: data.seven_day_sonnet ? "Sonnet" : "Opus",
				usedPercent: modelWindow.utilization,
			});
		}
		return { provider: "anthropic", displayName: "Claude", windows };
	} catch (e) {
		return {
			provider: "anthropic",
			displayName: "Claude",
			windows: [],
			error: String(e),
		};
	}
}
// ============================================================================
// Gemini Usage
// ============================================================================
async function fetchGeminiUsage(_modelRegistry) {
	// Existence check is a fast pre-flight so we surface a friendly "not logged
	// in" message without paying for the OAuth bootstrap inside snapshotGeminiCliAccount.
	const credPath = path.join(os.homedir(), ".gemini", "oauth_creds.json");
	if (!fs.existsSync(credPath)) {
		return {
			provider: "gemini",
			displayName: "Gemini",
			windows: [],
			error: "No ~/.gemini credentials",
		};
	}
	try {
		const snapshot = await snapshotGeminiCliAccount();
		if (!snapshot) {
			return {
				provider: "gemini",
				displayName: "Gemini",
				windows: [],
				error: "No Code Assist project or empty quota response",
			};
		}
		const windows = snapshot.models.map((m) => {
			const resetDate = m.resetTime ? new Date(m.resetTime) : undefined;
			return {
				label: m.modelId.replace(/^gemini-/, "").slice(0, 7),
				usedPercent: m.usedFraction * 100,
				resetDescription:
					resetDate && !Number.isNaN(resetDate.getTime())
						? formatReset(resetDate)
						: undefined,
			};
		});
		return { provider: "gemini", displayName: "Gemini", windows };
	} catch (e) {
		return {
			provider: "gemini",
			displayName: "Gemini",
			windows: [],
			error: String(e),
		};
	}
}
// ============================================================================
// Codex (OpenAI) Usage
// ============================================================================
async function fetchCodexUsage(modelRegistry) {
	// Try to get token from sf's auth storage first
	let accessToken;
	let accountId;
	try {
		// Try openai-codex provider first (sf's built-in)
		accessToken = await modelRegistry?.authStorage?.getApiKey?.("openai-codex");
		// Get account ID if available from OAuth credentials
		const cred = modelRegistry?.authStorage?.get?.("openai-codex");
		if (cred?.type === "oauth") {
			accountId = cred.accountId;
		}
	} catch {} // missing or invalid JSON → continue to codex fallback
	// Fallback to ~/.codex/auth.json if not in sf's auth
	if (!accessToken) {
		const codexHome =
			process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
		const authPath = path.join(codexHome, "auth.json");
		try {
			if (fs.existsSync(authPath)) {
				const data = JSON.parse(fs.readFileSync(authPath, "utf-8"));
				if (data.OPENAI_API_KEY) {
					accessToken = data.OPENAI_API_KEY;
				} else if (data.tokens?.access_token) {
					accessToken = data.tokens.access_token;
					accountId = data.tokens.account_id;
				}
			}
		} catch {} // codex auth missing or invalid → continue
	}
	if (!accessToken) {
		return {
			provider: "codex",
			displayName: "Codex",
			windows: [],
			error: "No credentials",
		};
	}
	try {
		const controller = new AbortController();
		setTimeout(() => controller.abort(), 5000);
		const headers = {
			Authorization: `Bearer ${accessToken}`,
			"User-Agent": "CodexBar",
			Accept: "application/json",
		};
		if (accountId) {
			headers["ChatGPT-Account-Id"] = accountId;
		}
		const res = await fetch("https://chatgpt.com/backend-api/wham/usage", {
			method: "GET",
			headers,
			signal: controller.signal,
		});
		if (res.status === 401 || res.status === 403) {
			return {
				provider: "codex",
				displayName: "Codex",
				windows: [],
				error: "Token expired",
			};
		}
		if (!res.ok) {
			return {
				provider: "codex",
				displayName: "Codex",
				windows: [],
				error: `HTTP ${res.status}`,
			};
		}
		const data = await res.json();
		const windows = [];
		// Primary window (usually 3-hour)
		if (data.rate_limit?.primary_window) {
			const pw = data.rate_limit.primary_window;
			const resetDate = pw.reset_at ? new Date(pw.reset_at * 1000) : undefined;
			const windowHours = Math.round((pw.limit_window_seconds || 10800) / 3600);
			windows.push({
				label: `${windowHours}h`,
				usedPercent: pw.used_percent || 0,
				resetDescription: resetDate ? formatReset(resetDate) : undefined,
			});
		}
		// Secondary window (usually daily)
		if (data.rate_limit?.secondary_window) {
			const sw = data.rate_limit.secondary_window;
			const resetDate = sw.reset_at ? new Date(sw.reset_at * 1000) : undefined;
			const windowHours = Math.round((sw.limit_window_seconds || 86400) / 3600);
			const label = windowHours >= 24 ? "Day" : `${windowHours}h`;
			windows.push({
				label,
				usedPercent: sw.used_percent || 0,
				resetDescription: resetDate ? formatReset(resetDate) : undefined,
			});
		}
		// Credits info
		let plan = data.plan_type;
		if (data.credits?.balance !== undefined && data.credits.balance !== null) {
			const balance =
				typeof data.credits.balance === "number"
					? data.credits.balance
					: parseFloat(data.credits.balance) || 0;
			plan = plan
				? `${plan} ($${balance.toFixed(2)})`
				: `$${balance.toFixed(2)}`;
		}
		return { provider: "codex", displayName: "Codex", windows, plan };
	} catch (e) {
		return {
			provider: "codex",
			displayName: "Codex",
			windows: [],
			error: String(e),
		};
	}
}
// ============================================================================
// Kiro (AWS)
// ============================================================================
function stripAnsi(text) {
	return text.replace(/\x1B\[[0-9;?]*[A-Za-z]|\x1B\].*?\x07/g, "");
}
function whichSync(cmd) {
	const result = spawnSync("which", [cmd], { encoding: "utf-8" });
	if (result.status !== 0 || !result.stdout) return null;
	return result.stdout.trim();
}
async function fetchKiroUsage() {
	const kiroBinary = whichSync("kiro-cli");
	if (!kiroBinary) {
		return {
			provider: "kiro",
			displayName: "Kiro",
			windows: [],
			error: "kiro-cli not found",
		};
	}
	try {
		// Check if logged in
		try {
			execSync("kiro-cli whoami", { encoding: "utf-8", timeout: 5000 });
		} catch {
			return {
				provider: "kiro",
				displayName: "Kiro",
				windows: [],
				error: "Not logged in",
			};
		}
		// Get usage
		const output = execSync("kiro-cli chat --no-interactive /usage", {
			encoding: "utf-8",
			timeout: 10000,
			env: { ...process.env, TERM: "xterm-256color" },
		});
		const stripped = stripAnsi(output);
		const windows = [];
		// Parse plan name from "| KIRO FREE" or similar
		let planName = "Kiro";
		const planMatch = stripped.match(/\|\s*(KIRO\s+\w+)/i);
		if (planMatch) {
			planName = planMatch[1].trim();
		}
		// Parse credits percentage from "████...█ X%"
		let creditsPercent = 0;
		const percentMatch = stripped.match(/█+\s*(\d+)%/);
		if (percentMatch) {
			creditsPercent = parseInt(percentMatch[1], 10);
		}
		// Parse credits used/total from "(X.XX of Y covered in plan)"
		let creditsUsed = 0;
		let creditsTotal = 50;
		const creditsMatch = stripped.match(/\((\d+\.?\d*)\s+of\s+(\d+)\s+covered/);
		if (creditsMatch) {
			creditsUsed = parseFloat(creditsMatch[1]);
			creditsTotal = parseFloat(creditsMatch[2]);
			if (!percentMatch && creditsTotal > 0) {
				creditsPercent = (creditsUsed / creditsTotal) * 100;
			}
		}
		// Parse reset date from "resets on 01/01"
		let resetsAt;
		const resetMatch = stripped.match(/resets on (\d{2}\/\d{2})/);
		if (resetMatch) {
			const [month, day] = resetMatch[1].split("/").map(Number);
			const now = new Date();
			const year = now.getFullYear();
			resetsAt = new Date(year, month - 1, day);
			if (resetsAt < now) resetsAt.setFullYear(year + 1);
		}
		windows.push({
			label: "Credits",
			usedPercent: creditsPercent,
			resetDescription: resetsAt ? formatReset(resetsAt) : undefined,
		});
		// Parse bonus credits
		const bonusMatch = stripped.match(/Bonus credits:\s*(\d+\.?\d*)\/(\d+)/);
		if (bonusMatch) {
			const bonusUsed = parseFloat(bonusMatch[1]);
			const bonusTotal = parseFloat(bonusMatch[2]);
			const bonusPercent = bonusTotal > 0 ? (bonusUsed / bonusTotal) * 100 : 0;
			const expiryMatch = stripped.match(/expires in (\d+) days?/);
			windows.push({
				label: "Bonus",
				usedPercent: bonusPercent,
				resetDescription: expiryMatch ? `${expiryMatch[1]}d left` : undefined,
			});
		}
		return { provider: "kiro", displayName: "Kiro", windows, plan: planName };
	} catch (e) {
		return {
			provider: "kiro",
			displayName: "Kiro",
			windows: [],
			error: String(e),
		};
	}
}
// ============================================================================
// z.ai
// ============================================================================
async function fetchZaiUsage() {
	// Check for API key in environment or sf/pi auth
	let apiKey = process.env.Z_AI_API_KEY;
	if (!apiKey) {
		// Try auth storage
		try {
			const data = loadAuthJson();
			if (data) {
				apiKey = data["z-ai"]?.access || data["zai"]?.access;
			}
		} catch {} // missing or invalid → continue to error
	}
	if (!apiKey) {
		return {
			provider: "zai",
			displayName: "z.ai",
			windows: [],
			error: "No API key",
		};
	}
	try {
		const controller = new AbortController();
		setTimeout(() => controller.abort(), 5000);
		const res = await fetch("https://api.z.ai/api/monitor/usage/quota/limit", {
			method: "GET",
			headers: {
				Authorization: `Bearer ${apiKey}`,
				Accept: "application/json",
			},
			signal: controller.signal,
		});
		if (!res.ok) {
			return {
				provider: "zai",
				displayName: "z.ai",
				windows: [],
				error: `HTTP ${res.status}`,
			};
		}
		const data = await res.json();
		if (!data.success || data.code !== 200) {
			return {
				provider: "zai",
				displayName: "z.ai",
				windows: [],
				error: data.msg || "API error",
			};
		}
		const windows = [];
		const limits = data.data?.limits || [];
		for (const limit of limits) {
			const type = limit.type;
			const _usage = limit.usage || 0;
			const _remaining = limit.remaining || 0;
			const percent = limit.percentage || 0;
			const nextReset = limit.nextResetTime
				? new Date(limit.nextResetTime)
				: undefined;
			// Unit: 1=days, 3=hours, 5=minutes
			let windowLabel = "Limit";
			if (limit.unit === 1) windowLabel = `${limit.number}d`;
			else if (limit.unit === 3) windowLabel = `${limit.number}h`;
			else if (limit.unit === 5) windowLabel = `${limit.number}m`;
			if (type === "TOKENS_LIMIT") {
				windows.push({
					label: `Tokens (${windowLabel})`,
					usedPercent: percent,
					resetDescription: nextReset ? formatReset(nextReset) : undefined,
				});
			} else if (type === "TIME_LIMIT") {
				windows.push({
					label: "Monthly",
					usedPercent: percent,
					resetDescription: nextReset ? formatReset(nextReset) : undefined,
				});
			}
		}
		const planName = data.data?.planName || data.data?.plan || undefined;
		return { provider: "zai", displayName: "z.ai", windows, plan: planName };
	} catch (e) {
		return {
			provider: "zai",
			displayName: "z.ai",
			windows: [],
			error: String(e),
		};
	}
}
// ============================================================================
// Helpers
// ============================================================================
function formatReset(date) {
	const diffMs = date.getTime() - Date.now();
	if (diffMs < 0) return "now";
	const diffMins = Math.floor(diffMs / 60000);
	if (diffMins < 60) return `${diffMins}m`;
	const hours = Math.floor(diffMins / 60);
	const mins = diffMins % 60;
	if (hours < 24) return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
	const days = Math.floor(hours / 24);
	if (days < 7) return `${days}d ${hours % 24}h`;
	return new Intl.DateTimeFormat("en-US", {
		month: "short",
		day: "numeric",
	}).format(date);
}
function getStatusEmoji(status) {
	if (!status) return "";
	switch (status.indicator) {
		case "none":
			return "✅";
		case "minor":
			return "⚠️";
		case "major":
			return "🟠";
		case "critical":
			return "🔴";
		case "maintenance":
			return "🔧";
		default:
			return "";
	}
}
// ============================================================================
// UI Component
// ============================================================================
class UsageComponent {
	usages = [];
	loading = true;
	tui;
	theme;
	onClose;
	modelRegistry;
	constructor(tui, theme, onClose, modelRegistry) {
		this.tui = tui;
		this.theme = theme;
		this.onClose = onClose;
		this.modelRegistry = modelRegistry;
		this.load();
	}
	async load() {
		const timeout = (p, ms, fallback) =>
			Promise.race([p, new Promise((r) => setTimeout(() => r(fallback), ms))]);
		// Fetch usage and status in parallel
		const [
			claude,
			gemini,
			codex,
			kiro,
			zai,
			claudeStatus,
			geminiStatus,
			codexStatus,
		] = await Promise.all([
			timeout(fetchClaudeUsage(), 6000, {
				provider: "anthropic",
				displayName: "Claude",
				windows: [],
				error: "Timeout",
			}),
			timeout(fetchGeminiUsage(this.modelRegistry), 6000, {
				provider: "gemini",
				displayName: "Gemini",
				windows: [],
				error: "Timeout",
			}),
			timeout(fetchCodexUsage(this.modelRegistry), 6000, {
				provider: "codex",
				displayName: "Codex",
				windows: [],
				error: "Timeout",
			}),
			timeout(fetchKiroUsage(), 6000, {
				provider: "kiro",
				displayName: "Kiro",
				windows: [],
				error: "Timeout",
			}),
			timeout(fetchZaiUsage(), 6000, {
				provider: "zai",
				displayName: "z.ai",
				windows: [],
				error: "Timeout",
			}),
			timeout(fetchProviderStatus("anthropic"), 3000, {
				indicator: "unknown",
			}),
			timeout(fetchGeminiStatus(), 3000, { indicator: "unknown" }),
			timeout(fetchProviderStatus("codex"), 3000, {
				indicator: "unknown",
			}),
		]);
		// Attach status to usage
		claude.status = claudeStatus;
		gemini.status = geminiStatus;
		codex.status = codexStatus;
		// Filter out providers with no data and no error (not configured)
		const allUsages = [claude, gemini, codex, kiro, zai];
		this.usages = allUsages.filter(
			(u) =>
				u.windows.length > 0 ||
				(u.error !== "No credentials" &&
					u.error !== "kiro-cli not found" &&
					u.error !== "No API key"),
		);
		this.loading = false;
		this.tui.requestRender();
	}
	handleInput(_data) {
		this.onClose();
	}
	invalidate() {}
	render(width) {
		const t = this.theme;
		const dim = (s) => t.fg("muted", s);
		const bold = (s) => t.bold(s);
		const accent = (s) => t.fg("accent", s);
		// Box dimensions: total width includes borders
		const totalW = Math.min(55, width - 4);
		const innerW = totalW - 4; // subtract "│ " and " │"
		const hLine = "─".repeat(totalW - 2); // subtract corners
		const box = (content) => {
			const contentW = visibleWidth(content);
			const pad = Math.max(0, innerW - contentW);
			return dim("│ ") + content + " ".repeat(pad) + dim(" │");
		};
		const lines = [];
		lines.push(dim(`╭${hLine}╮`));
		lines.push(box(bold(accent("AI Usage"))));
		lines.push(dim(`├${hLine}┤`));
		if (this.loading) {
			lines.push(box("Loading..."));
		} else {
			for (const u of this.usages) {
				// Provider header with status emoji and plan
				const statusEmoji = getStatusEmoji(u.status);
				const planStr = u.plan ? dim(` (${u.plan})`) : "";
				const statusStr = statusEmoji ? ` ${statusEmoji}` : "";
				lines.push(box(bold(u.displayName) + planStr + statusStr));
				// Show incident description if any
				if (
					u.status?.indicator &&
					u.status.indicator !== "none" &&
					u.status.indicator !== "unknown" &&
					u.status.description
				) {
					const desc =
						u.status.description.length > 40
							? u.status.description.substring(0, 37) + "..."
							: u.status.description;
					lines.push(box(t.fg("warning", `  ⚡ ${desc}`)));
				}
				if (u.error) {
					lines.push(box(dim(`  ${u.error}`)));
				} else if (u.windows.length === 0) {
					lines.push(box(dim("  No data")));
				} else {
					for (const w of u.windows) {
						const remaining = Math.max(0, 100 - w.usedPercent);
						const barW = 12;
						const filled = Math.min(
							barW,
							Math.round((w.usedPercent / 100) * barW),
						);
						const empty = barW - filled;
						const color =
							remaining <= 10
								? "error"
								: remaining <= 30
									? "warning"
									: "success";
						const bar =
							t.fg(color, "█".repeat(filled)) + dim("░".repeat(empty));
						const reset = w.resetDescription
							? dim(` ⏱ ${w.resetDescription}`)
							: "";
						lines.push(
							box(
								`  ${w.label.padEnd(7)} ${bar} ${remaining.toFixed(0).padStart(3)}%${reset}`,
							),
						);
					}
				}
				lines.push(box(""));
			}
		}
		lines.push(dim(`├${hLine}┤`));
		lines.push(box(dim("Press any key to close")));
		lines.push(dim(`╰${hLine}╯`));
		return lines;
	}
	dispose() {}
}
// ============================================================================
// Hook
// ============================================================================
export default function (pi) {
	pi.registerCommand("usage", {
		description: "Show AI provider usage statistics",
		handler: async (_args, ctx) => {
			if (!ctx.hasUI) {
				ctx.ui.notify("Usage requires interactive mode", "error");
				return;
			}
			const modelRegistry = ctx.modelRegistry;
			await ctx.ui.custom((tui, theme, _kb, done) => {
				return new UsageComponent(tui, theme, () => done(), modelRegistry);
			});
		},
	});
}
