"""
AbstractCore Configuration Manager

Provides centralized configuration management for AbstractCore.
"""

import json
import os
import importlib.util
from pathlib import Path
from typing import Dict, Any, Optional, Tuple, Union
from dataclasses import dataclass, asdict, fields
from datetime import datetime, timezone

from .capability_defaults import (
    CapabilityDefaultsConfig,
    CapabilityRouteDefault,
    capability_defaults_from_dict,
    capability_route_key,
    iter_capability_default_specs,
    split_capability_route,
)
from .provider_profiles import (
    ProviderProfile,
    ProviderProfilesConfig,
    normalize_base_url,
    normalize_profile_id,
    normalize_provider_family,
    normalize_string_list,
    profile_id_from_virtual_provider,
    provider_profiles_from_dict,
    split_api_key_value,
)

_SERVER_AUTH_TOKEN_ENV_VAR = "ABSTRACTCORE_AUTH_TOKEN"
_PROVIDER_MODEL_PREFIXES = {
    "anthropic",
    "google",
    "huggingface",
    "lmstudio",
    "ollama",
    "openai",
    "openai-compatible",
    "openai_compatible",
    "openrouter",
    "portkey",
    "vllm",
}


def _split_provider_model(value: str, *, default_provider: str) -> Tuple[str, str]:
    raw = str(value or "").strip()
    if not raw:
        raise ValueError("Model cannot be empty")
    if ":" in raw:
        provider, model = raw.split(":", 1)
        provider_clean = provider.strip().lower()
        if provider_clean in _PROVIDER_MODEL_PREFIXES:
            model_clean = model.strip()
            if not model_clean:
                raise ValueError("Model cannot be empty")
            return provider.strip(), model_clean
    if "/" in raw:
        provider, model = raw.split("/", 1)
        model_clean = model.strip()
        if not model_clean:
            raise ValueError("Model cannot be empty")
        return provider.strip() or default_provider, model_clean
    return default_provider, raw


@dataclass
class VisionConfig:
    """Vision configuration settings."""
    strategy: str = "disabled"
    caption_provider: Optional[str] = None
    caption_model: Optional[str] = None
    fallback_chain: list = None
    local_models_path: Optional[str] = None

    def __post_init__(self):
        if self.fallback_chain is None:
            self.fallback_chain = []


@dataclass
class AudioConfig:
    """Audio configuration settings (input policy + optional fallback)."""
    # Default: "auto" — use native audio when supported. Non-native STT fallback
    # is authorized by an explicit input.voice capability default or per-call policy.
    strategy: str = "auto"  # native_only|speech_to_text|caption|auto
    # Optional preferred STT backend (capabilities plugin backend_id).
    stt_backend_id: Optional[str] = None
    stt_language: Optional[str] = None
    # Reserved for future "audio caption" backends.
    caption_provider: Optional[str] = None
    caption_model: Optional[str] = None
    fallback_chain: list = None

    def __post_init__(self):
        if self.fallback_chain is None:
            self.fallback_chain = []


@dataclass
class VideoConfig:
    """Video configuration settings (input policy + optional fallback)."""
    # Default: prefer native video when supported. Sampled-frame fallback is
    # authorized by model capabilities or an explicit input.video capability default.
    strategy: str = "auto"  # native_only|frames_caption|auto

    # Frame sampling controls for frames-based fallback.
    max_frames: int = 3
    # Native video models typically require more temporal coverage than the fallback path.
    # This default is used when the selected model supports native video input (v0: HF only).
    max_frames_native: int = 8
    frame_format: str = "jpg"  # jpg|png
    sampling_strategy: str = "uniform"  # uniform|keyframes

    # Downscale extracted frames (preserve aspect ratio; never upscale). Helps memory + token pressure.
    # Applies to both frames_caption fallback and HF native video ingestion (which uses ffmpeg frames).
    max_frame_side: int = 1024

    # Maximum video size allowed for processing (bytes). None => use media handler defaults.
    max_video_size_bytes: Optional[int] = None


@dataclass
class EmbeddingsConfig:
    """Embeddings configuration settings."""
    provider: Optional[str] = "huggingface"
    model: Optional[str] = "all-minilm-l6-v2"
    base_url: Optional[str] = None


@dataclass
class AppDefaults:
    """Per-application default configurations."""
    cli_provider: Optional[str] = "huggingface"
    cli_model: Optional[str] = "unsloth/Qwen3-4B-Instruct-2507-GGUF"
    summarizer_provider: Optional[str] = "huggingface"
    summarizer_model: Optional[str] = "unsloth/Qwen3-4B-Instruct-2507-GGUF"
    extractor_provider: Optional[str] = "huggingface"
    extractor_model: Optional[str] = "unsloth/Qwen3-4B-Instruct-2507-GGUF"
    judge_provider: Optional[str] = "huggingface"
    judge_model: Optional[str] = "unsloth/Qwen3-4B-Instruct-2507-GGUF"
    intent_provider: Optional[str] = "huggingface"
    intent_model: Optional[str] = "unsloth/Qwen3-4B-Instruct-2507-GGUF"


@dataclass
class MaintenanceConfig:
    """Maintenance agent configuration (triage, stewardship)."""

    # LLM assist (optional, local-first).
    triage_llm_enabled: bool = False
    triage_llm_base_url: str = "http://localhost:1234"
    triage_llm_model: str = "qwen/qwen3-next-80b"
    triage_llm_temperature: float = 0.2
    triage_llm_max_tokens: int = 800
    triage_llm_timeout_s: float = 30.0


@dataclass
class EmailConfig:
    """Email defaults (SMTP outbound + IMAP inbound).

    These defaults are used by framework-native comms tools and gateway bridges when
    explicit parameters are omitted (env vars still take precedence).
    """

    # SMTP (outbound)
    smtp_host: str = ""
    smtp_port: int = 587
    smtp_username: str = ""
    smtp_password_env_var: str = "EMAIL_PASSWORD"
    smtp_use_starttls: bool = True
    from_email: Optional[str] = None
    reply_to: Optional[str] = None

    # IMAP (inbound)
    imap_host: str = ""
    imap_port: int = 993
    imap_username: str = ""
    imap_password_env_var: str = "EMAIL_PASSWORD"
    imap_folder: str = "INBOX"


@dataclass
class DefaultModels:
    """Global default model configurations."""
    global_provider: Optional[str] = None
    global_model: Optional[str] = None
    chat_model: Optional[str] = None
    code_model: Optional[str] = None


@dataclass
class ApiKeysConfig:
    """API keys configuration."""
    openai: Optional[str] = None
    anthropic: Optional[str] = None
    openrouter: Optional[str] = None
    portkey: Optional[str] = None
    openai_compatible: Optional[str] = None
    vllm: Optional[str] = None
    google: Optional[str] = None


@dataclass
class ServerConfig:
    """OpenAI-compatible HTTP gateway configuration."""

    # Inbound server auth token. When set, clients authenticate with
    # `Authorization: Bearer <token>` and can use provider keys configured on
    # the server.
    auth_token: Optional[str] = None

    # Dangerous local/dev escape hatch. Production should keep this false.
    allow_unauthenticated: bool = False

    # Comma-separated allowlists matching the corresponding server env vars.
    base_url_allowlist: Optional[str] = None
    url_fetch_allowlist: Optional[str] = None

    # Local HTTP media path controls.
    media_root: Optional[str] = None
    allow_local_files: bool = False

    # Optional default bind for `python -m abstractcore.server.app`.
    host: Optional[str] = None
    port: Optional[int] = None


@dataclass
class CacheConfig:
    """Cache configuration settings."""
    default_cache_dir: str = "~/.cache/abstractcore"
    huggingface_cache_dir: str = "~/.cache/huggingface"
    local_models_cache_dir: str = "~/.abstractcore/models"
    glyph_cache_dir: str = "~/.abstractcore/glyph_cache"


@dataclass
class LoggingConfig:
    """Logging configuration settings."""
    console_level: str = "ERROR"
    file_level: str = "DEBUG"
    file_logging_enabled: bool = False
    log_base_dir: Optional[str] = None
    verbatim_enabled: bool = True
    console_json: bool = False
    file_json: bool = True


@dataclass
class StreamingConfig:
    """Streaming configuration settings."""
    cli_stream_default: bool = False


@dataclass
class TimeoutConfig:
    """Timeout configuration settings."""
    # Default HTTP timeout for LLM providers (in seconds).
    # This is used as the *process-wide* default unless overridden per-provider/per-call.
    default_timeout: float = 7200.0  # 2 hours
    tool_timeout: float = 600.0     # 10 minutes for tool execution (in seconds)


@dataclass
class OfflineConfig:
    """Offline-first configuration settings."""
    offline_first: bool = True  # AbstractCore is designed offline-first for open source LLMs
    allow_network: bool = False  # Allow network access when offline_first is True (for API providers)
    force_local_files_only: bool = True  # Force local_files_only for HuggingFace transformers


@dataclass
class AbstractCoreConfig:
    """Main configuration class."""
    vision: VisionConfig
    audio: AudioConfig
    video: VideoConfig
    embeddings: EmbeddingsConfig
    app_defaults: AppDefaults
    default_models: DefaultModels
    capability_defaults: CapabilityDefaultsConfig
    provider_profiles: ProviderProfilesConfig
    api_keys: ApiKeysConfig
    server: ServerConfig
    cache: CacheConfig
    logging: LoggingConfig
    streaming: StreamingConfig
    timeouts: TimeoutConfig
    offline: OfflineConfig
    maintenance: MaintenanceConfig
    email: EmailConfig

    @classmethod
    def default(cls):
        """Create default configuration."""
        return cls(
            vision=VisionConfig(),
            audio=AudioConfig(),
            video=VideoConfig(),
            embeddings=EmbeddingsConfig(),
            app_defaults=AppDefaults(),
            default_models=DefaultModels(),
            capability_defaults=CapabilityDefaultsConfig(),
            provider_profiles=ProviderProfilesConfig(),
            api_keys=ApiKeysConfig(),
            server=ServerConfig(),
            cache=CacheConfig(),
            logging=LoggingConfig(),
            streaming=StreamingConfig(),
            timeouts=TimeoutConfig(),
            offline=OfflineConfig(),
            maintenance=MaintenanceConfig(),
            email=EmailConfig(),
        )


class ConfigurationManager:
    """Manages AbstractCore configuration."""

    def __init__(
        self,
        config_dir: Optional[Union[str, Path]] = None,
        config_file: Optional[Union[str, Path]] = None,
        *,
        apply_env: bool = True,
    ):
        # Backward-compatible meta flags (stored at top-level in the JSON file).
        self._audio_strategy_explicit = False

        if config_file is not None:
            self.config_file = Path(config_file).expanduser()
            self.config_dir = self.config_file.parent
        else:
            env_config_file = os.getenv("ABSTRACTCORE_CONFIG_FILE")
            env_config_dir = os.getenv("ABSTRACTCORE_CONFIG_DIR")
            if env_config_file:
                self.config_file = Path(env_config_file).expanduser()
                self.config_dir = self.config_file.parent
            else:
                self.config_dir = Path(config_dir or env_config_dir or (Path.home() / ".abstractcore" / "config")).expanduser()
                self.config_file = self.config_dir / "abstractcore.json"
        self._apply_env = bool(apply_env)
        self.config = self._load_config()
        self._apply_smart_defaults()
        if self._apply_env:
            self._apply_api_keys_to_env()
            self._apply_server_config_to_env()
        self._provider_config: Dict[str, Dict[str, Any]] = {}  # Runtime config (not persisted)

    def _filter_dataclass_kwargs(self, cls, data: Any) -> Dict[str, Any]:
        if not isinstance(data, dict):
            return {}
        allowed = {f.name for f in fields(cls)}
        return {k: v for k, v in data.items() if k in allowed}

    def _has_abstractvoice(self) -> bool:
        try:
            return importlib.util.find_spec("abstractvoice") is not None
        except Exception:
            return False

    def _apply_smart_defaults(self) -> None:
        """Apply non-persisted, environment-aware defaults.

        Goal: keep legacy `audio.strategy="auto"` usable for native-capable audio
        models while Core capability defaults decide whether STT fallback is enabled.
        """
        try:
            audio = getattr(self.config, "audio", None)
            if audio is None:
                return

            # Respect explicit user choice.
            if bool(getattr(self, "_audio_strategy_explicit", False)):
                return

            raw = str(getattr(audio, "strategy", "") or "").strip().lower()
            has_av = bool(self._has_abstractvoice())
            if has_av and raw in {"", "native_only", "native", "disabled"}:
                audio.strategy = "auto"
            elif (not has_av) and raw in {"auto", "speech_to_text", "stt"}:
                audio.strategy = "native_only"
        except Exception:
            # Never fail config initialization due to smart defaults.
            return

    def _apply_api_keys_to_env(self) -> None:
        """Inject config-persisted API keys into os.environ (if not already set).

        Providers read API keys from environment variables (e.g. OPENAI_API_KEY).
        This bridges the gap: keys saved via ``abstractcore --set-api-key`` are
        written to the config JSON but must appear in os.environ for providers
        to find them.

        Rule: environment variables **always win** — we never overwrite a key
        that is already set in the environment.  #FALLBACK: config-persisted
        keys are injected only when the env var is absent.
        """
        _KEY_MAP = {
            "openai": "OPENAI_API_KEY",
            "anthropic": "ANTHROPIC_API_KEY",
            "openrouter": "OPENROUTER_API_KEY",
            "portkey": "PORTKEY_API_KEY",
            "openai_compatible": "OPENAI_API_KEY",
            "vllm": "VLLM_API_KEY",
            "google": "GOOGLE_API_KEY",
        }
        try:
            api_keys = self.config.api_keys
            for attr, env_var in _KEY_MAP.items():
                key = getattr(api_keys, attr, None)
                if key and not os.environ.get(env_var):
                    os.environ[env_var] = key
        except Exception:
            # Never fail config initialization.
            pass

    def _apply_server_config_to_env(self) -> None:
        """Inject persisted server settings into os.environ when env vars are absent.

        Environment variables always win. This lets deployments override local
        config through Docker/Kubernetes/secrets managers while still making
        `abstractcore --config` useful for local server runs.
        """
        try:
            server = self.config.server

            value_map = {
                _SERVER_AUTH_TOKEN_ENV_VAR: server.auth_token,
                "ABSTRACTCORE_SERVER_BASE_URL_ALLOWLIST": server.base_url_allowlist,
                "ABSTRACTCORE_SERVER_URL_FETCH_ALLOWLIST": server.url_fetch_allowlist,
                "ABSTRACTCORE_SERVER_MEDIA_ROOT": server.media_root,
                "HOST": server.host,
                "PORT": str(server.port) if server.port is not None else None,
            }
            for env_var, value in value_map.items():
                if env_var == _SERVER_AUTH_TOKEN_ENV_VAR and os.environ.get(_SERVER_AUTH_TOKEN_ENV_VAR):
                    continue
                if value is not None and str(value).strip() and not os.environ.get(env_var):
                    os.environ[env_var] = str(value).strip()

            if server.allow_unauthenticated and not os.environ.get("ABSTRACTCORE_SERVER_ALLOW_UNAUTHENTICATED"):
                os.environ["ABSTRACTCORE_SERVER_ALLOW_UNAUTHENTICATED"] = "1"
            if server.allow_local_files and not os.environ.get("ABSTRACTCORE_SERVER_ALLOW_LOCAL_FILES"):
                os.environ["ABSTRACTCORE_SERVER_ALLOW_LOCAL_FILES"] = "1"
        except Exception:
            # Never fail config initialization.
            pass

    def _load_config(self) -> AbstractCoreConfig:
        """Load configuration from file or create default."""
        if self.config_file.exists():
            try:
                with open(self.config_file, 'r') as f:
                    data = json.load(f)
                # Backward compatible: accept both top-level and nested flags.
                if isinstance(data, dict):
                    if "audio_strategy_explicit" in data:
                        self._audio_strategy_explicit = bool(data.get("audio_strategy_explicit"))
                    else:
                        nested = data.get("audio", {})
                        if isinstance(nested, dict) and "strategy_explicit" in nested:
                            self._audio_strategy_explicit = bool(nested.get("strategy_explicit"))
                return self._dict_to_config(data)
            except Exception:
                # If loading fails, return default config
                return AbstractCoreConfig.default()
        else:
            return AbstractCoreConfig.default()

    def _dict_to_config(self, data: Dict[str, Any]) -> AbstractCoreConfig:
        """Convert dictionary to config object."""
        # Create config objects from dictionary data
        vision = VisionConfig(**self._filter_dataclass_kwargs(VisionConfig, data.get('vision', {})))
        audio = AudioConfig(**self._filter_dataclass_kwargs(AudioConfig, data.get('audio', {})))
        video = VideoConfig(**self._filter_dataclass_kwargs(VideoConfig, data.get('video', {})))
        embeddings = EmbeddingsConfig(**self._filter_dataclass_kwargs(EmbeddingsConfig, data.get('embeddings', {})))
        app_defaults = AppDefaults(**self._filter_dataclass_kwargs(AppDefaults, data.get('app_defaults', {})))
        default_models = DefaultModels(**self._filter_dataclass_kwargs(DefaultModels, data.get('default_models', {})))
        capability_defaults = capability_defaults_from_dict(data.get('capability_defaults', {}))
        provider_profiles = provider_profiles_from_dict(data.get('provider_profiles', {}))
        api_keys = ApiKeysConfig(**self._filter_dataclass_kwargs(ApiKeysConfig, data.get('api_keys', {})))
        server = ServerConfig(**self._filter_dataclass_kwargs(ServerConfig, data.get('server', {})))
        cache = CacheConfig(**self._filter_dataclass_kwargs(CacheConfig, data.get('cache', {})))
        logging = LoggingConfig(**self._filter_dataclass_kwargs(LoggingConfig, data.get('logging', {})))
        streaming = StreamingConfig(**self._filter_dataclass_kwargs(StreamingConfig, data.get('streaming', {})))
        timeouts = TimeoutConfig(**self._filter_dataclass_kwargs(TimeoutConfig, data.get('timeouts', {})))
        offline = OfflineConfig(**self._filter_dataclass_kwargs(OfflineConfig, data.get('offline', {})))
        maintenance = MaintenanceConfig(**self._filter_dataclass_kwargs(MaintenanceConfig, data.get('maintenance', {})))
        email_cfg = EmailConfig(**self._filter_dataclass_kwargs(EmailConfig, data.get('email', {})))

        return AbstractCoreConfig(
            vision=vision,
            audio=audio,
            video=video,
            embeddings=embeddings,
            app_defaults=app_defaults,
            default_models=default_models,
            capability_defaults=capability_defaults,
            provider_profiles=provider_profiles,
            api_keys=api_keys,
            server=server,
            cache=cache,
            logging=logging,
            streaming=streaming,
            timeouts=timeouts,
            offline=offline,
            maintenance=maintenance,
            email=email_cfg,
        )

    def _save_config(self):
        """Save configuration to file."""
        self.config_dir.mkdir(parents=True, exist_ok=True)

        # Convert config to dictionary
        config_dict = {
            # Meta flags (top-level for backward compatibility).
            "audio_strategy_explicit": bool(getattr(self, "_audio_strategy_explicit", False)),
            'vision': asdict(self.config.vision),
            'audio': asdict(self.config.audio),
            'video': asdict(self.config.video),
            'embeddings': asdict(self.config.embeddings),
            'app_defaults': asdict(self.config.app_defaults),
            'default_models': asdict(self.config.default_models),
            'capability_defaults': self.config.capability_defaults.to_dict(),
            'provider_profiles': self.config.provider_profiles.to_dict(),
            'api_keys': asdict(self.config.api_keys),
            'server': asdict(self.config.server),
            'cache': asdict(self.config.cache),
            'logging': asdict(self.config.logging),
            'streaming': asdict(self.config.streaming),
            'timeouts': asdict(self.config.timeouts),
            'offline': asdict(self.config.offline),
            'maintenance': asdict(self.config.maintenance),
            'email': asdict(self.config.email),
        }

        tmp = self.config_file.with_suffix(self.config_file.suffix + ".tmp")
        with open(tmp, 'w') as f:
            json.dump(config_dict, f, indent=2)
            f.write("\n")
        try:
            os.chmod(tmp, 0o600)
        except Exception:
            pass
        tmp.replace(self.config_file)
        try:
            os.chmod(self.config_file, 0o600)
        except Exception:
            pass

    def set_vision_provider(self, provider: str, model: str) -> bool:
        """Set vision provider and model."""
        try:
            self.config.vision.strategy = "two_stage"
            self.config.vision.caption_provider = provider
            self.config.vision.caption_model = model
            self._save_config()
            return True
        except Exception:
            return False

    def set_audio_strategy(self, strategy: str) -> bool:
        """Set default audio handling strategy (native_only|speech_to_text|auto)."""
        raw = str(strategy or "").strip().lower()
        if raw in {"native"}:
            raw = "native_only"
        if raw in {"stt"}:
            raw = "speech_to_text"
        if raw not in {"native_only", "speech_to_text", "auto", "caption"}:
            return False
        try:
            self.config.audio.strategy = raw
            self._audio_strategy_explicit = True
            self._save_config()
            return True
        except Exception:
            return False

    def set_stt_backend_id(self, backend_id: Optional[str]) -> bool:
        """Set preferred STT backend id for capability plugins (optional)."""
        bid = str(backend_id or "").strip()
        try:
            self.config.audio.stt_backend_id = bid or None
            self._save_config()
            return True
        except Exception:
            return False

    def set_stt_language(self, language: Optional[str]) -> bool:
        """Set default STT language hint (optional)."""
        lang = str(language or "").strip()
        try:
            self.config.audio.stt_language = lang or None
            self._save_config()
            return True
        except Exception:
            return False

    def set_video_strategy(self, strategy: str) -> bool:
        """Set default video handling strategy (native_only|frames_caption|auto)."""
        raw = str(strategy or "").strip().lower()
        if raw in {"native"}:
            raw = "native_only"
        if raw in {"frames", "frame_caption"}:
            raw = "frames_caption"
        if raw not in {"native_only", "frames_caption", "auto"}:
            return False
        try:
            self.config.video.strategy = raw
            self._save_config()
            return True
        except Exception:
            return False

    def set_video_max_frames(self, max_frames: int) -> bool:
        """Set max sampled frames for video frames fallback (>= 1)."""
        try:
            n = int(max_frames)
        except Exception:
            return False
        if n < 1:
            return False
        try:
            self.config.video.max_frames = n
            self._save_config()
            return True
        except Exception:
            return False

    def set_video_max_frames_native(self, max_frames_native: int) -> bool:
        """Set max frames for native video-capable models (>= 1)."""
        try:
            n = int(max_frames_native)
        except Exception:
            return False
        if n < 1:
            return False
        try:
            self.config.video.max_frames_native = n
            self._save_config()
            return True
        except Exception:
            return False

    def set_video_frame_format(self, frame_format: str) -> bool:
        """Set extracted frame image format (jpg|png)."""
        raw = str(frame_format or "").strip().lower()
        if raw in {"jpeg"}:
            raw = "jpg"
        if raw not in {"jpg", "png"}:
            return False
        try:
            self.config.video.frame_format = raw
            self._save_config()
            return True
        except Exception:
            return False

    def set_video_sampling_strategy(self, sampling_strategy: str) -> bool:
        """Set frame sampling strategy (uniform|keyframes)."""
        raw = str(sampling_strategy or "").strip().lower()
        if raw in {"keyframe"}:
            raw = "keyframes"
        if raw not in {"uniform", "keyframes"}:
            return False
        try:
            self.config.video.sampling_strategy = raw
            self._save_config()
            return True
        except Exception:
            return False

    def set_video_max_frame_side(self, max_frame_side: int) -> bool:
        """Set max side length for extracted frames (preserves aspect ratio; never upscales)."""
        try:
            n = int(max_frame_side)
        except Exception:
            return False
        if n < 1:
            return False
        try:
            self.config.video.max_frame_side = n
            self._save_config()
            return True
        except Exception:
            return False

    def set_video_max_video_size_bytes(self, max_video_size_bytes: Optional[int]) -> bool:
        """Set maximum allowed video size for processing (bytes). Use 0/None to clear."""
        if max_video_size_bytes is None:
            value = None
        else:
            try:
                value_i = int(max_video_size_bytes)
            except Exception:
                return False
            value = None if value_i <= 0 else value_i

        try:
            self.config.video.max_video_size_bytes = value
            self._save_config()
            return True
        except Exception:
            return False

    def set_vision_caption(self, model: str) -> bool:
        """Set vision caption model (deprecated)."""
        # Auto-detect provider from model name
        provider = self._detect_provider_from_model(model)
        if provider:
            return self.set_vision_provider(provider, model)
        return False

    def _detect_provider_from_model(self, model: str) -> Optional[str]:
        """Detect provider from model name."""
        model_lower = model.lower()

        if any(x in model_lower for x in ['qwen2.5vl', 'llama3.2-vision', 'llava']):
            return "ollama"
        elif any(x in model_lower for x in ['gpt-4', 'gpt-4o']):
            return "openai"
        elif any(x in model_lower for x in ['claude-3']):
            return "anthropic"
        elif '/' in model:
            return "lmstudio"

        return None

    def get_status(self) -> Dict[str, Any]:
        """Get configuration status."""
        embedding_route = self.get_capability_default("embedding", "text")
        embedding_route_configured = bool(
            embedding_route.get("provider")
            or embedding_route.get("model")
            or embedding_route.get("base_url")
            or embedding_route.get("options")
        )
        if embedding_route_configured:
            embedding_provider = embedding_route.get("provider")
            embedding_model = embedding_route.get("model")
            embedding_base_url = embedding_route.get("base_url")
            embedding_status = "✅ Ready" if embedding_provider and embedding_model else "⚠️ Partially configured"
            embedding_source = embedding_route.get("source") or "abstractcore.capability_defaults"
        else:
            embedding_provider = self.config.embeddings.provider
            embedding_model = self.config.embeddings.model
            embedding_base_url = self.config.embeddings.base_url
            embedding_status = "✅ Ready" if embedding_provider and embedding_model else "❌ Not configured"
            embedding_source = "abstractcore.embeddings_legacy"
        return {
            "config_file": str(self.config_file),
            "vision": {
                "strategy": self.config.vision.strategy,
                "status": "✅ Ready" if self.config.vision.caption_provider else "❌ Not configured",
                "caption_provider": self.config.vision.caption_provider,
                "caption_model": self.config.vision.caption_model
            },
            "audio": {
                "strategy": self.config.audio.strategy,
                "stt_backend_id": self.config.audio.stt_backend_id,
                "stt_language": self.config.audio.stt_language,
            },
            "video": {
                "strategy": self.config.video.strategy,
                "max_frames": self.config.video.max_frames,
                "max_frames_native": getattr(self.config.video, "max_frames_native", None),
                "frame_format": self.config.video.frame_format,
                "sampling_strategy": getattr(self.config.video, "sampling_strategy", None),
                "max_frame_side": getattr(self.config.video, "max_frame_side", None),
                "max_video_size_bytes": getattr(self.config.video, "max_video_size_bytes", None),
            },
            "app_defaults": {
                "cli": {
                    "provider": self.config.app_defaults.cli_provider,
                    "model": self.config.app_defaults.cli_model
                },
                "summarizer": {
                    "provider": self.config.app_defaults.summarizer_provider,
                    "model": self.config.app_defaults.summarizer_model
                },
                "extractor": {
                    "provider": self.config.app_defaults.extractor_provider,
                    "model": self.config.app_defaults.extractor_model
                },
                "judge": {
                    "provider": self.config.app_defaults.judge_provider,
                    "model": self.config.app_defaults.judge_model
                },
                "intent": {
                    "provider": self.config.app_defaults.intent_provider,
                    "model": self.config.app_defaults.intent_model
                }
            },
            "global_defaults": {
                "provider": self.config.default_models.global_provider,
                "model": self.config.default_models.global_model,
                "chat_model": self.config.default_models.chat_model,
                "code_model": self.config.default_models.code_model
            },
            "capability_defaults": self.list_capability_defaults(),
            "provider_profiles": self.list_provider_profiles(),
            "embeddings": {
                "status": embedding_status,
                "provider": embedding_provider,
                "model": embedding_model,
                "base_url": embedding_base_url,
                "route": "embedding.text",
                "source": embedding_source,
                "legacy": {
                    "provider": self.config.embeddings.provider,
                    "model": self.config.embeddings.model,
                    "base_url": self.config.embeddings.base_url,
                },
            },
            "streaming": {
                "cli_stream_default": self.config.streaming.cli_stream_default
            },
            "logging": {
                "console_level": self.config.logging.console_level,
                "file_level": self.config.logging.file_level,
                "file_logging_enabled": self.config.logging.file_logging_enabled
            },
            "timeouts": {
                "default_timeout": self.config.timeouts.default_timeout,
                "tool_timeout": self.config.timeouts.tool_timeout
            },
            "cache": {
                "default_cache_dir": self.config.cache.default_cache_dir,
                "huggingface_cache_dir": self.config.cache.huggingface_cache_dir,
                "local_models_cache_dir": self.config.cache.local_models_cache_dir,
                "glyph_cache_dir": self.config.cache.glyph_cache_dir,
            },
            "api_keys": {
                "openai": "✅ Set" if self.config.api_keys.openai else "❌ Not set",
                "anthropic": "✅ Set" if self.config.api_keys.anthropic else "❌ Not set",
                "openrouter": "✅ Set" if self.config.api_keys.openrouter else "❌ Not set",
                "portkey": "✅ Set" if self.config.api_keys.portkey else "❌ Not set",
                "openai-compatible": "✅ Set" if self.config.api_keys.openai_compatible else "❌ Not set",
                "vllm": "✅ Set" if self.config.api_keys.vllm else "❌ Not set",
                "google": "✅ Set" if self.config.api_keys.google else "❌ Not set"
            },
            "server": {
                "auth_token": "✅ Set" if self.config.server.auth_token else "❌ Not set",
                "allow_unauthenticated": bool(self.config.server.allow_unauthenticated),
                "auth_mode": (
                    "server_token"
                    if self.config.server.auth_token
                    else ("unauthenticated_dev" if self.config.server.allow_unauthenticated else "provider_key_only")
                ),
                "base_url_allowlist": self.config.server.base_url_allowlist,
                "url_fetch_allowlist": self.config.server.url_fetch_allowlist,
                "media_root": self.config.server.media_root,
                "allow_local_files": bool(self.config.server.allow_local_files),
                "host": self.config.server.host,
                "port": self.config.server.port,
            },
            "offline": {
                "offline_first": self.config.offline.offline_first,
                "allow_network": self.config.offline.allow_network,
                "status": "🔒 Offline-first" if self.config.offline.offline_first else "🌐 Network-enabled"
            }
        }

    def reset_configuration(self) -> bool:
        """Reset all configuration to built-in defaults."""
        try:
            self.config = AbstractCoreConfig.default()
            self._audio_strategy_explicit = False
            self._provider_config.clear()
            self._save_config()
            # Re-apply environment-aware defaults for the current process (non-persisted).
            self._apply_smart_defaults()
            return True
        except Exception:
            return False

    def list_provider_profiles(self, *, include_disabled: bool = True) -> list[Dict[str, Any]]:
        """Return redacted local provider endpoint profiles."""
        rows: list[Dict[str, Any]] = []
        for profile in sorted(self.config.provider_profiles.profiles.values(), key=lambda p: p.id.lower()):
            if not include_disabled and not profile.enabled:
                continue
            rows.append(profile.public_dict())
        return rows

    def get_provider_profile(self, profile_id: str) -> Optional[ProviderProfile]:
        """Return one provider profile by plain id or virtual provider id."""
        try:
            pid = normalize_profile_id(profile_id)
        except Exception:
            return None
        return self.config.provider_profiles.profiles.get(pid.lower())

    def resolve_provider_profile(self, provider: str, *, require_enabled: bool = True) -> Optional[ProviderProfile]:
        """Resolve ``endpoint:<id>`` or a plain profile id to a provider profile."""
        try:
            raw = str(provider or "").strip()
            pid = profile_id_from_virtual_provider(raw) or normalize_profile_id(raw)
            profile = self.config.provider_profiles.profiles.get(pid.lower())
            if profile is None:
                return None
            if require_enabled and not profile.enabled:
                return None
            return profile
        except Exception:
            return None

    def set_provider_profile(
        self,
        profile_id: str,
        *,
        display_name: Optional[str] = None,
        description: Optional[str] = None,
        provider_family: Optional[str] = None,
        base_url: Optional[str] = None,
        api_key: Optional[str] = None,
        clear_api_key: bool = False,
        allowed_models: Optional[list[str]] = None,
        enabled: Optional[bool] = None,
    ) -> ProviderProfile:
        """Create or update a local provider endpoint profile."""
        pid = normalize_profile_id(profile_id)
        existing = self.config.provider_profiles.profiles.get(pid.lower())
        now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
        parsed_api_key = None
        parsed_api_key_env_var = None
        if api_key is not None:
            parsed_api_key, parsed_api_key_env_var = split_api_key_value(api_key)

        profile = ProviderProfile(
            id=pid,
            display_name=display_name if display_name is not None else (existing.display_name if existing else pid),
            description=description if description is not None else (existing.description if existing else ""),
            provider_family=provider_family if provider_family is not None else (existing.provider_family if existing else "openai-compatible"),
            base_url=base_url if base_url is not None else (existing.base_url if existing else ""),
            api_key=(
                ""
                if clear_api_key
                else (parsed_api_key if parsed_api_key is not None else (existing.api_key if existing else ""))
            ),
            api_key_env_var=(
                ""
                if clear_api_key
                else (parsed_api_key_env_var if parsed_api_key_env_var is not None else (existing.api_key_env_var if existing else ""))
            ),
            allowed_models=allowed_models if allowed_models is not None else (existing.allowed_models if existing else []),
            enabled=bool(enabled) if enabled is not None else (existing.enabled if existing else True),
            created_at=existing.created_at if existing else now,
            updated_at=now,
        )

        # Run explicit normalization here to surface errors before saving.
        normalize_provider_family(profile.provider_family)
        normalize_base_url(profile.base_url)
        normalize_string_list(profile.allowed_models)

        self.config.provider_profiles.profiles[profile.id.lower()] = profile
        self._save_config()
        return profile

    def delete_provider_profile(self, profile_id: str) -> bool:
        """Delete a local provider endpoint profile."""
        pid = normalize_profile_id(profile_id)
        removed = self.config.provider_profiles.profiles.pop(pid.lower(), None)
        if removed is None:
            return False
        self._save_config()
        return True

    def get_capability_default(
        self,
        kind: str,
        modality: str,
    ) -> Dict[str, Any]:
        """Return the effective default route for a capability."""
        try:
            key = capability_route_key(kind, modality)
        except Exception:
            return {}

        if key == capability_route_key("output", "text"):
            route = self.config.capability_defaults.routes.get(capability_route_key("input", "text"))
            if route and route.configured():
                out = route.to_dict()
                out.update(
                    {
                        "key": key,
                        "source": "abstractcore.capability_defaults",
                        "derived_from": "input.text",
                        "read_only": True,
                    }
                )
                return out
            return {"key": key, "source": "not_configured", "derived_from": "input.text", "read_only": True}

        route = self.config.capability_defaults.routes.get(key)
        if route and route.configured():
            out = route.to_dict()
            out.update({"key": key, "source": "abstractcore.capability_defaults"})
            if key == capability_route_key("input", "image"):
                out = self._decorate_image_input_default(out)
            elif key == capability_route_key("input", "video"):
                out.setdefault("overrideable", True)
            elif key == capability_route_key("input", "sound"):
                out.setdefault("overrideable", True)
            elif key == capability_route_key("input", "music"):
                out.setdefault("overrideable", True)
            return out

        out = {"key": key, "source": "not_configured"}
        if key == capability_route_key("input", "image"):
            out = self._decorate_image_input_default(out)
        elif key == capability_route_key("input", "video"):
            out = self._decorate_video_input_default(out)
        elif key == capability_route_key("input", "sound"):
            out = self._decorate_sound_input_default(out)
        elif key == capability_route_key("input", "music"):
            out = self._decorate_music_input_default(out)
        return out

    def list_capability_defaults(self) -> list[Dict[str, Any]]:
        """Return all known capability routes with explicit persisted defaults."""
        rows: list[Dict[str, Any]] = []
        for spec in iter_capability_default_specs():
            route = self.get_capability_default(spec.kind, spec.modality)
            row = {**spec.to_dict(), **route}
            row["configured"] = bool(route.get("provider") or route.get("model") or route.get("base_url") or route.get("options"))
            if spec.key == capability_route_key("input", "image"):
                row = self._decorate_image_input_default(row)
            elif spec.key == capability_route_key("input", "video") and not row["configured"]:
                row = self._decorate_video_input_default(row)
            elif spec.key == capability_route_key("input", "sound") and not row["configured"]:
                row = self._decorate_sound_input_default(row)
            elif spec.key == capability_route_key("input", "music") and not row["configured"]:
                row = self._decorate_music_input_default(row)
            rows.append(row)
        return rows

    def _decorate_image_input_default(self, row: Dict[str, Any]) -> Dict[str, Any]:
        return self._decorate_text_covered_input_default(row, "image", read_only=True)

    def _decorate_video_input_default(self, row: Dict[str, Any]) -> Dict[str, Any]:
        return self._decorate_text_covered_input_default(
            row,
            "video",
            coverage_mode="video_frames",
            overrideable=True,
            read_only=False,
        )

    def _decorate_sound_input_default(self, row: Dict[str, Any]) -> Dict[str, Any]:
        return self._decorate_text_covered_input_default(row, "sound", overrideable=True, read_only=False)

    def _decorate_music_input_default(self, row: Dict[str, Any]) -> Dict[str, Any]:
        return self._decorate_text_covered_input_default(row, "music", overrideable=True, read_only=False)

    def _decorate_text_covered_input_default(
        self,
        row: Dict[str, Any],
        modality: str,
        *,
        coverage_mode: Optional[str] = None,
        overrideable: bool = False,
        read_only: bool = True,
    ) -> Dict[str, Any]:
        text_route = self.config.capability_defaults.routes.get(capability_route_key("input", "text"))
        if not text_route or not text_route.provider or not text_route.model:
            return row
        if not self._model_supports_input(text_route.model, modality):
            return row
        out = dict(row)
        out.update(
            {
                "provider": text_route.provider,
                "model": text_route.model,
                "base_url": text_route.base_url,
                "source": "abstractcore.capability_defaults",
                "configured": True,
                "covered_by": "input.text",
                "read_only": read_only,
            }
        )
        if coverage_mode:
            out["coverage_mode"] = coverage_mode
        if overrideable:
            out["overrideable"] = True
        if text_route.options:
            out["options"] = dict(text_route.options)
        return out

    @staticmethod
    def _model_supports_input(model: str, modality: str) -> bool:
        try:
            from ..providers.model_capabilities import (
                ModelInputCapability,
                model_matches_input_capabilities,
                model_supports_capability_route,
            )

            normalized_modality = str(modality or "").strip().lower()
            try:
                if model_supports_capability_route(str(model or ""), capability_route_key("input", normalized_modality)):
                    return True
            except Exception:
                pass

            capability = {
                "image": ModelInputCapability.IMAGE,
                "audio": ModelInputCapability.AUDIO,
                "sound": ModelInputCapability.SOUND,
                "voice": ModelInputCapability.VOICE,
                "music": ModelInputCapability.MUSIC,
                "video": ModelInputCapability.VIDEO,
            }.get(normalized_modality)
            if capability is None:
                return False
            return bool(model_matches_input_capabilities(str(model or ""), [capability]))
        except Exception:
            return False

    def set_capability_default(
        self,
        kind: str,
        modality: Optional[str] = None,
        *,
        provider: Optional[str] = None,
        model: Optional[str] = None,
        base_url: Optional[str] = None,
        options: Optional[Dict[str, Any]] = None,
    ) -> bool:
        """Persist a capability default route."""
        try:
            if modality is None:
                kind, modality = split_capability_route(kind)
            key = capability_route_key(kind, modality)
            if key == capability_route_key("output", "text"):
                key = capability_route_key("input", "text")
            route = CapabilityRouteDefault(
                provider=str(provider).strip() if isinstance(provider, str) and provider.strip() else None,
                model=str(model).strip() if isinstance(model, str) and model.strip() else None,
                base_url=str(base_url).strip() if isinstance(base_url, str) and base_url.strip() else None,
                options=dict(options or {}) if isinstance(options, dict) else {},
            )
            if route.configured():
                self.config.capability_defaults.routes[key] = route
                if key == capability_route_key("input", "text"):
                    self.config.capability_defaults.routes.pop(capability_route_key("output", "text"), None)
            else:
                self.config.capability_defaults.routes.pop(key, None)
            self._save_config()
            return True
        except Exception:
            return False

    def clear_capability_default(self, kind: str, modality: Optional[str] = None) -> bool:
        """Clear one persisted capability default route."""
        try:
            if modality is None:
                kind, modality = split_capability_route(kind)
            key = capability_route_key(kind, modality)
            if key == capability_route_key("output", "text"):
                key = capability_route_key("input", "text")
            self.config.capability_defaults.routes.pop(key, None)
            if key == capability_route_key("input", "text"):
                self.config.capability_defaults.routes.pop(capability_route_key("output", "text"), None)
            self._save_config()
            return True
        except Exception:
            return False

    def set_global_default_model(self, provider_model: str) -> bool:
        """Set global default model in provider/model format."""
        try:
            provider, model = _split_provider_model(provider_model, default_provider="ollama")

            self.config.default_models.global_provider = provider
            self.config.default_models.global_model = model
            self.config.capability_defaults.routes[capability_route_key("input", "text")] = CapabilityRouteDefault(
                provider=provider,
                model=model,
            )
            self.config.capability_defaults.routes.pop(capability_route_key("output", "text"), None)
            self._save_config()
            return True
        except Exception:
            return False

    def set_default_model(self, provider_model: str) -> bool:
        """Legacy alias for setting the global default model."""
        return self.set_global_default_model(provider_model)

    def set_global_default_provider(self, provider: str) -> bool:
        """Set global default provider (legacy)."""
        try:
            provider = str(provider or "").strip()
            if not provider:
                raise ValueError("Provider cannot be empty")
            self.config.default_models.global_provider = provider
            self._save_config()
            return True
        except Exception:
            return False

    def set_chat_model(self, provider_model: str) -> bool:
        """Set specialized chat model (provider/model string)."""
        try:
            model = str(provider_model or "").strip()
            if not model:
                raise ValueError("Model cannot be empty")
            self.config.default_models.chat_model = model
            self._save_config()
            return True
        except Exception:
            return False

    def set_code_model(self, provider_model: str) -> bool:
        """Set specialized code model (provider/model string)."""
        try:
            model = str(provider_model or "").strip()
            if not model:
                raise ValueError("Model cannot be empty")
            self.config.default_models.code_model = model
            self._save_config()
            return True
        except Exception:
            return False

    def set_embeddings_model(self, provider_model: str) -> bool:
        """Set embeddings provider/model from a provider/model string (preferred)."""
        try:
            value = str(provider_model or "").strip()
            if not value:
                raise ValueError("Embeddings model cannot be empty")

            provider, model = _split_provider_model(value, default_provider=str(self.config.embeddings.provider or "huggingface"))
            self.config.embeddings.provider = provider.strip() or self.config.embeddings.provider
            self.config.embeddings.model = model.strip()

            self._sync_embedding_capability_default()
            self._save_config()
            return True
        except Exception:
            return False

    def set_embeddings_provider(self, provider: str) -> bool:
        """Set embeddings provider."""
        try:
            value = str(provider or "").strip()
            if not value:
                raise ValueError("Embeddings provider cannot be empty")
            self.config.embeddings.provider = value
            self._sync_embedding_capability_default()
            self._save_config()
            return True
        except Exception:
            return False

    def set_embeddings_base_url(self, base_url: Optional[str]) -> bool:
        """Set optional embeddings provider base URL."""
        try:
            value = str(base_url or "").strip().rstrip("/")
            self.config.embeddings.base_url = value or None
            self._sync_embedding_capability_default()
            self._save_config()
            return True
        except Exception:
            return False

    def _sync_embedding_capability_default(self) -> None:
        route = CapabilityRouteDefault(
            provider=self.config.embeddings.provider,
            model=self.config.embeddings.model,
            base_url=self.config.embeddings.base_url,
        )
        key = capability_route_key("embedding", "text")
        if route.configured():
            self.config.capability_defaults.routes[key] = route
        else:
            self.config.capability_defaults.routes.pop(key, None)

    def set_default_cache_dir(self, path: str) -> bool:
        """Set default cache directory for AbstractCore."""
        try:
            value = str(path or "").strip()
            if not value:
                raise ValueError("Cache directory cannot be empty")
            self.config.cache.default_cache_dir = value
            self._save_config()
            return True
        except Exception:
            return False

    def set_huggingface_cache_dir(self, path: str) -> bool:
        """Set HuggingFace cache directory."""
        try:
            value = str(path or "").strip()
            if not value:
                raise ValueError("HuggingFace cache directory cannot be empty")
            self.config.cache.huggingface_cache_dir = value
            self._save_config()
            return True
        except Exception:
            return False

    def set_local_models_cache_dir(self, path: str) -> bool:
        """Set local models cache directory."""
        try:
            value = str(path or "").strip()
            if not value:
                raise ValueError("Local models cache directory cannot be empty")
            self.config.cache.local_models_cache_dir = value
            self._save_config()
            return True
        except Exception:
            return False

    def set_log_base_dir(self, path: str) -> bool:
        """Set log base directory."""
        try:
            value = str(path or "").strip()
            if not value:
                raise ValueError("Log base directory cannot be empty")
            self.config.logging.log_base_dir = value
            self._save_config()
            return True
        except Exception:
            return False

    def set_console_log_level(self, level: str) -> bool:
        """Set console logging level."""
        try:
            value = str(level or "").strip().upper()
            if not value:
                raise ValueError("Console log level cannot be empty")
            self.config.logging.console_level = value
            self._save_config()
            return True
        except Exception:
            return False

    def set_file_log_level(self, level: str) -> bool:
        """Set file logging level."""
        try:
            value = str(level or "").strip().upper()
            if not value:
                raise ValueError("File log level cannot be empty")
            self.config.logging.file_level = value
            self._save_config()
            return True
        except Exception:
            return False

    def enable_debug_logging(self) -> bool:
        """Enable debug logging for both console and file."""
        try:
            self.config.logging.console_level = "DEBUG"
            self.config.logging.file_level = "DEBUG"
            self._save_config()
            return True
        except Exception:
            return False

    def disable_console_logging(self) -> bool:
        """Disable console logging output."""
        try:
            self.config.logging.console_level = "NONE"
            self._save_config()
            return True
        except Exception:
            return False

    def enable_file_logging(self) -> bool:
        """Enable file logging."""
        try:
            self.config.logging.file_logging_enabled = True
            self._save_config()
            return True
        except Exception:
            return False

    def disable_file_logging(self) -> bool:
        """Disable file logging."""
        try:
            self.config.logging.file_logging_enabled = False
            self._save_config()
            return True
        except Exception:
            return False

    def set_streaming_default(self, app_name: str, enabled: bool) -> bool:
        """Set default streaming behavior for a given app (currently: cli)."""
        try:
            app = str(app_name or "").strip().lower()
            if app != "cli":
                return False
            self.config.streaming.cli_stream_default = bool(enabled)
            self._save_config()
            return True
        except Exception:
            return False

    def get_streaming_default(self, app_name: str) -> bool:
        """Get default streaming behavior for a given app (currently: cli)."""
        app = str(app_name or "").strip().lower()
        if app == "cli":
            return bool(self.config.streaming.cli_stream_default)
        return False

    def enable_cli_streaming(self) -> bool:
        """Enable streaming by default for the CLI."""
        return self.set_streaming_default("cli", True)

    def disable_cli_streaming(self) -> bool:
        """Disable streaming by default for the CLI."""
        return self.set_streaming_default("cli", False)

    def add_vision_fallback(self, provider: str, model: str) -> bool:
        """Add a vision fallback provider/model to the chain."""
        try:
            provider_val = str(provider or "").strip()
            model_val = str(model or "").strip()
            if not provider_val or not model_val:
                raise ValueError("Provider and model are required")

            self.config.vision.fallback_chain.append({"provider": provider_val, "model": model_val})
            # If vision is configured at all, assume two_stage (caption -> text model).
            if not self.config.vision.strategy or self.config.vision.strategy == "disabled":
                self.config.vision.strategy = "two_stage"
            self._save_config()
            return True
        except Exception:
            return False

    def disable_vision(self) -> bool:
        """Disable vision fallback for text-only models."""
        try:
            self.config.vision.strategy = "disabled"
            self.config.vision.caption_provider = None
            self.config.vision.caption_model = None
            self.config.vision.fallback_chain = []
            self._save_config()
            return True
        except Exception:
            return False


    def set_app_default(self, app_name: str, provider: str, model: str) -> bool:
        """Set app-specific default provider and model."""
        try:
            if app_name == "cli":
                self.config.app_defaults.cli_provider = provider
                self.config.app_defaults.cli_model = model
            elif app_name == "summarizer":
                self.config.app_defaults.summarizer_provider = provider
                self.config.app_defaults.summarizer_model = model
            elif app_name == "extractor":
                self.config.app_defaults.extractor_provider = provider
                self.config.app_defaults.extractor_model = model
            elif app_name == "judge":
                self.config.app_defaults.judge_provider = provider
                self.config.app_defaults.judge_model = model
            elif app_name == "intent":
                self.config.app_defaults.intent_provider = provider
                self.config.app_defaults.intent_model = model
            else:
                raise ValueError(f"Unknown app: {app_name}")

            self._save_config()
            return True
        except Exception:
            return False

    def set_api_key(self, provider: str, key: str) -> bool:
        """Set API key for a provider."""
        try:
            provider_key = str(provider or "").strip().lower().replace("-", "_")
            if provider_key not in {f.name for f in fields(ApiKeysConfig)}:
                return False
            setattr(self.config.api_keys, provider_key, key)

            self._save_config()
            self._apply_api_keys_to_env()
            return True
        except Exception:
            return False

    def set_server_auth_token(self, token: Optional[str]) -> bool:
        """Set or clear the HTTP server auth token."""
        try:
            value = str(token or "").strip()
            self.config.server.auth_token = value or None
            self._save_config()
            self._apply_server_config_to_env()
            return True
        except Exception:
            return False

    def set_server_allow_unauthenticated(self, enabled: bool) -> bool:
        """Allow unauthenticated HTTP server requests for explicit local/dev use."""
        try:
            self.config.server.allow_unauthenticated = bool(enabled)
            self._save_config()
            self._apply_server_config_to_env()
            return True
        except Exception:
            return False

    def set_server_base_url_allowlist(self, allowlist: Optional[str]) -> bool:
        """Set request-level provider base_url override allowlist."""
        try:
            value = str(allowlist or "").strip()
            self.config.server.base_url_allowlist = value or None
            self._save_config()
            self._apply_server_config_to_env()
            return True
        except Exception:
            return False

    def set_server_url_fetch_allowlist(self, allowlist: Optional[str]) -> bool:
        """Set URL media fetch allowlist for otherwise blocked non-public targets."""
        try:
            value = str(allowlist or "").strip()
            self.config.server.url_fetch_allowlist = value or None
            self._save_config()
            self._apply_server_config_to_env()
            return True
        except Exception:
            return False

    def set_server_media_root(self, path: Optional[str]) -> bool:
        """Set the safe local media root for HTTP requests."""
        try:
            value = str(path or "").strip()
            self.config.server.media_root = value or None
            self._save_config()
            self._apply_server_config_to_env()
            return True
        except Exception:
            return False

    def set_server_allow_local_files(self, enabled: bool) -> bool:
        """Enable or disable unsafe unrestricted local file access for HTTP requests."""
        try:
            self.config.server.allow_local_files = bool(enabled)
            self._save_config()
            self._apply_server_config_to_env()
            return True
        except Exception:
            return False

    def set_server_bind(self, host: Optional[str] = None, port: Optional[int] = None) -> bool:
        """Set default bind host/port for `python -m abstractcore.server.app`."""
        try:
            if host is not None:
                host_value = str(host or "").strip()
                self.config.server.host = host_value or None
            if port is not None:
                port_i = int(port)
                if port_i < 1 or port_i > 65535:
                    return False
                self.config.server.port = port_i
            self._save_config()
            self._apply_server_config_to_env()
            return True
        except Exception:
            return False

    def get_app_default(self, app_name: str) -> Tuple[str, str]:
        """Get default provider and model for an app."""
        app_defaults = self.config.app_defaults

        if app_name == "cli":
            return app_defaults.cli_provider, app_defaults.cli_model
        elif app_name == "summarizer":
            return app_defaults.summarizer_provider, app_defaults.summarizer_model
        elif app_name == "extractor":
            return app_defaults.extractor_provider, app_defaults.extractor_model
        elif app_name == "judge":
            return app_defaults.judge_provider, app_defaults.judge_model
        elif app_name == "intent":
            return app_defaults.intent_provider, app_defaults.intent_model
        else:
            # Return default fallback
            return "huggingface", "unsloth/Qwen3-4B-Instruct-2507-GGUF"

    def set_default_timeout(self, timeout: float) -> bool:
        """Set default HTTP request timeout in seconds."""
        try:
            # #[WARNING:TIMEOUT]
            # Contract: allow `0` to mean "unlimited" (the provider layer normalizes <=0 to None).
            timeout_f = float(timeout)
            if timeout_f < 0:
                raise ValueError("Timeout must be >= 0 (0 = unlimited)")
            self.config.timeouts.default_timeout = timeout_f
            self._save_config()
            return True
        except Exception:
            return False

    def set_tool_timeout(self, timeout: float) -> bool:
        """Set tool execution timeout in seconds."""
        try:
            # #[WARNING:TIMEOUT]
            # Contract: allow `0` to mean "unlimited".
            timeout_f = float(timeout)
            if timeout_f < 0:
                raise ValueError("Timeout must be >= 0 (0 = unlimited)")
            self.config.timeouts.tool_timeout = timeout_f
            self._save_config()
            return True
        except Exception:
            return False

    def get_default_timeout(self) -> float:
        """Get default HTTP request timeout in seconds."""
        return self.config.timeouts.default_timeout

    def get_tool_timeout(self) -> float:
        """Get tool execution timeout in seconds."""
        return self.config.timeouts.tool_timeout

    def set_offline_first(self, enabled: bool) -> bool:
        """Enable or disable offline-first mode."""
        try:
            self.config.offline.offline_first = enabled
            self._save_config()
            return True
        except Exception:
            return False

    def set_allow_network(self, enabled: bool) -> bool:
        """Allow network access when in offline-first mode."""
        try:
            self.config.offline.allow_network = enabled
            self._save_config()
            return True
        except Exception:
            return False

    def is_offline_first(self) -> bool:
        """Check if offline-first mode is enabled."""
        return self.config.offline.offline_first

    def is_network_allowed(self) -> bool:
        """Check if network access is allowed."""
        return self.config.offline.allow_network

    def should_force_local_files_only(self) -> bool:
        """Check if local_files_only should be forced for transformers."""
        return self.config.offline.force_local_files_only

    def configure_provider(self, provider: str, **kwargs) -> None:
        """
        Configure runtime settings for a provider.

        Args:
            provider: Provider name ('ollama', 'lmstudio', 'openai', 'anthropic')
            **kwargs: Configuration options (base_url, timeout, etc.)

        Example:
            configure_provider('ollama', base_url='http://192.168.1.100:11434')
        """
        provider = provider.lower()
        if provider not in self._provider_config:
            self._provider_config[provider] = {}

        for key, value in kwargs.items():
            if value is None:
                # Remove config (revert to env var / default)
                self._provider_config[provider].pop(key, None)
            else:
                self._provider_config[provider][key] = value

    def get_provider_config(self, provider: str) -> Dict[str, Any]:
        """
        Get runtime configuration for a provider.

        Args:
            provider: Provider name

        Returns:
            Dict with configured settings, or empty dict if no config
        """
        provider_key = str(provider or "").strip().lower()
        runtime_config = self._provider_config.get(provider_key, {}).copy()

        profile = self.resolve_provider_profile(provider_key)
        if profile is None:
            return runtime_config

        profile_resolution = profile.private_resolution()
        profile_config = {
            "provider_family": profile_resolution.get("provider_family"),
            "base_url": profile_resolution.get("base_url"),
            "api_key": profile_resolution.get("api_key"),
            "allowed_models": profile_resolution.get("allowed_models") or [],
            "virtual_provider": profile_resolution.get("virtual_provider"),
            "provider_profile_id": profile_resolution.get("id"),
        }
        profile_config = {k: v for k, v in profile_config.items() if v not in (None, "", [])}
        return {**profile_config, **runtime_config}

    def clear_provider_config(self, provider: Optional[str] = None) -> None:
        """
        Clear runtime provider configuration.

        Args:
            provider: Provider name, or None to clear all
        """
        if provider is None:
            self._provider_config.clear()
        else:
            self._provider_config.pop(provider.lower(), None)


# Global instance
_config_manager = None


def get_config_manager() -> ConfigurationManager:
    """Get the global configuration manager instance."""
    global _config_manager
    if _config_manager is None:
        _config_manager = ConfigurationManager()
    return _config_manager
