"""Shared constants for the mindroom package.

This module contains constants that are used across multiple modules
to avoid circular imports. It does not import anything from the internal
codebase.
"""

import os
import shutil
import sys
from pathlib import Path
from typing import cast

from dotenv import load_dotenv

load_dotenv()

# Agent names
ROUTER_AGENT_NAME = "router"

# Default path to agents configuration file. Allow overriding via environment
# variable so deployments can place the writable configuration file on a
# persistent volume instead of the package directory (which may be read-only).
_CONFIG_PATH_ENV = os.getenv("MINDROOM_CONFIG_PATH")

# Search order for existing files: env var > ./config.yaml > ~/.mindroom/config.yaml
_CONFIG_SEARCH_PATHS = [Path("config.yaml"), Path.home() / ".mindroom" / "config.yaml"]


def config_search_locations() -> list[Path]:
    """Return the ordered list of locations where MindRoom looks for config.

    This is the single source of truth for config file discovery.
    """
    seen: set[Path] = set()
    locations: list[Path] = []
    if _CONFIG_PATH_ENV:
        resolved = Path(_CONFIG_PATH_ENV).expanduser().resolve()
        seen.add(resolved)
        locations.append(resolved)
    for p in _CONFIG_SEARCH_PATHS:
        resolved = p.resolve()
        if resolved not in seen:
            seen.add(resolved)
            locations.append(resolved)
    return locations


def _config_base_dir(config_path: Path | None = None) -> Path:
    """Return the absolute directory containing the active config file."""
    resolved_config_path = (config_path or CONFIG_PATH).expanduser().resolve()
    return resolved_config_path.parent


def resolve_config_relative_path(raw_path: str | Path, *, config_path: Path | None = None) -> Path:
    """Resolve a configured path, treating relative values as config-directory-relative."""
    unresolved = Path(raw_path).expanduser()
    if unresolved.is_absolute():
        return unresolved.resolve()
    return (_config_base_dir(config_path) / unresolved).resolve()


def find_config() -> Path:
    """Find the first existing config file, or fall back to ~/.mindroom/config.yaml.

    Returns the original (possibly relative) path, not a resolved one,
    so that derived paths like STORAGE_PATH stay relative and display
    cleanly in CLI help text.
    """
    if _CONFIG_PATH_ENV:
        return Path(_CONFIG_PATH_ENV).expanduser()
    for path in _CONFIG_SEARCH_PATHS:
        if path.exists():
            return path
    return _CONFIG_SEARCH_PATHS[-1]  # default to ~/.mindroom/config.yaml for creation


CONFIG_PATH = find_config()

# Also load .env from the config directory so that API keys placed next to the
# config file (e.g. ~/.mindroom/.env) are picked up even when CWD is elsewhere.
# override=True makes config-adjacent .env authoritative for runtime config.
_config_dotenv = CONFIG_PATH.parent / ".env"
if _config_dotenv.is_file():
    load_dotenv(_config_dotenv, override=True)

# Optional template path used to seed the writable config file if it does not
# exist yet. Defaults to the same location as CONFIG_PATH so the
# behaviour is unchanged when no overrides are provided.
_CONFIG_TEMPLATE_ENV = os.getenv("MINDROOM_CONFIG_TEMPLATE")
_CONFIG_TEMPLATE_PATH = Path(_CONFIG_TEMPLATE_ENV).expanduser() if _CONFIG_TEMPLATE_ENV else CONFIG_PATH

_STORAGE_PATH_ENV = os.getenv("MINDROOM_STORAGE_PATH")
STORAGE_PATH = _STORAGE_PATH_ENV or str(CONFIG_PATH.parent / "mindroom_data")
STORAGE_PATH_OBJ = Path(STORAGE_PATH).expanduser().resolve()

# Specific files and directories
MATRIX_STATE_FILE = STORAGE_PATH_OBJ / "matrix_state.yaml"
TRACKING_DIR = STORAGE_PATH_OBJ / "tracking"
_MEMORY_DIR = STORAGE_PATH_OBJ / "memory"
CREDENTIALS_DIR = STORAGE_PATH_OBJ / "credentials"
ENCRYPTION_KEYS_DIR = STORAGE_PATH_OBJ / "encryption_keys"


def env_flag(name: str, *, default: bool = False) -> bool:
    """Read a boolean environment flag."""
    value = os.getenv(name)
    if value is None:
        return default
    return value.strip().lower() in {"1", "true", "yes", "on"}


# Other constants
VOICE_PREFIX = "🎤 "
ORIGINAL_SENDER_KEY = "com.mindroom.original_sender"
VOICE_RAW_AUDIO_FALLBACK_KEY = "com.mindroom.voice_raw_audio_fallback"
ATTACHMENT_IDS_KEY = "com.mindroom.attachment_ids"
AI_RUN_METADATA_KEY = "io.mindroom.ai_run"
ENABLE_AI_CACHE = env_flag("MINDROOM_ENABLE_AI_CACHE", default=True)

# Matrix
MATRIX_HOMESERVER = os.getenv("MATRIX_HOMESERVER", "http://localhost:8008")
# (for federation setups where hostname != server_name)
MATRIX_SERVER_NAME = os.getenv("MATRIX_SERVER_NAME", None)
# Optional installation namespace suffix used to avoid collisions on shared homeservers.
# When set, managed users/rooms are namespaced as "<name>_<namespace>".
MINDROOM_NAMESPACE = os.getenv("MINDROOM_NAMESPACE", "").strip().lower() or None

# Placeholder used in starter config templates. `mindroom connect` can
# automatically replace this token with the owner Matrix user ID returned
# by the provisioning service.
OWNER_MATRIX_USER_ID_PLACEHOLDER = "__MINDROOM_OWNER_USER_ID_FROM_PAIRING__"
MATRIX_SSL_VERIFY = env_flag("MATRIX_SSL_VERIFY", default=True)


# Canonical mapping from provider name to the environment variable it requires.
# Other modules derive their own views from this single source of truth.
PROVIDER_ENV_KEYS: dict[str, str] = {
    "anthropic": "ANTHROPIC_API_KEY",
    "openai": "OPENAI_API_KEY",
    "google": "GOOGLE_API_KEY",
    "openrouter": "OPENROUTER_API_KEY",
    "deepseek": "DEEPSEEK_API_KEY",
    "cerebras": "CEREBRAS_API_KEY",
    "groq": "GROQ_API_KEY",
    "ollama": "OLLAMA_HOST",
}
VERTEXAI_CLAUDE_ENV_KEYS: tuple[str, str] = ("ANTHROPIC_VERTEX_PROJECT_ID", "CLOUD_ML_REGION")

_CHROMADB_PY314_PATCHED = False


def env_key_for_provider(provider: str) -> str | None:
    """Get the environment variable name for a provider's API key.

    Handles the gemini→google alias so callers don't need to.
    """
    if provider == "gemini":
        return PROVIDER_ENV_KEYS.get("google")
    return PROVIDER_ENV_KEYS.get(provider)


def patch_chromadb_for_python314() -> None:
    """Patch pydantic internals so chromadb works on Python 3.14+.

    chromadb currently relies on pydantic v1 `BaseSettings` behavior and defines
    untyped fields in its settings model. This runtime shim can be removed once
    chromadb ships an upstream fix.
    """
    global _CHROMADB_PY314_PATCHED
    if _CHROMADB_PY314_PATCHED or sys.version_info < (3, 14):
        return

    import pydantic  # noqa: PLC0415
    from pydantic._internal import _model_construction  # noqa: PLC0415
    from pydantic_settings import BaseSettings  # noqa: PLC0415

    # pydantic-settings v2 defaults to extra="forbid", but pydantic v1's
    # BaseSettings silently ignored env vars / .env keys that didn't match
    # any field.  chromadb relies on that tolerance, so we must restore it.
    class _PermissiveBaseSettings(BaseSettings):
        model_config = BaseSettings.model_config.copy()
        model_config["extra"] = "ignore"

    pydantic.BaseSettings = _PermissiveBaseSettings

    original_inspect_namespace = _model_construction.inspect_namespace

    def _patched_inspect_namespace(*args: object, **kwargs: object) -> object:
        try:
            return original_inspect_namespace(*args, **kwargs)
        except pydantic.errors.PydanticUserError as exc:
            if "non-annotated attribute" not in str(exc):
                raise

            namespace = args[0] if args else kwargs.get("namespace")
            raw_annotations = args[1] if len(args) > 1 else kwargs.get("raw_annotations")
            if not isinstance(namespace, dict) or not isinstance(raw_annotations, dict):
                raise
            namespace_dict = cast("dict[str, object]", namespace)
            raw_annotations_dict = cast("dict[str, object]", raw_annotations)

            for field in (
                "chroma_coordinator_host",
                "chroma_logservice_host",
                "chroma_logservice_port",
            ):
                if field in namespace_dict and field not in raw_annotations_dict:
                    raw_annotations_dict[field] = type(namespace_dict[field])
            return original_inspect_namespace(*args, **kwargs)

    _model_construction.inspect_namespace = _patched_inspect_namespace
    _CHROMADB_PY314_PATCHED = True


def safe_replace(tmp_path: Path, target_path: Path) -> None:
    """Replace *target_path* with *tmp_path*, with a fallback for bind mounts.

    ``Path.replace`` performs an atomic rename which fails on some filesystems
    (e.g. Docker bind mounts) with ``OSError: [Errno 16] Device or resource
    busy``.  When that happens we fall back to a non-atomic copy.
    """
    try:
        tmp_path.replace(target_path)
    except OSError:
        shutil.copy2(tmp_path, target_path)
        tmp_path.unlink(missing_ok=True)


def ensure_writable_config_path(*, create_minimal: bool = False) -> bool:
    """Ensure the writable config path exists when running from a managed template.

    Returns whether a config file exists after the call.
    """
    CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)

    if CONFIG_PATH.exists():
        return True

    if _CONFIG_TEMPLATE_PATH != CONFIG_PATH and _CONFIG_TEMPLATE_PATH.exists():
        shutil.copyfile(_CONFIG_TEMPLATE_PATH, CONFIG_PATH)
        CONFIG_PATH.chmod(0o600)
        print(f"Seeded config from template {_CONFIG_TEMPLATE_PATH} -> {CONFIG_PATH}")
        return True

    if not create_minimal:
        return False

    CONFIG_PATH.write_text("agents: {}\nmodels: {}\n", encoding="utf-8")
    CONFIG_PATH.chmod(0o600)
    print(f"Created new config file at {CONFIG_PATH}")
    return True
