"""
scrylo/core/config.py
---------------------
Loads ~/.scrylo/config.yaml on startup.
Creates the file with sensible defaults if it doesn't exist yet.
All other modules import settings from here — single source of truth.
"""

from __future__ import annotations

import os
from pathlib import Path
from typing import Any

import yaml

# ── Paths ──────────────────────────────────────────────────────────────────
SCRYLO_DIR = Path.home() / ".scrylo"
CONFIG_PATH = SCRYLO_DIR / "config.yaml"
MODELS_DIR = SCRYLO_DIR / "models"
DB_PATH = SCRYLO_DIR / "scrylo.db"
LOGS_DIR = SCRYLO_DIR / "logs"

# ── Default config written on first run ────────────────────────────────────
DEFAULTS: dict[str, Any] = {
    "app": {
        "version": "0.1.0",
        "host": "127.0.0.1",
        "port": 8080,
        "debug": False,
        "log_level": "info",
    },
    "llm": {
        # Phi-3 Mini Q4_K_M — 2.2 GB download, ~2.4 GB loaded
        # Non-negotiable default on 4 GB RAM — see strategy blueprint
        "model": "phi3-mini-q4_k_m.gguf",
        "groq_api_key": "",    # BYOK — Llama 3.1 70B, free tier 1000 req/day
                               # sign up at console.groq.com — any email, instant
                               # recommended: set this for best pitch quality
        "prefer_ollama": False,  # set True if running Ollama locally
        "repo_id": "microsoft/Phi-3-mini-4k-instruct-gguf",
        "n_ctx": 2048,          # KV-cache budget — critical on 4 GB
        "n_threads": 4,
        "n_gpu_layers": 0,      # CPU-only, always
        "max_tokens": 300,
        "temperature": 0.7,
    },
    "scraper": {
        "max_parallel_requests": 10,
        "request_timeout": 15,
        "rate_limit_delay": 1.5,
        "serper_api_key": "",       # optional — fallback when DDG is blocked
                                    # free tier: 2,500 searches, no credit card
                                    # get key at: https://serper.dev
        "github_token": "",         # optional — raises GitHub rate limit from 60 to 5000 req/hr
                                    # create at: github.com/settings/tokens (no scopes needed)
        "user_agent": (
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/124.0.0.0 Safari/537.36"
        ),
    },
    "email": {
        "smtp_host": "",
        "smtp_port": 587,
        "smtp_user": "",
        "smtp_password": "",
        "from_name": "",
        "from_address": "",
        "daily_send_limit": 50,     # start conservative — build reputation
        "min_delay_seconds": 90,    # randomised delay between sends
        "max_delay_seconds": 180,
        "tracking_enabled": True,
    },
    "compliance": {
        "physical_address": "",     # required by CAN-SPAM
        "unsubscribe_enabled": True,
    },
    "icp": {
        # Plain-English description of your ideal customer.
        # Used by MiniLM similarity scorer (falls back to keyword matching).
        # e.g. "B2B SaaS founder or CEO, 1-50 employees, English-speaking market"
        "profile": "",
    },
}


def _ensure_dirs() -> None:
    """Create ~/.scrylo and sub-directories if they don't exist."""
    for d in (SCRYLO_DIR, MODELS_DIR, LOGS_DIR):
        d.mkdir(parents=True, exist_ok=True)


def _write_defaults() -> None:
    CONFIG_PATH.write_text(yaml.dump(DEFAULTS, default_flow_style=False, sort_keys=False))


def load() -> dict[str, Any]:
    """
    Load config from disk, merging any missing keys from DEFAULTS.
    Safe to call multiple times — always returns a complete config dict.
    """
    _ensure_dirs()

    if not CONFIG_PATH.exists():
        _write_defaults()

    with CONFIG_PATH.open() as f:
        user_cfg: dict[str, Any] = yaml.safe_load(f) or {}

    # Deep merge — user values win, missing keys fall back to defaults
    cfg = _deep_merge(DEFAULTS, user_cfg)

    # Write merged config back — ensures new default keys are persisted
    # for users upgrading from older versions (safe: user values are preserved)
    save(cfg)

    # Sanitise — replace None values with empty strings for string fields
    # This prevents AttributeError when user leaves yaml keys blank
    cfg = _sanitise(cfg)

    return cfg


def _sanitise(cfg: dict) -> dict:
    """Replace None with '' for all string-typed config fields."""
    STRING_FIELDS = {
        "llm":      ["groq_api_key", "model", "repo_id"],
        "scraper":  ["serper_api_key", "github_token", "user_agent"],
        "email":    ["smtp_host", "smtp_user", "smtp_password", "from_name", "from_address"],
        "icp":      ["profile"],
        "pitch":    ["service_description"],
        "compliance": ["physical_address"],
    }
    for section, keys in STRING_FIELDS.items():
        if section in cfg and isinstance(cfg[section], dict):
            for key in keys:
                if cfg[section].get(key) is None:
                    cfg[section][key] = ""
    return cfg


def safe_str(cfg: dict, *keys: str) -> str:
    """Safely traverse nested config keys, always returning a string."""
    val = cfg
    for k in keys:
        if not isinstance(val, dict):
            return ""
        val = val.get(k, "")
    return (val or "")


def _deep_merge(base: dict, override: dict) -> dict:
    """
    Merge override into base recursively.
    - Keys in override win over base (user config wins)
    - Keys in base but NOT in override are kept (new defaults fill gaps)
    - This ensures new config fields added in updates reach existing users
    """
    result = dict(base)
    for key, val in override.items():
        if key in result and isinstance(result[key], dict) and isinstance(val, dict):
            result[key] = _deep_merge(result[key], val)
        else:
            result[key] = val
    return result


def save(cfg: dict[str, Any]) -> None:
    """Persist a modified config dict back to disk."""
    _ensure_dirs()
    CONFIG_PATH.write_text(yaml.dump(cfg, default_flow_style=False, sort_keys=False))


# Module-level singleton — import settings directly:
#   from scrylo.core.config import settings
#   settings["llm"]["n_ctx"]
settings: dict[str, Any] = load()
