#!/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
}

# 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
# =============================================================================

# Build request body for Qwen3 (Alibaba DashScope format)
# Usage: build_qwen_request <prompt> <model>
build_qwen_request() {
    local prompt="$1"
    local model="${2:-qwen-max}"

    jq -n \
        --arg model "$model" \
        --arg prompt "$prompt" \
        '{
            model: $model,
            input: {
                messages: [
                    { role: "user", content: $prompt }
                ]
            },
            parameters: {
                result_format: "message"
            }
        }'
}

# 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-4}"
    local max_tokens="${3:-4096}"
    local reasoning_effort="${4:-}"

    jq -n \
        --arg model "$model" \
        --arg prompt "$prompt" \
        --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]
build_anthropic_request() {
    local prompt="$1"
    local model="${2:-claude-sonnet-5}"
    local max_tokens="${3:-16384}"

    jq -n \
        --arg model "$model" \
        --arg prompt "$prompt" \
        --argjson max_tokens "$max_tokens" \
        '{
            model: $model,
            max_tokens: $max_tokens,
            messages: [
                { role: "user", content: $prompt }
            ]
        }'
}

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

    jq -n \
        --arg prompt "$prompt" \
        --arg thinking_level "$thinking_level" \
        '{
            contents: [
                { parts: [{ text: $prompt }] }
            ],
            generationConfig: {
                maxOutputTokens: 4096
            }
        }
        | 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
    content=$(echo "$response" | jq -r '.choices[0].message.content // empty' 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"

    local content
    content=$(echo "$response" | jq -r '.candidates[0].content.parts[0].text // empty' 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)
            output_tokens=$(echo "$response" | jq -r '.usageMetadata.candidatesTokenCount // 0' 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)
            output_tokens=$(echo "$response" | jq -r '.usageMetadata.candidatesTokenCount // 0' 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
# =============================================================================

# 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

    # Build authorization headers based on auth style
    local auth_headers=()
    case "$auth_style" in
        none)
            # No auth header needed
            ;;
        google_ai)
            # Google AI uses x-goog-api-key header (more secure than URL param)
            auth_headers+=("-H" "x-goog-api-key: $api_key")
            ;;
        anthropic)
            auth_headers+=("-H" "x-api-key: $api_key")
            auth_headers+=("-H" "anthropic-version: 2023-06-01")
            ;;
        apikey)
            auth_headers+=("-H" "X-API-Key: $api_key")
            ;;
        *)  # Default to bearer token
            auth_headers+=("-H" "Authorization: Bearer $api_key")
            ;;
    esac

    # Temporary files
    local temp_response=$(mktemp)
    local 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: ${timeout_seconds}s, max retry: $MAX_RETRIES)..."

    local attempt=1
    local last_http_code=0
    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" \
            "${auth_headers[@]}" \
            -H "Content-Type: application/json" \
            -m "$timeout_seconds" \
            -d "$request_body" 2>"$error_file")

        local curl_exit=$?
        last_http_code="$http_code"

        # Handle curl-level errors
        if [[ $curl_exit -eq 28 ]]; then
            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
            log_warn "[$consultant_name] Network error (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
                    log_warn "[$consultant_name] Empty response body"
                    ((attempt++)) || true
                    if (( attempt <= MAX_RETRIES )); then
                        sleep "$RETRY_DELAY_SECONDS"
                    fi
                fi
                ;;
            auth)
                log_error "[$consultant_name] Authentication failed (HTTP $http_code)"
                log_error "[$consultant_name] Check that $api_key_var is correct"
                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
                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)
                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)
                log_error "[$consultant_name] Client error (HTTP $http_code)"
                # Log response body for debugging (sanitized to avoid leaking sensitive data)
                if [[ -s "$temp_response" ]]; then
                    local error_msg=$(jq -r '.error.message // .message // .' "$temp_response" 2>/dev/null | head -5)
                    log_debug "[$consultant_name] Response: $(sanitize_error_message "$error_msg")"
                fi
                rm -f "$temp_response" "$temp_headers" "$error_file"
                return 1  # No retry for client errors
                ;;
            *)
                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 HTTP: $last_http_code)"
        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.
resolve_codex_effort() {
    local model="$1" effort="${2:-}"
    [[ "$model" != gpt-6-astra || -n "$effort" ]] || effort=high
    [[ -n "$effort" ]] || return 0
    effort=$(validate_reasoning_effort "$effort" Codex) || return 1
    if [[ "$model" == gpt-6-astra && ( "$effort" == none || "$effort" == minimal ) ]]; then
        log_error "[Codex] Astra 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)'
}
