#!/usr/bin/env python3
"""
AbstractCore CLI - Unified Configuration System

Provides configuration commands for all AbstractCore settings:
- Default models and providers
- Vision fallback configuration
- Embeddings settings
- API keys and authentication
- Provider preferences

Usage:
    # General configuration
    abstractcore --set-default-model ollama/llama3:8b
    abstractcore --set-default-provider ollama
    abstractcore --status
    abstractcore --configure  # alias: --config

    # Vision fallback (for text-only models)
    abstractcore --set-vision-provider ollama qwen2.5vl:7b
    abstractcore --disable-vision

    # Audio/video defaults (attachments)
    abstractcore --set-audio-strategy auto          # requires: pip install "abstractcore[voice]"
    abstractcore --set-stt-language fr              # optional STT hint
    abstractcore --set-video-strategy auto          # frames fallback requires ffmpeg
    abstractcore --set-video-max-frames 6

    # Embeddings configuration
    abstractcore --set-embeddings-model sentence-transformers/all-MiniLM-L6-v2
    abstractcore --set-embeddings-provider huggingface

    # API keys
    abstractcore --set-api-key openai sk-...
    abstractcore --set-api-key anthropic ant_...
    abstractcore --set-api-key portkey pk_...

    # HTTP server auth/hardening
    abstractcore --set-server-auth-token acore-secret
    abstractcore --set-server-base-url-allowlist https://example.com/v1
"""

import os
import sys
import argparse
import json
import logging
import shutil
import subprocess
import secrets
from pathlib import Path
from typing import List, Optional

# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent.parent))

# Import config manager with fallback
try:
    from abstractcore.config import get_config_manager
    CONFIG_AVAILABLE = True
except ImportError:
    CONFIG_AVAILABLE = False
    get_config_manager = None

def download_vision_model(model_name: str = "blip-base-caption") -> bool:
    """Download a vision model for local use."""
    AVAILABLE_MODELS = {
        "blip-base-caption": {
            "hf_id": "Salesforce/blip-image-captioning-base",
            "size": "990MB",
            "description": "BLIP base image captioning model"
        },
        "blip-large-caption": {
            "hf_id": "Salesforce/blip-image-captioning-large",
            "size": "1.8GB",
            "description": "BLIP large image captioning model (better quality)"
        },
        "vit-gpt2": {
            "hf_id": "nlpconnect/vit-gpt2-image-captioning",
            "size": "500MB",
            "description": "ViT + GPT-2 image captioning model (CPU friendly)"
        },
        "git-base": {
            "hf_id": "microsoft/git-base",
            "size": "400MB",
            "description": "Microsoft GIT base captioning model (smallest)"
        }
    }

    if model_name not in AVAILABLE_MODELS:
        print(f"❌ Unknown model: {model_name}")
        print(f"Available models: {', '.join(AVAILABLE_MODELS.keys())}")
        return False

    model_info = AVAILABLE_MODELS[model_name]
    print(f"📋 Model: {model_info['description']} ({model_info['size']})")

    try:
        # Check if transformers is available
        try:
            import transformers
            from transformers import BlipProcessor, BlipForConditionalGeneration
            from transformers import VisionEncoderDecoderModel, ViTImageProcessor, AutoTokenizer
            from transformers import GitProcessor, GitForCausalLM
        except ImportError:
            print("❌ Required libraries not found. Installing transformers...")
            import subprocess
            import sys

            # Install transformers and dependencies
            subprocess.check_call([sys.executable, "-m", "pip", "install", "transformers", "torch", "torchvision", "Pillow"])
            print("✅ Installed transformers and dependencies")

            # Re-import after installation
            from transformers import BlipProcessor, BlipForConditionalGeneration
            from transformers import VisionEncoderDecoderModel, ViTImageProcessor, AutoTokenizer
            from transformers import GitProcessor, GitForCausalLM

        # Create models directory
        from pathlib import Path
        models_dir = Path.home() / ".abstractcore" / "models" / model_name
        models_dir.mkdir(parents=True, exist_ok=True)

        print(f"📁 Download path: {models_dir}")
        print(f"🔄 Downloading {model_info['description']}...")

        hf_id = model_info["hf_id"]

        # Download based on model type
        if "blip" in model_name:
            print("📥 Downloading BLIP model and processor...")
            processor = BlipProcessor.from_pretrained(hf_id, use_fast=False, cache_dir=str(models_dir))
            model = BlipForConditionalGeneration.from_pretrained(hf_id, cache_dir=str(models_dir))

            # Save to specific directory structure
            processor.save_pretrained(models_dir / "processor")
            model.save_pretrained(models_dir / "model")

        elif "vit-gpt2" in model_name:
            print("📥 Downloading ViT-GPT2 model...")
            model = VisionEncoderDecoderModel.from_pretrained(hf_id, cache_dir=str(models_dir))
            feature_extractor = ViTImageProcessor.from_pretrained(hf_id, cache_dir=str(models_dir))
            tokenizer = AutoTokenizer.from_pretrained(hf_id, cache_dir=str(models_dir))

            # Save components
            model.save_pretrained(models_dir / "model")
            feature_extractor.save_pretrained(models_dir / "feature_extractor")
            tokenizer.save_pretrained(models_dir / "tokenizer")

        elif "git" in model_name:
            print("📥 Downloading GIT model...")
            processor = GitProcessor.from_pretrained(hf_id, cache_dir=str(models_dir))
            model = GitForCausalLM.from_pretrained(hf_id, cache_dir=str(models_dir))

            processor.save_pretrained(models_dir / "processor")
            model.save_pretrained(models_dir / "model")

        # Create a marker file to indicate successful download
        marker_file = models_dir / "download_complete.txt"
        with open(marker_file, 'w') as f:
            f.write(f"Model: {model_info['description']}\n")
            f.write(f"HuggingFace ID: {hf_id}\n")
            f.write(f"Downloaded: {Path(__file__).parent}\n")

        print(f"✅ Successfully downloaded {model_info['description']}")
        print(f"📁 Model saved to: {models_dir}")

        # Configure AbstractCore to use this model
        if CONFIG_AVAILABLE:
            config_manager = get_config_manager()
            # Use the proper HuggingFace model identifier
            config_manager.set_vision_provider("huggingface", hf_id)
        else:
            print("⚠️  Config system not available - manual configuration required")

        print(f"✅ Configured AbstractCore to use HuggingFace model: {hf_id}")
        print(f"🎯 Vision fallback is now enabled!")

        return True

    except Exception as e:
        print(f"❌ Download failed: {e}")
        import traceback
        traceback.print_exc()
        return False

def add_arguments(parser: argparse.ArgumentParser):
    """Add all AbstractCore configuration arguments with organized groups."""

    # General configuration group
    general_group = parser.add_argument_group('General Configuration')
    general_group.add_argument("--status", action="store_true",
                              help="Show current configuration status with change commands")
    general_group.add_argument(
        "--configure",
        "--config",
        action="store_true",
        help="Interactive guided setup for first-time users (alias: --config)",
    )
    general_group.add_argument("--reset", action="store_true",
                              help="Reset all configuration to built-in defaults")
    general_group.add_argument(
        "--install",
        action="store_true",
        help="Check all subsystems and download/install missing models and dependencies",
    )
    general_group.add_argument(
        "--yes", "-y",
        action="store_true",
        help="Auto-accept all downloads during --install (non-interactive)",
    )

    # Model configuration group
    model_group = parser.add_argument_group('Model Configuration')
    model_group.add_argument("--set-global-default", metavar="PROVIDER/MODEL",
                            help="Set fallback model for all apps (e.g., ollama/llama3:8b or lmstudio:qwen/qwen3.6-35b-a3b)")
    model_group.add_argument("--set-app-default", nargs=3, metavar=("APP", "PROVIDER", "MODEL"),
                            help="Set app-specific model (apps: cli, summarizer, extractor, judge)")
    model_group.add_argument("--set-chat-model", metavar="PROVIDER/MODEL",
                            help="Set specialized chat model (optional)")
    model_group.add_argument("--set-code-model", metavar="PROVIDER/MODEL",
                            help="Set specialized coding model (optional)")
    # Authentication group
    auth_group = parser.add_argument_group('Authentication')
    auth_group.add_argument("--set-api-key", nargs=2, metavar=("PROVIDER", "KEY"),
                           help="Set API key for providers (openai, anthropic, openrouter, portkey, openai-compatible, vllm, google, etc.)")
    auth_group.add_argument("--list-api-keys", action="store_true",
                           help="Show which providers have API keys configured")

    # HTTP server configuration group
    server_group = parser.add_argument_group('HTTP Server Configuration')
    server_group.add_argument("--set-server-auth-token", dest="set_server_auth_token", metavar="TOKEN",
                             help="Set the AbstractCore HTTP server auth token")
    server_group.add_argument("--clear-server-auth-token", dest="clear_server_auth_token", action="store_true",
                             help="Clear the persisted AbstractCore HTTP server auth token")
    server_group.add_argument("--allow-unauthenticated-server", action="store_true",
                             help="Allow unauthenticated HTTP server requests (local/dev only)")
    server_group.add_argument("--disallow-unauthenticated-server", action="store_true",
                             help="Require server auth or explicit provider keys for HTTP server requests")
    server_group.add_argument("--set-server-base-url-allowlist", metavar="CSV",
                             help="Set server base_url override allowlist (comma-separated URLs/hosts/globs; blank clears)")
    server_group.add_argument("--set-server-url-fetch-allowlist", metavar="CSV",
                             help="Set server URL media fetch allowlist (comma-separated URLs/hosts/globs; blank clears)")
    server_group.add_argument("--set-server-media-root", metavar="PATH",
                             help="Set safe local media root for HTTP requests (blank clears)")
    server_group.add_argument("--allow-server-local-files", action="store_true",
                             help="Allow unrestricted local file paths in HTTP requests (unsafe)")
    server_group.add_argument("--disallow-server-local-files", action="store_true",
                             help="Disable unrestricted local file paths in HTTP requests")
    server_group.add_argument("--set-server-host", metavar="HOST",
                             help="Set default bind host for `abstractcore serve`")
    server_group.add_argument("--set-server-port", type=int, metavar="PORT",
                             help="Set default bind port for `abstractcore serve`")

    # Media processing group
    media_group = parser.add_argument_group('Media & Vision Configuration')
    media_group.add_argument("--set-vision-provider", nargs=2, metavar=("PROVIDER", "MODEL"),
                            help="Set vision model for image analysis with text-only models")
    media_group.add_argument("--add-vision-fallback", nargs=2, metavar=("PROVIDER", "MODEL"),
                            help="Add backup vision provider to fallback chain")
    media_group.add_argument("--download-vision-model", nargs="?", const="blip-base-caption", metavar="MODEL",
                            help="Download local vision model (default: blip-base-caption, ~1GB)")
    media_group.add_argument("--disable-vision", action="store_true",
                            help="Disable vision fallback for text-only models")
    media_group.add_argument(
        "--set-audio-strategy",
        choices=["native_only", "speech_to_text", "auto"],
        help=(
            "Set default audio handling strategy for attachments "
            "(default: auto when `abstractvoice` is installed; otherwise native_only)."
        ),
    )
    media_group.add_argument(
        "--set-stt-backend-id",
        metavar="BACKEND_ID",
        help="Set preferred STT backend id for capability plugins (optional).",
    )
    media_group.add_argument(
        "--set-stt-language",
        metavar="LANG",
        help="Set default STT language hint (optional; e.g. en, fr).",
    )
    media_group.add_argument(
        "--set-video-strategy",
        choices=["native_only", "frames_caption", "auto"],
        help="Set default video handling strategy for attachments (default: auto).",
    )
    media_group.add_argument(
        "--set-video-max-frames",
        metavar="N",
        type=int,
        help="Set max frames sampled for video frame fallback (default: 3).",
    )
    media_group.add_argument(
        "--set-video-max-frames-native",
        metavar="N",
        type=int,
        help="Set max frames used for native video-capable models (default: 8).",
    )
    media_group.add_argument(
        "--set-video-frame-format",
        choices=["jpg", "png"],
        help="Set extracted frame format for video frame fallback (default: jpg).",
    )
    media_group.add_argument(
        "--set-video-sampling-strategy",
        choices=["uniform", "keyframes"],
        help="Set frame sampling strategy for video frame fallback (default: uniform).",
    )
    media_group.add_argument(
        "--set-video-max-frame-side",
        metavar="PX",
        type=int,
        help="Set max side length for extracted video frames (default: 1024).",
    )
    media_group.add_argument(
        "--set-video-max-size-bytes",
        metavar="BYTES",
        type=int,
        help="Set max allowed video size for processing (bytes). Use 0 to clear.",
    )

    # Embeddings group
    embed_group = parser.add_argument_group('Embeddings Configuration')
    embed_group.add_argument("--set-embeddings-model", metavar="MODEL",
                            help="Set model for semantic search (format: provider/model or provider:model)")
    embed_group.add_argument("--set-embeddings-provider", nargs="?", const=True, metavar="PROVIDER",
                            help="Set embeddings provider (huggingface, openai, etc.)")
    embed_group.add_argument("--set-embeddings-base-url", metavar="URL",
                            help="Set optional embeddings provider base URL")

    # Legacy compatibility (hidden in advanced section)
    legacy_group = parser.add_argument_group('Legacy Options')
    legacy_group.add_argument("--set-default-model", metavar="MODEL",
                             help="Set global default model (use --set-global-default instead)")
    legacy_group.add_argument("--set-default-provider", metavar="PROVIDER",
                             help="Set default provider only (use --set-global-default instead)")
    legacy_group.add_argument("--set-vision-caption", metavar="MODEL",
                             help="DEPRECATED: Use --set-vision-provider instead")

    # Storage and logging group
    storage_group = parser.add_argument_group('Storage & Logging')
    storage_group.add_argument("--set-default-cache-dir", metavar="PATH",
                              help="Set default cache directory for models and data")
    storage_group.add_argument("--set-huggingface-cache-dir", metavar="PATH",
                              help="Set HuggingFace models cache directory")
    storage_group.add_argument("--set-local-models-cache-dir", metavar="PATH",
                              help="Set local vision/embedding models cache directory")
    storage_group.add_argument("--set-log-base-dir", metavar="PATH",
                              help="Set directory for log files")

    # Logging control group
    logging_group = parser.add_argument_group('Logging Control')
    logging_group.add_argument("--set-console-log-level", metavar="LEVEL",
                              choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "NONE"],
                              help="Set console logging level (default: WARNING)")
    logging_group.add_argument("--set-file-log-level", metavar="LEVEL",
                              choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "NONE"],
                              help="Set file logging level (default: DEBUG)")
    logging_group.add_argument("--enable-debug-logging", action="store_true",
                              help="Enable debug logging for both console and file")
    logging_group.add_argument("--disable-console-logging", action="store_true",
                              help="Disable all console logging output")
    logging_group.add_argument("--enable-file-logging", action="store_true",
                              help="Enable saving logs to files")
    logging_group.add_argument("--disable-file-logging", action="store_true",
                              help="Disable file logging")

    # Streaming configuration group
    streaming_group = parser.add_argument_group('Streaming Configuration')
    streaming_group.add_argument("--stream", choices=["on", "off"],
                                 help="Set default streaming behavior for CLI (on/off)")
    streaming_group.add_argument("--enable-streaming", action="store_true",
                                help="Enable streaming by default for CLI")
    streaming_group.add_argument("--disable-streaming", action="store_true",
                                help="Disable streaming by default for CLI")

    # Timeout configuration group
    timeout_group = parser.add_argument_group('Timeout Configuration')
    timeout_group.add_argument("--set-default-timeout", type=float, metavar="SECONDS",
                              help="Set default HTTP request timeout in seconds (default: 7200 = 2 hours; 0 = unlimited)")
    timeout_group.add_argument("--set-tool-timeout", type=float, metavar="SECONDS",
                              help="Set tool execution timeout in seconds (default: 600 = 10 minutes; 0 = unlimited)")

def print_status():
    """Print comprehensive configuration status with improved readability."""
    if not CONFIG_AVAILABLE or get_config_manager is None:
        print("❌ Configuration system not available")
        print("💡 The AbstractCore configuration module is missing")
        return

    config_manager = get_config_manager()
    status = config_manager.get_status()

    # Header with clear context
    print("📋 AbstractCore Default Configuration Status")
    print("   (Explicit parameters in commands override these defaults)")
    print("=" * 75)

    # ESSENTIAL SECTION - What users care about most
    print("\n┌─ ESSENTIAL CONFIGURATION")
    print("│")

    # App defaults with improved formatting
    print("│  🎯 Application Defaults")
    app_defaults = status["app_defaults"]

    apps = [
        ("CLI (utils)", app_defaults["cli"]),
        ("Summarizer", app_defaults["summarizer"]),
        ("Extractor", app_defaults["extractor"]),
        ("Judge", app_defaults["judge"]),
        ("Intent", app_defaults["intent"])
    ]

    for app_name, app_info in apps:
        status_icon = "✅" if app_info["provider"] and app_info["model"] else "⚠️"
        model_text = f"{app_info['provider']}/{app_info['model']}" if app_info["provider"] and app_info["model"] else "Using global fallback"
        print(f"│     {status_icon} {app_name:<12} {model_text}")

    # Global fallback
    print("│")
    print("│  🌐 Global Fallback")
    defaults = status["global_defaults"]
    if defaults["provider"] and defaults["model"]:
        print(f"│     ✅ Default         {defaults['provider']}/{defaults['model']}")
    else:
        print(f"│     ⚠️  Default         Using built-in default (huggingface/unsloth/Qwen3-4B-Instruct-2507-GGUF)")

    # Show specialized models if set
    chat_model = defaults['chat_model']
    code_model = defaults['code_model']
    if chat_model or code_model:
        print("│     ┌─ Specialized Models")
        if chat_model:
            print(f"│     │  💬 Chat          {chat_model}")
        if code_model:
            print(f"│     │  💻 Code          {code_model}")

    capability_defaults = status.get("capability_defaults") or []
    configured_capability_defaults = [
        row for row in capability_defaults
        if isinstance(row, dict) and bool(row.get("configured"))
    ]
    if configured_capability_defaults:
        print("│")
        print("│  🧭 Capability Defaults")
        for row in configured_capability_defaults:
            key = str(row.get("key") or f"{row.get('kind')}.{row.get('modality')}")
            provider = str(row.get("provider") or "-")
            model = str(row.get("model") or "-")
            options = row.get("options")
            option_text = ""
            if isinstance(options, dict) and options:
                option_text = " " + ", ".join(f"{k}={v}" for k, v in sorted(options.items()))
            print(f"│     ✅ {key:<16} {provider}/{model}{option_text}")

    # API Keys status (simplified)
    print("│")
    print("│  🔑 Provider Access")
    api_keys = status["api_keys"]
    configured_keys = [provider for provider, status_text in api_keys.items() if "✅" in status_text]
    missing_keys = [provider for provider, status_text in api_keys.items() if "❌" in status_text]

    if configured_keys:
        print(f"│     ✅ Configured       {', '.join(configured_keys)}")
    if missing_keys:
        print(f"│     ⚠️  Missing keys     {', '.join(missing_keys)}")

    # HTTP server auth/gateway posture
    print("│")
    print("│  🛡️  HTTP Server Gateway")
    server = status.get("server", {})
    if "✅" in str(server.get("auth_token", "")):
        print("│     ✅ Server auth     Auth token configured")
        print("│     🔐 Provider keys   Server-configured keys are available to authenticated clients")
    elif server.get("allow_unauthenticated"):
        print("│     ⚠️  Server auth     Unauthenticated local/dev mode enabled")
        print("│     🔑 Provider keys   Clients must bring provider keys for server-held providers")
    else:
        print("│     ⚠️  Server auth     No auth token configured")
        print("│     🔑 Provider keys   Clients can bring keys via X-AbstractCore-Provider-API-Key")
    if server.get("base_url_allowlist"):
        print(f"│     🌐 base_url list   {server['base_url_allowlist']}")
    if server.get("url_fetch_allowlist"):
        print(f"│     🌐 URL fetch list  {server['url_fetch_allowlist']}")
    if server.get("media_root"):
        print(f"│     📁 Media root      {server['media_root']}")
    if server.get("allow_local_files"):
        print("│     ⚠️  Local files     Unrestricted local files enabled")
    if server.get("host") or server.get("port"):
        host = server.get("host") or "<runtime default>"
        port = server.get("port") or "<runtime default>"
        print(f"│     🖥️  Bind default   {host}:{port}")

    print("└─")

    # SECONDARY SECTION - Important but less frequently changed
    print("\n┌─ SECONDARY CONFIGURATION")
    print("│")

    # Vision with user-friendly descriptions
    print("│  👁️  Media Processing")
    vision = status["vision"]
    strategy_desc = {
        "two_stage": "Smart captioning for text-only models",
        "disabled": "Media processing disabled",
        "basic_metadata": "Basic metadata extraction only"
    }
    vision_status = "✅ Ready" if "✅" in vision['status'] else "⚠️ Not configured"
    strategy_text = strategy_desc.get(vision['strategy'], vision['strategy'])
    print(f"│     {vision_status:<12} {strategy_text}")
    if vision["caption_provider"] and vision["caption_model"]:
        print(f"│     📷 Vision Model     {vision['caption_provider']}/{vision['caption_model']}")

    # Audio (STT fallback via capability plugins)
    audio = status.get("audio", {})
    audio_strategy = str(audio.get("strategy") or "native_only").strip()
    abstractvoice_installed = False
    try:
        import importlib.util

        abstractvoice_installed = importlib.util.find_spec("abstractvoice") is not None
    except Exception:
        abstractvoice_installed = False
    audio_desc = {
        "native_only": "Native audio only (errors on text-only models)",
        "speech_to_text": "Speech-to-text fallback (requires `abstractvoice`)",
        "auto": "Native when supported, otherwise STT (requires `abstractvoice`)",
        "caption": "Audio caption fallback (reserved)",
    }
    audio_strategy_norm = audio_strategy.strip().lower()
    if audio_strategy_norm in {"speech_to_text", "auto"} and abstractvoice_installed:
        audio_status = "✅ Enabled"
    elif audio_strategy_norm in {"speech_to_text", "auto"} and not abstractvoice_installed:
        audio_status = "⚠️ Disabled"
    else:
        audio_status = "⚠️ Disabled"
    print(f"│     🎧 Audio           {audio_status:<10} {audio_desc.get(audio_strategy_norm, audio_strategy)}")
    stt_backend_id = audio.get("stt_backend_id")
    stt_language = audio.get("stt_language")
    if stt_backend_id:
        print(f"│     🔎 STT backend     {stt_backend_id}")
    if stt_language:
        print(f"│     🌐 STT language    {stt_language}")

    # Video (native where supported; otherwise frames fallback via ffmpeg)
    video = status.get("video", {})
    video_strategy = str(video.get("strategy") or "auto").strip()
    video_desc = {
        "native_only": "Native video only (errors unless the model supports video input)",
        "frames_caption": "Frames fallback (sample frames via ffmpeg; requires vision handling)",
        "auto": "Native when supported, otherwise frames fallback (ffmpeg)",
    }
    video_status = "✅ Enabled" if video_strategy in {"frames_caption", "auto"} else "⚠️ Strict"
    print(f"│     🎞️  Video           {video_status:<10} {video_desc.get(video_strategy, video_strategy)}")
    max_frames = video.get("max_frames")
    max_frames_native = video.get("max_frames_native")
    frame_format = video.get("frame_format")
    sampling_strategy = video.get("sampling_strategy")
    max_frame_side = video.get("max_frame_side")
    max_video_size_bytes = video.get("max_video_size_bytes")
    details = []
    if max_frames is not None:
        details.append(f"max_frames={max_frames}")
    if max_frames_native is not None:
        details.append(f"max_frames_native={max_frames_native}")
    if sampling_strategy:
        details.append(f"sampling={sampling_strategy}")
    if frame_format:
        details.append(f"format={frame_format}")
    if max_frame_side is not None:
        details.append(f"max_side={max_frame_side}")
    if max_video_size_bytes is not None:
        details.append(f"max_bytes={max_video_size_bytes}")
    if details:
        print(f"│     🖼️  Video frames    {', '.join(details)}")

    # Embeddings
    print("│")
    print("│  🔗 Embeddings")
    embeddings = status["embeddings"]
    emb_status = "✅ Ready" if "✅" in embeddings['status'] else "⚠️ Not configured"
    print(f"│     {emb_status:<12} {embeddings['provider']}/{embeddings['model']}")
    if embeddings.get("base_url"):
        print(f"│     🌐 base_url      {embeddings['base_url']}")
    if embeddings.get("source"):
        print(f"│     route          {embeddings.get('route') or 'embedding.text'} ({embeddings['source']})")

    # Streaming configuration
    print("│")
    print("│  🌊 Streaming")
    streaming = status["streaming"]
    stream_status = "✅ Enabled" if streaming['cli_stream_default'] else "⚠️ Disabled"
    stream_desc = "Real-time response display by default" if streaming['cli_stream_default'] else "Complete response display by default"
    print(f"│     {stream_status:<12} {stream_desc}")

    print("└─")

    # ADVANCED SECTION - System-level settings
    print("\n┌─ ADVANCED CONFIGURATION")
    print("│")

    # Logging with dual system display
    print("│  📝 Logging")
    logging_info = status["logging"]

    console_level = logging_info['console_level']
    file_level = logging_info['file_level']
    file_enabled = logging_info['file_logging_enabled']

    # Console logging status
    console_status = "✅" if console_level not in ["NONE", "CRITICAL"] else "❌"
    print(f"│     {console_status} Console        {console_level}")

    # File logging status
    if file_enabled:
        file_status = "✅"
        print(f"│     {file_status} File           {file_level}")
    else:
        file_status = "❌"
        print(f"│     {file_status} File           Disabled")

    # Overall summary
    if console_level == "NONE" and not file_enabled:
        overall_desc = "No logging output"
    elif console_level == "DEBUG" and file_enabled:
        overall_desc = "Full debug logging enabled"
    elif file_enabled:
        overall_desc = "Dual logging active"
    else:
        overall_desc = "Console logging only"

    print(f"│     📊 Summary        {overall_desc}")

    # Cache (simplified)
    print("│")
    print("│  💾 Storage")
    cache = status["cache"]
    print(f"│     ✅ Configured      Cache: {cache['default_cache_dir']}")

    # Timeouts
    print("│")
    print("│  ⏱️  Timeouts")
    timeouts = status["timeouts"]
    default_timeout = timeouts['default_timeout']
    tool_timeout = timeouts['tool_timeout']

    # Format timeout values for display (convert seconds to minutes if >= 60)
    def format_timeout(seconds):
        if seconds >= 60:
            minutes = seconds / 60
            if minutes == int(minutes):
                return f"{int(minutes)}m"
            else:
                return f"{minutes:.1f}m"
        else:
            return f"{int(seconds)}s"

    default_timeout_str = format_timeout(default_timeout)
    tool_timeout_str = format_timeout(tool_timeout)

    print(f"│     ⏱️  HTTP Requests   {default_timeout_str} ({default_timeout}s)")
    print(f"│     🔧 Tool Execution  {tool_timeout_str} ({tool_timeout}s)")

    print("└─")

    # HELP SECTION - Separate actionable commands
    print("\n┌─ QUICK CONFIGURATION COMMANDS")
    print("│")
    print("│  🚀 Common Tasks")
    print("│     abstractcore --set-global-default PROVIDER/MODEL")
    print("│     abstractcore --set-app-default APPNAME PROVIDER MODEL")
    print("│     abstractcore --set-api-key PROVIDER YOUR_KEY")
    print("│     abstractcore --set-server-auth-token SERVER_TOKEN")
    print("│")
    print("│  🔧 Media & Behavior")
    print("│     abstractcore --set-vision-provider PROVIDER MODEL")
    print("│     abstractcore --download-vision-model  (local models)")
    print("│     abstractcore --set-audio-strategy {native_only|speech_to_text|auto}")
    print("│     abstractcore --set-stt-language LANG  (optional)")
    print("│     abstractcore --set-video-strategy {native_only|frames_caption|auto}")
    print("│     abstractcore --set-video-max-frames N  (frame fallback budget)")
    print("│     abstractcore --stream on/off")
    print("│     abstractcore --enable-streaming / --disable-streaming")
    print("│")
    print("│  📊 Logging & Storage")
    print("│     abstractcore --enable-debug-logging")
    print("│     abstractcore --set-console-log-level LEVEL")
    print("│     abstractcore --set-file-log-level LEVEL")
    print("│     abstractcore --enable-file-logging / --disable-file-logging")
    print("│     abstractcore --set-default-cache-dir PATH")
    print("│")
    print("│  ⏱️  Performance & Timeouts")
    print("│     abstractcore --set-default-timeout SECONDS  (HTTP requests, default: 7200; 0 = unlimited)")
    print("│     abstractcore --set-tool-timeout SECONDS  (Tool execution, default: 600; 0 = unlimited)")
    print("│")
    print("│  🎯 Specialized Models")
    print("│     abstractcore --set-chat-model PROVIDER/MODEL")
    print("│     abstractcore --set-code-model PROVIDER/MODEL")
    print("│     abstractcore --set-embeddings-model PROVIDER/MODEL")
    print("│")
    print("│  🎛️  Advanced")
    print("│     abstractcore --configure / --config  (interactive setup)")
    print("│     abstractcore --reset  (reset to defaults)")
    print("│     abstractcore --list-api-keys  (check API status)")
    print("│     abstractcore --set-server-base-url-allowlist CSV")
    print("│")
    print("│  📖 More Help")
    print("│     abstractcore --help")
    print("│     docs/centralized-config.md")
    print("└─")

    print(f"\n📁 Configuration file: {status['config_file']}")

def interactive_configure():
    """Interactive configuration setup."""
    config_manager = get_config_manager()

    print("🚀 AbstractCore Interactive Configuration")
    print("=" * 50)

    # Ask about default model
    print("\n1. Default Model Setup")
    default_choice = input("Set a default model? [y/N]: ").lower().strip()
    if default_choice == 'y':
        model = input("Enter model (provider/model format): ").strip()
        if model:
            config_manager.set_default_model(model)
            print(f"✅ Set default model to: {model}")

            # Determine the provider from the model string.
            if "/" in model:
                selected_provider = model.split("/", 1)[0].lower()
            else:
                selected_provider = "ollama"

            # For local providers, the base URL matters — ask about it.
            # These are the providers where the server address is user-configurable.
            _LOCAL_PROVIDER_ENV_VARS = {
                "ollama": ("OLLAMA_BASE_URL", "http://localhost:11434"),
                "lmstudio": ("LMSTUDIO_BASE_URL", "http://localhost:1234/v1"),
                "vllm": ("VLLM_BASE_URL", "http://localhost:8000/v1"),
                "openai-compatible": ("OPENAI_BASE_URL", "http://localhost:1234/v1"),
            }
            if selected_provider in _LOCAL_PROVIDER_ENV_VARS:
                env_var, default_url = _LOCAL_PROVIDER_ENV_VARS[selected_provider]
                current_url = os.environ.get(env_var, "")
                if current_url:
                    print(f"   ℹ️  {env_var} is already set to: {current_url}")
                else:
                    print(f"   ℹ️  {selected_provider} uses env var {env_var} (default: {default_url})")
                    base_url = input(f"   Enter base URL for {selected_provider} (or press Enter for default): ").strip()
                    if base_url:
                        os.environ[env_var] = base_url
                        print(f"   ✅ Set {env_var}={base_url} (current session)")
                        print(f"   💡 To make this permanent, add to your shell profile:")
                        print(f'      export {env_var}="{base_url}"')
                    else:
                        print(f"   ✅ Using default: {default_url}")

    # Ask about vision
    print("\n2. Vision Fallback Setup")
    vision_choice = input("Configure vision fallback for text-only models? [y/N]: ").lower().strip()
    if vision_choice == 'y':
        print("Vision fallback supports any provider and any model (local or cloud).")
        print("Examples (non-exhaustive):")
        print("  lmstudio/qwen/qwen2.5-vl-7b, huggingface/Salesforce/blip-image-captioning-base, mlx/<vision-model>")
        print("  openai/gpt-4o, anthropic/claude-3-5-sonnet, openai-compatible/my-vision-model")
        print("Tip: use `abstractcore --download-vision-model` for offline caption models.")

        provider_raw = input("Enter vision provider id (or provider/model): ").strip()
        model = input("Enter vision model name (or leave blank if provider/model): ").strip()
        # Allow provider/model in a single input to keep the prompt flexible.
        if provider_raw and not model and "/" in provider_raw:
            provider, model = provider_raw.split("/", 1)
        else:
            provider = provider_raw
        if provider and model:
            # Keep the vision fallback provider-agnostic to match runtime capabilities.
            config_manager.set_vision_provider(provider, model)
            print(f"✅ Set vision to: {provider}/{model}")
        else:
            print("⚠️  Skipped vision fallback (provider and model are required).")

    # Ask about API keys
    print("\n3. API Keys Setup")
    api_choice = input("Configure API keys? [y/N]: ").lower().strip()
    if api_choice == 'y':
        for provider in ["openai", "anthropic", "openrouter", "portkey", "openai-compatible", "vllm", "google"]:
            key = input(f"Enter {provider} API key (or press Enter to skip): ").strip()
            if key:
                config_manager.set_api_key(provider, key)
                print(f"✅ Set {provider} API key")

    # Ask about HTTP server auth and hardening.
    print("\n4. HTTP Server / Gateway Auth")
    print("The optional OpenAI-compatible server can use provider keys saved above.")
    print("With a server auth token, clients authenticate to AbstractCore and can use server-configured provider keys.")
    print("Without a server auth token, clients must bring provider keys via X-AbstractCore-Provider-API-Key.")
    print("Provider keys in request bodies or query strings are disabled.")
    server_choice = input("Configure HTTP server auth/hardening? [y/N]: ").lower().strip()
    if server_choice == 'y':
        server_token = input("Server auth token (blank to skip, 'generate' to create one, 'clear' to remove): ").strip()
        server_token_lower = server_token.lower()
        if server_token_lower == "generate":
            server_token = secrets.token_urlsafe(32)
            print(f"Generated server auth token: {server_token}")
        if server_token_lower == "clear":
            config_manager.set_server_auth_token(None)
            print("✅ Cleared AbstractCore server auth token")
        elif server_token:
            config_manager.set_server_auth_token(server_token)
            print("✅ Set AbstractCore server auth token")

        unauth = input("Allow unauthenticated local/dev server requests? [y/N]: ").lower().strip()
        if unauth in ("y", "yes"):
            config_manager.set_server_allow_unauthenticated(True)
            print("⚠️  Enabled unauthenticated server mode (local/dev only)")
        elif unauth in ("n", "no", ""):
            config_manager.set_server_allow_unauthenticated(False)

        base_url_allowlist = input("Additional base_url allowlist CSV (blank = loopback only, 'clear' to remove): ").strip()
        if base_url_allowlist.lower() == "clear":
            config_manager.set_server_base_url_allowlist(None)
            print("✅ Cleared server base_url allowlist")
        elif base_url_allowlist:
            config_manager.set_server_base_url_allowlist(base_url_allowlist)
            print(f"✅ Set server base_url allowlist: {base_url_allowlist}")

        url_fetch_allowlist = input("URL media fetch allowlist CSV (blank = public URLs only, 'clear' to remove): ").strip()
        if url_fetch_allowlist.lower() == "clear":
            config_manager.set_server_url_fetch_allowlist(None)
            print("✅ Cleared server URL fetch allowlist")
        elif url_fetch_allowlist:
            config_manager.set_server_url_fetch_allowlist(url_fetch_allowlist)
            print(f"✅ Set server URL fetch allowlist: {url_fetch_allowlist}")

        media_root = input("Safe local media root for HTTP file paths (blank to skip, 'clear' to remove): ").strip()
        if media_root.lower() == "clear":
            config_manager.set_server_media_root(None)
            print("✅ Cleared server media root")
        elif media_root:
            config_manager.set_server_media_root(media_root)
            print(f"✅ Set server media root: {media_root}")

        allow_local_files = input("Allow unrestricted local file paths? [y/N]: ").lower().strip()
        if allow_local_files in ("y", "yes"):
            config_manager.set_server_allow_local_files(True)
            print("⚠️  Enabled unrestricted local file paths for HTTP requests")
        elif allow_local_files in ("n", "no", ""):
            config_manager.set_server_allow_local_files(False)

        host = input("Default server host (blank = runtime default): ").strip()
        if host:
            if config_manager.set_server_bind(host=host):
                print(f"✅ Set server host: {host}")
            else:
                print(f"⚠️  Invalid server host: {host}")

        port_raw = input("Default server port (blank = runtime default): ").strip()
        if port_raw:
            try:
                port = int(port_raw)
            except ValueError:
                print(f"⚠️  Invalid server port: {port_raw}")
            else:
                if config_manager.set_server_bind(port=port):
                    print(f"✅ Set server port: {port}")
                else:
                    print(f"⚠️  Invalid server port: {port_raw}")

    # Ask about audio strategy (voice/STT fallback for audio attachments)
    print("\n5. Audio Strategy (voice/STT fallback)")
    print("How should AbstractCore handle audio attachments with text-only models?")
    print("  auto           — native when supported, otherwise STT via abstractvoice (recommended)")
    print("  speech_to_text — always transcribe via abstractvoice")
    print("  native_only    — only models with native audio support (no fallback)")
    audio_choice = input("Audio strategy [auto]: ").strip().lower()
    if not audio_choice:
        audio_choice = "auto"
    if audio_choice in ("native_only", "auto", "speech_to_text"):
        config_manager.set_audio_strategy(audio_choice)
        print(f"✅ Set audio strategy to: {audio_choice}")
        if audio_choice in ("auto", "speech_to_text"):
            print('   💡 Requires: pip install "abstractcore[voice]"')
    else:
        print("⚠️  Invalid choice; keeping existing audio strategy.")

    # Ask about video strategy (frame fallback for video attachments)
    print("\n6. Video Strategy (frame fallback)")
    print("How should AbstractCore handle video attachments?")
    print("  auto           — native when supported, otherwise sample frames via ffmpeg (recommended)")
    print("  frames_caption — always sample frames via ffmpeg")
    print("  native_only    — only models with native video support (no fallback)")
    video_choice = input("Video strategy [auto]: ").strip().lower()
    if not video_choice:
        video_choice = "auto"
    if video_choice in ("native_only", "auto", "frames_caption"):
        config_manager.set_video_strategy(video_choice)
        print(f"✅ Set video strategy to: {video_choice}")
        if video_choice in ("auto", "frames_caption"):
            print("   💡 Requires: ffmpeg/ffprobe on PATH + a vision-capable model or vision fallback")
    else:
        print("⚠️  Invalid choice; keeping existing video strategy.")

    # Ask about embeddings provider/model
    print("\n7. Embeddings Setup")
    print("Embeddings are used for semantic search, RAG pipelines, and knowledge graph retrieval.")
    print("Supported: huggingface (local), ollama, lmstudio, openai, openrouter, portkey, openai-compatible.")
    emb_choice = input("Configure embeddings provider/model? [y/N]: ").lower().strip()
    if emb_choice == 'y':
        print("Examples:")
        print("  huggingface/all-minilm-l6-v2          (local, lightweight, default)")
        print("  huggingface/BAAI/bge-small-en-v1.5    (local, good quality)")
        print("  ollama/nomic-embed-text               (local, via Ollama server)")
        print("  lmstudio/<embedding-model>            (local, via LMStudio server)")
        print("  openai/text-embedding-3-small         (cloud, requires API key)")
        print("  openrouter/<embedding-model>          (cloud, via OpenRouter gateway)")
        print("  portkey/<embedding-model>             (cloud, via Portkey gateway)")
        print("  openai-compatible/<model>             (any OpenAI-compatible endpoint)")
        emb_model = input("Enter embeddings model (provider/model format, or press Enter to keep default): ").strip()
        if emb_model:
            # Validate the provider before saving
            if "/" in emb_model:
                emb_prov = emb_model.split("/", 1)[0].lower()
            else:
                emb_prov = "huggingface"
            _valid_emb_provs = ("huggingface", "ollama", "lmstudio", "openai",
                                "openrouter", "portkey", "openai-compatible")
            if emb_prov not in _valid_emb_provs:
                print(f"⚠️  Provider '{emb_prov}' is not supported for embeddings.")
                print(f"   Supported: {', '.join(_valid_emb_provs)}")
                print(f"   Keeping current config.")
            else:
                config_manager.set_embeddings_model(emb_model)
                print(f"✅ Set embeddings model to: {emb_model}")
        else:
            print("   ✅ Keeping default: huggingface/all-minilm-l6-v2")

    # Ask about console log verbosity
    print("\n8. Console Logging Verbosity")
    print("Choose console verbosity level:")
    print("  none | error | warning | info | debug")
    level = input("Console log level [error]: ").strip().lower()
    if not level:
        level = "error"
    level_map = {
        "none": "NONE",
        "error": "ERROR",
        "warning": "WARNING",
        "info": "INFO",
        "debug": "DEBUG",
    }
    if level in level_map:
        config_manager.set_console_log_level(level_map[level])
        print(f"✅ Set console log level to: {level_map[level]}")
    else:
        print("⚠️  Invalid level; keeping existing console log level.")

    print("\n✅ Configuration complete! Run 'abstractcore --status' to see current settings.")


# ---------------------------------------------------------------------------
# Preflight: comprehensive readiness check
# ---------------------------------------------------------------------------

def _ask_yes(prompt: str, auto_accept: bool) -> bool:
    """Ask a yes/no question. If *auto_accept*, return True without prompting."""
    if auto_accept:
        print(f"{prompt} → auto-accepted (--yes)")
        return True
    answer = input(f"{prompt} [y/N]: ").strip().lower()
    return answer in ("y", "yes")


def install_check(auto_accept: bool = False) -> None:
    """Check all subsystems and download/install missing models and dependencies.

    For each area, report ✅ (ready), ⚠️ (degraded but functional),
    or ❌ (missing / broken).  When a gap is fixable (download, prefetch),
    offer to fix it — or auto-fix if *auto_accept* is True.
    """
    if not CONFIG_AVAILABLE or get_config_manager is None:
        print("❌ Configuration system not available — cannot run preflight.")
        return

    config_manager = get_config_manager()
    status = config_manager.get_status()

    print("📦 AbstractCore Install Check")
    print("=" * 60)

    total = 0
    passed = 0
    warnings = 0
    failed = 0
    actions: list[str] = []  # summary of actions taken

    def _pass(label: str, detail: str = ""):
        nonlocal total, passed
        total += 1
        passed += 1
        suffix = f" — {detail}" if detail else ""
        print(f"  ✅ {label}{suffix}")

    def _warn(label: str, detail: str = ""):
        nonlocal total, warnings
        total += 1
        warnings += 1
        suffix = f" — {detail}" if detail else ""
        print(f"  ⚠️  {label}{suffix}")

    def _fail(label: str, detail: str = ""):
        nonlocal total, failed
        total += 1
        failed += 1
        suffix = f" — {detail}" if detail else ""
        print(f"  ❌ {label}{suffix}")

    # ------------------------------------------------------------------
    # 1. Default model configured
    # ------------------------------------------------------------------
    print("\n┌─ 1. Default Model")
    defaults = status["global_defaults"]
    if defaults["provider"] and defaults["model"]:
        _pass("Default model", f"{defaults['provider']}/{defaults['model']}")
    else:
        _fail("Default model", "not configured")
        print("     💡 Fix: abstractcore --config  (step 1)")

    # ------------------------------------------------------------------
    # 2. Provider reachable (for local providers, quick HTTP probe)
    # ------------------------------------------------------------------
    print("\n┌─ 2. Provider Connectivity")
    provider = (defaults.get("provider") or "").lower()
    _PROVIDER_HEALTH_URLS = {
        "ollama": (os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434"), "/"),
        "lmstudio": (os.environ.get("LMSTUDIO_BASE_URL", "http://localhost:1234/v1"), "/models"),
        "vllm": (os.environ.get("VLLM_BASE_URL", "http://localhost:8000/v1"), "/models"),
    }
    if provider in _PROVIDER_HEALTH_URLS:
        base, path = _PROVIDER_HEALTH_URLS[provider]
        url = f"{base.rstrip('/')}{path}"
        try:
            import httpx
            resp = httpx.get(url, timeout=3.0)
            if resp.status_code < 500:
                _pass(f"{provider} reachable", url)
            else:
                _warn(f"{provider} responded {resp.status_code}", url)
        except Exception as exc:
            _fail(f"{provider} unreachable", f"{url} ({type(exc).__name__})")
            print(f"     💡 Ensure your {provider} server is running.")
    elif provider in ("openai", "anthropic", "openrouter", "portkey", "google"):
        # Cloud providers — check API key instead of connectivity
        api_keys = status.get("api_keys", {})
        key_status = api_keys.get(provider, "")
        if "✅" in key_status:
            _pass(f"{provider} API key", "configured")
        else:
            _fail(f"{provider} API key", "missing")
            print(f"     💡 Fix: abstractcore --set-api-key {provider} <YOUR_KEY>")
    elif provider:
        _warn(f"{provider}", "connectivity check not implemented for this provider")
    else:
        _warn("Provider", "no default provider configured")

    # ------------------------------------------------------------------
    # 3. Embeddings model
    # ------------------------------------------------------------------
    print("\n┌─ 3. Embeddings")
    emb_cfg = status.get("embeddings", {})
    emb_model = emb_cfg.get("model", "all-minilm-l6-v2")
    emb_provider = (emb_cfg.get("provider") or "huggingface").lower()

    from ..embeddings.models import (
        get_provider_config,
        list_available_providers as list_embedding_providers,
        list_providers_requiring_local_model_files,
    )

    # Only in-process providers require local model-file checks. Endpoint-backed
    # providers may target loopback, LAN, or hosted URLs depending on base_url.
    _LOCAL_MODEL_FILE_EMB_PROVIDERS = set(list_providers_requiring_local_model_files())
    _SUPPORTED_EMB_PROVIDERS = set(list_embedding_providers())

    if emb_provider not in _SUPPORTED_EMB_PROVIDERS:
        _fail("Embeddings provider", f"'{emb_provider}' is not supported for embeddings")
        print(f"     💡 Supported providers: {', '.join(sorted(_SUPPORTED_EMB_PROVIDERS))}")
        print(f"     💡 Fix: abstractcore --set-embeddings-model <provider>/<model>")
        print(f"     💡 Examples: huggingface/all-minilm-l6-v2, ollama/nomic-embed-text, openai/text-embedding-3-small")
    elif emb_provider not in _LOCAL_MODEL_FILE_EMB_PROVIDERS:
        _pass("Embeddings provider", f"{emb_provider}/{emb_model} (endpoint-backed)")
        provider_config = get_provider_config(emb_provider)
        configured_base_url = str(emb_cfg.get("base_url") or "").strip()
        env_base_url = ""
        for env_var in provider_config.base_url_env_vars:
            env_base_url = str(os.environ.get(env_var) or "").strip()
            if env_base_url:
                break
        base = configured_base_url or env_base_url or provider_config.default_base_url or ""
        probe_path = ""
        if emb_provider == "ollama":
            probe_path = "/api/tags"
        elif (
            provider_config.transport == "openai_compatible_http"
            and emb_provider not in {"openrouter", "portkey"}
        ):
            probe_path = "/models"
        if base and probe_path:
            try:
                import httpx
                resp = httpx.get(f"{base.rstrip('/')}{probe_path}", timeout=3.0)
                if resp.status_code < 500:
                    _pass(f"Embeddings server ({emb_provider})", f"reachable at {base}")
                else:
                    _warn(f"Embeddings server ({emb_provider})", f"responded {resp.status_code} at {base}")
            except Exception:
                _warn(
                    f"Embeddings server ({emb_provider})",
                    f"unreachable at {base} — ensure it is running",
                )
        elif emb_provider in ("openai", "openrouter", "portkey"):
            # Cloud provider — check API key
            api_keys = status.get("api_keys", {})
            key_status = api_keys.get(emb_provider, "")
            if "✅" in key_status:
                _pass(f"{emb_provider} API key (for embeddings)", "configured")
            else:
                _warn(f"{emb_provider} API key (for embeddings)", f"not set — required for {emb_provider} embeddings")
                print(f"     💡 Fix: abstractcore --set-api-key {emb_provider} <YOUR_KEY>")
    else:
        # HuggingFace / local provider: needs sentence-transformers + a downloaded model.
        try:
            import importlib.util
            st_available = importlib.util.find_spec("sentence_transformers") is not None
        except Exception:
            st_available = False

        if st_available:
            # Check if the model is cached locally
            model_cached = False
            try:
                cache_dir = Path(os.environ.get(
                    "SENTENCE_TRANSFORMERS_HOME",
                    os.environ.get("HF_HOME", Path.home() / ".cache" / "huggingface"),
                ))
                hub_dir = cache_dir / "hub"
                model_slug = emb_model.replace("/", "--")
                if hub_dir.exists():
                    candidates = list(hub_dir.glob(f"models--*{model_slug}*"))
                    model_cached = len(candidates) > 0
            except Exception:
                pass

            if model_cached:
                _pass("Embeddings model", f"{emb_provider}/{emb_model} (cached)")
            else:
                _warn("Embeddings model", f"{emb_provider}/{emb_model} (not cached — will download on first use)")
                if _ask_yes("     Download embeddings model now?", auto_accept):
                    try:
                        print(f"     ⏳ Downloading {emb_model}...")
                        from sentence_transformers import SentenceTransformer
                        SentenceTransformer(emb_model)
                        _pass("Embeddings model", f"{emb_model} downloaded ✅")
                        actions.append(f"Downloaded embeddings model: {emb_model}")
                    except Exception as exc:
                        _warn("Embeddings download", str(exc))
        else:
            _warn("Embeddings", f"sentence-transformers not installed ({emb_provider}/{emb_model} configured)")
            if _ask_yes('     Install embeddings dependencies now? (pip install "abstractcore[embeddings]")', auto_accept):
                try:
                    print('     ⏳ Running: pip install "abstractcore[embeddings]"')
                    subprocess.run(
                        [sys.executable, "-m", "pip", "install", "abstractcore[embeddings]"],
                        check=True,
                    )
                    _pass("Embeddings deps", "installed ✅")
                    actions.append("Installed abstractcore[embeddings]")
                    # Now try to download the model too
                    if _ask_yes(f"     Download embeddings model ({emb_model}) now?", auto_accept):
                        try:
                            print(f"     ⏳ Downloading {emb_model}...")
                            from sentence_transformers import SentenceTransformer
                            SentenceTransformer(emb_model)
                            _pass("Embeddings model", f"{emb_model} downloaded ✅")
                            actions.append(f"Downloaded embeddings model: {emb_model}")
                        except Exception as exc:
                            _warn("Embeddings download", str(exc))
                except Exception as exc:
                    _warn("Embeddings install", str(exc))

    # ------------------------------------------------------------------
    # 4. Vision fallback
    # ------------------------------------------------------------------
    print("\n┌─ 4. Vision Fallback")
    vision = status.get("vision", {})
    vision_strategy = vision.get("strategy", "disabled")
    if vision_strategy != "disabled" and vision.get("caption_provider") and vision.get("caption_model"):
        _pass("Vision fallback", f"{vision['caption_provider']}/{vision['caption_model']}")
    elif vision_strategy == "disabled":
        _warn("Vision fallback", "disabled (image input on text-only models will fail)")
        print("     💡 Fix: abstractcore --set-vision-provider <PROVIDER> <MODEL>")
        print("     💡  Or: abstractcore --download-vision-model  (local offline model)")
        if _ask_yes("     Download a local vision model (blip-base-caption, ~1GB)?", auto_accept):
            success = download_vision_model("blip-base-caption")
            if success:
                actions.append("Downloaded local vision model: blip-base-caption")
    else:
        _warn("Vision fallback", f"strategy={vision_strategy} but model not fully configured")

    # ------------------------------------------------------------------
    # 5. Audio / Voice (STT + TTS via abstractvoice)
    # ------------------------------------------------------------------
    print("\n┌─ 5. Voice & Audio")
    try:
        import importlib.util
        av_available = importlib.util.find_spec("abstractvoice") is not None
    except Exception:
        av_available = False

    if av_available:
        _pass("abstractvoice", "installed")

        # Check STT model (faster-whisper)
        stt_ready = False
        try:
            # faster-whisper caches under HF cache; check for a small model
            hf_cache = Path(os.environ.get("HF_HOME", Path.home() / ".cache" / "huggingface"))
            hub = hf_cache / "hub"
            if hub.exists():
                stt_candidates = list(hub.glob("models--Systran--faster-whisper-*"))
                stt_ready = len(stt_candidates) > 0
        except Exception:
            pass

        if stt_ready:
            _pass("STT model (faster-whisper)", "cached")
        else:
            _warn("STT model (faster-whisper)", "not prefetched")
            if _ask_yes("     Prefetch STT model (small, ~500MB)?", auto_accept):
                try:
                    print("     ⏳ Running: abstractvoice-prefetch --stt small")
                    subprocess.run(
                        [sys.executable, "-m", "abstractvoice", "download", "--stt", "small"],
                        check=False,  # command may exit 0 even on failure
                    )
                    # Re-check filesystem — don't trust exit code alone
                    stt_rechecked = False
                    try:
                        if hub.exists():
                            stt_rechecked = len(list(hub.glob("models--Systran--faster-whisper-*"))) > 0
                    except Exception:
                        pass
                    if stt_rechecked:
                        _pass("STT model", "prefetched ✅")
                        actions.append("Prefetched STT model: small")
                    else:
                        _warn("STT prefetch", "download failed — model not found in HF cache (network issue?)")
                except Exception as exc:
                    _warn("STT prefetch", f"download failed ({exc})")

        # Check TTS model (Piper)
        piper_dir = Path.home() / ".piper" / "models"
        piper_has_models = piper_dir.exists() and any(piper_dir.glob("*.onnx"))
        if piper_has_models:
            _pass("TTS model (Piper)", "voice models found")
        else:
            _warn("TTS model (Piper)", "no voice models prefetched")
            if _ask_yes("     Prefetch Piper English voice (~50MB)?", auto_accept):
                try:
                    print("     ⏳ Running: abstractvoice-prefetch --piper en")
                    subprocess.run(
                        [sys.executable, "-m", "abstractvoice", "download", "--piper", "en"],
                        check=False,  # command may exit 0 even on failure
                    )
                    # Re-check filesystem — don't trust exit code alone
                    piper_has_models = piper_dir.exists() and any(piper_dir.glob("*.onnx"))
                    if piper_has_models:
                        _pass("TTS model", "prefetched ✅")
                        actions.append("Prefetched TTS voice: en")
                    else:
                        _warn("TTS prefetch", "download failed — no .onnx model found in ~/.piper/models/ (network issue?)")
                except Exception as exc:
                    _warn("TTS prefetch", f"download failed ({exc})")
    else:
        _warn("abstractvoice", "not installed (TTS/STT unavailable)")
        print('     💡 Fix: pip install "abstractcore[voice]"')
        print('     💡 Then: abstractvoice-prefetch --stt small --piper en')

    # Audio strategy
    audio = status.get("audio", {})
    audio_strategy = (audio.get("strategy") or "native_only").strip().lower()
    if audio_strategy in ("auto", "speech_to_text") and av_available:
        _pass("Audio strategy", audio_strategy)
    elif audio_strategy in ("auto", "speech_to_text") and not av_available:
        _fail("Audio strategy", f"{audio_strategy} configured but abstractvoice not installed")
    else:
        _warn("Audio strategy", f"{audio_strategy} (audio attachments limited to native-capable models)")

    # ------------------------------------------------------------------
    # 6. Video (ffmpeg)
    # ------------------------------------------------------------------
    print("\n┌─ 6. Video Processing")
    ffmpeg_path = shutil.which("ffmpeg")
    ffprobe_path = shutil.which("ffprobe")
    if ffmpeg_path and ffprobe_path:
        _pass("ffmpeg + ffprobe", "found on PATH")
    elif ffmpeg_path:
        _warn("ffprobe", "not found on PATH (video frame extraction may fail)")
    else:
        _warn("ffmpeg", "not found on PATH (video frame fallback unavailable)")
        print("     💡 Install ffmpeg: https://ffmpeg.org/download.html")
        print("     💡 macOS: brew install ffmpeg")
        print("     💡 Ubuntu: sudo apt install ffmpeg")

    video = status.get("video", {})
    video_strategy = (video.get("strategy") or "auto").strip().lower()
    if video_strategy in ("auto", "frames_caption"):
        if ffmpeg_path:
            _pass("Video strategy", video_strategy)
        else:
            _warn("Video strategy", f"{video_strategy} but ffmpeg not available")
    else:
        _warn("Video strategy", f"{video_strategy} (video input limited to native-capable models)")

    # ------------------------------------------------------------------
    # 7. Image generation (abstractvision — optional)
    # ------------------------------------------------------------------
    print("\n┌─ 7. Image Generation (optional)")
    try:
        import importlib.util
        avis_available = importlib.util.find_spec("abstractvision") is not None
    except Exception:
        avis_available = False

    if avis_available:
        _pass("abstractvision", "installed")
    else:
        _warn("abstractvision", "not installed (image generation unavailable)")
        print('     💡 Fix: pip install "abstractcore[vision]"')

    # ------------------------------------------------------------------
    # 8. API keys summary
    # ------------------------------------------------------------------
    print("\n┌─ 8. API Keys")
    api_keys = status.get("api_keys", {})
    has_any_key = False
    for prov, key_status in api_keys.items():
        if "✅" in key_status:
            _pass(f"{prov} key", "configured")
            has_any_key = True
        else:
            _warn(f"{prov} key", "not set")
    if not has_any_key:
        print("     ℹ️  No provider API keys configured (local-only usage is fine)")
        print("     💡 Fix: abstractcore --set-api-key <provider> <key>")

    # ------------------------------------------------------------------
    # Summary
    # ------------------------------------------------------------------
    print("\n" + "=" * 60)
    print(f"📦 Install Summary: {passed} passed, {warnings} warnings, {failed} failed  (total: {total})")

    if actions:
        print("\n📋 Actions taken:")
        for action in actions:
            print(f"   • {action}")

    if failed == 0 and warnings == 0:
        print("\n🟢 All systems ready!")
    elif failed == 0:
        print(f"\n🟡 Functional with {warnings} warning(s). Run 'abstractcore --status' for details.")
    else:
        print(f"\n🔴 {failed} critical issue(s). Fix them and re-run 'abstractcore --install'.")

    print()


def _parse_capability_options(items: List[str]) -> dict:
    """Parse repeated KEY=VALUE CLI options, accepting JSON scalar/object values."""
    out = {}
    for item in items or []:
        raw = str(item or "").strip()
        if not raw:
            continue
        if "=" not in raw:
            raise ValueError(f"Invalid capability option {raw!r}; expected KEY=VALUE")
        key, value_raw = raw.split("=", 1)
        key = key.strip()
        if not key:
            raise ValueError(f"Invalid capability option {raw!r}; key is empty")
        value_text = value_raw.strip()
        try:
            value = json.loads(value_text)
        except Exception:
            value = value_text
        out[key] = value
    return out


def _config_manager_for_cli(args):
    if not CONFIG_AVAILABLE:
        raise RuntimeError("Configuration system not available")
    from abstractcore.config.manager import ConfigurationManager

    config_file = getattr(args, "config_file", None)
    config_dir = getattr(args, "config_dir", None)
    if config_file or config_dir:
        return ConfigurationManager(config_file=config_file, config_dir=config_dir)
    return get_config_manager()


def _print_capability_defaults(payload: dict) -> None:
    print("AbstractCore capability defaults")
    print(f"- config_file: {payload.get('config_file')}")
    for item in payload.get("routes", []):
        if not isinstance(item, dict):
            continue
        key = item.get("key") or f"{item.get('kind')}.{item.get('modality')}"
        provider = item.get("provider") or "-"
        model = item.get("model") or "-"
        source = item.get("source") or "-"
        print(f"- {key}: {provider}/{model} ({source})")


def _config_payload(manager) -> dict:
    return {
        "ok": True,
        "version": 1,
        "authority": "abstractcore.local",
        "writable": True,
        "source": "abstractcore.local",
        "config_file": str(manager.config_file),
        "routes": manager.list_capability_defaults(),
        "errors": [],
    }


def _provider_profiles_payload(manager) -> dict:
    return {
        "ok": True,
        "version": 1,
        "authority": "abstractcore.local",
        "writable": True,
        "source": "abstractcore.local",
        "config_file": str(manager.config_file),
        "profiles": manager.list_provider_profiles(),
        "errors": [],
    }


def _print_provider_profiles(payload: dict) -> None:
    print("AbstractCore provider endpoint profiles")
    print(f"- config_file: {payload.get('config_file')}")
    profiles = payload.get("profiles", [])
    if not profiles:
        print("- no profiles configured")
        return
    for profile in profiles:
        enabled = "enabled" if profile.get("enabled") else "disabled"
        key_state = "key set" if profile.get("api_key_set") else "no key"
        models = profile.get("allowed_models") or []
        model_text = "live discovery" if not models else f"{len(models)} fixed model(s)"
        print(
            f"- {profile.get('virtual_provider')}: {profile.get('display_name')} "
            f"({profile.get('provider_family')}, {enabled}, {key_state}, {model_text})"
        )


def _handle_config_defaults(args) -> int:
    manager = _config_manager_for_cli(args)
    payload = _config_payload(manager)
    if getattr(args, "json", False):
        print(json.dumps(payload, indent=2, sort_keys=True))
    else:
        _print_capability_defaults(payload)
    return 0


def _handle_config_set_default(args) -> int:
    manager = _config_manager_for_cli(args)
    try:
        options = _parse_capability_options(getattr(args, "option", []) or [])
    except ValueError as e:
        print(f"❌ Error: {e}")
        return 1
    ok = manager.set_capability_default(
        args.route,
        provider=getattr(args, "provider", None),
        model=getattr(args, "model", None),
        base_url=getattr(args, "base_url", None),
        options=options,
    )
    if not ok:
        print(f"❌ Error: Failed to set capability default for {args.route}")
        return 1
    print(f"✅ Set capability default for {args.route}")
    return 0


def _handle_config_clear_default(args) -> int:
    manager = _config_manager_for_cli(args)
    ok = manager.clear_capability_default(args.route)
    if not ok:
        print(f"❌ Error: Failed to clear capability default for {args.route}")
        return 1
    print(f"✅ Cleared capability default for {args.route}")
    return 0


def _handle_config_providers(args) -> int:
    manager = _config_manager_for_cli(args)
    payload = _provider_profiles_payload(manager)
    if getattr(args, "json", False):
        print(json.dumps(payload, indent=2, sort_keys=True))
    else:
        _print_provider_profiles(payload)
    return 0


def _handle_config_provider(args) -> int:
    manager = _config_manager_for_cli(args)
    profile = manager.get_provider_profile(args.profile)
    if profile is None:
        print(f"❌ Error: Provider profile not found: {args.profile}")
        return 1
    payload = {
        "ok": True,
        "version": 1,
        "authority": "abstractcore.local",
        "config_file": str(manager.config_file),
        "profile": profile.public_dict(),
        "errors": [],
    }
    if getattr(args, "json", False):
        print(json.dumps(payload, indent=2, sort_keys=True))
    else:
        _print_provider_profiles({"config_file": str(manager.config_file), "profiles": [payload["profile"]]})
    return 0


def _handle_config_set_provider(args) -> int:
    manager = _config_manager_for_cli(args)
    try:
        allowed_models = None
        if getattr(args, "clear_models", False):
            allowed_models = []
        elif getattr(args, "allow_model", None):
            allowed_models = list(args.allow_model or [])

        profile = manager.set_provider_profile(
            args.profile,
            display_name=getattr(args, "name", None),
            description=getattr(args, "description", None),
            provider_family=getattr(args, "family", None),
            base_url=getattr(args, "base_url", None),
            api_key=getattr(args, "api_key", None),
            clear_api_key=bool(getattr(args, "clear_api_key", False)),
            allowed_models=allowed_models,
            enabled=(
                False
                if bool(getattr(args, "disabled", False))
                else (True if bool(getattr(args, "enabled", False)) else None)
            ),
        )
    except Exception as e:
        print(f"❌ Error: {e}")
        return 1

    if getattr(args, "json", False):
        print(json.dumps({"ok": True, "profile": profile.public_dict()}, indent=2, sort_keys=True))
    else:
        print(f"✅ Set provider profile {profile.virtual_provider_id}")
    return 0


def _handle_config_delete_provider(args) -> int:
    manager = _config_manager_for_cli(args)
    try:
        ok = manager.delete_provider_profile(args.profile)
    except Exception as e:
        print(f"❌ Error: {e}")
        return 1
    if not ok:
        print(f"❌ Error: Provider profile not found: {args.profile}")
        return 1
    print(f"✅ Deleted provider profile {args.profile}")
    return 0


def _provider_for_model_discovery(manager, provider: str) -> str:
    raw = str(provider or "").strip()
    if raw.lower().startswith("endpoint:"):
        profile = manager.resolve_provider_profile(raw)
        return profile.virtual_provider_id if profile is not None else raw
    try:
        from abstractcore.providers.registry import get_provider_registry

        if get_provider_registry().get_provider_info(raw) is not None:
            return raw
    except Exception:
        pass
    profile = manager.resolve_provider_profile(raw)
    return profile.virtual_provider_id if profile is not None else raw


def _bind_cli_manager_for_registry(manager) -> None:
    """Make provider registry helpers use the same manager selected by CLI flags."""
    try:
        import abstractcore.config.manager as manager_module
        import abstractcore.providers.registry as registry_module

        manager_module._config_manager = manager
        registry_module._registry = None
    except Exception:
        return


def _handle_config_models(args) -> int:
    manager = _config_manager_for_cli(args)
    _bind_cli_manager_for_registry(manager)
    provider = _provider_for_model_discovery(manager, args.provider)
    try:
        from abstractcore.providers.registry import get_available_models_for_provider

        models = get_available_models_for_provider(
            provider,
            force_live_discovery=bool(getattr(args, "live", False)),
            raise_on_error=bool(getattr(args, "raise_on_error", False)),
        )
    except Exception as e:
        print(f"❌ Error: {e}")
        return 1
    payload = {
        "ok": True,
        "provider": provider,
        "models": list(models),
        "count": len(models),
        "errors": [],
    }
    if getattr(args, "json", False):
        print(json.dumps(payload, indent=2, sort_keys=True))
    else:
        print(f"Models for {provider}:")
        if not models:
            print("- no models discovered")
        for model in models:
            print(f"- {model}")
    return 0


def _handle_config_test_provider(args) -> int:
    args.live = True
    args.raise_on_error = True
    rc = _handle_config_models(args)
    if rc == 0:
        if not getattr(args, "json", False):
            print(f"✅ Provider profile reachable: {_provider_for_model_discovery(_config_manager_for_cli(args), args.provider)}")
    return rc


def _handle_config_subcommand(argv: List[str]) -> int:
    parser = argparse.ArgumentParser(
        prog="abstractcore config",
        description="Inspect and edit AbstractCore configuration.",
    )
    parser.add_argument("--config-file", default=None, help="Use a specific AbstractCore config JSON file")
    parser.add_argument("--config-dir", default=None, help="Use a directory containing abstractcore.json")
    sub = parser.add_subparsers(dest="cmd")

    defaults = sub.add_parser("defaults", help="Show effective capability routing defaults")
    defaults.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
    defaults.set_defaults(func=_handle_config_defaults)

    set_default = sub.add_parser("set-default", help="Persist one Core capability routing default")
    set_default.add_argument("route", help="Capability route, e.g. output.text, input.image, embedding.text")
    set_default.add_argument("--provider", default=None, help="Provider/backend id")
    set_default.add_argument("--model", default=None, help="Model id")
    set_default.add_argument("--base-url", default=None, help="Optional provider base URL")
    set_default.add_argument("--option", action="append", default=[], metavar="KEY=VALUE", help="Optional JSON-capable parameter; repeatable")
    set_default.set_defaults(func=_handle_config_set_default)

    clear_default = sub.add_parser("clear-default", help="Clear one Core capability routing default")
    clear_default.add_argument("route", help="Capability route, e.g. output.text")
    clear_default.set_defaults(func=_handle_config_clear_default)

    providers = sub.add_parser("providers", help="Show local provider endpoint profiles")
    providers.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
    providers.set_defaults(func=_handle_config_providers)

    provider = sub.add_parser("provider", help="Show one local provider endpoint profile")
    provider.add_argument("profile", help="Profile id or endpoint:<id>")
    provider.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
    provider.set_defaults(func=_handle_config_provider)

    set_provider = sub.add_parser("set-provider", help="Create or update a local provider endpoint profile")
    set_provider.add_argument("profile", help="Profile id, e.g. ovh-provider")
    set_provider.add_argument("--family", default=None, help="Provider family, default: openai-compatible")
    set_provider.add_argument("--base-url", default=None, help="Endpoint base URL, e.g. https://host/v1")
    set_provider.add_argument(
        "--api-key",
        default=None,
        help="API key value. Use a quoted literal like '$OVH_AI_API_KEY' to store an environment reference.",
    )
    set_provider.add_argument("--clear-api-key", action="store_true", help="Clear the stored API key or environment reference")
    set_provider.add_argument("--name", default=None, help="Human-readable display name")
    set_provider.add_argument("--description", default=None, help="Human-readable profile description")
    set_provider.add_argument("--allow-model", action="append", default=None, help="Expose one fixed model id; repeatable")
    set_provider.add_argument("--clear-models", action="store_true", help="Clear fixed model allowlist and use live discovery")
    set_provider.add_argument("--enabled", action="store_true", help="Enable this profile")
    set_provider.add_argument("--disabled", action="store_true", help="Disable this profile")
    set_provider.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
    set_provider.set_defaults(func=_handle_config_set_provider)

    delete_provider = sub.add_parser("delete-provider", help="Delete a local provider endpoint profile")
    delete_provider.add_argument("profile", help="Profile id or endpoint:<id>")
    delete_provider.set_defaults(func=_handle_config_delete_provider)

    models = sub.add_parser("models", help="List models for a provider or endpoint profile")
    models.add_argument("provider", help="Provider id, profile id, or endpoint:<id>")
    models.add_argument("--live", action="store_true", help="Force live model discovery instead of fixed allowlist")
    models.add_argument("--raise-on-error", action="store_true", help="Fail instead of returning an empty list")
    models.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
    models.set_defaults(func=_handle_config_models)

    test_provider = sub.add_parser("test-provider", help="Force live model discovery for a provider endpoint profile")
    test_provider.add_argument("provider", help="Provider id, profile id, or endpoint:<id>")
    test_provider.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
    test_provider.set_defaults(func=_handle_config_test_provider)

    args = parser.parse_args(argv)
    if not getattr(args, "cmd", None):
        args = parser.parse_args([*(argv or []), "defaults"])
    try:
        return args.func(args)
    except Exception as e:
        print(f"❌ Error: {e}")
        return 1


def _handle_serve_subcommand(argv: List[str]) -> int:
    from abstractcore.server.app import run_server_with_args

    run_server_with_args(argv, prog="abstractcore serve")
    return 0


def handle_commands(args) -> bool:
    """Handle AbstractCore configuration commands."""
    if not CONFIG_AVAILABLE or get_config_manager is None:
        print("❌ Error: Configuration system not available")
        print("💡 The AbstractCore configuration module is missing or not properly installed")
        print("💡 Please reinstall AbstractCore or check your installation")
        return True

    config_manager = get_config_manager()
    handled = False

    # Status and configuration
    if args.status:
        print_status()
        handled = True

    if args.configure:
        interactive_configure()
        handled = True

    if args.install:
        install_check(auto_accept=args.yes)
        handled = True

    if args.reset:
        config_manager.reset_configuration()
        print("✅ Configuration reset to defaults")
        handled = True

    # Global default model settings
    if args.set_global_default:
        config_manager.set_global_default_model(args.set_global_default)
        print(f"✅ Set global default to: {args.set_global_default}")
        handled = True

    if args.set_default_model:  # Legacy compatibility
        config_manager.set_global_default_model(args.set_default_model)
        print(f"✅ Set global default to: {args.set_default_model}")
        handled = True

    if args.set_default_provider:
        config_manager.set_global_default_provider(args.set_default_provider)
        print(f"✅ Set global default provider to: {args.set_default_provider}")
        handled = True

    # App-specific defaults
    if args.set_app_default:
        app, provider, model = args.set_app_default
        try:
            config_manager.set_app_default(app, provider, model)
            print(f"✅ Set {app} default to: {provider}/{model}")
        except ValueError as e:
            print(f"❌ Error: {e}")
        handled = True

    if args.set_chat_model:
        config_manager.set_chat_model(args.set_chat_model)
        print(f"✅ Set chat model to: {args.set_chat_model}")
        handled = True

    if args.set_code_model:
        config_manager.set_code_model(args.set_code_model)
        print(f"✅ Set code model to: {args.set_code_model}")
        handled = True

    # Vision configuration
    if args.set_vision_caption:
        print("⚠️  WARNING: --set-vision-caption is deprecated")
        print("💡 Use instead: abstractcore --set-vision-provider PROVIDER MODEL")
        print("   This provides clearer, more reliable configuration")
        print()
        config_manager.set_vision_caption(args.set_vision_caption)
        print(f"✅ Set vision caption model to: {args.set_vision_caption}")
        handled = True

    if args.set_vision_provider:
        provider, model = args.set_vision_provider
        config_manager.set_vision_provider(provider, model)
        print(f"✅ Set vision provider to: {provider}/{model}")
        handled = True

    if args.add_vision_fallback:
        provider, model = args.add_vision_fallback
        config_manager.add_vision_fallback(provider, model)
        print(f"✅ Added vision fallback: {provider}/{model}")
        handled = True

    if args.disable_vision:
        config_manager.disable_vision()
        print("✅ Disabled vision fallback")
        handled = True

    if args.download_vision_model:
        print(f"📥 Starting download of vision model: {args.download_vision_model}")
        success = download_vision_model(args.download_vision_model)
        if success:
            print(f"✅ Successfully downloaded and configured: {args.download_vision_model}")
        else:
            print(f"❌ Failed to download: {args.download_vision_model}")
        handled = True

    # Audio configuration (speech-to-text fallback via capability plugins, e.g. abstractvoice)
    if getattr(args, "set_audio_strategy", None):
        ok = config_manager.set_audio_strategy(args.set_audio_strategy)
        if ok:
            print(f"✅ Set audio strategy to: {args.set_audio_strategy}")
        else:
            print(f"❌ Error: Invalid audio strategy: {args.set_audio_strategy}")
        handled = True

    if getattr(args, "set_stt_backend_id", None) is not None:
        ok = config_manager.set_stt_backend_id(args.set_stt_backend_id)
        if ok:
            if args.set_stt_backend_id:
                print(f"✅ Set STT backend id to: {args.set_stt_backend_id}")
            else:
                print("✅ Cleared STT backend id")
        else:
            print("❌ Error: Failed to update STT backend id")
        handled = True

    if getattr(args, "set_stt_language", None) is not None:
        ok = config_manager.set_stt_language(args.set_stt_language)
        if ok:
            if args.set_stt_language:
                print(f"✅ Set STT language to: {args.set_stt_language}")
            else:
                print("✅ Cleared STT language")
        else:
            print("❌ Error: Failed to update STT language")
        handled = True

    # Video configuration (native video where supported; otherwise frames fallback via ffmpeg)
    if getattr(args, "set_video_strategy", None):
        ok = config_manager.set_video_strategy(args.set_video_strategy)
        if ok:
            print(f"✅ Set video strategy to: {args.set_video_strategy}")
        else:
            print(f"❌ Error: Invalid video strategy: {args.set_video_strategy}")
        handled = True

    if getattr(args, "set_video_max_frames", None) is not None:
        ok = config_manager.set_video_max_frames(args.set_video_max_frames)
        if ok:
            print(f"✅ Set video max frames (fallback) to: {args.set_video_max_frames}")
        else:
            print(f"❌ Error: Invalid video max frames: {args.set_video_max_frames}")
        handled = True

    if getattr(args, "set_video_max_frames_native", None) is not None:
        ok = config_manager.set_video_max_frames_native(args.set_video_max_frames_native)
        if ok:
            print(f"✅ Set video max frames (native) to: {args.set_video_max_frames_native}")
        else:
            print(f"❌ Error: Invalid video max frames native: {args.set_video_max_frames_native}")
        handled = True

    if getattr(args, "set_video_frame_format", None):
        ok = config_manager.set_video_frame_format(args.set_video_frame_format)
        if ok:
            print(f"✅ Set video frame format to: {args.set_video_frame_format}")
        else:
            print(f"❌ Error: Invalid video frame format: {args.set_video_frame_format}")
        handled = True

    if getattr(args, "set_video_sampling_strategy", None):
        ok = config_manager.set_video_sampling_strategy(args.set_video_sampling_strategy)
        if ok:
            print(f"✅ Set video sampling strategy to: {args.set_video_sampling_strategy}")
        else:
            print(f"❌ Error: Invalid video sampling strategy: {args.set_video_sampling_strategy}")
        handled = True

    if getattr(args, "set_video_max_frame_side", None) is not None:
        ok = config_manager.set_video_max_frame_side(args.set_video_max_frame_side)
        if ok:
            print(f"✅ Set video max frame side to: {args.set_video_max_frame_side}")
        else:
            print(f"❌ Error: Invalid video max frame side: {args.set_video_max_frame_side}")
        handled = True

    if getattr(args, "set_video_max_size_bytes", None) is not None:
        ok = config_manager.set_video_max_video_size_bytes(args.set_video_max_size_bytes)
        if ok:
            if int(args.set_video_max_size_bytes) <= 0:
                print("✅ Cleared video max size")
            else:
                print(f"✅ Set video max size to: {args.set_video_max_size_bytes} bytes")
        else:
            print(f"❌ Error: Invalid video max size bytes: {args.set_video_max_size_bytes}")
        handled = True

    # Embeddings configuration
    if args.set_embeddings_model:
        config_manager.set_embeddings_model(args.set_embeddings_model)
        print(f"✅ Set embeddings model to: {args.set_embeddings_model}")
        handled = True

    if args.set_embeddings_provider:
        if isinstance(args.set_embeddings_provider, str):
            config_manager.set_embeddings_provider(args.set_embeddings_provider)
            print(f"✅ Set embeddings provider to: {args.set_embeddings_provider}")
        handled = True

    if getattr(args, "set_embeddings_base_url", None) is not None:
        config_manager.set_embeddings_base_url(args.set_embeddings_base_url)
        if args.set_embeddings_base_url:
            print(f"✅ Set embeddings base URL to: {args.set_embeddings_base_url}")
        else:
            print("✅ Cleared embeddings base URL")
        handled = True

    # API keys
    if args.set_api_key:
        provider, key = args.set_api_key
        ok = config_manager.set_api_key(provider, key)
        if ok:
            print(f"✅ Set API key for: {provider}")
        else:
            print(f"❌ Error: Unsupported API key provider: {provider}")
        handled = True

    if args.list_api_keys:
        status = config_manager.get_status()
        print("🔑 API Key Status:")
        for provider, status_text in status["api_keys"].items():
            print(f"   {provider}: {status_text}")
        handled = True

    # HTTP server configuration
    if getattr(args, "set_server_auth_token", None):
        config_manager.set_server_auth_token(args.set_server_auth_token)
        print("✅ Set AbstractCore server auth token")
        handled = True

    if getattr(args, "clear_server_auth_token", False):
        config_manager.set_server_auth_token(None)
        print("✅ Cleared AbstractCore server auth token")
        handled = True

    if getattr(args, "allow_unauthenticated_server", False):
        config_manager.set_server_allow_unauthenticated(True)
        print("⚠️  Enabled unauthenticated HTTP server mode (local/dev only)")
        handled = True

    if getattr(args, "disallow_unauthenticated_server", False):
        config_manager.set_server_allow_unauthenticated(False)
        print("✅ Disabled unauthenticated HTTP server mode")
        handled = True

    if getattr(args, "set_server_base_url_allowlist", None) is not None:
        config_manager.set_server_base_url_allowlist(args.set_server_base_url_allowlist)
        if args.set_server_base_url_allowlist:
            print(f"✅ Set server base_url allowlist to: {args.set_server_base_url_allowlist}")
        else:
            print("✅ Cleared server base_url allowlist")
        handled = True

    if getattr(args, "set_server_url_fetch_allowlist", None) is not None:
        config_manager.set_server_url_fetch_allowlist(args.set_server_url_fetch_allowlist)
        if args.set_server_url_fetch_allowlist:
            print(f"✅ Set server URL fetch allowlist to: {args.set_server_url_fetch_allowlist}")
        else:
            print("✅ Cleared server URL fetch allowlist")
        handled = True

    if getattr(args, "set_server_media_root", None) is not None:
        config_manager.set_server_media_root(args.set_server_media_root)
        if args.set_server_media_root:
            print(f"✅ Set server media root to: {args.set_server_media_root}")
        else:
            print("✅ Cleared server media root")
        handled = True

    if getattr(args, "allow_server_local_files", False):
        config_manager.set_server_allow_local_files(True)
        print("⚠️  Enabled unrestricted local file paths for HTTP server requests")
        handled = True

    if getattr(args, "disallow_server_local_files", False):
        config_manager.set_server_allow_local_files(False)
        print("✅ Disabled unrestricted local file paths for HTTP server requests")
        handled = True

    if getattr(args, "set_server_host", None) is not None:
        ok = config_manager.set_server_bind(host=args.set_server_host)
        if ok:
            print(f"✅ Set server host to: {args.set_server_host}")
        else:
            print(f"❌ Error: Invalid server host: {args.set_server_host}")
        handled = True

    if getattr(args, "set_server_port", None) is not None:
        ok = config_manager.set_server_bind(port=args.set_server_port)
        if ok:
            print(f"✅ Set server port to: {args.set_server_port}")
        else:
            print(f"❌ Error: Invalid server port: {args.set_server_port}")
        handled = True

    # Cache configuration
    if args.set_default_cache_dir:
        config_manager.set_default_cache_dir(args.set_default_cache_dir)
        print(f"✅ Set default cache directory to: {args.set_default_cache_dir}")
        handled = True

    if args.set_huggingface_cache_dir:
        config_manager.set_huggingface_cache_dir(args.set_huggingface_cache_dir)
        print(f"✅ Set HuggingFace cache directory to: {args.set_huggingface_cache_dir}")
        handled = True

    if args.set_local_models_cache_dir:
        config_manager.set_local_models_cache_dir(args.set_local_models_cache_dir)
        print(f"✅ Set local models cache directory to: {args.set_local_models_cache_dir}")
        handled = True

    # Logging configuration
    if args.set_console_log_level:
        config_manager.set_console_log_level(args.set_console_log_level)
        print(f"✅ Set console log level to: {args.set_console_log_level}")
        handled = True

    if args.set_file_log_level:
        config_manager.set_file_log_level(args.set_file_log_level)
        print(f"✅ Set file log level to: {args.set_file_log_level}")
        handled = True

    if args.set_log_base_dir:
        config_manager.set_log_base_dir(args.set_log_base_dir)
        print(f"✅ Set log base directory to: {args.set_log_base_dir}")
        handled = True

    if args.enable_debug_logging:
        config_manager.enable_debug_logging()
        print("✅ Enabled debug logging for both console and file")
        handled = True

    if args.disable_console_logging:
        config_manager.disable_console_logging()
        print("✅ Disabled console logging")
        handled = True

    if args.enable_file_logging:
        config_manager.enable_file_logging()
        print("✅ Enabled file logging")
        handled = True

    if args.disable_file_logging:
        config_manager.disable_file_logging()
        print("✅ Disabled file logging")
        handled = True

    # Streaming configuration
    if args.stream:
        enabled = args.stream == "on"
        config_manager.set_streaming_default("cli", enabled)
        status = "enabled" if enabled else "disabled"
        print(f"✅ CLI streaming {status} by default")
        handled = True

    if args.enable_streaming:
        config_manager.enable_cli_streaming()
        print("✅ Enabled CLI streaming by default")
        handled = True

    if args.disable_streaming:
        config_manager.disable_cli_streaming()
        print("✅ Disabled CLI streaming by default")
        handled = True

    # Timeout configuration
    # #[WARNING:TIMEOUT]
    if args.set_default_timeout is not None:
        try:
            ok = config_manager.set_default_timeout(args.set_default_timeout)
            if not ok:
                raise RuntimeError("Configuration update failed")
            # Format display (show in minutes if >= 60 seconds)
            if args.set_default_timeout <= 0:
                display = "unlimited"
            elif args.set_default_timeout >= 60:
                minutes = args.set_default_timeout / 60
                display = f"{minutes:.1f} minutes" if minutes != int(minutes) else f"{int(minutes)} minutes"
            else:
                display = f"{args.set_default_timeout} seconds"
            suffix = "" if args.set_default_timeout <= 0 else f" ({args.set_default_timeout}s)"
            print(f"✅ Set default HTTP timeout to: {display}{suffix}")
        except Exception as e:
            print(f"❌ Failed to set default HTTP timeout: {e}")
        handled = True

    # #[WARNING:TIMEOUT]
    if args.set_tool_timeout is not None:
        try:
            ok = config_manager.set_tool_timeout(args.set_tool_timeout)
            if not ok:
                raise RuntimeError("Configuration update failed")
            # Format display (show in minutes if >= 60 seconds)
            if args.set_tool_timeout <= 0:
                display = "unlimited"
            elif args.set_tool_timeout >= 60:
                minutes = args.set_tool_timeout / 60
                display = f"{minutes:.1f} minutes" if minutes != int(minutes) else f"{int(minutes)} minutes"
            else:
                display = f"{args.set_tool_timeout} seconds"
            suffix = "" if args.set_tool_timeout <= 0 else f" ({args.set_tool_timeout}s)"
            print(f"✅ Set tool execution timeout to: {display}{suffix}")
        except Exception as e:
            print(f"❌ Failed to set tool execution timeout: {e}")
        handled = True

    return handled

_CONFIG_SUBCOMMANDS = {
    "defaults",
    "set-default",
    "clear-default",
    "providers",
    "provider",
    "set-provider",
    "delete-provider",
    "models",
    "test-provider",
}


def main(argv: List[str] = None):
    """Main CLI entry point."""
    if argv is None:
        argv = sys.argv[1:]

    if argv and argv[0] == "serve":
        return _handle_serve_subcommand(argv[1:])
    if argv and argv[0] == "config":
        return _handle_config_subcommand(argv[1:])
    if Path(sys.argv[0]).name == "abstractcore-config" and (
        not argv or argv[0] in _CONFIG_SUBCOMMANDS or argv[0].startswith("--")
    ):
        return _handle_config_subcommand(argv)

    parser = argparse.ArgumentParser(
        prog="abstractcore",
        description="AbstractCore Unified Configuration System",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
QUICK START:
  abstractcore --status                           # Show current configuration
  abstractcore --configure                       # Interactive guided setup (8 steps)
  abstractcore serve                              # Start OpenAI-compatible HTTP server
  abstractcore --install                          # Check & install missing models/deps
  abstractcore --install --yes                   # Auto-download everything that's missing

COMMON TASKS:
  # Set default model for all apps
  abstractcore --set-global-default ollama llama3:8b

  # Set different models for specific apps
  abstractcore --set-app-default cli lmstudio qwen/qwen3-next-80b
  abstractcore --set-app-default summarizer openai gpt-4o-mini
  abstractcore --set-app-default extractor ollama qwen3:4b-instruct

  # Configure API keys
  abstractcore --set-api-key openai sk-your-key-here
  abstractcore --set-api-key anthropic your-anthropic-key
  abstractcore --set-api-key openai-compatible endpoint-key

  # Configure HTTP server auth/gateway behavior
  abstractcore --set-server-auth-token acore-server-secret
  abstractcore --set-server-base-url-allowlist https://example.com/v1
  abstractcore serve --host 127.0.0.1 --port 8000

  # Setup vision for images (with text-only models)
  abstractcore --set-vision-provider ollama qwen2.5vl:7b
  abstractcore --download-vision-model

  # Configure logging
  abstractcore --enable-debug-logging            # Enable debug mode
  abstractcore --set-console-log-level WARNING   # Reduce console output
  abstractcore --enable-file-logging             # Save logs to files

SPECIALIZED MODELS:
  abstractcore --set-chat-model openai/gpt-4o-mini      # For chat applications
  abstractcore --set-code-model anthropic/claude-haiku-4-5    # For coding tasks (cost-effective default)

PRIORITY SYSTEM:
  1. Explicit parameters (highest):  summarizer doc.pdf --provider openai --model gpt-4o
  2. App-specific config:           --set-app-default summarizer openai gpt-4o-mini
  3. Global config:                 --set-global-default openai/gpt-4o-mini
  4. Built-in defaults (lowest):    huggingface/unsloth/Qwen3-4B-Instruct-2507-GGUF

APPS:
  cli        Interactive CLI (python -m abstractcore.utils.cli)
  summarizer Document summarization (summarizer document.pdf)
  extractor  Entity/relationship extraction (extractor data.txt)
  judge      Text evaluation and scoring (judge essay.md)

TROUBLESHOOTING:
  abstractcore --status                          # Check current settings
  abstractcore --reset                          # Reset to defaults
  abstractcore --list-api-keys                  # Check API key status

  If apps show "no provider/model configured":
  abstractcore --set-global-default ollama llama3:8b

DOCUMENTATION: docs/centralized-config.md
        """
    )

    add_arguments(parser)
    args = parser.parse_args(argv)

    try:
        # Handle configuration commands
        if handle_commands(args):
            return 0

        # If no commands were handled, show help
        parser.print_help()
        return 1

    except Exception as e:
        print(f"❌ Error: {e}")
        return 1


if __name__ == "__main__":
    logging.basicConfig(level=logging.ERROR)
    sys.exit(main())
