"""Application settings — split app home from vault data.

Two distinct locations:
  ~/.agntspace/                       ← app_dir (FIXED — config, credentials, logs, db)
  │   ├── config.toml
  │   ├── credentials.json
  │   ├── agntspace.db
  │   └── logs/

  User's Folder/                      ← root_dir (user's choice — vault data)
  ├── agntspace-inbox/                ← drop zone
  ├── agntspace-journal/              ← user journal + agent observations
  ├── agntspace-knowledge/            ← knowledge bases
  ├── agntspace-projects/             ← projects + tasks + goals
  ├── agntspace/                      ← vault_dir (agent internals)
  │   ├── system/                     ← identity, skills, agents, security
  │   └── memory/                     ← MEMORY.md, graph, summaries
  ├── User's Notes/                   ← UNTOUCHED
  └── anything else/                  ← UNTOUCHED

When root_dir is an external folder, config stays at home, data goes there.
"""

from __future__ import annotations

import tomllib
from pathlib import Path
from typing import Any

from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict


class ScheduleSettings(BaseModel):
    """Background automation intervals and timing."""

    heartbeat_seconds: int = 900
    # Minimum seconds between autonomous goal-pursuit dispatches. Independent
    # of heartbeat cadence so the heartbeat can run faster (fresher insights,
    # faster LLM-health checks, quicker SkillSchool learning) without turning
    # goal pursuit into a busy worker. Defaults to 900 to preserve
    # pre-session-70 behavior; bump heartbeat_seconds to 300 without touching
    # this to keep goals paced at one dispatch per 15 minutes.
    goal_pursuit_seconds: int = 900
    scheduler_seconds: int = 180
    morning_hour: int = 7
    evening_hour: int = 21
    weekly_compaction_day: int = 6
    auto_launch: bool = False  # auto-start background services on server boot
    timezone: str = ""  # IANA timezone (e.g. "Europe/Stockholm"); empty = auto-detect


class I18nSettings(BaseModel):
    """Internationalization settings."""

    locale: str = "en"  # en, sv, etc.


class LLMSettings(BaseModel):
    """LLM provider configuration."""

    mode: str = "cloud"
    cloud_provider: str = "anthropic"
    cloud_model: str = "claude-sonnet-4-20250514"
    quality_profile: str = "balanced"  # quality, balanced, economy
    local_provider: str = "ollama"
    local_model: str = "llama3"
    fallback_model: str = ""  # e.g. "openai/gpt-5.4" — used when primary fails


class Settings(BaseSettings):
    """Root application settings.

    app_dir:   ~/.agntspace/ — FIXED, never moves. Config, creds, logs, DB.
    root_dir:  user's chosen folder (may == home, or an external vault).
    vault_dir: root_dir/agntspace/ — identity, journal, wiki, KB, etc.
    """

    model_config = SettingsConfigDict(
        env_prefix="AGNTSPACE_",
        env_file=".env",
        env_file_encoding="utf-8",
        extra="ignore",
    )

    # Root directory — the user's chosen folder for vault data
    root_dir: Path = Field(default_factory=lambda: Path.home())
    host: str = "127.0.0.1"
    port: int = 8000
    llm: LLMSettings = Field(default_factory=LLMSettings)
    schedule: ScheduleSettings = Field(default_factory=ScheduleSettings)
    i18n: I18nSettings = Field(default_factory=I18nSettings)

    anthropic_api_key: str = Field(default="", alias="ANTHROPIC_API_KEY")
    openai_api_key: str = Field(default="", alias="OPENAI_API_KEY")

    # Whether root_dir was explicitly configured (vs home default)
    root_explicit: bool = False

    # ── Agent-mediated flows (Rule #17) ──
    # When true, background flows (watcher, heartbeat, cron, webhooks) route
    # their work through ``Agent.invoke_skill()`` instead of calling service
    # methods directly. The agent dispatches to the right specialist via the
    # orchestrator, and the mutation counter verifies real work happened.
    # When false, background flows fall back to direct service calls — this
    # is the legacy path, kept as a safety valve during migration.
    # Default: true — the migration is staged, new callers should assume on.
    agent_mediated_flows: bool = True

    # ── App home: FIXED at ~/.agntspace/ — config, credentials, logs ──

    @property
    def app_dir(self) -> Path:
        """Fixed app directory at ~/.agntspace/. Never changes."""
        return Path.home() / ".agntspace"

    @property
    def config_path(self) -> Path:
        return self.app_dir / "config.toml"

    @property
    def db_path(self) -> Path:
        """Database lives INSIDE the vault — each vault owns its data.

        Conversations, decisions, dispatch logs, canvas items are vault-
        specific content and must travel with the vault folder. Switching
        vaults naturally switches databases — no purge logic needed.

        The old location (~/agntspace/agntspace.db) is migrated on first
        boot via ``_migrate_shared_db()`` in main.py.
        """
        return self.vault_dir / "agntspace.db"

    @property
    def shared_db_path(self) -> Path:
        """Legacy shared DB path. Used only for one-time migration."""
        return self.app_dir / "agntspace.db"

    # ── Legacy app_dir paths — read-only, used only for one-time migrations ──

    @property
    def legacy_whatsapp_dir(self) -> Path:
        """Pre-2026-04-19 WhatsApp session location at ``~/.agntspace/``.

        Bridge used this as its ``AGNTSPACE_HOME``; session creds +
        state file + bridge log all lived underneath.
        """
        return self.app_dir

    @property
    def legacy_plugin_data_dir(self) -> Path:
        """Pre-2026-04-19 plugin data dir.

        This path also served as the plugin discovery root for
        user-installed (non-bundled) plugins. Discovery stays; only
        the per-plugin runtime data moves.
        """
        return self.app_dir / "plugins"

    @property
    def legacy_mcp_config_path(self) -> Path:
        """Pre-2026-04-19 MCP client config location."""
        return self.app_dir / "mcp-servers.json"

    @property
    def credentials_path(self) -> Path:
        return self.app_dir / "credentials.json"

    @property
    def log_dir(self) -> Path:
        return self.app_dir / "logs"

    # ── Vault: root_dir/agntspace/ — agent internals ──

    @property
    def vault_dir(self) -> Path:
        """The agntspace/ folder inside root_dir. Agent internals live here."""
        return self.root_dir / "agntspace"

    @property
    def vault_paths(self) -> "VaultPaths":  # noqa: F821, UP037
        """Path resolver mapping logical paths to physical locations."""
        from agntspace.vault.paths import VaultPaths
        return VaultPaths(self.root_dir)

    @property
    def skills_dir(self) -> Path:
        return self.vault_dir / "system" / "skills"

    @property
    def identity_dir(self) -> Path:
        return self.vault_dir / "system" / "identity"

    # ── Per-vault channel / plugin / MCP state (moved from app_dir 2026-04-19) ──

    @property
    def whatsapp_dir(self) -> Path:
        """Per-vault WhatsApp session + bridge state.

        Bridge uses this as its ``AGNTSPACE_HOME`` and writes:
          - ``data/whatsapp-session/``        (Baileys auth state)
          - ``data/whatsapp-bridge-state.json`` (connect/disconnect events)
          - ``whatsapp-bridge.log``             (bridge log)

        Switching vaults = different identity = re-pair via QR scan.
        """
        return self.vault_dir / "channels" / "whatsapp"

    @property
    def plugin_data_dir(self) -> Path:
        """Per-vault plugin runtime state.

        Separate from plugin DISCOVERY (which still scans bundled/
        ``~/.agntspace/plugins/``/``<vault>/plugins/``). Plugins write
        their data under ``<plugin_data_dir>/<plugin_name>/data/`` so
        state doesn't leak across vaults.
        """
        return self.vault_dir / "plugin-data"

    @property
    def plugin_state_path(self) -> Path:
        """Per-vault plugin enable/disable registry.

        Enabling a plugin in one vault doesn't enable it in another.
        Matches the per-vault semantic of ``plugin_data_dir`` — splitting
        data from enablement across vaults created weird states.
        """
        return self.vault_dir / "plugins.json"

    @property
    def legacy_plugin_state_path(self) -> Path:
        """Pre-2026-04-19 plugin enable state. One-time migration source."""
        return self.app_dir / "plugins.json"

    @property
    def mcp_config_path(self) -> Path:
        """Per-vault MCP client config (external servers to auto-connect).

        Each vault has its own server registry — a vault for work and
        a vault for personal can point at different MCP sets.
        """
        return self.vault_dir / "mcp-servers.json"

    # ── User-facing directories at root level ──

    @property
    def inbox_dir(self) -> Path:
        return self.root_dir / "agntspace-inbox"

    @property
    def journal_dir(self) -> Path:
        return self.root_dir / "agntspace-journal"

    @property
    def knowledge_dir(self) -> Path:
        return self.root_dir / "agntspace-knowledge"

    @property
    def projects_dir(self) -> Path:
        return self.root_dir / "agntspace-projects"

    # ── Backwards compat ──

    @property
    def home_dir(self) -> Path:
        return self.app_dir

    @property
    def data_dir(self) -> Path:
        return self.vault_dir

    @property
    def system_dir(self) -> Path:
        return self.app_dir

    # ── LLM helpers ──

    @property
    def active_api_key(self) -> str:
        if self.llm.mode == "local":
            return "ollama"
        if self.llm.cloud_provider == "anthropic":
            return self.anthropic_api_key
        return self.openai_api_key

    @property
    def active_model(self) -> str:
        if self.llm.mode == "local":
            return self.llm.local_model
        return self.llm.cloud_model


def load_config_toml(path: Path) -> dict[str, Any]:
    if not path.exists():
        return {}
    with open(path, "rb") as f:
        return tomllib.load(f)


def load_settings() -> Settings:
    """Load settings from env vars, then overlay with config.toml.

    Config is always read from ~/.agntspace/config.toml (fixed location).
    Vault data location is determined by root_dir in the config.
    """
    base = Settings()

    # Config is ALWAYS at ~/.agntspace/config.toml
    toml_data = load_config_toml(base.app_dir / "config.toml")

    # Legacy search if not found at standard location
    if not toml_data:
        for legacy in [
            Path.home() / ".agntspace" / "vault" / "config.toml",
            Path.home() / "agntspace" / "vault" / "config.toml",
        ]:
            toml_data = load_config_toml(legacy)
            if toml_data:
                break

    if not toml_data:
        return base

    # Overlay TOML values
    llm_data = toml_data.get("llm", {})
    if llm_data:
        base.llm = LLMSettings(**{**base.llm.model_dump(), **llm_data})

    # UI writes to [daily], config schema uses [schedule] — accept both,
    # with [daily] winning on conflict (it's the user-facing surface).
    schedule_data = {**toml_data.get("schedule", {}), **toml_data.get("daily", {})}
    if schedule_data:
        base.schedule = ScheduleSettings(**{**base.schedule.model_dump(), **schedule_data})

    server_data = toml_data.get("server", {})
    if "host" in server_data:
        base.host = server_data["host"]
    if "port" in server_data:
        base.port = server_data["port"]

    # root_dir override
    if "root_dir" in toml_data:
        base.root_dir = Path(toml_data["root_dir"])
        base.root_explicit = True
    elif "vault_dir" in toml_data:
        base.root_dir = Path(toml_data["vault_dir"]).parent
        base.root_explicit = True
    elif "home_dir" in toml_data:
        base.root_dir = Path(toml_data["home_dir"]).parent
        base.root_explicit = True

    # Sync direct mode: vault data goes to external folder
    sync_data = toml_data.get("sync", {})
    if sync_data.get("mode") == "direct" and sync_data.get("external_path"):
        direct_path = Path(sync_data["external_path"])
        if direct_path.is_dir():
            base.root_dir = direct_path
            base.root_explicit = True

    # If a vault exists at root_dir AND it's not the home directory, enable
    # root scanning. Home always has ~/.agntspace/ (app_dir) so it's a false
    # positive — only external folders with agntspace/ prove user choice.
    if not base.root_explicit and base.root_dir != Path.home():
        if (base.root_dir / "agntspace").is_dir():
            base.root_explicit = True

    return base


_settings: Settings | None = None


def get_settings() -> Settings:
    global _settings
    if _settings is None:
        _settings = load_settings()
    return _settings


def reset_settings() -> None:
    global _settings
    _settings = None
