#!/bin/bash
# vault-cache.sh - Local Vault Secret Cache Manager
# 
# PURPOSE: Cache Vault secrets locally to avoid repeated Vault API calls
# - Secrets are encrypted at rest using the Vault approle credentials
# - Cache auto-refreshes when stale (default: 1 hour TTL)
# - Supports selective sync, full sync, and cache invalidation
#
# USAGE:
#   vault-cache.sh sync                    # Full sync of all accessible secrets
#   vault-cache.sh sync <category>         # Sync specific category (e.g., infrastructure)
#   vault-cache.sh get <path> [field]      # Get a specific secret from cache
#   vault-cache.sh list                    # List cached categories and keys
#   vault-cache.sh invalidate [path|all]   # Invalidate cache entry or all
#   vault-cache.sh status                  # Show cache status and freshness
#
# EXAMPLES:
#   vault-cache.sh sync infrastructure     # Sync only infrastructure secrets
#   vault-cache.sh get api/data/nocodb token    # Get nocodb token
#   vault-cache.sh get api/data/qdrant api-key  # Get qdrant API key

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CACHE_DIR="${HOME}/.cache/vault"
CACHE_DB="${CACHE_DIR}/secrets.db"
CACHE_META="${CACHE_DIR}/meta.json"
VAULT_URL="https://beavault.beawit.net:8200"
ROLE_ID="75d2dcfb-9c65-7f60-59b4-eee8c7f8dc0e"
SECRET_ID="6202b465-2f25-547c-ec07-f47cfc4dda3e"
DEFAULT_TTL=3600  # 1 hour in seconds

# Ensure cache directory exists with restricted permissions
init_cache() {
    if [[ ! -d "$CACHE_DIR" ]]; then
        mkdir -p "$CACHE_DIR"
        chmod 700 "$CACHE_DIR"
        echo "Created cache directory: $CACHE_DIR"
    fi
    
    # Initialize metadata if doesn't exist
    if [[ ! -f "$CACHE_META" ]]; then
        echo '{"version":1,"created":"'"$(date -Iseconds)"'","entries":{}}' > "$CACHE_META"
        chmod 600 "$CACHE_META"
    fi
}

# Get Vault token (with caching)
get_vault_token() {
    local token_file="${CACHE_DIR}/.vault_token"
    local token_age=999999
    
    if [[ -f "$token_file" ]]; then
        token_age=$(($(date +%s) - $(stat -c %Y "$token_file")))
    fi
    
    # Vault tokens are valid for ~32 days, but let's refresh after 24 hours
    if [[ "$token_age" -gt 86400 ]] || [[ ! -s "$token_file" ]]; then
        curl -sk -X POST \
            -d "{\"role_id\":\"${ROLE_ID}\",\"secret_id\":\"${SECRET_ID}\"}" \
            "${VAULT_URL}/v1/auth/approle/login" | \
            jq -r '.auth.client_token' > "$token_file"
        chmod 600 "$token_file"
    fi
    
    cat "$token_file"
}

# Fetch a secret from Vault
fetch_secret() {
    local path="$1"
    local vault_token
    vault_token=$(get_vault_token)
    
    local response
    response=$(curl -sk -H "X-Vault-Token: ${vault_token}" \
        "${VAULT_URL}/v1/kv/data/${path}")
    
    if echo "$response" | jq -e '.data.data' > /dev/null 2>&1; then
        echo "$response" | jq '.data.data'
    else
        echo "ERROR: Failed to fetch secret at ${path}" >&2
        echo "$response" | jq -r '.errors[]' >&2
        return 1
    fi
}

# List secrets in a category
list_category_secrets() {
    local category="$1"
    local vault_token
    vault_token=$(get_vault_token)
    
    curl -sk -H "X-Vault-Token: ${vault_token}" \
        "${VAULT_URL}/v1/kv/metadata/api/${category}?list=true" 2>/dev/null | \
        jq -r '.data.keys[]?' 2>/dev/null || true
}

# Cache a secret to local storage
cache_secret() {
    local path="$1"
    local data="$2"
    local cache_file="${CACHE_DIR}/$(echo "$path" | tr '/' '_').json"
    
    echo "$data" > "$cache_file"
    chmod 600 "$cache_file"
    
    # Update metadata
    local meta
    meta=$(jq --arg path "$path" --arg time "$(date +%s)" \
        '.entries[$path] = {"cached_at":$time,"ttl":'$DEFAULT_TTL'}' "$CACHE_META")
    echo "$meta" > "$CACHE_META"
    chmod 600 "$CACHE_META"
}

# Get from cache (returns empty if stale/missing)
get_cached() {
    local path="$1"
    local cache_file="${CACHE_DIR}/$(echo "$path" | tr '/' '_').json"
    
    if [[ ! -f "$cache_file" ]]; then
        return 1
    fi
    
    # Check freshness
    local cached_time
    cached_time=$(jq -r --arg path "$path" '.entries[$path].cached_at // 0' "$CACHE_META")
    local age=$(( $(date +%s) - cached_time ))
    local ttl
    ttl=$(jq -r --arg path "$path" '.entries[$path].ttl // '$DEFAULT_TTL'' "$CACHE_META")
    
    if [[ "$age" -gt "$ttl" ]]; then
        return 1  # Stale
    fi
    
    cat "$cache_file"
}

# Sync a single secret (fetch if stale)
sync_secret() {
    local path="$1"
    local force="${2:-false}"
    
    if [[ "$force" == "true" ]] || ! get_cached "$path" > /dev/null 2>&1; then
        echo "  Fetching: ${path}..."
        local data
        if data=$(fetch_secret "$path"); then
            cache_secret "$path" "$data"
            echo "    ✓ Cached"
        else
            echo "    ✗ Failed"
            return 1
        fi
    else
        echo "  Skipping (fresh): ${path}"
    fi
}

# Sync an entire category
sync_category() {
    local category="$1"
    echo "=== Syncing category: ${category} ==="
    
    local secrets
    secrets=$(list_category_secrets "$category")
    
    if [[ -z "$secrets" ]]; then
        echo "  (empty or no access)"
        return
    fi
    
    local count=0
    while IFS= read -r secret; do
        [[ -z "$secret" ]] && continue
        # Remove trailing slash
        secret="${secret%/}"
        sync_secret "api/${category}/${secret}" || true
        count=$((count + 1))
    done <<< "$secrets"
    
    echo "  Processed ${count} secrets"
}

# Full sync of all categories
sync_all() {
    echo "=== Full Vault Cache Sync ==="
    echo "Started: $(date)"
    echo ""
    
    # Categories we know exist (from discovery)
    local categories=(business communication data infrastructure marketing research)
    
    for category in "${categories[@]}"; do
        sync_category "$category"
        echo ""
    done
    
    echo "=== Sync Complete ==="
    echo "Finished: $(date)"
}

# Get a value from cache (with auto-fetch if missing)
get_value() {
    local path="$1"
    local field="${2:-}"
    local force_refresh="${3:-false}"
    
    # Auto-fetch if missing or stale
    if [[ "$force_refresh" == "true" ]] || ! get_cached "$path" > /dev/null 2>&1; then
        local data
        if data=$(fetch_secret "$path"); then
            cache_secret "$path" "$data"
        else
            echo "ERROR: Could not retrieve ${path}" >&2
            return 1
        fi
    fi
    
    local cached
    cached=$(get_cached "$path")
    
    if [[ -n "$field" ]]; then
        # Try the field name directly, then with underscores instead of hyphens, then bracket notation
        local result
        result=$(echo "$cached" | jq -r ".${field} // empty" 2>/dev/null || true)
        if [[ -z "$result" ]]; then
            # Try with underscores replacing hyphens
            local field_underscore
            field_underscore=$(echo "$field" | tr '-' '_')
            result=$(echo "$cached" | jq -r ".${field_underscore} // empty" 2>/dev/null || true)
        fi
        if [[ -z "$result" ]]; then
            # Try bracket notation for hyphenated keys
            result=$(echo "$cached" | jq -r ".[\"${field}\"] // empty" 2>/dev/null || true)
        fi
        echo "$result"
    else
        echo "$cached" | jq -r 'to_entries | .[] | "\(.key): \(.value)"'
    fi
}

# List cached items
list_cache() {
    echo "=== Cached Secrets ==="
    
    local entries
    entries=$(jq -r '.entries | keys[]' "$CACHE_META" 2>/dev/null)
    
    if [[ -z "$entries" ]]; then
        echo "  (cache is empty)"
        return
    fi
    
    local total=0
    while IFS= read -r path; do
        [[ -z "$path" ]] && continue
        local cached_time
        cached_time=$(jq -r --arg p "$path" '.entries[$p].cached_at // 0' "$CACHE_META")
        local age=$(( $(date +%s) - cached_time ))
        local age_str
        
        if [[ $age -lt 60 ]]; then
            age_str="${age}s ago"
        elif [[ $age -lt 3600 ]]; then
            age_str="$((age / 60))m ago"
        else
            age_str="$((age / 3600))h ago"
        fi
        
        local ttl
        ttl=$(jq -r --arg p "$path" '.entries[$p].ttl // '$DEFAULT_TTL'' "$CACHE_META")
        local status="✓"
        [[ $age -gt $ttl ]] && status="✗ STALE"
        
        printf "  %-50s %s %s\n" "$path" "$status" "$age_str"
        total=$((total + 1))
    done <<< "$entries"
    
    echo ""
    echo "Total cached: $total"
}

# Show cache status
show_status() {
    echo "=== Vault Cache Status ==="
    echo "Cache directory: $CACHE_DIR"
    
    if [[ -d "$CACHE_DIR" ]]; then
        local disk_usage
        disk_usage=$(du -sh "$CACHE_DIR" 2>/dev/null | cut -f1)
        echo "Disk usage: $disk_usage"
        
        local file_count
        file_count=$(find "$CACHE_DIR" -name '*.json' | wc -l)
        echo "Cached secrets: $file_count"
        
        # Count stale entries
        local stale_count=0
        local entries
        entries=$(jq -r '.entries | keys[]' "$CACHE_META" 2>/dev/null)
        while IFS= read -r path; do
            [[ -z "$path" ]] && continue
            local cached_time
            cached_time=$(jq -r --arg p "$path" '.entries[$p].cached_at // 0' "$CACHE_META")
            local ttl
            ttl=$(jq -r --arg p "$path" '.entries[$p].ttl // '$DEFAULT_TTL'' "$CACHE_META")
            if [[ $(( $(date +%s) - cached_time )) -gt $ttl ]]; then
                stale_count=$((stale_count + 1))
            fi
        done <<< "$entries"
        
        echo "Stale entries: $stale_count"
    else
        echo "Status: Not initialized"
    fi
    
    # Vault token status
    local token_file="${CACHE_DIR}/.vault_token"
    if [[ -f "$token_file" ]]; then
        local token_age
        token_age=$(($(date +%s) - $(stat -c %Y "$token_file")))
        echo "Vault token age: $((token_age / 3600))h $(((token_age % 3600) / 60))m"
    else
        echo "Vault token: Not cached"
    fi
}

# Invalidate cache entries
invalidate_cache() {
    local target="${1:-all}"
    
    if [[ "$target" == "all" ]]; then
        rm -f "${CACHE_DIR}"/*.json
        echo '{"version":1,"created":"'"$(date -Iseconds)"'","entries":{}}' > "$CACHE_META"
        echo "Cache fully invalidated"
    else
        local cache_file="${CACHE_DIR}/$(echo "$target" | tr '/' '_').json"
        rm -f "$cache_file"
        
        # Update metadata
        local meta
        meta=$(jq --arg path "$target" 'del(.entries[$path])' "$CACHE_META")
        echo "$meta" > "$CACHE_META"
        echo "Invalidated: $target"
    fi
}

# Main command dispatcher
main() {
    init_cache
    
    local cmd="${1:-status}"
    
    case "$cmd" in
        sync)
            if [[ -n "${2:-}" ]]; then
                sync_category "$2"
            else
                sync_all
            fi
            ;;
        get)
            if [[ -z "${2:-}" ]]; then
                echo "Usage: $0 get <path> [field]"
                exit 1
            fi
            get_value "$2" "${3:-}"
            ;;
        list|ls)
            list_cache
            ;;
        invalidate|rm|clear)
            invalidate_cache "${2:-all}"
            ;;
        status|info)
            show_status
            ;;
        *)
            echo "Vault Cache Manager"
            echo ""
            echo "Usage: $0 <command> [args]"
            echo ""
            echo "Commands:"
            echo "  sync [category]     Sync all or specific category"
            echo "  get <path> [field]  Get secret from cache (auto-fetch if needed)"
            echo "  list                List all cached secrets"
            echo "  invalidate [path]   Invalidate cache (default: all)"
            echo "  status              Show cache status"
            echo ""
            echo "Examples:"
            echo "  $0 sync infrastructure"
            echo "  $0 get api/data/nocodb token"
            echo "  $0 get api/data/qdrant api-key"
            echo "  $0 invalidate api/integration/grist"
            exit 1
            ;;
    esac
}

main "$@"
