/**
 * Error classifier for provider/network/server failures.
 *
 * Consolidates patterns from:
 *  - isTransientNetworkError()  in preferences-models.ts
 *  - classifyProviderError()    in provider-error-pause.ts
 *
 * Single entry point: classifyError(errorMsg, retryAfterMs?)
 *
 * @see https://github.com/singularity-forge/sf-run/issues/2577
 */
export function createRetryState() {
	return {
		networkRetryCount: 0,
		consecutiveTransientCount: 0,
		currentRetryModelId: undefined,
	};
}
export function resetRetryState(state) {
	state.networkRetryCount = 0;
	state.consecutiveTransientCount = 0;
	state.currentRetryModelId = undefined;
}
// ── Classification ──────────────────────────────────────────────────────────
const PERMANENT_RE =
	/auth|unauthorized|forbidden|invalid.*key|invalid.*api|billing|quota exceeded|account/i;
// Include provider-specific quota-window phrasing like "hit your limit", "usage limit", "quota reached"
const RATE_LIMIT_RE =
	/rate.?limit|too many requests|429|hit your limit|usage limit|quota (?:reached|hit|will reset)|limit.*resets?|exhausted (?:your|the) (?:quota|capacity|usage)/i;
const RESET_QUOTA_DELAY_RE = /reset(?:s)?(?:\s+(?:in|after))?\s+(\d+)s/i;
// Unsupported-model: provider rejected the model for the current account/plan (#4513).
// Checked before `permanent` because PERMANENT_RE also matches /account/i.
const UNSUPPORTED_MODEL_MODEL_RE = /\b(?:model|deployment)\b/i;
const UNSUPPORTED_MODEL_INDICATOR_RE =
	/\bnot support(?:ed|s)?\b|\bunsupported\b|\bnot available\b|\bunavailable\b|\bno access\b|\bdoes(?:n['']t| not) (?:have access|support)\b|\bnot authori[sz]ed\b/i;
const UNSUPPORTED_MODEL_SCOPE_RE = /\b(?:account|plan|tier|subscription)\b/i;
// OpenRouter affordability-style quota errors should be treated as transient
// so core retry logic can lower maxTokens and continue in-session.
const AFFORDABILITY_RE =
	/requires more credits|can only afford|insufficient (?:credits|balance)|not enough credits|credit balance|fewer max_tokens/i;
const NETWORK_RE =
	/network|ECONNRESET|ETIMEDOUT|ECONNREFUSED|socket hang up|fetch failed|connection.*reset|dns/i;
const SERVER_RE =
	/internal server error|500|502|503|overloaded|server_error|api_error|service.?unavailable|no capacity (?:available|left)|capacity (?:unavailable|exhausted)|no capacity .* on the server/i;
// ECONNRESET/ECONNREFUSED are in NETWORK_RE (same-model retry first).
const CONNECTION_RE =
	/terminated|connection.?(?:refused|error)|other side closed|EPIPE|network.?(?:is\s+)?unavailable|stream_exhausted(?:_without_result)?/i;
// Catch-all for V8 JSON.parse errors: all modern variants end with "in JSON at position \d+".
// This eliminates the need to enumerate every error message variant individually.
const STREAM_RE =
	/in JSON at position \d+|Unexpected end of JSON|SyntaxError.*JSON/i;
const RESET_DELAY_RE = /reset in (\d+)s/i;
/**
 * Classify an error message into one of the ErrorClass kinds.
 *
 * Classification order:
 *  1. Permanent (auth/billing/quota) — unless also rate-limited
 *  2. Rate limit (429, rate.?limit, too many requests)
 *  3. Network (ECONNRESET, ETIMEDOUT, socket hang up, fetch failed, dns)
 *  4. Stream truncation (malformed JSON from mid-stream cut)
 *  5. Server (500/502/503, overloaded, server_error)
 *  6. Connection (terminated, ECONNREFUSED, EPIPE, other side closed)
 *  7. Unknown
 */
export function classifyError(errorMsg, retryAfterMs) {
	const isPermanent = PERMANENT_RE.test(errorMsg);
	const isRateLimit =
		RATE_LIMIT_RE.test(errorMsg) || AFFORDABILITY_RE.test(errorMsg);
	const isUnsupportedModel =
		UNSUPPORTED_MODEL_MODEL_RE.test(errorMsg) &&
		UNSUPPORTED_MODEL_INDICATOR_RE.test(errorMsg) &&
		UNSUPPORTED_MODEL_SCOPE_RE.test(errorMsg);
	// 0. Unsupported model (account/plan entitlement rejection) — checked before
	//    `permanent` because PERMANENT_RE also matches /account/i and would
	//    otherwise swallow these errors, blocking the blocklist-driven fallback.
	//    Rate limit still wins when both patterns appear (a throttled account is
	//    not an entitlement failure).
	if (isUnsupportedModel && !isRateLimit) {
		return { kind: "unsupported-model" };
	}
	// 1. Permanent — but rate limit takes precedence
	if (isPermanent && !isRateLimit) {
		return { kind: "permanent" };
	}
	// 2. Rate limit
	if (isRateLimit) {
		if (retryAfterMs != null && retryAfterMs > 0) {
			return { kind: "rate-limit", retryAfterMs };
		}
		// Try the existing "reset in Ns" first, then the broader
		// "reset(s)? (in|after) Ns" form that catches "Your quota will reset
		// after 51s" — common across providers (Anthropic capacity exhaustion,
		// OpenAI usage caps, etc.).
		const resetMatch =
			errorMsg.match(RESET_DELAY_RE) ?? errorMsg.match(RESET_QUOTA_DELAY_RE);
		const delayMs = resetMatch ? Number(resetMatch[1]) * 1000 : 60_000;
		return { kind: "rate-limit", retryAfterMs: delayMs };
	}
	// 3. Network errors — same-model retry candidate
	if (NETWORK_RE.test(errorMsg)) {
		// Exclude if also matches permanent signals (already handled above for
		// rate-limit, but double-check for non-rate-limit permanent overlap like
		// "billing" appearing alongside "network").
		return { kind: "network", retryAfterMs: retryAfterMs ?? 3_000 };
	}
	// 4. Stream truncation — downstream symptom of connection drop
	if (STREAM_RE.test(errorMsg)) {
		return { kind: "stream", retryAfterMs: retryAfterMs ?? 15_000 };
	}
	// 5. Server errors — try fallback model
	if (SERVER_RE.test(errorMsg)) {
		return { kind: "server", retryAfterMs: retryAfterMs ?? 30_000 };
	}
	// 6. Connection errors — try fallback model
	if (CONNECTION_RE.test(errorMsg)) {
		return { kind: "connection", retryAfterMs: retryAfterMs ?? 15_000 };
	}
	// 7. Unknown
	return { kind: "unknown" };
}
// ── Helpers ─────────────────────────────────────────────────────────────────
/** Returns true for all transient (auto-resumable) error kinds. */
export function isTransient(cls) {
	switch (cls.kind) {
		case "network":
		case "rate-limit":
		case "server":
		case "stream":
		case "connection":
			return true;
		default:
			return false;
	}
}
/**
 * Backward-compatible thin wrapper.
 *
 * Returns true when the error is a transient *network* error specifically
 * (worth retrying the same model). Permanent signals (auth, billing, quota)
 * cause this to return false even if a network keyword is present.
 */
export function isTransientNetworkError(errorMsg) {
	if (!errorMsg) return false;
	const cls = classifyError(errorMsg);
	return cls.kind === "network";
}
