//! Configuration loading for the backup-manager via [`platform_config::ConfigLoader`].
//!
//! Configuration files are searched along the standard hierarchy
//! (`~/.config/provisioning/platform/config/backup-manager.ncl` on Linux,
//! `~/Library/Application Support/...` on macOS) and merged with environment
//! variable overrides (`BACKUP_*` mapped to JSON paths). Nickel contracts
//! validate the merged configuration BEFORE serde deserialisation, so type
//! errors surface as Nickel error messages, not serde "expected struct field".
//!
//! See ADR-015 (`adrs/adr-015-config-driven-platform-config.ncl`) for the
//! mandate that every platform crate uses this loader.

use std::path::Path;

use platform_config::ConfigLoader;
use serde::{Deserialize, Serialize};

/// Top-level configuration for the backup-manager binary across all four modes.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BackupManagerConfig {
    /// Daemon-mode runtime parameters. Ignored in `one-shot` and `standalone`.
    #[serde(default)]
    pub daemon: DaemonConfig,

    /// Provider configuration (default provider, version pinning).
    #[serde(default)]
    pub providers: ProvidersConfig,

    /// Path (or glob) to the directory holding policy Nickel files.
    /// Resolved at runtime by invoking `nickel export` on each match.
    #[serde(default)]
    pub policies: PoliciesRef,

    /// secretumvault client configuration.
    #[serde(default)]
    pub vault: VaultClientConfig,

    /// platform-nats client configuration.
    #[serde(default)]
    pub nats: NatsClientConfig,

    /// Prometheus metrics emission configuration.
    #[serde(default)]
    pub metrics: MetricsConfig,
}

/// Daemon-mode parameters: priority queue, concurrency, API endpoint.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DaemonConfig {
    /// HTTP API listen address (e.g. `127.0.0.1:9099`).
    #[serde(default = "default_daemon_http_addr")]
    pub http_addr: String,

    /// Unix socket path for local CLI communication.
    #[serde(default = "default_daemon_unix_socket")]
    pub unix_socket: String,

    /// Maximum concurrent backup operations across all destinations.
    #[serde(default = "default_max_parallel_backups")]
    pub max_parallel_backups: usize,

    /// Maximum concurrent operations per destination.
    #[serde(default = "default_max_parallel_per_destination")]
    pub max_parallel_per_destination: usize,

    /// Path where the daemon persists its state (queue, last-run timestamps).
    #[serde(default = "default_state_path")]
    pub state_path: String,
}

impl Default for DaemonConfig {
    fn default() -> Self {
        Self {
            http_addr: default_daemon_http_addr(),
            unix_socket: default_daemon_unix_socket(),
            max_parallel_backups: default_max_parallel_backups(),
            max_parallel_per_destination: default_max_parallel_per_destination(),
            state_path: default_state_path(),
        }
    }
}

fn default_daemon_http_addr() -> String { "127.0.0.1:9099".to_string() }
fn default_daemon_unix_socket() -> String { "/run/prvng-backup.sock".to_string() }
fn default_max_parallel_backups() -> usize { 4 }
fn default_max_parallel_per_destination() -> usize { 2 }
fn default_state_path() -> String { "/var/lib/prvng-backup/state".to_string() }

/// Provider defaults and version pinning.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProvidersConfig {
    /// Provider used when a `BackupPolicy` does not specify one explicitly.
    #[serde(default = "default_provider_name")]
    pub default: String,

    /// Minimum acceptable restic CLI version. The daemon emits a warning if
    /// the installed binary is older. Format: `semver` ("0.16.0").
    #[serde(default)]
    pub restic_min_version: Option<String>,

    /// Minimum acceptable kopia CLI version, when kopia is referenced.
    #[serde(default)]
    pub kopia_min_version: Option<String>,
}

impl Default for ProvidersConfig {
    fn default() -> Self {
        Self {
            default: default_provider_name(),
            restic_min_version: None,
            kopia_min_version: None,
        }
    }
}

fn default_provider_name() -> String { "restic".to_string() }

/// Reference to the directory tree where `BackupPolicy` / `BackupGroup` /
/// `SystemBackupDef` Nickel files are committed.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PoliciesRef {
    /// Root directory containing component policies (`infra/<workspace>/components/`).
    #[serde(default = "default_components_path")]
    pub components_path: String,

    /// Path to the BackupGroup declarations file.
    #[serde(default = "default_groups_path")]
    pub groups_path: String,

    /// Path to the SystemBackupDef declarations file.
    #[serde(default = "default_system_backups_path")]
    pub system_backups_path: String,

    /// Nickel `--import-path` value used when exporting policies.
    #[serde(default = "default_nickel_import_path")]
    pub nickel_import_path: String,
}

impl Default for PoliciesRef {
    fn default() -> Self {
        Self {
            components_path: default_components_path(),
            groups_path: default_groups_path(),
            system_backups_path: default_system_backups_path(),
            nickel_import_path: default_nickel_import_path(),
        }
    }
}

fn default_components_path() -> String {
    "infra/libre-wuji/components".to_string()
}
fn default_groups_path() -> String {
    "infra/libre-wuji/backup-groups.ncl".to_string()
}
fn default_system_backups_path() -> String {
    "infra/libre-wuji/system-backups.ncl".to_string()
}
fn default_nickel_import_path() -> String { "provisioning".to_string() }

/// secretumvault connection parameters.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct VaultClientConfig {
    /// Vault HTTP endpoint (e.g. `https://vault.libre-wuji.svc.cluster.local:8200`).
    #[serde(default)]
    pub endpoint: Option<String>,

    /// Path inside vault where the manager's NKey/JWT lives for auth.
    #[serde(default)]
    pub auth_path: Option<String>,

    /// Audit log target (subject prefix or file path).
    #[serde(default)]
    pub audit_target: Option<String>,
}

/// platform-nats connection parameters (mirrors `NatsConnectionConfig`).
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct NatsClientConfig {
    /// NATS URL (e.g. `nats://nats.ops-system.svc:4222`).
    #[serde(default)]
    pub url: Option<String>,

    /// Path inside vault where the NKey seed is stored.
    #[serde(default)]
    pub nkey_seed_vault_path: Option<String>,

    /// Require signed messages on subscribed subjects.
    #[serde(default)]
    pub require_signed_messages: bool,
}

/// Prometheus metrics configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricsConfig {
    /// Metrics emission mode. Daemon mode uses HTTP; one-shot uses textfile.
    #[serde(default)]
    pub mode: MetricsMode,

    /// HTTP listen address when `mode = "http"`.
    #[serde(default = "default_metrics_http_addr")]
    pub http_addr: String,

    /// Textfile path when `mode = "textfile"` (consumed by node_exporter).
    #[serde(default = "default_metrics_textfile_path")]
    pub textfile_path: String,
}

impl Default for MetricsConfig {
    fn default() -> Self {
        Self {
            mode: MetricsMode::default(),
            http_addr: default_metrics_http_addr(),
            textfile_path: default_metrics_textfile_path(),
        }
    }
}

/// Metrics emission mode.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum MetricsMode {
    /// HTTP endpoint scraped by Prometheus (used by daemon mode).
    #[default]
    Http,
    /// Textfile written to a directory scraped by node_exporter (used by one-shot).
    Textfile,
}

fn default_metrics_http_addr() -> String { "127.0.0.1:9100".to_string() }
fn default_metrics_textfile_path() -> String {
    "/var/lib/node-exporter/textfile_collector/prvng-backup.prom".to_string()
}

impl ConfigLoader for BackupManagerConfig {
    fn service_name() -> &'static str {
        "backup-manager"
    }

    fn collect_env_overrides() -> serde_json::Value {
        // Map `BACKUP_*` environment variables to nested JSON paths matching
        // the structure of `BackupManagerConfig`. Only set keys are included;
        // the merge happens inside `platform_config::format::load_config_with_overrides`.
        let mut overrides = serde_json::Map::new();

        let mut daemon = serde_json::Map::new();
        if let Ok(v) = std::env::var("BACKUP_DAEMON_HTTP_ADDR") {
            daemon.insert("http_addr".into(), serde_json::Value::String(v));
        }
        if let Ok(v) = std::env::var("BACKUP_DAEMON_UNIX_SOCKET") {
            daemon.insert("unix_socket".into(), serde_json::Value::String(v));
        }
        if !daemon.is_empty() {
            overrides.insert("daemon".into(), serde_json::Value::Object(daemon));
        }

        let mut vault = serde_json::Map::new();
        if let Ok(v) = std::env::var("BACKUP_VAULT_ENDPOINT") {
            vault.insert("endpoint".into(), serde_json::Value::String(v));
        }
        if !vault.is_empty() {
            overrides.insert("vault".into(), serde_json::Value::Object(vault));
        }

        let mut nats = serde_json::Map::new();
        if let Ok(v) = std::env::var("BACKUP_NATS_URL") {
            nats.insert("url".into(), serde_json::Value::String(v));
        }
        if !nats.is_empty() {
            overrides.insert("nats".into(), serde_json::Value::Object(nats));
        }

        let mut providers = serde_json::Map::new();
        if let Ok(v) = std::env::var("BACKUP_PROVIDER_DEFAULT") {
            providers.insert("default".into(), serde_json::Value::String(v));
        }
        if !providers.is_empty() {
            overrides.insert("providers".into(), serde_json::Value::Object(providers));
        }

        serde_json::Value::Object(overrides)
    }

    fn from_path<P: AsRef<Path>>(
        path: P,
    ) -> std::result::Result<Self, Box<dyn std::error::Error + Send + Sync>> {
        let path = path.as_ref();
        let overrides = Self::collect_env_overrides();
        let json_value = platform_config::format::load_config_with_overrides(path, &overrides)
            .map_err(|e| -> Box<dyn std::error::Error + Send + Sync> { Box::new(e) })?;

        serde_json::from_value(json_value).map_err(|e| {
            let err_msg = format!(
                "Failed to deserialize backup-manager config from {:?}: {}",
                path, e
            );
            Box::new(std::io::Error::new(std::io::ErrorKind::InvalidData, err_msg))
                as Box<dyn std::error::Error + Send + Sync>
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn defaults_are_consistent() {
        let cfg = BackupManagerConfig::default();
        assert_eq!(cfg.daemon.http_addr, "127.0.0.1:9099");
        assert_eq!(cfg.providers.default, "restic");
        assert_eq!(cfg.metrics.mode, MetricsMode::Http);
        assert!(!cfg.nats.require_signed_messages);
    }

    #[test]
    fn service_name_matches_practice_node_artifact_path() {
        assert_eq!(BackupManagerConfig::service_name(), "backup-manager");
    }

    #[test]
    fn env_overrides_emit_only_set_keys() {
        // Without env vars set, the override map should be empty.
        // (The test runs in an isolated process; if BACKUP_* leaks from the
        // host shell the assertion is loose: just ensure it's a JSON object.)
        let v = BackupManagerConfig::collect_env_overrides();
        assert!(v.is_object());
    }
}
