"""
agentpaas.config — Global configuration via Pydantic Settings.

Supports: environment variables (AGENTPAAS_ prefix), .env file, defaults.
Principle P3: SQLite + memory queue for dev; PostgreSQL + Redis for production.

Data directory hierarchy (all runtime-generated files):
  AGENTPAAS_DATA_DIR  (default: ~/.agentpaas/data  or  /data in Docker)
  ├── agentpaas.db            SQLite database
  ├── instances/              Agent instance data
  │   └── {name}/
  │       ├── instance.yml    Config (minimal, user-editable)
  │       ├── workspace/      Per-run artifacts (input/output/trace/cost)
  │       └── wiki/           Compiled LLM wiki
  ├── knowledge_bases/        Knowledge base indexes
  │   └── {kb_id}/
  │       ├── rag_index.json
  │       ├── rag_vectors_v2.npy
  │       ├── rag_vectors_meta_v2.pkl
  │       └── jobs/           Build job logs
  └── logs/                   Rotating application logs
      ├── agentpaas.log
      └── agentpaas.log.1
"""
from __future__ import annotations
import os
import secrets
from pathlib import Path
from typing import Optional


# The setup wizard / launcher persists user preferences (api_key, and —
# FR-002 — a chosen data_dir) to this fixed path. It lives OUTSIDE data_dir
# precisely so it can carry the data_dir choice itself across restarts.
_CONFIG_JSON = os.path.join(os.path.expanduser("~"), ".agentpaas", "config.json")

# ── Bootstrap: load ~/.agentpaas/.env into os.environ ────────────────────────
# This file stores user API keys (DASHSCOPE_API_KEY, OPENAI_API_KEY, …) set
# via the Setup Wizard or `agentpaas providers set` CLI. We load it here —
# once, before any config value is read — so all downstream code can simply
# call os.environ.get("DASHSCOPE_API_KEY", "") without needing python-dotenv.
# Format: KEY=value, one per line; comments (#) and blank lines are ignored;
# values may be optionally quoted with ' or ".
def _load_dotenv(path: str = "") -> None:
    env_path = path or os.path.join(os.path.expanduser("~"), ".agentpaas", ".env")
    try:
        with open(env_path) as _f:
            for _line in _f:
                _line = _line.strip()
                if not _line or _line.startswith("#") or "=" not in _line:
                    continue
                _k, _v = _line.split("=", 1)
                _k = _k.strip()
                _v = _v.strip().strip('"').strip("'")
                if _k and _k not in os.environ:  # don't override explicit env
                    os.environ[_k] = _v
    except FileNotFoundError:
        pass  # no .env yet — fine, wizard will create it later
    except Exception:
        pass  # never crash startup due to a malformed .env

# Test isolation: when AGENTPAAS_TESTING is truthy (set by tests/conftest.py),
# skip loading the developer's machine-level ~/.agentpaas/.env and
# providers.json. Otherwise their personal PORT/DATA_DIR/API-key settings leak
# into the test process and make "default value" assertions machine-dependent.
def is_testing() -> bool:
    """单一事实来源：AGENTPAAS_TESTING 是否开启。

    只认 1/true/yes（大小写不敏感）；"0"/"false"/"" 都视为关闭——
    修复 app.py 此前 `os.getenv(...)` 真值判断把 "0" 当成开启的歧义。
    """
    return os.environ.get("AGENTPAAS_TESTING", "").strip().lower() in ("1", "true", "yes")

_IS_TESTING = is_testing()

if not _IS_TESTING:
    _load_dotenv()


# ── Bootstrap: load ~/.agentpaas/providers.json into os.environ ──────────────
# The UI "环境配置" page saves keys to providers.json (via POST /providers/{id}).
# We read it here so those keys survive server restarts.
# Priority: providers.json OVERRIDES .env (UI-set values are more authoritative).
_PROVIDER_ENV_KEYS: dict = {
    "anthropic": "ANTHROPIC_API_KEY",
    "dashscope": "DASHSCOPE_API_KEY",
    "deepseek":  "DEEPSEEK_API_KEY",
    "openai":    "OPENAI_API_KEY",
    "zhipu":     "ZHIPU_API_KEY",
    "moonshot":  "MOONSHOT_API_KEY",
}

def _load_providers_json(path: str = "") -> None:
    json_path = path or os.path.join(os.path.expanduser("~"), ".agentpaas", "providers.json")
    try:
        import json as _json
        with open(json_path) as _f:
            _data = _json.load(_f)
        for _pid, _entry in _data.items():
            _env_key = _PROVIDER_ENV_KEYS.get(_pid, "")
            _api_key = (_entry or {}).get("api_key", "")
            if _env_key and _api_key:
                os.environ[_env_key] = _api_key  # override .env — UI setting wins
    except FileNotFoundError:
        pass
    except Exception:
        pass

if not _IS_TESTING:
    _load_providers_json()


def _data_dir_from_config_json() -> str:
    """Read a persisted data_dir override from ~/.agentpaas/config.json.

    FR-002: the first-run wizard lets a desktop user pick where their
    research data lives. We persist that choice here (not under data_dir,
    which would be circular) and honor it on every subsequent startup.
    Returns "" if no override is set or the file is unreadable.
    """
    try:
        import json
        with open(_CONFIG_JSON) as f:
            return (json.load(f) or {}).get("data_dir", "") or ""
    except Exception:
        return ""


def _default_data_dir(mode: str = "desktop") -> str:
    """
    Resolve the data directory in priority order:
    1. AGENTPAAS_DATA_DIR env var (explicit override, used by Docker: /data)
    2. data_dir persisted in ~/.agentpaas/config.json (FR-002 wizard choice)
    3. mode-aware default:
       - desktop: ~/LambdAgentDesktop  (user-visible, branded — CR FR-002)
       - lab / paas: ~/.agentpaas/data (hidden, standard config-dir style)

    Rationale (CR-20260607-001 rev.2 §FR-002): desktop users are non-
    technical scientists — they need to find the data directory in
    Finder/Explorer without showing hidden folders. A capitalized,
    project-branded dir name in $HOME accomplishes that. lab / paas
    deployments keep the hidden dir to follow OS conventions.
    """
    explicit = os.getenv("AGENTPAAS_DATA_DIR", "")
    if explicit:
        return explicit
    persisted = _data_dir_from_config_json()
    if persisted:
        return persisted
    if mode == "desktop":
        return str(Path.home() / "LambdAgentDesktop")
    return str(Path.home() / ".agentpaas" / "data")


class AgentPaaSConfig:
    """Configuration with sensible defaults for dev mode."""

    def __init__(self):
        # ── Deployment mode (CR-20260607-001 rev.2) ──
        # `desktop` (default): single-user local installer; auto-bootstrap;
        #   /admin /billing routes return 404; recommended bind 127.0.0.1.
        # `lab`: team-shared deployment (1 tenant, N user); SetupWizard;
        #   admin routes visible; default bind 127.0.0.1 + nginx.
        # `paas`: full multi-tenant cloud deployment; all routes visible.
        # Set via env: AGENTPAAS_DEPLOYMENT_MODE=desktop|lab|paas
        _mode = os.getenv("AGENTPAAS_DEPLOYMENT_MODE", "desktop").lower()
        if _mode not in ("desktop", "lab", "paas"):
            raise ValueError(
                f"AGENTPAAS_DEPLOYMENT_MODE must be one of desktop|lab|paas, "
                f"got: {_mode!r}"
            )
        self.deployment_mode: str = _mode

        # ── Network bind (CR rev.2 §Q12, audit critical #1 same posture) ──
        # `desktop`: 127.0.0.1 only — locked down by default, matches the
        #   loopback-only /setup/bootstrap defense. Users with truly local
        #   reverse-proxy needs can override via AGENTPAAS_HOST=0.0.0.0.
        # `lab`: 127.0.0.1 by default too — operator opens LAN explicitly
        #   via nginx (recommended) or AGENTPAAS_HOST=0.0.0.0 (advanced).
        # `paas`: 0.0.0.0 (always behind a reverse proxy in production).
        _default_host = "0.0.0.0" if _mode == "paas" else "127.0.0.1"
        self.host: str = os.getenv("AGENTPAAS_HOST", _default_host)
        self.port: int = int(os.getenv("AGENTPAAS_PORT", "8000"))
        self.debug: bool = os.getenv("AGENTPAAS_DEBUG", "false").lower() == "true"

        # ── Data directory (single source of truth for all runtime files) ──
        # Mode-aware: desktop uses ~/LambdAgentDesktop (visible to users),
        # lab/paas use ~/.agentpaas/data (hidden, follows OS convention).
        self.data_dir: str = _default_data_dir(_mode)

        # Derived sub-directories — callers should use these, not hardcode paths
        self.instances_dir: str = os.getenv(
            "AGENTPAAS_INSTANCES_DIR",
            os.path.join(self.data_dir, "instances")
        )
        self.knowledge_bases_dir: str = os.getenv(
            "AGENTPAAS_KB_DIR",
            os.path.join(self.data_dir, "knowledge_bases")
        )
        self.logs_dir: str = os.getenv(
            "AGENTPAAS_LOGS_DIR",
            os.path.join(self.data_dir, "logs")
        )
        # M2 Phase D: installed AgentPacks live here, one dir per id/version
        # (docs/AGENTPACK_SPEC.md §1).
        self.agentpacks_dir: str = os.getenv(
            "AGENTPAAS_AGENTPACKS_DIR",
            os.path.join(self.data_dir, "agentpacks")
        )
        # 智能体工作目录基底：创建实例时自动开辟 <base>/<agent名>/
        # （work_dir 根 + data/ 来源 + workspace/run_* 运行产物 — 参照
        # Research-Vault/Workspace/multi-reviewer67 的布局）。
        # 不在启动时 mkdir — 首次创建智能体时才建。
        self.workspace_base: str = os.getenv(
            "AGENTPAAS_WORKSPACE_BASE",
            os.path.join(self.data_dir, "Workspace")
        )

        # Auto-create essential directories on startup
        for _d in (self.data_dir, self.instances_dir, self.knowledge_bases_dir,
                   self.logs_dir, self.agentpacks_dir):
            os.makedirs(_d, exist_ok=True)

        # ── Database — SQLite for dev/single-user, PostgreSQL for prod ──
        # If an explicit URL is set, use it; otherwise put SQLite in data_dir.
        _db_default = f"sqlite:///{os.path.join(self.data_dir, 'agentpaas.db')}"
        self.database_url: str = os.getenv("AGENTPAAS_DATABASE_URL", _db_default)

        # Redis — None means in-memory fallback
        self.redis_url: Optional[str] = os.getenv("AGENTPAAS_REDIS_URL")

        # ── lambdagent integration ──
        self.sandbox_level: int = int(os.getenv("AGENTPAAS_SANDBOX_LEVEL", "0"))
        self.default_timeout: int = int(os.getenv("AGENTPAAS_DEFAULT_TIMEOUT", "120"))
        self.max_steps: int = int(os.getenv("AGENTPAAS_MAX_STEPS", "100"))
        self.max_tokens: int = int(os.getenv("AGENTPAAS_MAX_TOKENS", "100000"))

        # ── Workspace retention (days before old run dirs are cleaned up) ──
        self.workspace_retention_days: int = int(
            os.getenv("AGENTPAAS_WORKSPACE_RETENTION_DAYS", "30")
        )

        # ── Auth — SEC-05: master key MUST be set in production ──
        self.master_key: str = os.getenv("AGENTPAAS_MASTER_KEY", "")
        if not self.master_key:
            if self.debug or self.database_url.startswith("sqlite"):
                # Dev mode: auto-generate (acceptable for local dev only)
                self.master_key = secrets.token_hex(32)
            else:
                raise ValueError(
                    "AGENTPAAS_MASTER_KEY required in production. "
                    "Generate with: python -c 'import secrets; print(secrets.token_hex(32))'"
                )
        self.jwt_secret: str = os.getenv("AGENTPAAS_JWT_SECRET", "")

        # ── Rate limiting ──
        self.global_rate_limit: int = int(os.getenv("AGENTPAAS_GLOBAL_RATE_LIMIT", "10000"))
        self.default_tenant_rate_limit: int = int(os.getenv("AGENTPAAS_TENANT_RATE_LIMIT", "60"))

        # ── Observability ──
        self.log_level: str = os.getenv("AGENTPAAS_LOG_LEVEL", "INFO")
        self.log_format: str = os.getenv("AGENTPAAS_LOG_FORMAT", "console")  # json / console
        self.log_max_bytes: int = int(os.getenv("AGENTPAAS_LOG_MAX_BYTES", str(10 * 1024 * 1024)))  # 10 MB
        self.log_backup_count: int = int(os.getenv("AGENTPAAS_LOG_BACKUP_COUNT", "5"))


# Singleton
settings = AgentPaaSConfig()
