from pathlib import Path

from pydantic import model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env", env_file_encoding="utf-8", extra="ignore", frozen=True
    )

    ollama_host: str = "http://localhost:11434"
    ollama_api_key: str = ""
    ollama_default_model: str = "gemma4:31b-cloud"
    ollama_num_ctx: int = 65536
    ollama_think: bool = True
    ollama_request_timeout: int = 30

    # Embedding model for memory vector search (Ollama API)
    # When embedding_ollama_host is empty, falls back to ollama_host.
    embedding_ollama_host: str = ""
    embedding_ollama_api_key: str = ""
    embedding_model: str = "nomic-embed-text:latest"
    embedding_dimensions: int = 768

    openai_api_key: str = ""
    openai_model: str = "gpt-4"
    openai_base_url: str | None = None
    anthropic_api_key: str = ""

    # Web search fallbacks (used when DuckDuckGo returns no results)
    # Brave Search API: free tier = 2000 req/month — https://brave.com/search/api/
    brave_search_api_key: str = ""
    # SearXNG: self-hosted meta-search, e.g. "http://localhost:8888"
    searxng_url: str = ""

    # Filesystem tool: comma-separated allowed root paths
    fs_allowed_paths: str = "*"

    # Terminal tool: "*" = allow all commands (recommended for local use)
    # or comma-separated list of allowed executables, e.g. "ls,cat,git"
    terminal_allowed_commands: str = "*"
    # Terminal allowlist for non-admin users (multi-user mode).
    # Admin bypasses this restriction.
    terminal_user_allowed_commands: str = (
        "ls,cat,git,grep,egrep,fgrep,head,tail,wc,find,locate,"
        "ps,df,du,free,uptime,whoami,id,mkdir,touch,cp,mv,rm,chmod,chown,"
        "echo,printf,sort,uniq,awk,sed,tr,cut,date,uname,hostname,which,whereis,"
        "python,python3,node,npm,npx,pip,pip3,env,export,source,"
        "make,cmake,gcc,g++,rustc,cargo,javac,java,go,tsc"
    )

    # SSH tool: path to JSON file with named connections
    ssh_hosts_file: str = "ssh_hosts.json"

    # Ollama multi-backend fallback: path to JSON file with server list [{host, api_key?}, ...]
    # When set, overrides ollama_host / ollama_api_key and enables server+model fallback.
    ollama_backends_file: str = ""

    # Database — PostgreSQL is required (SQLite support removed).
    database_url: str = ""

    log_level: str = "INFO"

    # Directory for user-defined tools (auto-discovered at startup)
    tools_dir: str = "tools"

    # Directory for user-defined context providers (auto-discovered at startup)
    context_providers_dir: str = "context_providers"

    # Session file uploads
    session_files_dir: str = "session_files"
    session_files_max_size_mb: int = 200
    session_messages_window: int = 1000  # max hot messages per session; older -> archive
    ws_replay_buffer_size: int = 500   # max events retained for WS reconnect replay
    share_file_max_size_mb: int = 1024

    # Public base URL used by share_file tool to build download links.
    # Change if the server is behind a reverse proxy or runs on a different port.
    public_url: str = "http://localhost:8000"

    # Gmail IMAP/SMTP (App Password auth)
    gmail_address: str = ""
    gmail_app_password: str = ""

    # gnexus-auth OAuth integration
    gnauth_base_url: str = "http://gnexus-auth.local"
    gnauth_client_id: str = ""
    gnauth_client_secret: str = ""
    gnauth_redirect_uri: str = "http://localhost:8000/auth/callback"
    gnauth_admin_role_slug: str = "navi_admin"
    gnauth_user_role_slug: str = "navi_user"
    gnauth_profile_path: str = "/account/profile"  # appended to gnauth_base_url for profile links

    # Master auth switch. When false, Navi skips OAuth/API-token checks and
    # treats every request as the anonymous admin user. Use only for trusted
    # single-user/local deployments.
    navi_auth_enabled: bool = True

    # Default profile used when a client creates a session without specifying
    # a profile_id. Empty string means "no default"; the client must supply one.
    navi_default_profile_id: str = ""

    # Auth session cookie encryption (Fernet key, 32-byte base64)
    navi_auth_encryption_key: str = ""
    navi_auth_cookie_name: str = "navi_auth_session"
    navi_auth_cookie_secure: bool = False
    navi_auth_cookie_samesite: str = "lax"
    navi_auth_cookie_max_age_days: int = 30

    # LLM call timeouts
    # complete() is non-streaming (planning, compression) — blocked until full response
    llm_complete_timeout: int = 120
    # stream_complete(): how long to wait for the FIRST token (prefill phase)
    # Large contexts can take 60-90s to prefill; 90s matches user expectation
    llm_stream_first_chunk_timeout: int = 90
    # stream_complete(): max gap between any two subsequent tokens
    llm_stream_chunk_timeout: int = 60

    # Context compression
    context_compression_enabled: bool = True
    context_compression_threshold: float = 0.90   # trigger at 90% of ollama_num_ctx
    context_compression_target: float = 0.65       # hysteresis target — after compression the kept region should fit ~65% of the window (trigger 90% → target 65% leaves real headroom instead of skimming just below the trigger and re-firing a few messages later)
    context_keep_recent: int = 8                   # conversational turns to keep verbatim
    context_summary_temperature: float = 0.3
    context_summary_max_tokens: int = 4000         # max output tokens for the summary LLM call
    output_reserve_tokens: int = 2048              # headroom reserved for model response in context checks
    # Per-message token budget for the LLM context view. A single tool/assistant
    # message whose estimated size exceeds this is head/tail-truncated in the
    # built context (never in stored history) so one huge tool result cannot
    # alone blow the window. 0 = auto (ollama_num_ctx // 6).
    context_message_token_budget: int = 0

    # Global personality prompt prepended to every agent's system prompt.
    # Multi-line values don't survive .env parsing reliably, so prefer
    # navi_persona_file (path to a plain .txt file) over inline navi_persona.
    navi_persona: str = ""
    navi_persona_file: str = ""

    @model_validator(mode="before")
    def _load_persona_from_file(cls, values: dict) -> dict:
        if not values.get("navi_persona") and values.get("navi_persona_file"):
            try:
                values["navi_persona"] = Path(values["navi_persona_file"]).read_text(
                    encoding="utf-8"
                ).strip()
            except Exception:
                pass
        return values

    @property
    def fs_allowed_paths_list(self) -> list[str]:
        return [p.strip() for p in self.fs_allowed_paths.split(",") if p.strip()]

    @property
    def terminal_allowed_commands_list(self) -> list[str]:
        return [c.strip() for c in self.terminal_allowed_commands.split(",") if c.strip()]

    @property
    def terminal_user_allowed_commands_list(self) -> list[str]:
        return [c.strip() for c in self.terminal_user_allowed_commands.split(",") if c.strip()]


settings = Settings()
