import get from 'lodash-es/get.js'
import isnum from 'wsemi/src/isnum.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import isbol from 'wsemi/src/isbol.mjs'
import cdbl from 'wsemi/src/cdbl.mjs'
import toQuotaLabel from './toQuotaLabel.mjs'
// toQuotaWindow.mjs — 將各家額度窗口正規化為統一結構
//
// 【為何要正規化】三家回傳形狀互異:Claude給utilization(已用百分比)與ISO字串resets_at,
// Codex給usedPercent與unix秒數resetsAt外加windowDurationMins,
// Antigravity給remaining_fraction(剩餘比例)與ISO字串reset_time;
// 呼叫端若直接吃原始形狀,每加一家就要改一次判斷。統一於此後,
// 呼叫端只認一組欄位,各家差異收斂在各自的轉接器內。
//
// 【為何同時給resetAt與resetAfterSeconds】前者適合顯示絕對時間,後者適合倒數與排程判斷;
// 兩者能互推,故任一有值即補算另一,令呼叫端不必自行換算。
//
// 【label由誰決定】未給label時由windowSeconds經toQuotaLabel推導;轉接器需要帶範圍之標籤
// (如「7天(Fable)」)時, 亦應以toQuotaLabel取基底再拼接, 不得手寫「7天」字串。
//
// 【key是供應商原生識別, 刻意不統一; 跨家比較用windowSeconds+scope】統一的是信封(上列欄位),
// key則原樣透傳各家自己的窗口識別——Claude為limits[].kind(session/weekly_all/weekly_scoped;
// 回退舊欄位時為five_hour/seven_day_opus等欄位名)、Codex為rateLimits之primary/secondary物件名、
// agy為buckets[].id(gemini-5h/gemini-weekly/3p-5h/3p-weekly); 僅巢狀限額需組唯一鍵時
// 由轉接器以「<識別>:primary|secondary」複合(如code_review:primary)。保留原生值可回溯raw,
// 也不必為求一致把agy之4桶2群組硬壓成2個。故呼叫端要「跨家找5小時窗口」請以
// windowSeconds===18000(7天為604800)配scope判斷, 不得比對key字串。
/**
* 將各家額度窗口正規化為統一結構
*
* resetAt可給ISO字串或unix秒數(整數),一律轉為ISO字串;
* resetAt與resetAfterSeconds任一有值即自動補算另一;
* usedPercent夾至0~100,remainingPercent由其推得,未知時兩者皆為null(不假造100);
* label未給時由windowSeconds推導,故供應商調整窗口長度時標籤自動跟著正確
*
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {String} [opt.key=''] 輸入窗口之機器可讀鍵字串,為供應商原生識別原樣透傳(Claude之limits[].kind如'session'、Codex之'primary'/'secondary'、agy之buckets[].id如'gemini-5h'),各家不同且刻意不統一;跨家比較請用windowSeconds配scope而非key,預設''
* @param {String} [opt.label=''] 輸入窗口之人類可讀標籤字串,預設''代表由windowSeconds推導
* @param {Number} [opt.windowSeconds=null] 輸入窗口長度秒數,預設null代表供應商未提供
* @param {Number} [opt.usedPercent=null] 輸入已用百分比數值(0~100),預設null代表未知
* @param {String|Number} [opt.resetAt=''] 輸入窗口重置時刻,可為ISO字串或unix秒數整數,預設''
* @param {Number} [opt.resetAfterSeconds=null] 輸入距重置之剩餘秒數,預設null代表由resetAt推算
* @param {String} [opt.scope=''] 輸入窗口適用範圍字串,例如模型名稱或群組名稱,預設''代表全域
* @param {Boolean} [opt.active=false] 輸入是否為當前生效(最先觸頂)窗口布林值,預設false
* @param {String} [opt.severity=''] 輸入嚴重度字串,例如'normal'、'warning'、'exhausted',預設''代表由usedPercent推導(用罄為'exhausted'、其餘'normal'、usedPercent未知則'')
* @returns {Object} 回傳窗口物件,內含key(供應商原生識別,各家不同)、label、windowSeconds(跨家比較之正規化維度:18000=5小時、604800=7天)、usedPercent、remainingPercent、resetAt(ISO字串)、resetAfterSeconds、scope、active、severity
* @example
*
* import toQuotaWindow from './src/quota/toQuotaWindow.mjs'
*
* let w = toQuotaWindow({ key: 'five_hour', windowSeconds: 18000, usedPercent: 11 })
* console.log(w.label, w.remainingPercent)
* // => 5小時 89
*
*/
function toQuotaWindow(opt = {}) {
//key
let key = get(opt, 'key', '')
if (!isestr(key)) {
key = ''
}
//windowSeconds, 無效視為供應商未提供
let windowSeconds = get(opt, 'windowSeconds', null)
if (!isnum(windowSeconds)) {
windowSeconds = null
}
else {
windowSeconds = cdbl(windowSeconds)
}
//label, 未給時由windowSeconds推導
let label = get(opt, 'label', '')
if (!isestr(label)) {
label = toQuotaLabel(windowSeconds)
}
//usedPercent, 無效視為未知; 有效則夾至0~100避免供應商回傳越界值污染剩餘量
let usedPercent = get(opt, 'usedPercent', null)
if (!isnum(usedPercent)) {
usedPercent = null
}
else {
usedPercent = Math.min(100, Math.max(0, cdbl(usedPercent)))
}
//remainingPercent, 由usedPercent推得, 未知時同為未知
let remainingPercent = null
if (usedPercent !== null) {
remainingPercent = Math.round((100 - usedPercent) * 100) / 100
}
//resetAt, 接受ISO字串或unix秒數, 一律轉ISO字串
let resetAt = get(opt, 'resetAt', '')
if (isnum(resetAt) && cdbl(resetAt) > 0) {
resetAt = new Date(cdbl(resetAt) * 1000).toISOString()
}
else if (isestr(resetAt)) {
let d = new Date(resetAt)
resetAt = isNaN(d.getTime()) ? '' : d.toISOString()
}
else {
resetAt = ''
}
//resetAfterSeconds, 無效時由resetAt推算, 令呼叫端不論供應商給哪一種都拿得到倒數
let resetAfterSeconds = get(opt, 'resetAfterSeconds', null)
if (!isnum(resetAfterSeconds)) {
resetAfterSeconds = null
if (resetAt !== '') {
let dt = Math.round((new Date(resetAt).getTime() - Date.now()) / 1000)
resetAfterSeconds = Math.max(0, dt)
}
}
else {
resetAfterSeconds = Math.max(0, Math.round(cdbl(resetAfterSeconds)))
}
//resetAt, 供應商只給倒數秒數時反推絕對時刻
if (resetAt === '' && resetAfterSeconds !== null) {
resetAt = new Date(Date.now() + resetAfterSeconds * 1000).toISOString()
}
//scope, 例如僅適用某模型或某群組之窗口
let scope = get(opt, 'scope', '')
if (!isestr(scope)) {
scope = ''
}
//active
let active = get(opt, 'active', null)
if (!isbol(active)) {
active = false
}
//severity, 供應商有給(如Claude之limits[].severity)即用之; 未給則由usedPercent推導——
//用罄為'exhausted'、其餘'normal'、usedPercent未知則''。三家對稱: 曾因codex路徑未給而為空字串,
//與claude/agy之'normal'不對稱, 呼叫端得分家判斷; 收斂於此後任一家未給皆有一致預設
let severity = get(opt, 'severity', '')
if (!isestr(severity)) {
severity = ''
if (usedPercent !== null) {
severity = usedPercent >= 100 ? 'exhausted' : 'normal'
}
}
return {
key,
label,
windowSeconds,
usedPercent,
remainingPercent,
resetAt,
resetAfterSeconds,
scope,
active,
severity,
}
}
export default toQuotaWindow