"""Configuration file loading and profile resolution.

Config files use TOML format.  Search order:
  1. --config PATH                             (explicit)
  2. $SB_SCOUT_CONFIG                          (environment variable)
  3. ./sb-scout.toml                           (working directory)
  4. ~/.config/sb-scout/config.toml            (user config)

Settings are resolved by layering:
  built-in defaults → config [defaults] → active profiles → CLI flags

Multiple profiles can be combined. Profile composition is left-to-right and
later profiles override earlier ones.

CLI flags always have the highest priority.
"""

from __future__ import annotations

import json
import os
import tomllib
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from sb_scout.models import DEFAULT_FEED_CURRENCY, DEFAULT_GPU_HINTS, FEED_URLS


# ─── Built-in defaults ───────────────────────────────────────────────────────

BUILTIN_DEFAULTS: dict[str, Any] = {
    "url": None,
    "currency": DEFAULT_FEED_CURRENCY,
    "top": 5,
    "format": "text",
    "sort_by": "overall",
    "active_profiles": [],
    "ecc_only": True,
    "min_ram_gb": 0,
    "min_storage_gb": 0,
    "min_ssd_drives": 0,
    "min_hdd_drives": 0,
    "ssd_only": False,
    "only_gpu": False,
    "inic_bonus": 0.10,
    "gpu_bonus": 0.15,
    "cpu_rank_compute_weight": 0.75,
    "storage_rank_storage_weight": 0.75,
    "overall_weights": "0.40,0.30,0.30",
    "gpu_hints": list(DEFAULT_GPU_HINTS),
    "cache_dir": str(Path(os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache") / "sb-scout" / "feeds"),
}


# ─── Built-in profiles ───────────────────────────────────────────────────────

BUILTIN_PROFILES: dict[str, dict[str, Any]] = {
    "ecc": {
        "description": "ECC-only servers",
        "ecc_only": True,
    },
    "mixed-storage": {
        "description": "At least 2 SSD/NVMe drives and 2 HDD drives",
        "min_ssd_drives": 2,
        "min_hdd_drives": 2,
    },
    "ssd-only": {
        "description": "SSD/NVMe-only servers",
        "ssd_only": True,
    },
    "high-memory": {
        "description": "At least 64 GB RAM",
        "min_ram_gb": 64,
    },
    "gpu": {
        "description": "GPU-tagged servers",
        "only_gpu": True,
    },
    "budget": {
        "description": "Best value under EUR 60/month",
        "price_cap": 60.0,
    },
    "storage-dense": {
        "description": "Maximum storage per EUR, sorted by raw storage value",
        "sort_by": "raw-storage",
        "min_storage_gb": 8000,
    },
    "ssd-compute": {
        "description": "Convenience profile: ecc + ssd-only + high-memory",
        "include": ["ecc", "ssd-only", "high-memory"],
    },
    "cultscale": {
        "description": "Compatibility alias: ecc + mixed-storage",
        "include": ["ecc", "mixed-storage"],
    },
}


# ─── Config file discovery ───────────────────────────────────────────────────

def _config_search_paths() -> list[Path]:
    xdg = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")
    return [
        Path("sb-scout.toml"),
        Path(xdg) / "sb-scout" / "config.toml",
    ]


def find_config_file(explicit_path: str | None = None) -> Path | None:
    """Find the config file, or None."""
    if explicit_path:
        p = Path(explicit_path)
        if not p.is_file():
            raise SystemExit(f"Config file not found: {explicit_path}")
        return p

    env_path = os.environ.get("SB_SCOUT_CONFIG")
    if env_path:
        p = Path(env_path)
        if not p.is_file():
            raise SystemExit(f"Config file from $SB_SCOUT_CONFIG not found: {env_path}")
        return p

    for candidate in _config_search_paths():
        resolved = candidate.expanduser().resolve()
        if resolved.is_file():
            return resolved
    return None


# ─── TOML parsing ────────────────────────────────────────────────────────────

def _load_toml(path: Path) -> dict[str, Any]:
    try:
        with path.open("rb") as f:
            return tomllib.load(f)
    except tomllib.TOMLDecodeError as exc:
        raise SystemExit(f"Invalid TOML in {path}: {exc}") from exc


_BONUS_MAP = {"inic": "inic_bonus", "gpu": "gpu_bonus"}
_WEIGHT_MAP = {
    "cpu_rank_compute": "cpu_rank_compute_weight",
    "storage_rank_storage": "storage_rank_storage_weight",
    "overall": "overall_weights",
}


def _flatten_section(section: dict[str, Any]) -> dict[str, Any]:
    """Flatten nested ``filters``, ``bonuses``, ``weights``, ``alerts`` sub-tables."""
    flat: dict[str, Any] = {}
    for key, value in section.items():
        if key == "filters" and isinstance(value, dict):
            flat.update(value)
        elif key == "bonuses" and isinstance(value, dict):
            for bk, bv in value.items():
                flat[_BONUS_MAP.get(bk, bk)] = bv
        elif key == "weights" and isinstance(value, dict):
            for wk, wv in value.items():
                flat[_WEIGHT_MAP.get(wk, wk)] = wv
        elif key == "alerts" and isinstance(value, dict):
            for ak, av in value.items():
                flat[f"alert_{ak}"] = av
        elif key == "active_profiles" and isinstance(value, str):
            flat[key] = [part.strip() for part in value.split(",") if part.strip()]
        elif key == "include" and isinstance(value, str):
            flat[key] = [part.strip() for part in value.split(",") if part.strip()]
        else:
            flat[key] = value
    return flat


# ─── Loaded config ───────────────────────────────────────────────────────────

@dataclass(frozen=True)
class LoadedConfig:
    defaults: dict[str, Any]
    profiles: dict[str, dict[str, Any]]
    extra_cpu_specs: dict[str, dict[str, int]]
    source_path: Path | None

    def list_profiles(self) -> dict[str, str]:
        return {
            name: data.get("description", "(no description)")
            for name, data in sorted(self.profiles.items())
        }


def load_config(explicit_path: str | None = None) -> LoadedConfig:
    """Load configuration, merging built-ins with any config file."""
    config_file = find_config_file(explicit_path)

    defaults = dict(BUILTIN_DEFAULTS)
    profiles: dict[str, dict[str, Any]] = {
        n: dict(d) for n, d in BUILTIN_PROFILES.items()
    }
    extra_cpu_specs: dict[str, dict[str, int]] = {}

    if config_file:
        raw = _load_toml(config_file)
        if "defaults" in raw:
            defaults.update(_flatten_section(raw["defaults"]))
        for name, section in raw.get("profiles", {}).items():
            flattened = _flatten_section(section)
            if name in profiles:
                profiles[name].update(flattened)
            else:
                profiles[name] = flattened
        for model, spec in raw.get("cpu_specs", {}).items():
            if isinstance(spec, dict) and "cores" in spec and "threads" in spec:
                extra_cpu_specs[model] = {
                    "cores": int(spec["cores"]),
                    "threads": int(spec["threads"]),
                }

    return LoadedConfig(
        defaults=defaults,
        profiles=profiles,
        extra_cpu_specs=extra_cpu_specs,
        source_path=config_file,
    )


# ─── Settings resolution ─────────────────────────────────────────────────────

def _normalize_profile_names(value: str | list[str] | tuple[str, ...] | None) -> list[str]:
    if value is None:
        return []
    if isinstance(value, str):
        return [part.strip() for part in value.split(",") if part.strip()]

    names: list[str] = []
    for item in value:
        if isinstance(item, str):
            names.extend(part.strip() for part in item.split(",") if part.strip())
    return names


def _apply_profile(
    loaded: LoadedConfig,
    settings: dict[str, Any],
    profile_name: str,
    *,
    stack: list[str],
    applied: list[str],
) -> None:
    if profile_name in applied:
        return
    if profile_name in stack:
        cycle = " -> ".join(stack + [profile_name])
        raise SystemExit(f"Profile include cycle detected: {cycle}")
    if profile_name not in loaded.profiles:
        available = ", ".join(sorted(loaded.profiles.keys()))
        raise SystemExit(
            f"Unknown profile: {profile_name}\n"
            f"Available profiles: {available or '(none)'}"
        )

    profile = loaded.profiles[profile_name]
    includes = _normalize_profile_names(profile.get("include"))
    for include_name in includes:
        _apply_profile(loaded, settings, include_name, stack=stack + [profile_name], applied=applied)

    for key, value in profile.items():
        if key not in {"description", "include"}:
            settings[key] = value
    applied.append(profile_name)


def resolve_settings(
    loaded: LoadedConfig,
    profile_names: str | list[str] | tuple[str, ...] | None,
    cli_overrides: dict[str, Any],
) -> tuple[dict[str, Any], list[str]]:
    """Layer: built-in defaults → config defaults → active profiles → CLI overrides."""
    settings = dict(loaded.defaults)

    active_profiles = _normalize_profile_names(profile_names)
    if active_profiles:
        settings["active_profiles"] = active_profiles
    else:
        active_profiles = _normalize_profile_names(settings.get("active_profiles"))

    applied_profiles: list[str] = []
    for profile_name in active_profiles:
        _apply_profile(loaded, settings, profile_name, stack=[], applied=applied_profiles)

    settings.update(cli_overrides)
    settings["active_profiles"] = active_profiles
    settings["resolved_profiles"] = applied_profiles

    currency = str(settings.get("currency") or DEFAULT_FEED_CURRENCY).upper()
    if currency not in FEED_URLS:
        available = ", ".join(sorted(FEED_URLS))
        raise SystemExit(f"Unsupported currency: {currency} (choose from: {available})")

    url = settings.get("url")
    if url:
        detected_currency = next((cur for cur, feed_url in FEED_URLS.items() if feed_url == url), None)
        if detected_currency:
            currency = detected_currency
    else:
        url = FEED_URLS[currency]

    settings["currency"] = currency
    settings["url"] = url
    return settings, applied_profiles


def dump_settings(
    settings: dict[str, Any],
    *,
    profiles: list[str],
    source: Path | None,
) -> str:
    payload: dict[str, Any] = {}
    if source:
        payload["_config_file"] = str(source)
    if profiles:
        payload["_profiles"] = profiles
    payload.update(settings)
    return json.dumps(payload, indent=2, default=str)


# ─── Example config ──────────────────────────────────────────────────────────

EXAMPLE_CONFIG = """\
# sb-scout.toml — sb-scout configuration
#
# Place this file in one of these locations (checked in order):
#   1. ./sb-scout.toml                                     (project-local)
#   2. ~/.config/sb-scout/config.toml                      (user-level)
#   3. Any path via --config or $SB_SCOUT_CONFIG
#
# Settings are resolved by layering:
#   built-in defaults → [defaults] → active profiles → CLI flags
#
# Profiles are composable. Later profiles override earlier ones.
# CLI flags always take highest priority.

# ─── Global defaults ─────────────────────────────────────────────────────────

[defaults]
# url = "https://www.hetzner.com/_resources/app/data/app/live_data_sb_USD.json"
# currency = "USD"                 # USD (default) or EUR
# cache_dir = "~/.cache/sb-scout/feeds"
# top = 5
# format = "text"                   # text | markdown
# sort_by = "overall"               # raw-cpu | raw-ram | raw-storage | cpu-rank | storage-rank | overall
# active_profiles = ["ecc"]         # default profile set; later profiles override earlier ones
# history_file = "~/.local/share/sb-scout/history.jsonl"

[defaults.filters]
# ecc_only = true
# price_cap = 100.0
# min_ram_gb = 0
# max_ram_gb = 512
# min_storage_gb = 0
# max_storage_gb = 50000
# min_ssd_drives = 0
# min_hdd_drives = 0
# ssd_only = false
# datacenter = "HEL1"
# exclude_datacenter = "NBG1"
# cpu_regex = "epyc|ryzen"
# exclude_cpu_regex = "xeon e3"
# only_gpu = false
# price_per_thread_cap = 5.0

[defaults.bonuses]
# inic = 0.10                       # +10%
# gpu = 0.15                        # +15%

[defaults.weights]
# cpu_rank_compute = 0.75
# storage_rank_storage = 0.75
# overall = "0.40,0.30,0.30"       # compute, ram, storage

[defaults.alerts]
# min_overall_score = 0.80          # Fire when any server scores >= 80%
# min_cpu_score = 0.90
# min_storage_score = 0.90
# max_price = 40.0                  # Fire when net price <= 40 (currency follows feed)
# notify_command = "notify-send 'sb-scout alert'"
# email_to = ["ops@example.com"]
# email_from = "sb-scout@example.com"
# email_subject = "sb-scout alert"
# email_account = "default"        # optional msmtp account name

# gpu_hints = ["GPU", "NVIDIA", "RTX", "TESLA", "A100", "A6000", "H100", "L40", "L4", "V100"]
# cache_dir = "~/.cache/sb-scout/feeds"

# ─── Profiles ────────────────────────────────────────────────────────────────
# Activate with repeated flags:
#   sb-scout --profile ecc --profile mixed-storage
#   sb-scout --profile ecc,gpu,budget

[profiles.ecc]
description = "ECC-only servers"

[profiles.ecc.filters]
ecc_only = true

[profiles.mixed-storage]
description = "At least 2 SSD/NVMe drives and 2 HDD drives"

[profiles.mixed-storage.filters]
min_ssd_drives = 2
min_hdd_drives = 2

[profiles.ssd-only]
description = "SSD/NVMe-only servers"

[profiles.ssd-only.filters]
ssd_only = true

[profiles.high-memory]
description = "At least 64 GB RAM"

[profiles.high-memory.filters]
min_ram_gb = 64

[profiles.gpu]
description = "GPU-tagged servers"

[profiles.gpu.filters]
only_gpu = true

[profiles.budget]
description = "Best value under EUR 60/month"

[profiles.budget.filters]
price_cap = 60.0

[profiles.storage-dense]
description = "Maximum storage per EUR"
sort_by = "raw-storage"

[profiles.storage-dense.filters]
min_storage_gb = 8000

[profiles.ssd-compute]
description = "Convenience profile: ecc + ssd-only + high-memory"
include = ["ecc", "ssd-only", "high-memory"]

[profiles.cultscale]
description = "Compatibility alias: ecc + mixed-storage"
include = ["ecc", "mixed-storage"]

# ─── Extra CPU models ────────────────────────────────────────────────────────

# [cpu_specs]
# "AMD EPYC 9654" = { cores = 96, threads = 192 }
# "Intel Xeon w9-3475X" = { cores = 36, threads = 72 }
"""


def print_example_config() -> None:
    print(EXAMPLE_CONFIG, end="")
