#!/bin/bash
# api.sh - HTTP/API abstraction layer for AI Consultants v2.0
#
# Provides functions for making HTTP API calls to external AI services
# with retry logic, error handling, and response parsing.
#
# Supported APIs: Qwen3 (DashScope), GLM (Zhipu), Grok (xAI)

# Load configuration (if not already loaded)
SCRIPT_DIR_API="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -z "${AI_CONSULTANTS_VERSION:-}" ]]; then
    source "$SCRIPT_DIR_API/../config.sh"
fi

# =============================================================================
# SECURITY: ERROR MESSAGE SANITIZATION
# =============================================================================

# Sanitize error messages to prevent leaking sensitive data
# Removes API keys, tokens, and other sensitive patterns
# Usage: sanitize_error_message <message>
sanitize_error_message() {
    local message="$1"

    # Pattern list for sensitive data that should be redacted
    # - API keys (various formats)
    # - Bearer tokens
    # - Passwords in URLs
    # - Authorization headers

    echo "$message" | sed -E \
        -e 's/(api[_-]?key[[:space:]]*[:=][[:space:]]*)[^[:space:]"'\'']+/\1[REDACTED]/gi' \
        -e 's/(bearer[[:space:]]+)[^[:space:]"'\'']+/\1[REDACTED]/gi' \
        -e 's/(authorization[[:space:]]*:[[:space:]]*)[^[:space:]]+/\1[REDACTED]/gi' \
        -e 's/(password[[:space:]]*[:=][[:space:]]*)[^[:space:]"'\'']+/\1[REDACTED]/gi' \
        -e 's/(token[[:space:]]*[:=][[:space:]]*)[^[:space:]"'\'']+/\1[REDACTED]/gi' \
        -e 's/(sk-[a-zA-Z0-9]{20,})/[REDACTED_KEY]/g' \
        -e 's/([a-zA-Z0-9_-]{32,})/[POSSIBLE_KEY]/g'
}

# =============================================================================
# HTTP ERROR HANDLING
# =============================================================================

# Classify HTTP error codes
# Usage: classify_http_error <http_code>
# Returns: auth|rate_limit|server|client|success|unknown
classify_http_error() {
    local http_code="$1"

    case "$http_code" in
        200|201)
            echo "success"
            ;;
        400)
            echo "client"
            ;;
        401|403)
            echo "auth"
            ;;
        404)
            echo "not_found"
            ;;
        429)
            echo "rate_limit"
            ;;
        500|502|503|504)
            echo "server"
            ;;
        *)
            echo "unknown"
            ;;
    esac
}

# One-line, secret-sanitized provider error ("code: message") from a JSON
# error body, so auth/permission failures say WHY (e.g. an invalid key vs an
# account without access to the model) without a manual re-request.
# Usage: provider_error_summary <response_body_file>
provider_error_summary() {
    local file="$1" summary
    [[ -s "$file" ]] || return 0
    # "error" is an object for most providers and a plain string for some
    # (xAI: {"code": "...", "error": "Incorrect API key provided: ..."}).
    summary=$(jq -r '
        (.error // .) as $e
        | if ($e | type) == "object" then
            [($e.code // $e.type // empty | tostring), ($e.message // empty | tostring)]
          elif ($e | type) == "string" then
            [(.code // empty | tostring), $e]
          else [] end
        | map(select(length > 0)) | join(": ")' "$file" 2>/dev/null | head -1 | cut -c1-200)
    [[ -n "$summary" ]] || return 0
    sanitize_error_message "$summary"
}

# Calculate exponential backoff delay
# Usage: calculate_backoff <attempt_number>
calculate_backoff() {
    local attempt="$1"
    local base_delay="${API_BASE_BACKOFF:-2}"
    local max_delay="${API_MAX_BACKOFF:-60}"

    # Exponential backoff: base * 2^(attempt-1)
    local delay=$((base_delay * (1 << (attempt - 1))))

    # Cap at max delay
    if [[ $delay -gt $max_delay ]]; then
        delay=$max_delay
    fi

    echo "$delay"
}

# Extract Retry-After header value from response headers
# Usage: extract_retry_after <headers_file>
extract_retry_after() {
    local headers_file="$1"

    if [[ -f "$headers_file" ]]; then
        local retry_after
        retry_after=$(grep -i "^retry-after:" "$headers_file" 2>/dev/null | head -1 | cut -d: -f2 | tr -d ' \r\n')
        if [[ -n "$retry_after" && "$retry_after" =~ ^[0-9]+$ ]]; then
            echo "$retry_after"
            return 0
        fi
    fi
    echo ""
    return 1
}

# =============================================================================
# REQUEST BUILDERS
# =============================================================================
# The prompt (question plus inlined context) reaches jq through stdin
# (--rawfile /dev/stdin), never argv: Linux caps one argument at 128 KiB, the
# whole argv is capped by ARG_MAX, and argv is visible in the process list. An
# oversized --arg made jq fail and left an empty request body.

# Build request body for Qwen3 (Alibaba DashScope format)
# Usage: build_qwen_request <prompt> <model> [max_tokens]
build_qwen_request() {
    local prompt="$1"
    local model="${2:-qwen3.8-max}"
    local max_tokens="${3:-16384}"

    printf '%s' "$prompt" | jq -n \
        --arg model "$model" \
        --rawfile prompt /dev/stdin \
        --argjson max_tokens "$max_tokens" \
        '{
            model: $model,
            input: {
                messages: [
                    { role: "user", content: $prompt }
                ]
            },
            parameters: {
                result_format: "message",
                max_tokens: $max_tokens
            }
        }'
}

# Validate a reasoning-effort value, echoing it lowercased.
# Usage: validate_reasoning_effort <value> <consultant_name>
# Returns 0 and echoes the normalized value, or returns 1 and echoes nothing.
#
# Deliberately fails instead of falling back to a default: silently ignoring a
# requested effort level would make the knob a no-op the user cannot detect.
# (get_api_format does fall back, because there a correct per-agent default
# exists; here the only "default" would be discarding the user's request.)
#
# The accepted set is the provider's own enum, read off a live 400 from the
# Qwen Cloud Token Plan endpoint (2026-07-21):
#   'reasoning_effort' must be one of: 'none', 'minimal', 'low', 'medium',
#   'high', 'xhigh', 'max'
# Note this is WIDER than the low|high|xhigh that the public write-ups for
# qwen3.8-max all reported — 'minimal', 'medium' and 'max' are accepted
# by the model too. Taken from the API rather than from documentation for
# exactly that reason.
#
# The per-MODEL subset is deliberately NOT enforced here: encoding per-model
# facts in shell is what produced the stale-model bugs this release also fixes,
# and the same builder serves GLM/Grok/DeepSeek/MiniMax. An unsupported
# combination is rejected by the provider with a 400 that run_api_query
# surfaces without retrying — e.g. 'none' on qwen3.8-max returns "The
# value of the enable_thinking parameter is restricted to True", which is a
# model fact, not a syntax error.
validate_reasoning_effort() {
    local value="$1"
    local consultant="${2:-consultant}"

    value=$(echo "$value" | tr '[:upper:]' '[:lower:]')

    case "$value" in
        none|minimal|low|medium|high|xhigh|max)
            echo "$value"
            return 0
            ;;
        *)
            log_error "[$consultant] Invalid reasoning effort '$1' (expected: none|minimal|low|medium|high|xhigh|max)"
            return 1
            ;;
    esac
}

# Build request body for OpenAI-compatible APIs (GLM, Grok, Codex API mode, Mistral API mode)
# Usage: build_openai_request <prompt> <model> [max_tokens] [reasoning_effort]
#
# reasoning_effort is optional and additive: when empty the object is
# byte-identical to what this function produced before the parameter existed,
# which matters because six consultants share this builder.
build_openai_request() {
    local prompt="$1"
    local model="${2:-gpt-6.1-sol}"
    local max_tokens="${3:-4096}"
    local reasoning_effort="${4:-}"

    printf '%s' "$prompt" | jq -n \
        --arg model "$model" \
        --rawfile prompt /dev/stdin \
        --argjson max_tokens "$max_tokens" \
        --arg effort "$reasoning_effort" \
        '{
            model: $model,
            messages: [
                { role: "user", content: $prompt }
            ],
            max_tokens: $max_tokens
        }
        + (if $effort == "" then {} else { reasoning_effort: $effort } end)'
}

# Build request body for Anthropic API (Claude API mode)
# Usage: build_anthropic_request <prompt> <model> [max_tokens] [effort]
# The effort goes in output_config.effort; without it the model's own
# default applies (medium on Opus 5.5 and Haiku 5.5, high elsewhere).
build_anthropic_request() {
    local prompt="$1"
    local model="${2:-claude-fable-5-1}"
    local max_tokens="${3:-16384}"
    local effort="${4:-}"

    printf '%s' "$prompt" | jq -n \
        --arg model "$model" \
        --rawfile prompt /dev/stdin \
        --argjson max_tokens "$max_tokens" \
        --arg effort "$effort" \
        '{
            model: $model,
            max_tokens: $max_tokens,
            messages: [
                { role: "user", content: $prompt }
            ]
        }
        + (if $effort == "" then {} else { output_config: { effort: $effort } } end)'
}

# Build request body for Google AI API (Gemini API mode)
# Usage: build_google_ai_request <prompt> [thinking_level] [max_output_tokens]
# Note: Model is appended to URL, not in request body for Google AI
build_google_ai_request() {
    local prompt="$1"
    local thinking_level="${2:-}"
    local max_output_tokens="${3:-4096}"

    printf '%s' "$prompt" | jq -n \
        --rawfile prompt /dev/stdin \
        --arg thinking_level "$thinking_level" \
        --argjson max_output_tokens "$max_output_tokens" \
        '{
            contents: [
                { parts: [{ text: $prompt }] }
            ],
            generationConfig: {
                maxOutputTokens: $max_output_tokens
            }
        }
        | if $thinking_level == "" then .
          else .generationConfig.thinkingConfig = {thinkingLevel: $thinking_level}
          end'
}

# =============================================================================
# RESPONSE PARSERS
# =============================================================================

# Parse Qwen3 response and extract content
# Usage: parse_qwen_response <response_json>
parse_qwen_response() {
    local response="$1"

    # Try different response paths (Qwen format varies)
    local content
    content=$(echo "$response" | jq -r '.output.choices[0].message.content // .output.text // empty' 2>/dev/null)

    if [[ -n "$content" && "$content" != "null" ]]; then
        echo "$content"
        return 0
    fi

    # Fallback: return raw output if structured parsing fails
    echo "$response" | jq -r '.output // empty' 2>/dev/null
}

# Parse OpenAI-compatible response (GLM, Grok, Codex API mode, Mistral API mode)
# Usage: parse_openai_response <response_json>
parse_openai_response() {
    local response="$1"

    local content
    # Reasoning models (Mistral Large 4, Magistral) return content as chunks:
    # a "thinking" chunk followed by "text" chunks. Keep only the visible text.
    content=$(echo "$response" | jq -r '
        .choices[0].message.content
        | if type == "array" then
            [.[]? | select(type == "object" and .type == "text") | (.text // "")] | join("")
          elif type == "string" then .
          else empty end' 2>/dev/null)

    if [[ -n "$content" && "$content" != "null" ]]; then
        echo "$content"
        return 0
    fi

    echo ""
    return 1
}

# Parse Anthropic API response (Claude API mode)
# Usage: parse_anthropic_response <response_json>
parse_anthropic_response() {
    local response="$1"

    local content
    # Opus 5 may put a thinking block before the visible text block. Select
    # every text block instead of assuming content[0] is user-visible output.
    content=$(echo "$response" | jq -r \
        '[.content[]? | select(.type == "text") | (.text // "")] | join("\n")' \
        2>/dev/null)

    if [[ -n "$content" && "$content" != "null" ]]; then
        echo "$content"
        return 0
    fi

    echo ""
    return 1
}

# Parse Google AI API response (Gemini API mode)
# Usage: parse_google_ai_response <response_json>
parse_google_ai_response() {
    local response="$1"

    # Thinking models (Gemini 4 Argon) can split the answer across several
    # parts and, when thoughts are requested, prefix thought parts. Join the
    # visible text parts only.
    local content
    content=$(echo "$response" | jq -r '
        [.candidates[0].content.parts[]?
         | select(type == "object" and (.thought // false) != true)
         | (.text // "")] | join("")' 2>/dev/null)

    if [[ -n "$content" && "$content" != "null" ]]; then
        echo "$content"
        return 0
    fi

    echo ""
    return 1
}

# Extract token usage from API response
# Usage: extract_token_usage <response_json> <format>
# format: "qwen", "openai", "anthropic", or "google_ai"
extract_token_usage() {
    local response="$1"
    local format="${2:-openai}"

    local input_tokens=0
    local output_tokens=0

    case "$format" in
        qwen)
            input_tokens=$(echo "$response" | jq -r '.usage.input_tokens // 0' 2>/dev/null)
            output_tokens=$(echo "$response" | jq -r '.usage.output_tokens // 0' 2>/dev/null)
            ;;
        anthropic)
            input_tokens=$(echo "$response" | jq -r '.usage.input_tokens // 0' 2>/dev/null)
            output_tokens=$(echo "$response" | jq -r '.usage.output_tokens // 0' 2>/dev/null)
            ;;
        google_ai)
            input_tokens=$(echo "$response" | jq -r '.usageMetadata.promptTokenCount // 0' 2>/dev/null)
            # Google bills candidates plus thoughts as output. Cached input is
            # already included in promptTokenCount and must not be added again.
            output_tokens=$(echo "$response" | jq -r '
                def token: if type == "number" and . >= 0 and floor == . then . else 0 end;
                .usageMetadata | ((.candidatesTokenCount | token) + (.thoughtsTokenCount | token))' 2>/dev/null)
            ;;
        *)  # openai format (default)
            input_tokens=$(echo "$response" | jq -r '.usage.prompt_tokens // 0' 2>/dev/null)
            output_tokens=$(echo "$response" | jq -r '.usage.completion_tokens // 0' 2>/dev/null)
            ;;
    esac

    [[ "$input_tokens"  =~ ^[0-9]+$ ]] || input_tokens=0
    [[ "$output_tokens" =~ ^[0-9]+$ ]] || output_tokens=0

    local total=$((input_tokens + output_tokens))
    echo "$total"
}

# Same extraction, but preserving the provider's prompt/completion split.
# Usage: extract_token_split <response_json> <format>  ->  "<input> <output>"
#
# The split is what cost actually depends on: output rates run 3-6x input rates
# and consultations are large-context/short-reply, so collapsing to a total and
# re-splitting it 60/40 overstates API-mode cost several-fold - while still
# being labeled "measured".
extract_token_split() {
    local response="$1"
    local format="${2:-openai}"

    local input_tokens=0
    local output_tokens=0

    case "$format" in
        qwen|anthropic)
            input_tokens=$(echo "$response" | jq -r '.usage.input_tokens // 0' 2>/dev/null)
            output_tokens=$(echo "$response" | jq -r '.usage.output_tokens // 0' 2>/dev/null)
            ;;
        google_ai)
            input_tokens=$(echo "$response" | jq -r '.usageMetadata.promptTokenCount // 0' 2>/dev/null)
            # Google bills candidates plus thoughts as output. Cached input is
            # already included in promptTokenCount and must not be added again.
            output_tokens=$(echo "$response" | jq -r '
                def token: if type == "number" and . >= 0 and floor == . then . else 0 end;
                .usageMetadata | ((.candidatesTokenCount | token) + (.thoughtsTokenCount | token))' 2>/dev/null)
            ;;
        *)  # openai format (default)
            input_tokens=$(echo "$response" | jq -r '.usage.prompt_tokens // 0' 2>/dev/null)
            output_tokens=$(echo "$response" | jq -r '.usage.completion_tokens // 0' 2>/dev/null)
            ;;
    esac

    [[ "$input_tokens"  =~ ^[0-9]+$ ]] || input_tokens=0
    [[ "$output_tokens" =~ ^[0-9]+$ ]] || output_tokens=0

    echo "$input_tokens $output_tokens"
}

# =============================================================================
# MAIN API QUERY FUNCTION
# =============================================================================

# Request headers for an auth style, one per line (read by curl -H @file).
# Usage: _api_request_headers <auth_style> <api_key>
_api_request_headers() {
    case "$1" in
        none)
            # No auth header needed
            ;;
        google_ai)
            # Google AI uses x-goog-api-key header (more secure than URL param)
            printf '%s\n' "x-goog-api-key: $2"
            ;;
        anthropic)
            printf '%s\n' "x-api-key: $2" "anthropic-version: 2023-06-01"
            ;;
        apikey)
            printf '%s\n' "X-API-Key: $2"
            ;;
        *)  # Default to bearer token
            printf '%s\n' "Authorization: Bearer $2"
            ;;
    esac
    printf '%s\n' "Content-Type: application/json"
}

# Execute an HTTP API query with retry and timeout
#
# Usage: run_api_query <consultant_name> <output_file> <timeout_sec> \
#                      <api_endpoint> <api_key_var> <request_body> [auth_style]
#
# Parameters:
#   consultant_name - Display name for logging (e.g., "Qwen3")
#   output_file     - Path to write response body
#   timeout_sec     - Request timeout in seconds
#   api_endpoint    - Full API URL
#   api_key_var     - Name of env var containing API key (not the value!)
#                     For auth_style="none", pass empty string ""
#   request_body    - JSON request body
#   auth_style      - Optional: "bearer" (default), "apikey", "anthropic", or "none"
#
# Returns:
#   0 on success
#   1 on auth failure (no retry)
#   2 on rate limit (after max retries)
#   3 on server error (after max retries)
#   124 on timeout
run_api_query() {
    local consultant_name="$1"
    local output_file="$2"
    local timeout_seconds="$3"
    local api_endpoint="$4"
    local api_key_var="$5"
    local request_body="$6"
    local auth_style="${7:-bearer}"

    # Check rate limiting before proceeding
    check_rate_limit "$consultant_name"

    # Resolve API key from env var name (skip for "none" auth style)
    local api_key=""
    if [[ "$auth_style" != "none" ]]; then
        api_key="${!api_key_var:-}"
        if [[ -z "$api_key" ]]; then
            log_error "[$consultant_name] API key not available: $api_key_var"
            return 1
        fi
    fi

    # Temporary files for the response only. The request headers (API key)
    # and body (prompt and context) reach curl through pipes (process
    # substitution below): never argv, which other local users can read in the
    # process list and Linux caps at 128 KiB per argument, and never a file on
    # disk that a query stopped by the host (or killed) would leave behind.
    local temp_response temp_headers
    temp_response=$(mktemp)
    temp_headers=$(mktemp)
    local error_file="${output_file}.err"

    # Note: We rely on explicit cleanup before each return rather than a trap,
    # because traps in functions affect the entire shell and can interfere
    # with other code paths. Cleanup happens in each exit branch below.

    log_info "[$consultant_name] Querying API (timeout: $(format_timeout_label "$timeout_seconds"), max retry: $MAX_RETRIES)..."

    # curl has no wall-clock limit unless one is set; 0 means none.
    local -a max_time_args=()
    [[ "${timeout_seconds:-0}" == "0" ]] || max_time_args=(-m "$timeout_seconds")

    local attempt=1
    local last_failure="no attempt completed"
    local success=false

    while (( attempt <= MAX_RETRIES )); do
        log_debug "[$consultant_name] Attempt $attempt of $MAX_RETRIES..."

        # Execute curl request
        local http_code
        http_code=$(curl -s -w "%{http_code}" \
            -o "$temp_response" \
            -D "$temp_headers" \
            -X POST "$api_endpoint" \
            -H @<(_api_request_headers "$auth_style" "$api_key") \
            "${max_time_args[@]+"${max_time_args[@]}"}" \
            --data-binary @<(printf '%s' "$request_body") 2>"$error_file")

        local curl_exit=$?

        # Handle curl-level errors
        if [[ $curl_exit -eq 28 ]]; then
            last_failure="timed out after ${timeout_seconds}s"
            log_warn "[$consultant_name] Timeout after ${timeout_seconds}s"
            ((attempt++)) || true
            if (( attempt <= MAX_RETRIES )); then
                local backoff=$(calculate_backoff "$attempt")
                log_info "[$consultant_name] Waiting ${backoff}s before retry..."
                sleep "$backoff"
            fi
            continue
        elif [[ $curl_exit -ne 0 ]]; then
            last_failure="network error: $(describe_curl_exit "$curl_exit") (curl exit $curl_exit)"
            log_warn "[$consultant_name] Network error: $(describe_curl_exit "$curl_exit") (curl exit $curl_exit)"
            local error_msg=""
            [[ -f "$error_file" ]] && error_msg=$(head -3 "$error_file" 2>/dev/null)
            # Sanitize error message to avoid leaking sensitive data
            [[ -n "$error_msg" ]] && log_debug "[$consultant_name] Error: $(sanitize_error_message "$error_msg")"
            ((attempt++)) || true
            if (( attempt <= MAX_RETRIES )); then
                sleep "$RETRY_DELAY_SECONDS"
            fi
            continue
        fi

        # Classify HTTP response
        local error_type=$(classify_http_error "$http_code")

        case "$error_type" in
            success)
                # Verify response is not empty
                if [[ -s "$temp_response" ]]; then
                    log_success "[$consultant_name] Response received (HTTP $http_code, $(wc -c < "$temp_response" | tr -d ' ') bytes)"
                    cp "$temp_response" "$output_file"
                    success=true
                    break
                else
                    last_failure="empty response body (HTTP $http_code)"
                    log_warn "[$consultant_name] Empty response body"
                    ((attempt++)) || true
                    if (( attempt <= MAX_RETRIES )); then
                        sleep "$RETRY_DELAY_SECONDS"
                    fi
                fi
                ;;
            auth)
                local auth_reason
                auth_reason=$(provider_error_summary "$temp_response")
                log_error "[$consultant_name] Authentication failed (HTTP $http_code)${auth_reason:+: $auth_reason}"
                log_error "[$consultant_name] Check that $api_key_var is correct and that the account has access to this model"
                rm -f "$temp_response" "$temp_headers" "$error_file"
                return 1  # No retry for auth failures
                ;;
            rate_limit)
                # Try to get retry-after header
                local retry_after=$(extract_retry_after "$temp_headers")
                local backoff
                last_failure="rate limited (HTTP $http_code)"
                if [[ -n "$retry_after" ]]; then
                    backoff="$retry_after"
                    log_warn "[$consultant_name] Rate limited, server requests ${retry_after}s wait"
                else
                    backoff=$(calculate_backoff "$attempt")
                    log_warn "[$consultant_name] Rate limited (HTTP 429), backing off ${backoff}s"
                fi
                ((attempt++)) || true
                if (( attempt <= MAX_RETRIES )); then
                    sleep "$backoff"
                fi
                ;;
            server)
                # A request that ran past the provider's own time limit runs
                # past it again (Meta Model API: code gateway_timeout).
                if [[ "$http_code" == 504 ]] \
                    && jq -e '.error.code? == "gateway_timeout"' "$temp_response" >/dev/null 2>&1; then
                    log_error "[$consultant_name] Provider time limit exceeded (HTTP 504 gateway_timeout); not retrying. Lower the reasoning effort or the context size."
                    rm -f "$temp_response" "$temp_headers" "$error_file"
                    return 3
                fi
                last_failure="server error (HTTP $http_code)"
                log_warn "[$consultant_name] Server error (HTTP $http_code)"
                ((attempt++)) || true
                if (( attempt <= MAX_RETRIES )); then
                    local backoff=$(calculate_backoff "$attempt")
                    log_info "[$consultant_name] Waiting ${backoff}s before retry..."
                    sleep "$backoff"
                fi
                ;;
            client)
                local client_reason
                client_reason=$(provider_error_summary "$temp_response")
                log_error "[$consultant_name] Client error (HTTP $http_code)${client_reason:+: $client_reason}"
                rm -f "$temp_response" "$temp_headers" "$error_file"
                return 1  # No retry for client errors
                ;;
            *)
                last_failure="unexpected response (HTTP $http_code)"
                log_warn "[$consultant_name] Unexpected response (HTTP $http_code)"
                ((attempt++)) || true
                if (( attempt <= MAX_RETRIES )); then
                    sleep "$RETRY_DELAY_SECONDS"
                fi
                ;;
        esac
    done

    # Cleanup
    rm -f "$temp_response" "$temp_headers" "$error_file"

    if [[ "$success" == "true" ]]; then
        return 0
    else
        log_error "[$consultant_name] All $MAX_RETRIES attempts failed: $last_failure"
        return 3
    fi
}

# =============================================================================
# API CONFIGURATION DEFAULTS
# =============================================================================

# Base backoff delay in seconds
API_BASE_BACKOFF="${API_BASE_BACKOFF:-2}"

# Maximum backoff delay in seconds
API_MAX_BACKOFF="${API_MAX_BACKOFF:-60}"

# =============================================================================
# RATE LIMITING
# =============================================================================

# Rate limiting configuration
# Requests per minute limit per consultant
API_RATE_LIMIT="${API_RATE_LIMIT:-30}"

# Rate limit state file (per consultant)
RATE_LIMIT_DIR="${RATE_LIMIT_DIR:-${_AI_CONSULTANTS_XDG_CACHE:-/tmp/ai_consultants}/ratelimit}"

# Initialize rate limit directory
_init_rate_limit_dir() {
    if [[ ! -d "$RATE_LIMIT_DIR" ]]; then
        mkdir -p "$RATE_LIMIT_DIR"
        chmod 700 "$RATE_LIMIT_DIR"
    fi
}

# Check and enforce rate limit for a consultant
# Returns 0 if request can proceed, 1 if rate limited (with delay)
# Usage: check_rate_limit <consultant_name>
check_rate_limit() {
    local consultant_name="$1"
    local limit="${API_RATE_LIMIT:-30}"
    local window=60  # 1 minute window

    _init_rate_limit_dir

    # Normalize consultant name for filename
    local safe_name=$(echo "$consultant_name" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]' '_')
    local state_file="${RATE_LIMIT_DIR}/${safe_name}_ratelimit"

    local now=$(date +%s)
    local window_start=$((now - window))

    # Create state file if it doesn't exist
    if [[ ! -f "$state_file" ]]; then
        touch "$state_file"
        chmod 600 "$state_file"
    fi

    # Read timestamps from state file and count requests in current window
    local count=0
    local new_timestamps=""

    while IFS= read -r timestamp; do
        if [[ -n "$timestamp" ]] && [[ "$timestamp" =~ ^[0-9]+$ ]]; then
            if [[ $timestamp -ge $window_start ]]; then
                ((count++)) || true
                new_timestamps+="$timestamp"$'\n'
            fi
        fi
    done < "$state_file"

    # Check if we're at the limit
    if [[ $count -ge $limit ]]; then
        # Calculate wait time until oldest request expires
        local oldest=$(echo "$new_timestamps" | head -1)
        if [[ -n "$oldest" ]]; then
            local wait_time=$((oldest + window - now + 1))
            if [[ $wait_time -gt 0 ]]; then
                log_warn "[$consultant_name] Rate limit reached ($count/$limit requests/min), waiting ${wait_time}s..."
                sleep "$wait_time"
            fi
        fi
    fi

    # Record this request
    new_timestamps+="$now"$'\n'
    echo "$new_timestamps" > "$state_file"

    return 0
}

# Resolve per request, without exporting automatic effort into later tiers.
# GPT-6 Astra, GPT-6.1 Sol and GPT-6 Luna accept low|medium|high|xhigh|max
# (none/minimal unsupported); an unsupported explicit value fails before the
# request is billed. As panel consultants Astra and Sol get high unless an
# effort is configured; economy Luna keeps the provider default.
# Terra 6.1 keeps its transport default, supports API none, and rejects minimal.
# Codex CLI does not offer none for Terra.
_codex_frontier_model() {
    case "$1" in
        gpt-6-astra|gpt-6.1-sol) return 0 ;;
        *) return 1 ;;
    esac
}
_codex_gpt6_model() {
    case "$1" in
        gpt-6-astra|gpt-6.1-sol|gpt-6-luna) return 0 ;;
        *) return 1 ;;
    esac
}
resolve_codex_effort() {
    local model="$1" effort="${2:-}" transport="${3:-api}"
    if [[ -z "$effort" ]] && _codex_frontier_model "$model"; then
        effort=high
    fi
    [[ -n "$effort" ]] || return 0
    effort=$(validate_reasoning_effort "$effort" Codex) || return 1
    if [[ "$model" == gpt-6.1-terra ]] &&
        { [[ "$effort" == minimal ]] || [[ "$transport" == cli && "$effort" == none ]]; }; then
        log_error "[Codex] $model does not support reasoning effort $effort on $transport"
        return 1
    fi
    if _codex_gpt6_model "$model" && [[ "$effort" == none || "$effort" == minimal ]]; then
        log_error "[Codex] $model requires low|medium|high|xhigh|max reasoning effort"
        return 1
    fi
    printf '%s' "$effort"
}

# Codex-only wire adaptation; other OpenAI-compatible providers keep their body.
build_codex_request() {
    local model="$2" effort
    effort=$(resolve_codex_effort "$model" "${4:-}") || return 1
    build_openai_request "$1" "$model" "${3:-16384}" "$effort" |
        jq '.max_completion_tokens = .max_tokens | del(.max_tokens)'
}

# Meta Model API: Muse Wonder and Muse Spark always reason (none returns HTTP
# 400) and take max_completion_tokens (max_tokens is a deprecated alias).
build_meta_request() {
    local model="$2" effort="${4:-}"
    if [[ "$effort" == none ]]; then
        log_error "[Meta] $model always reasons: META_REASONING_EFFORT must be minimal|low|medium|high|xhigh|max"
        return 1
    fi
    build_openai_request "$1" "$model" "${3:-32768}" "$effort" |
        jq '.max_completion_tokens = .max_tokens | del(.max_tokens)'
}
