#!/bin/bash
# routing.sh - Smart consultant selection based on question type
#
# Selects the most suitable consultants based on the question category.

# =============================================================================
# AFFINITY MATRIX
# =============================================================================
# The affinity matrix is loaded from references/affinity.json at first use
# and cached for the life of the shell. To override at runtime, set:
#   AFFINITY_FILE=/path/to/custom.json
# Affinity scores: 10 = perfect match, 1 = poor match.
# Prior to v2.11.0 this lived as nested case statements in this file; see
# docs/SMART_ROUTING.md for the schema and customization guide.

_ROUTING_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Single source of truth for user-config dir resolution: lib/user_config.sh.
# Sourced defensively here so routing.sh works when imported standalone
# (e.g. test_routing_parity.sh). Sourcing only defines functions — no side
# effect — so repeat sources are harmless.
if ! declare -f get_user_config_dir >/dev/null 2>&1; then
    # shellcheck source=user_config.sh
    source "$_ROUTING_LIB_DIR/user_config.sh"
fi

_AFFINITY_LOADED_FILE=""
_AFFINITY_DATA=""
# Per-key result cache: each entry is " CATEGORY|CONSULTANT=score" (leading
# space delimiter). The cache string is initialized to a single space so that
# the first key is bracketed by spaces too, preventing prefix collisions
# (e.g. lookup of "DEBUG|Codex=" against cached "BUG_DEBUG|Codex=10" — the
# space in front of every key kills the false-positive substring match).
# Bash 3.2 has no associative arrays, so this string-based scheme is the
# portable workaround. At ~135 entries x ~25 chars = ~3KB, memory is a
# non-issue.
_AFFINITY_RESULT_CACHE=" "

# Resolve the affinity file path using the same search precedence as user
# config (v2.12+): explicit AFFINITY_FILE > user config dir > bundled default.
_resolve_affinity_path() {
    if [[ -n "${AFFINITY_FILE:-}" ]]; then
        echo "$AFFINITY_FILE"
        return 0
    fi
    local user_dir
    user_dir=$(get_user_config_dir)
    if [[ -n "$user_dir" && -f "$user_dir/affinity.json" ]]; then
        echo "$user_dir/affinity.json"
        return 0
    fi
    echo "$_ROUTING_LIB_DIR/../../references/affinity.json"
}

# Load (and cache) the affinity JSON. Resets the result cache if the resolved
# path changes (e.g. AFFINITY_FILE was set, or a file was added in the user
# config dir).
_load_affinity_data() {
    local file
    file=$(_resolve_affinity_path)
    if [[ "$_AFFINITY_LOADED_FILE" != "$file" ]]; then
        if [[ -r "$file" ]]; then
            _AFFINITY_DATA=$(cat "$file")
        else
            _AFFINITY_DATA=""
        fi
        _AFFINITY_LOADED_FILE="$file"
        _AFFINITY_RESULT_CACHE=" "
    fi
}

# =============================================================================
# SELECTION FUNCTIONS
# =============================================================================

# Get affinity for a category-consultant combination
# Usage: get_affinity <category> <consultant>
# First call per (category, consultant) invokes jq; subsequent calls hit the
# in-memory cache. ~14 calls per consultation, so ~13 cached after warm-up.
get_affinity() {
    local category="$1"
    local consultant="$2"

    _load_affinity_data

    # Fast path: cache hit (key is bracketed by leading/trailing space to
    # prevent substring collisions — see _AFFINITY_RESULT_CACHE comment).
    local key=" ${category}|${consultant}="
    case "$_AFFINITY_RESULT_CACHE" in
        *"${key}"*)
            local cached="${_AFFINITY_RESULT_CACHE#*"${key}"}"
            echo "${cached%% *}"
            return 0
            ;;
    esac

    # If JSON missing or jq unavailable, fall back to a safe default.
    if [[ -z "$_AFFINITY_DATA" ]] || ! command -v jq >/dev/null 2>&1; then
        echo "${AFFINITY_DEFAULT:-5}"
        return 0
    fi

    local score
    score=$(jq -r --arg cat "$category" --arg c "$consultant" '
        if (.known_consultants | index($c)) == null then
            .default_score
        elif (.categories[$cat] // null) == null then
            .general_score
        else
            (.categories[$cat][$c] // .default_score)
        end
    ' <<<"$_AFFINITY_DATA" 2>/dev/null)

    [[ -z "$score" || "$score" == "null" ]] && score="${AFFINITY_DEFAULT:-5}"
    _AFFINITY_RESULT_CACHE+="${key#" "}${score} "  # strip leading space (already in cache)
    echo "$score"
}

# Select the best consultants for a category
# Usage: select_consultants <category> [min_affinity] [max_consultants]
# Returns: list of consultants sorted by affinity
select_consultants() {
    local category="$1"
    local min_affinity="${2:-7}"
    local max_consultants="${3:-8}"

    # Known roster order matches ALL_CONSULTANTS in config.sh. Eligibility is
    # applied here because smart-routing callers consume this output directly.
    local consultants=("Gemini" "Codex" "Mistral" "Kimi" "Claude" "Qwen3" "GLM" "Grok" "DeepSeek" "MiniMax")
    local selected=()
    local scores=()
    local custom_consultants=()

    # Collect affinities. Consultants at or above the min-affinity threshold are
    # selected, ranked by raw category affinity.
    for c in "${consultants[@]}"; do
        local consultant_flag enable_var
        consultant_flag=$(printf '%s' "$c" | tr '[:lower:]' '[:upper:]')
        enable_var="ENABLE_${consultant_flag}"
        [[ "${!enable_var:-true}" == "true" ]] || continue
        if declare -f should_skip_consultant >/dev/null 2>&1 && should_skip_consultant "$consultant_flag"; then
            continue
        fi

        local score=$(get_affinity "$category" "$c")
        if [[ $score -ge $min_affinity ]]; then
            selected+=("$c")
            scores+=("$score")
        fi
    done

    # Custom agents have no affinity entry. Their explicit ENABLE_* + API URL
    # configuration is the eligibility signal, so reserve panel slots for them
    # after affinity-ranked known consultants instead of silently omitting them.
    if declare -f _list_custom_api_agents >/dev/null 2>&1; then
        while IFS= read -r c; do
            [[ -n "$c" ]] && custom_consultants+=("$c")
        done < <(_list_custom_api_agents | sort -u)
    fi

    # Sort by score (simple bubble sort)
    local n=${#selected[@]}
    for ((i=0; i<n-1; i++)); do
        for ((j=0; j<n-i-1; j++)); do
            if [[ ${scores[j]} -lt ${scores[j+1]} ]]; then
                # Swap (array indices are arithmetic context; no $/$(()) needed)
                local tmp="${selected[j]}"
                selected[j]="${selected[j+1]}"
                selected[j+1]="$tmp"

                tmp="${scores[j]}"
                scores[j]="${scores[j+1]}"
                scores[j+1]="$tmp"
            fi
        done
    done

    # Limit to the requested panel size while keeping room for configured
    # custom agents, which are appended in stable name order.
    local count=0
    local known_limit=$(( max_consultants - ${#custom_consultants[@]} ))
    [[ $known_limit -lt 0 ]] && known_limit=0
    for c in ${selected[@]+"${selected[@]}"}; do
        if [[ $count -ge $known_limit ]]; then
            break
        fi
        echo "$c"
        count=$((count + 1))
    done
    for c in ${custom_consultants[@]+"${custom_consultants[@]}"}; do
        [[ $count -ge $max_consultants ]] && break
        echo "$c"
        count=$((count + 1))
    done
}

# Check if a consultant is recommended for a category
# Usage: is_recommended <category> <consultant> [min_affinity]
is_recommended() {
    local category="$1"
    local consultant="$2"
    local min_affinity="${3:-7}"

    local score=$(get_affinity "$category" "$consultant")
    if [[ $score -ge $min_affinity ]]; then
        return 0
    fi
    return 1
}

# Backward-compatible routing utilities. The orchestrator selects directly by
# affinity; these helpers remain for users that source routing.sh.
get_routing_mode() {
    local category="$1"
    case "$category" in
        SECURITY) echo "full" ;;
        QUICK_SYNTAX) echo "single" ;;
        CODE_REVIEW|BUG_DEBUG|ARCHITECTURE) echo "selective" ;;
        *) echo "full" ;;
    esac
}

get_recommended_count() {
    local mode
    mode=$(get_routing_mode "$1")
    case "$mode" in
        full) echo 10 ;;
        selective) echo 5 ;;
        single) echo 1 ;;
        *) echo 10 ;;
    esac
}

get_category_timeout() {
    case "$1" in
        QUICK_SYNTAX) echo 60 ;;
        BUG_DEBUG|CODE_REVIEW|ALGORITHM|API_DESIGN) echo 180 ;;
        ARCHITECTURE|SECURITY) echo 240 ;;
        DATABASE|TESTING) echo 120 ;;
        GENERAL|*) echo 180 ;;
    esac
}

# =============================================================================
# COST-AWARE ROUTING (v2.3)
# =============================================================================

# Ensure costs.sh is sourced (lazy loading helper)
_ensure_costs_sourced() {
    if ! type get_economic_model &>/dev/null; then
        local script_dir
        script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
        source "$script_dir/costs.sh" 2>/dev/null || true
    fi
}

# Select consultants based on cost efficiency
# Usage: select_consultants_cost_aware <category> <complexity> [min_affinity]
# Returns: list of consultants optimized for cost/quality balance
select_consultants_cost_aware() {
    local category="$1"
    local complexity="${2:-5}"
    local min_affinity="${3:-7}"

    # Check if cost-aware routing is enabled
    if [[ "${ENABLE_COST_AWARE_ROUTING:-false}" != "true" ]]; then
        # Fall back to standard selection
        select_consultants "$category" "$min_affinity"
        return
    fi

    _ensure_costs_sourced

    local simple_threshold="${COMPLEXITY_THRESHOLD_SIMPLE:-3}"
    local medium_threshold="${COMPLEXITY_THRESHOLD_MEDIUM:-6}"

    # Simple queries: only economic models, fewer consultants
    if [[ $complexity -le $simple_threshold ]]; then
        select_consultants "$category" "$min_affinity" 2
        return
    fi

    # Medium complexity: balanced selection
    if [[ $complexity -le $medium_threshold ]]; then
        select_consultants "$category" "$min_affinity" 4
        return
    fi

    # Complex queries: full selection
    select_consultants "$category" "$min_affinity"
}

# Get model override for cost-aware routing
# Usage: get_cost_aware_model <consultant> <complexity>
# Returns: model name to use, or empty for default
get_cost_aware_model() {
    local consultant="$1"
    local complexity="${2:-5}"

    # Check if cost-aware routing is enabled
    if [[ "${ENABLE_COST_AWARE_ROUTING:-false}" != "true" ]]; then
        echo ""
        return
    fi

    # Check if we should use economic models for simple queries
    if [[ "${USE_ECONOMIC_MODELS_FOR_SIMPLE:-true}" != "true" ]]; then
        echo ""
        return
    fi

    local simple_threshold="${COMPLEXITY_THRESHOLD_SIMPLE:-3}"

    # Simple queries: use economic model
    if [[ $complexity -le $simple_threshold ]]; then
        _ensure_costs_sourced
        local economic_model
        economic_model=$(get_economic_model "$consultant" 2>/dev/null || echo "")
        echo "$economic_model"
        return
    fi

    # Complex queries: use default model
    echo ""
}

# =============================================================================
# FALLBACK ESCALATION (v2.3 Quality Review)
# =============================================================================

# Fallback escalation threshold - escalate to premium if confidence below this
FALLBACK_CONFIDENCE_THRESHOLD="${FALLBACK_CONFIDENCE_THRESHOLD:-7}"

# Check if response needs escalation based on confidence
# Usage: needs_escalation <response_file>
# Returns: 0 if escalation needed, 1 otherwise
needs_escalation() {
    local response_file="$1"

    if [[ ! -f "$response_file" || ! -s "$response_file" ]]; then
        return 0  # No response = needs escalation
    fi

    local confidence
    confidence=$(jq -r '.confidence.score // 5' "$response_file" 2>/dev/null)

    if [[ ! "$confidence" =~ ^[0-9]+$ ]]; then
        confidence=5
    fi

    local threshold="${FALLBACK_CONFIDENCE_THRESHOLD:-7}"

    if [[ $confidence -lt $threshold ]]; then
        return 0  # Needs escalation
    fi

    return 1  # No escalation needed
}

# Get premium model for escalation
# Delegates to get_model_for_tier() in config.sh (single source of truth)
# Usage: get_premium_model <consultant>
get_premium_model() {
    local consultant="$1"
    if type get_model_for_tier &>/dev/null; then
        get_model_for_tier "$consultant" "premium"
    else
        echo ""
    fi
}

# Check if escalation is enabled
# Usage: is_escalation_enabled
is_escalation_enabled() {
    # Escalation is enabled when cost-aware routing is on
    [[ "${ENABLE_COST_AWARE_ROUTING:-false}" == "true" ]]
}

# Get escalation summary for a response
# Usage: get_escalation_summary <response_file> <consultant>
get_escalation_summary() {
    local response_file="$1"
    local consultant="$2"

    local needs_it="false"
    local confidence=0
    local threshold="${FALLBACK_CONFIDENCE_THRESHOLD:-7}"

    if [[ -f "$response_file" && -s "$response_file" ]]; then
        confidence=$(jq -r '.confidence.score // 5' "$response_file" 2>/dev/null)
        [[ ! "$confidence" =~ ^[0-9]+$ ]] && confidence=5
    fi

    if [[ $confidence -lt $threshold ]]; then
        needs_it="true"
    fi

    local premium_model
    premium_model=$(get_premium_model "$consultant")

    cat << EOF
{
  "consultant": "$consultant",
  "confidence": $confidence,
  "threshold": $threshold,
  "needs_escalation": $needs_it,
  "premium_model": "$premium_model"
}
EOF
}
