use rns_ctl::state::{
    LaunchProcessSnapshot, ProcessControlCommand, ServerConfigApplyPlan, ServerConfigChange,
    ServerConfigFieldSchema, ServerConfigMutationMode, ServerConfigMutationResult,
    ServerConfigSchemaSnapshot, ServerConfigSnapshot, ServerConfigValidationSnapshot,
    ServerHttpConfigSnapshot, SharedState,
};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::mpsc;

use crate::args::Args;
use crate::supervisor::{
    ProcessCommand, ProcessReadiness, ProcessSpec, ReadinessTarget, RnsdDrainConfig, Role,
    SupervisorConfig,
};
use rns_net::RpcAddr;

#[derive(Debug, Clone)]
pub struct ServerConfig {
    pub config_path: Option<PathBuf>,
    pub resolved_config_dir: PathBuf,
    pub server_config_file_path: PathBuf,
    pub server_config_file_present: bool,
    pub file_config: ServerConfigFile,
    pub stats_db_path: PathBuf,
    pub rnsd_bin: PathBuf,
    pub sentineld_bin: PathBuf,
    pub statsd_bin: PathBuf,
    pub http: HttpConfig,
    pub rnsd_rpc_addr: std::net::SocketAddr,
}

#[derive(Debug, Clone)]
pub struct HttpConfig {
    pub enabled: bool,
    pub host: String,
    pub port: u16,
    pub auth_token: Option<String>,
    pub disable_auth: bool,
    pub daemon_mode: bool,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ServerConfigFile {
    #[serde(default)]
    pub stats_db_path: Option<String>,
    #[serde(default)]
    pub rnsd_bin: Option<String>,
    #[serde(default)]
    pub sentineld_bin: Option<String>,
    #[serde(default)]
    pub statsd_bin: Option<String>,
    #[serde(default)]
    pub http: ServerHttpConfigFile,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ServerHttpConfigFile {
    #[serde(default)]
    pub enabled: Option<bool>,
    #[serde(default)]
    pub host: Option<String>,
    #[serde(default)]
    pub port: Option<u16>,
    #[serde(default)]
    pub auth_token: Option<String>,
    #[serde(default)]
    pub disable_auth: Option<bool>,
}

impl ServerConfig {
    pub fn from_args(args: &Args) -> Self {
        let config_path = args.config_path().map(PathBuf::from);
        let resolved_config_dir =
            rns_net::storage::resolve_config_dir(args.config_path().map(Path::new));
        let server_config_file_path = resolved_config_dir.join("rns-server.json");
        let (file_cfg, file_present) = Self::load_config_file(&server_config_file_path)
            .unwrap_or_else(|err| {
                log::warn!(
                    "failed to load server config file {}: {}",
                    server_config_file_path.display(),
                    err
                );
                (ServerConfigFile::default(), false)
            });
        Self::build(
            config_path,
            resolved_config_dir,
            server_config_file_path,
            file_present,
            &file_cfg,
            Some(args),
        )
    }

    pub fn validate_json_with_current_context(
        &self,
        body: &[u8],
    ) -> Result<ServerConfigValidationSnapshot, String> {
        let candidate = Self::parse_config_json(body)?;
        let mut warnings = Self::validate_file_config(&candidate)?;
        let validated = self.with_file_config(&candidate, self.server_config_file_present);
        warnings.push(format!(
            "Validation used config dir {} and did not write any files.",
            self.resolved_config_dir.display()
        ));

        Ok(ServerConfigValidationSnapshot {
            valid: true,
            config: validated.snapshot(),
            warnings,
        })
    }

    pub fn mutate_json_with_current_context(
        &self,
        mode: ServerConfigMutationMode,
        body: &[u8],
        control_tx: Option<mpsc::Sender<ProcessControlCommand>>,
    ) -> Result<ServerConfigMutationResult, String> {
        let candidate = Self::parse_config_json(body)?;
        let warnings = Self::validate_file_config(&candidate)?;
        let next = self.with_file_config(&candidate, true);
        let apply_plan = self.apply_plan(&next);

        std::fs::create_dir_all(&self.resolved_config_dir).map_err(|err| {
            format!(
                "failed to create config dir {}: {}",
                self.resolved_config_dir.display(),
                err
            )
        })?;
        let serialized = serde_json::to_vec_pretty(&candidate)
            .map_err(|err| format!("failed to serialize server config JSON: {}", err))?;
        std::fs::write(&self.server_config_file_path, serialized).map_err(|err| {
            format!(
                "failed to write {}: {}",
                self.server_config_file_path.display(),
                err
            )
        })?;

        if matches!(mode, ServerConfigMutationMode::Apply) {
            if let Some(tx) = control_tx {
                for process in &apply_plan.processes_to_restart {
                    tx.send(ProcessControlCommand::Restart(process.clone()))
                        .map_err(|_| {
                            format!("failed to queue restart for process '{}'", process)
                        })?;
                }
            }
        }

        Ok(ServerConfigMutationResult {
            action: match mode {
                ServerConfigMutationMode::Save => "save".into(),
                ServerConfigMutationMode::Apply => "apply".into(),
            },
            config: next.snapshot(),
            apply_plan,
            warnings,
        })
    }

    pub fn supervisor_config(
        &self,
        shared_state: Option<SharedState>,
        control_rx: Option<mpsc::Receiver<ProcessControlCommand>>,
    ) -> SupervisorConfig {
        SupervisorConfig {
            specs: self.process_specs(),
            shared_state,
            control_rx,
            readiness: self.readiness_checks(),
            log_dir: Some(self.resolved_config_dir.join("logs")),
            rnsd_drain: self.rnsd_drain_config(),
        }
    }

    pub fn ensure_runtime_bootstrap(&self) -> Result<(), String> {
        let paths = rns_net::storage::ensure_storage_dirs(&self.resolved_config_dir)
            .map_err(|err| format!("failed to create runtime storage dirs: {}", err))?;
        rns_net::storage::load_or_create_identity(&paths.identities)
            .map_err(|err| format!("failed to initialize node identity: {}", err))?;
        Ok(())
    }

    pub fn process_specs(&self) -> Vec<ProcessSpec> {
        vec![
            ProcessSpec {
                role: Role::Rnsd,
                command: self.command_for_override(&self.rnsd_bin),
                args: self.rnsd_args(),
            },
            ProcessSpec {
                role: Role::Sentineld,
                command: self.command_for_override(&self.sentineld_bin),
                args: self.sentineld_args(),
            },
            ProcessSpec {
                role: Role::Statsd,
                command: self.command_for_override(&self.statsd_bin),
                args: self.statsd_args(),
            },
        ]
    }

    pub fn snapshot(&self) -> ServerConfigSnapshot {
        ServerConfigSnapshot {
            config_path: self
                .config_path
                .as_ref()
                .map(|path| path.display().to_string()),
            resolved_config_dir: self.resolved_config_dir.display().to_string(),
            server_config_file_path: self.server_config_file_path.display().to_string(),
            server_config_file_present: self.server_config_file_present,
            server_config_file_json: self.editable_file_json(),
            stats_db_path: self.stats_db_path.display().to_string(),
            rnsd_bin: self.binary_mode_label(&self.rnsd_bin, "rnsd"),
            sentineld_bin: self.binary_mode_label(&self.sentineld_bin, "rns-sentineld"),
            statsd_bin: self.binary_mode_label(&self.statsd_bin, "rns-statsd"),
            http: ServerHttpConfigSnapshot {
                enabled: self.http.enabled,
                host: self.http.host.clone(),
                port: self.http.port,
                auth_mode: if self.http.disable_auth {
                    "disabled".into()
                } else {
                    "bearer-token".into()
                },
                token_configured: self.http.auth_token.is_some(),
                daemon_mode: self.http.daemon_mode,
            },
            launch_plan: self
                .process_specs()
                .into_iter()
                .map(|spec| LaunchProcessSnapshot {
                    name: spec.role.display_name().to_string(),
                    bin: spec.command.display(spec.role),
                    args: spec.args.clone(),
                    command_line: spec.command_line(),
                })
                .collect(),
        }
    }

    pub fn schema_snapshot(&self) -> ServerConfigSchemaSnapshot {
        ServerConfigSchemaSnapshot {
            format: "rns-server.json".into(),
            example_config_json: Self::example_config_json(),
            notes: vec![
                format!(
                    "The config file is loaded from {}.",
                    self.server_config_file_path.display()
                ),
                "Only fields present in rns-server.json are persisted; CLI flags still override them at startup.".into(),
                "By default, child roles self-spawn from the running rns-server binary via /proc/self/exe with current_exe() fallback. Explicit child binary paths remain available as advanced overrides.".into(),
                "Changing process launch settings restarts only the affected child processes. Embedded HTTP auth settings reload in place; bind host, port, and enablement changes still require restarting rns-server.".into(),
            ],
            fields: vec![
                ServerConfigFieldSchema {
                    field: "stats_db_path".into(),
                    field_type: "string".into(),
                    required: false,
                    default_value: self.resolved_config_dir.join("stats.db").display().to_string(),
                    description: "SQLite database path used by rns-statsd.".into(),
                    effect: "Restarts rns-statsd when changed.".into(),
                },
                ServerConfigFieldSchema {
                    field: "rnsd_bin".into(),
                    field_type: "string".into(),
                    required: false,
                    default_value: "(self-spawn via /proc/self/exe)".into(),
                    description: "Advanced override for the Reticulum daemon executable; unset uses self-spawn from rns-server.".into(),
                    effect: "Restarts rnsd when changed.".into(),
                },
                ServerConfigFieldSchema {
                    field: "sentineld_bin".into(),
                    field_type: "string".into(),
                    required: false,
                    default_value: "(self-spawn via /proc/self/exe)".into(),
                    description: "Advanced override for the sentinel sidecar executable; unset uses self-spawn from rns-server.".into(),
                    effect: "Restarts rns-sentineld when changed.".into(),
                },
                ServerConfigFieldSchema {
                    field: "statsd_bin".into(),
                    field_type: "string".into(),
                    required: false,
                    default_value: "(self-spawn via /proc/self/exe)".into(),
                    description: "Advanced override for the stats sidecar executable; unset uses self-spawn from rns-server.".into(),
                    effect: "Restarts rns-statsd when changed.".into(),
                },
                ServerConfigFieldSchema {
                    field: "http.enabled".into(),
                    field_type: "boolean".into(),
                    required: false,
                    default_value: "true".into(),
                    description: "Enable or disable the embedded HTTP control plane.".into(),
                    effect: "Requires restarting rns-server.".into(),
                },
                ServerConfigFieldSchema {
                    field: "http.host".into(),
                    field_type: "string".into(),
                    required: false,
                    default_value: "127.0.0.1".into(),
                    description: "Bind host for the embedded HTTP control plane.".into(),
                    effect: "Requires restarting rns-server.".into(),
                },
                ServerConfigFieldSchema {
                    field: "http.port".into(),
                    field_type: "u16".into(),
                    required: false,
                    default_value: "8080".into(),
                    description: "Bind port for the embedded HTTP control plane.".into(),
                    effect: "Requires restarting rns-server.".into(),
                },
                ServerConfigFieldSchema {
                    field: "http.auth_token".into(),
                    field_type: "string".into(),
                    required: false,
                    default_value: "(generated if auth is enabled and no token is configured)".into(),
                    description: "Optional fixed bearer token for the embedded HTTP control plane.".into(),
                    effect: "Reloads embedded HTTP auth in place.".into(),
                },
                ServerConfigFieldSchema {
                    field: "http.disable_auth".into(),
                    field_type: "boolean".into(),
                    required: false,
                    default_value: "false".into(),
                    description: "Disable bearer-token authentication on the embedded HTTP control plane.".into(),
                    effect: "Reloads embedded HTTP auth in place.".into(),
                },
            ],
        }
    }

    pub fn http_enabled(&self) -> bool {
        self.http.enabled
    }

    pub fn ctl_args(&self, verbosity: u8) -> rns_ctl::args::Args {
        let mut argv = vec!["--daemon".to_string()];
        if let Some(config_path) = &self.config_path {
            argv.push("--config".into());
            argv.push(config_path.display().to_string());
        }
        argv.push("--host".into());
        argv.push(self.http.host.clone());
        argv.push("--port".into());
        argv.push(self.http.port.to_string());
        if let Some(token) = &self.http.auth_token {
            argv.push("--token".into());
            argv.push(token.clone());
        }
        if self.http.disable_auth {
            argv.push("--disable-auth".into());
        }
        if verbosity > 0 {
            argv.push(format!("-{}", "v".repeat(verbosity as usize)));
        }
        rns_ctl::args::Args::parse_from(argv)
    }

    pub fn control_http_command_line(&self) -> String {
        let mut parts = vec!["embedded rns-ctl http".to_string(), "--daemon".to_string()];
        if let Some(config) = &self.config_path {
            parts.push("--config".to_string());
            parts.push(config.display().to_string());
        }
        parts.push("--host".to_string());
        parts.push(self.http.host.clone());
        parts.push("--port".to_string());
        parts.push(self.http.port.to_string());
        if let Some(token) = &self.http.auth_token {
            parts.push("--token".to_string());
            parts.push(token.clone());
        }
        if self.http.disable_auth {
            parts.push("--disable-auth".to_string());
        }
        parts.join(" ")
    }

    fn command_for_override(&self, path: &PathBuf) -> ProcessCommand {
        if path.as_os_str().is_empty() {
            ProcessCommand::SelfInvoke
        } else {
            ProcessCommand::External(path.clone())
        }
    }

    fn binary_mode_label(&self, path: &PathBuf, role: &str) -> String {
        if path.as_os_str().is_empty() {
            format!("self-spawn ({role})")
        } else {
            path.display().to_string()
        }
    }

    fn readiness_checks(&self) -> Vec<ProcessReadiness> {
        vec![
            ProcessReadiness {
                role: Role::Rnsd,
                target: ReadinessTarget::Tcp(self.rnsd_rpc_addr),
            },
            ProcessReadiness {
                role: Role::Sentineld,
                target: ReadinessTarget::ReadyFile(self.sentineld_ready_file_path()),
            },
            ProcessReadiness {
                role: Role::Statsd,
                target: ReadinessTarget::ReadyFile(self.statsd_ready_file_path()),
            },
        ]
    }

    fn rnsd_drain_config(&self) -> Option<RnsdDrainConfig> {
        let paths = rns_net::storage::ensure_storage_dirs(&self.resolved_config_dir).ok()?;
        let identity = rns_net::storage::load_or_create_identity(&paths.identities).ok()?;
        let private_key = identity.get_private_key()?;
        Some(RnsdDrainConfig {
            rpc_addr: RpcAddr::Tcp(
                self.rnsd_rpc_addr.ip().to_string(),
                self.rnsd_rpc_addr.port(),
            ),
            auth_key: rns_net::rpc::derive_auth_key(&private_key),
            timeout: std::time::Duration::from_secs(3),
            poll_interval: std::time::Duration::from_millis(100),
        })
    }

    fn rnsd_args(&self) -> Vec<String> {
        let mut args = Vec::new();
        if let Some(path) = &self.config_path {
            args.push("--config".into());
            args.push(path.display().to_string());
        }
        args
    }

    fn sentineld_args(&self) -> Vec<String> {
        let mut args = self.rnsd_args();
        args.push("--ready-file".into());
        args.push(self.sentineld_ready_file_path().display().to_string());
        args
    }

    fn statsd_args(&self) -> Vec<String> {
        let mut args = self.rnsd_args();
        args.push("--db".into());
        args.push(self.stats_db_path.display().to_string());
        args.push("--ready-file".into());
        args.push(self.statsd_ready_file_path().display().to_string());
        args
    }

    fn sentineld_ready_file_path(&self) -> PathBuf {
        self.resolved_config_dir.join("rns-sentineld.ready")
    }

    fn statsd_ready_file_path(&self) -> PathBuf {
        self.resolved_config_dir.join("rns-statsd.ready")
    }

    fn ctl_args_from_server_args(args: &Args) -> rns_ctl::args::Args {
        let mut argv = vec!["--daemon".to_string()];
        if let Some(config_path) = args.config_path() {
            argv.push("--config".into());
            argv.push(config_path.to_string());
        }
        if let Some(host) = args.get("http-host") {
            argv.push("--host".into());
            argv.push(host.to_string());
        }
        if let Some(port) = args.get("http-port") {
            argv.push("--port".into());
            argv.push(port.to_string());
        }
        if let Some(token) = args.get("http-token") {
            argv.push("--token".into());
            argv.push(token.to_string());
        }
        if args.has("disable-auth") {
            argv.push("--disable-auth".into());
        }
        rns_ctl::args::Args::parse_from(argv)
    }

    fn resolve_rpc_port(config_dir: &Path) -> u16 {
        let config_file = config_dir.join("config");
        let parsed = if config_file.exists() {
            rns_net::config::parse_file(&config_file).ok()
        } else {
            rns_net::config::parse("").ok()
        };

        parsed
            .as_ref()
            .map(|cfg| cfg.reticulum.instance_control_port)
            .unwrap_or(37429)
    }

    fn build(
        config_path: Option<PathBuf>,
        resolved_config_dir: PathBuf,
        server_config_file_path: PathBuf,
        server_config_file_present: bool,
        file_cfg: &ServerConfigFile,
        args: Option<&Args>,
    ) -> Self {
        let ctl_cfg = args
            .map(Self::ctl_args_from_server_args)
            .map(|ctl_args| rns_ctl::config::from_args_and_env(&ctl_args));

        let stats_db_path = args
            .and_then(|args| args.get("stats-db"))
            .map(PathBuf::from)
            .or_else(|| file_cfg.stats_db_path.as_ref().map(PathBuf::from))
            .unwrap_or_else(|| resolved_config_dir.join("stats.db"));
        let rnsd_bin = args
            .and_then(|args| args.get("rnsd-bin"))
            .map(PathBuf::from)
            .or_else(|| file_cfg.rnsd_bin.as_ref().map(PathBuf::from))
            .unwrap_or_default();
        let sentineld_bin = args
            .and_then(|args| args.get("sentineld-bin"))
            .map(PathBuf::from)
            .or_else(|| file_cfg.sentineld_bin.as_ref().map(PathBuf::from))
            .unwrap_or_default();
        let statsd_bin = args
            .and_then(|args| args.get("statsd-bin"))
            .map(PathBuf::from)
            .or_else(|| file_cfg.statsd_bin.as_ref().map(PathBuf::from))
            .unwrap_or_default();

        let http_enabled = if args.is_some_and(|args| args.has("no-http")) {
            false
        } else {
            file_cfg.http.enabled.unwrap_or(true)
        };
        let http_host = ctl_cfg
            .as_ref()
            .map(|cfg| cfg.host.clone())
            .filter(|_| {
                args.is_some_and(|args| args.get("http-host").is_some())
                    || env_present("RNSCTL_HOST")
            })
            .or_else(|| file_cfg.http.host.clone())
            .unwrap_or_else(|| "127.0.0.1".into());
        let http_port = ctl_cfg
            .as_ref()
            .map(|cfg| cfg.port)
            .filter(|_| {
                args.is_some_and(|args| args.get("http-port").is_some())
                    || env_present("RNSCTL_HTTP_PORT")
            })
            .or(file_cfg.http.port)
            .unwrap_or(8080);
        let http_auth_token = ctl_cfg
            .as_ref()
            .and_then(|cfg| cfg.auth_token.clone())
            .filter(|_| {
                args.is_some_and(|args| args.get("http-token").is_some())
                    || env_present("RNSCTL_AUTH_TOKEN")
            })
            .or_else(|| file_cfg.http.auth_token.clone());
        let http_disable_auth = if args.is_some_and(|args| args.has("disable-auth"))
            || env_true("RNSCTL_DISABLE_AUTH")
        {
            true
        } else {
            file_cfg.http.disable_auth.unwrap_or(false)
        };

        let rpc_port = Self::resolve_rpc_port(&resolved_config_dir);

        Self {
            config_path,
            resolved_config_dir,
            server_config_file_path,
            server_config_file_present,
            file_config: file_cfg.clone(),
            stats_db_path,
            rnsd_bin,
            sentineld_bin,
            statsd_bin,
            http: HttpConfig {
                enabled: http_enabled,
                host: http_host,
                port: http_port,
                auth_token: http_auth_token,
                disable_auth: http_disable_auth,
                daemon_mode: true,
            },
            rnsd_rpc_addr: format!("127.0.0.1:{rpc_port}")
                .parse()
                .unwrap_or_else(|_| "127.0.0.1:37429".parse().unwrap()),
        }
    }

    fn load_config_file(path: &Path) -> Result<(ServerConfigFile, bool), String> {
        if !path.exists() {
            return Ok((ServerConfigFile::default(), false));
        }
        let body = std::fs::read(path)
            .map_err(|err| format!("failed to read {}: {}", path.display(), err))?;
        let cfg = Self::parse_config_json(&body)?;
        Self::validate_file_config(&cfg)?;
        Ok((cfg, true))
    }

    fn parse_config_json(body: &[u8]) -> Result<ServerConfigFile, String> {
        serde_json::from_slice(body).map_err(|err| format!("invalid server config JSON: {}", err))
    }

    fn validate_file_config(file_cfg: &ServerConfigFile) -> Result<Vec<String>, String> {
        let mut warnings = Vec::new();

        validate_optional_nonempty("stats_db_path", file_cfg.stats_db_path.as_deref())?;
        validate_optional_nonempty("rnsd_bin", file_cfg.rnsd_bin.as_deref())?;
        validate_optional_nonempty("sentineld_bin", file_cfg.sentineld_bin.as_deref())?;
        validate_optional_nonempty("statsd_bin", file_cfg.statsd_bin.as_deref())?;
        validate_optional_nonempty("http.host", file_cfg.http.host.as_deref())?;
        validate_optional_nonempty("http.auth_token", file_cfg.http.auth_token.as_deref())?;

        if matches!(file_cfg.http.enabled, Some(true)) && matches!(file_cfg.http.port, Some(0)) {
            return Err("http.port must be greater than 0 when HTTP is enabled".into());
        }

        if matches!(file_cfg.http.enabled, Some(false))
            && (file_cfg.http.host.is_some()
                || file_cfg.http.port.is_some()
                || file_cfg.http.auth_token.is_some()
                || file_cfg.http.disable_auth.is_some())
        {
            warnings.push(
                "HTTP config fields are present while http.enabled=false; they will be saved but remain inactive until HTTP is re-enabled."
                    .into(),
            );
        }

        if matches!(file_cfg.http.disable_auth, Some(true)) && file_cfg.http.auth_token.is_some() {
            warnings.push(
                "http.auth_token is set but disable_auth=true, so the token will be ignored until auth is enabled again."
                    .into(),
            );
        }

        Ok(warnings)
    }

    fn with_file_config(&self, file_cfg: &ServerConfigFile, file_present: bool) -> Self {
        Self::build(
            self.config_path.clone(),
            self.resolved_config_dir.clone(),
            self.server_config_file_path.clone(),
            file_present,
            file_cfg,
            None,
        )
    }

    fn apply_plan(&self, next: &Self) -> ServerConfigApplyPlan {
        let current_specs = self.process_specs();
        let next_specs = next.process_specs();
        let mut processes_to_restart = Vec::new();
        let mut changes = Vec::new();

        for current in &current_specs {
            let Some(next_spec) = next_specs.iter().find(|spec| spec.role == current.role) else {
                continue;
            };
            if current.command != next_spec.command || current.args != next_spec.args {
                let name = current.role.display_name().to_string();
                processes_to_restart.push(name.clone());
                if current.command != next_spec.command {
                    changes.push(ServerConfigChange {
                        field: format!("{name}.bin"),
                        before: current.command.display(current.role),
                        after: next_spec.command.display(next_spec.role),
                        effect: format!("restart {name}"),
                    });
                }
                if current.args != next_spec.args {
                    changes.push(ServerConfigChange {
                        field: format!("{name}.args"),
                        before: if current.args.is_empty() {
                            "(none)".into()
                        } else {
                            current.args.join(" ")
                        },
                        after: if next_spec.args.is_empty() {
                            "(none)".into()
                        } else {
                            next_spec.args.join(" ")
                        },
                        effect: format!("restart {name}"),
                    });
                }
            }
        }

        let control_plane_reload_required = self.http.auth_token != next.http.auth_token
            || self.http.disable_auth != next.http.disable_auth;
        let control_plane_restart_required = self.http.enabled != next.http.enabled
            || self.http.host != next.http.host
            || self.http.port != next.http.port;

        if self.stats_db_path != next.stats_db_path {
            changes.push(ServerConfigChange {
                field: "stats_db_path".into(),
                before: self.stats_db_path.display().to_string(),
                after: next.stats_db_path.display().to_string(),
                effect: "restart rns-statsd".into(),
            });
        }
        if self.http.enabled != next.http.enabled {
            changes.push(ServerConfigChange {
                field: "http.enabled".into(),
                before: self.http.enabled.to_string(),
                after: next.http.enabled.to_string(),
                effect: "restart rns-server".into(),
            });
        }
        if self.http.host != next.http.host {
            changes.push(ServerConfigChange {
                field: "http.host".into(),
                before: self.http.host.clone(),
                after: next.http.host.clone(),
                effect: "restart rns-server".into(),
            });
        }
        if self.http.port != next.http.port {
            changes.push(ServerConfigChange {
                field: "http.port".into(),
                before: self.http.port.to_string(),
                after: next.http.port.to_string(),
                effect: "restart rns-server".into(),
            });
        }
        if self.http.disable_auth != next.http.disable_auth {
            changes.push(ServerConfigChange {
                field: "http.disable_auth".into(),
                before: self.http.disable_auth.to_string(),
                after: next.http.disable_auth.to_string(),
                effect: "reload embedded HTTP auth".into(),
            });
        }
        if self.http.auth_token != next.http.auth_token {
            changes.push(ServerConfigChange {
                field: "http.auth_token".into(),
                before: mask_token(&self.http.auth_token),
                after: mask_token(&next.http.auth_token),
                effect: "reload embedded HTTP auth".into(),
            });
        }

        let mut notes = Vec::new();
        if processes_to_restart.is_empty() {
            notes.push("No supervised child restart is required for this config.".into());
        } else {
            notes.push(format!(
                "Restart required for: {}.",
                processes_to_restart.join(", ")
            ));
        }
        if control_plane_restart_required {
            notes.push(
                "Embedded control-plane HTTP settings changed and will only take effect after restarting rns-server."
                    .into(),
            );
        }
        if control_plane_reload_required && !control_plane_restart_required {
            notes.push("Embedded control-plane auth settings will be reloaded in place.".into());
        }
        let overall_action = match (
            processes_to_restart.is_empty(),
            control_plane_reload_required,
            control_plane_restart_required,
        ) {
            (true, false, false) => "none",
            (false, false, false) => "restart_children",
            (true, true, false) => "reload_control_plane",
            (false, true, false) => "restart_children_and_reload_control_plane",
            (true, _, true) => "restart_server",
            (false, _, true) => "restart_children_and_server",
        };

        ServerConfigApplyPlan {
            overall_action: overall_action.into(),
            processes_to_restart,
            control_plane_reload_required,
            control_plane_restart_required,
            notes,
            changes,
        }
    }

    fn editable_file_json(&self) -> String {
        serde_json::to_string_pretty(&self.file_config)
            .unwrap_or_else(|_| "{\n  \"http\": {}\n}".into())
    }

    fn example_config_json() -> String {
        serde_json::to_string_pretty(&ServerConfigFile {
            stats_db_path: Some("stats.db".into()),
            rnsd_bin: None,
            sentineld_bin: None,
            statsd_bin: None,
            http: ServerHttpConfigFile {
                enabled: Some(true),
                host: Some("127.0.0.1".into()),
                port: Some(8080),
                auth_token: None,
                disable_auth: Some(false),
            },
        })
        .unwrap_or_else(|_| "{\n  \"http\": {}\n}".into())
    }
}

fn validate_optional_nonempty(field: &str, value: Option<&str>) -> Result<(), String> {
    if value.is_some_and(|raw| raw.trim().is_empty()) {
        return Err(format!("{field} cannot be empty"));
    }
    Ok(())
}

fn env_present(name: &str) -> bool {
    std::env::var_os(name).is_some()
}

fn env_true(name: &str) -> bool {
    std::env::var(name)
        .map(|value| value == "true" || value == "1")
        .unwrap_or(false)
}

fn mask_token(token: &Option<String>) -> String {
    match token {
        Some(value) if !value.is_empty() => format!("set({} chars)", value.len()),
        _ => "unset".into(),
    }
}

#[cfg(test)]
mod tests {
    use super::{HttpConfig, ServerConfig, ServerConfigFile, ServerHttpConfigFile};
    use crate::supervisor::{ProcessCommand, ReadinessTarget, Role};
    use std::path::PathBuf;

    fn test_config() -> ServerConfig {
        ServerConfig {
            config_path: Some(PathBuf::from("/tmp/rns")),
            resolved_config_dir: PathBuf::from("/tmp/rns"),
            server_config_file_path: PathBuf::from("/tmp/rns/rns-server.json"),
            server_config_file_present: true,
            file_config: ServerConfigFile::default(),
            stats_db_path: PathBuf::from("/tmp/rns/stats.db"),
            rnsd_bin: PathBuf::new(),
            sentineld_bin: PathBuf::new(),
            statsd_bin: PathBuf::new(),
            http: HttpConfig {
                enabled: true,
                host: "127.0.0.1".into(),
                port: 8080,
                auth_token: None,
                disable_auth: false,
                daemon_mode: true,
            },
            rnsd_rpc_addr: "127.0.0.1:37429".parse().unwrap(),
        }
    }

    #[test]
    fn apply_plan_restarts_statsd_when_db_changes() {
        let current = test_config();
        let next = current.with_file_config(
            &ServerConfigFile {
                stats_db_path: Some("/tmp/rns/other-stats.db".into()),
                ..ServerConfigFile::default()
            },
            true,
        );

        let plan = current.apply_plan(&next);

        assert_eq!(plan.overall_action, "restart_children");
        assert_eq!(plan.processes_to_restart, vec!["rns-statsd".to_string()]);
        assert!(plan
            .changes
            .iter()
            .any(|change| change.field == "stats_db_path"));
    }

    #[test]
    fn apply_plan_requires_server_restart_for_http_port_change() {
        let current = test_config();
        let next = current.with_file_config(
            &ServerConfigFile {
                http: ServerHttpConfigFile {
                    port: Some(9090),
                    ..ServerHttpConfigFile::default()
                },
                ..ServerConfigFile::default()
            },
            true,
        );

        let plan = current.apply_plan(&next);

        assert_eq!(plan.overall_action, "restart_server");
        assert!(!plan.control_plane_reload_required);
        assert!(plan.control_plane_restart_required);
        assert!(plan.processes_to_restart.is_empty());
        assert!(plan
            .changes
            .iter()
            .any(|change| change.field == "http.port" && change.after == "9090"));
    }

    #[test]
    fn apply_plan_is_noop_when_config_does_not_change() {
        let current = test_config();
        let next = current.with_file_config(&ServerConfigFile::default(), true);

        let plan = current.apply_plan(&next);

        assert_eq!(plan.overall_action, "none");
        assert!(plan.processes_to_restart.is_empty());
        assert!(!plan.control_plane_reload_required);
        assert!(!plan.control_plane_restart_required);
    }

    #[test]
    fn apply_plan_reloads_auth_when_token_changes() {
        let current = test_config();
        let next = current.with_file_config(
            &ServerConfigFile {
                http: ServerHttpConfigFile {
                    auth_token: Some("new-token".into()),
                    ..ServerHttpConfigFile::default()
                },
                ..ServerConfigFile::default()
            },
            true,
        );

        let plan = current.apply_plan(&next);

        assert_eq!(plan.overall_action, "reload_control_plane");
        assert!(plan.control_plane_reload_required);
        assert!(!plan.control_plane_restart_required);
    }

    #[test]
    fn validation_rejects_unknown_fields() {
        let err = ServerConfig::parse_config_json(br#"{"unknown":true}"#).unwrap_err();
        assert!(err.contains("unknown field"));
    }

    #[test]
    fn validation_rejects_empty_strings() {
        let err = ServerConfig::validate_file_config(&ServerConfigFile {
            stats_db_path: Some("   ".into()),
            ..ServerConfigFile::default()
        })
        .unwrap_err();
        assert_eq!(err, "stats_db_path cannot be empty");
    }

    #[test]
    fn validation_warns_when_http_fields_are_disabled() {
        let warnings = ServerConfig::validate_file_config(&ServerConfigFile {
            http: ServerHttpConfigFile {
                enabled: Some(false),
                port: Some(8080),
                ..ServerHttpConfigFile::default()
            },
            ..ServerConfigFile::default()
        })
        .unwrap();
        assert_eq!(warnings.len(), 1);
        assert!(warnings[0].contains("http.enabled=false"));
    }

    #[test]
    fn process_specs_include_sidecar_ready_file_args() {
        let config = test_config();
        let specs = config.process_specs();
        assert!(matches!(specs[0].command, ProcessCommand::SelfInvoke));

        let sentineld = specs
            .iter()
            .find(|spec| spec.role == Role::Sentineld)
            .unwrap();
        assert!(sentineld.args.windows(2).any(|pair| {
            pair[0] == "--ready-file" && pair[1] == "/tmp/rns/rns-sentineld.ready"
        }));

        let statsd = specs.iter().find(|spec| spec.role == Role::Statsd).unwrap();
        assert!(statsd
            .args
            .windows(2)
            .any(|pair| { pair[0] == "--ready-file" && pair[1] == "/tmp/rns/rns-statsd.ready" }));
    }

    #[test]
    fn readiness_checks_use_ready_files_for_sidecars() {
        let config = test_config();
        let readiness = config.readiness_checks();

        let sentineld = readiness
            .iter()
            .find(|entry| entry.role == Role::Sentineld)
            .unwrap();
        match &sentineld.target {
            ReadinessTarget::ReadyFile(path) => {
                assert_eq!(path, &PathBuf::from("/tmp/rns/rns-sentineld.ready"));
            }
            _ => panic!("unexpected sentineld readiness target"),
        }

        let statsd = readiness
            .iter()
            .find(|entry| entry.role == Role::Statsd)
            .unwrap();
        match &statsd.target {
            ReadinessTarget::ReadyFile(path) => {
                assert_eq!(path, &PathBuf::from("/tmp/rns/rns-statsd.ready"));
            }
            _ => panic!("unexpected statsd readiness target"),
        }
    }

    #[test]
    fn explicit_child_override_uses_external_binary_command() {
        let current = test_config();
        let next = current.with_file_config(
            &ServerConfigFile {
                rnsd_bin: Some("/opt/custom-rnsd".into()),
                ..ServerConfigFile::default()
            },
            true,
        );

        let specs = next.process_specs();
        assert!(matches!(
            specs[0].command,
            ProcessCommand::External(ref path) if path == &PathBuf::from("/opt/custom-rnsd")
        ));
    }

    #[test]
    fn ensure_runtime_bootstrap_creates_identity_once() {
        let config_dir =
            std::env::temp_dir().join(format!("rns-server-bootstrap-test-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&config_dir);
        let config = ServerConfig {
            config_path: Some(config_dir.clone()),
            resolved_config_dir: config_dir.clone(),
            server_config_file_path: config_dir.join("rns-server.json"),
            server_config_file_present: false,
            file_config: ServerConfigFile::default(),
            stats_db_path: config_dir.join("stats.db"),
            rnsd_bin: PathBuf::new(),
            sentineld_bin: PathBuf::new(),
            statsd_bin: PathBuf::new(),
            http: HttpConfig {
                enabled: true,
                host: "127.0.0.1".into(),
                port: 8080,
                auth_token: None,
                disable_auth: false,
                daemon_mode: true,
            },
            rnsd_rpc_addr: "127.0.0.1:37429".parse().unwrap(),
        };

        config.ensure_runtime_bootstrap().unwrap();
        config.ensure_runtime_bootstrap().unwrap();

        let identity_path = config_dir.join("storage/identities/identity");
        assert!(identity_path.exists());
        assert_eq!(std::fs::metadata(identity_path).unwrap().len(), 64);

        let _ = std::fs::remove_dir_all(&config_dir);
    }
}
