//
// OpenRocky — Voice-first AI Agent
// https://github.com/openrocky
//
// Developed by everettjf with the assistance of Claude Code and Codex.
// Date: 2026-05-02
// Copyright (c) 2026 everettjf. All rights reserved.
//

import Foundation

/// Maps raw error strings (NSError descriptions, server JSON bodies, transport
/// errors) into one-line hints the user can act on. Returns nil when the input
/// doesn't match a known pattern, so the caller can fall back to the raw text.
///
/// Patterns are matched against a lowercased copy so we don't need to enumerate
/// every casing the upstream uses.
nonisolated enum OpenRockyVoiceErrorTriage {
    /// True when the error is something a reconnect-with-backoff can never fix
    /// (bad key, no quota, wrong model). The caller should bail out of the
    /// retry loop and surface a hint via `hint(for:)` instead of burning three
    /// attempts on guaranteed failures.
    nonisolated static func isUnrecoverable(_ raw: String) -> Bool {
        let needle = raw.lowercased()
        // Auth — the key won't get more valid by retrying.
        if needle.contains("incorrect_api_key")
            || needle.contains("invalid_api_key")
            || needle.contains("401")
            || needle.contains("403")
            || needle.contains("unauthorized")
            || needle.contains("forbidden") {
            return true
        }
        // Billing — same story.
        if needle.contains("insufficient_quota") || needle.contains("billing") {
            return true
        }
        // Model gating — picking a model the account can't see won't change in 9s.
        if needle.contains("model_not_found")
            || needle.contains("does not exist")
            || needle.contains("does not have access") {
            return true
        }
        return false
    }

    nonisolated static func hint(for raw: String) -> String? {
        let needle = raw.lowercased()

        // Auth: missing/incorrect API key. Realtime returns "incorrect_api_key"
        // in the error body; URLSession sometimes surfaces 401 directly.
        if needle.contains("incorrect_api_key")
            || needle.contains("invalid_api_key")
            || needle.contains("401")
            || needle.contains("unauthorized") {
            return "API key rejected. Check the key in Settings → Model."
        }

        // Quota / billing. insufficient_quota = paid endpoint with no balance.
        if needle.contains("insufficient_quota") || needle.contains("billing") {
            return "OpenAI account has no remaining quota. Check billing or switch model."
        }

        // Rate limiting. Different from quota — the right action is to wait.
        if needle.contains("rate_limit") || needle.contains("rate limit") || needle.contains("429") {
            return "Rate-limited by OpenAI. Wait a few seconds and try again."
        }

        // Model not found / not accessible to this account.
        if needle.contains("model_not_found")
            || needle.contains("does not exist")
            || needle.contains("does not have access") {
            return "Selected model is unavailable for this account. Pick another in Settings → Model."
        }

        // Transport: NSURLErrorDomain codes for offline / timeout / unreachable.
        if needle.contains("nsurlerrordomain") || needle.contains("the internet connection appears to be offline") {
            return "Network unreachable. Check your connection and try again."
        }
        if needle.contains("timed out") || needle.contains("timeout") {
            return "Voice service timed out. Tap to retry."
        }

        // Realtime "session not connected" usually means the bridge tried to
        // send before sessionUpdated landed; surface as a soft retry.
        if needle.contains("not connected") {
            return "Voice session not connected. Tap to reconnect."
        }

        return nil
    }
}
