use std::{collections::BTreeMap, env, fs, net::SocketAddr, path::PathBuf, str::FromStr};

use anyhow::Context;
use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

use crate::transport::StreamTransportBackend;

const DEFAULT_LISTEN: &str = "127.0.0.1:8000";

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum Profile {
    Sim,
    Replay,
    WaveshareUgv,
}

impl Profile {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Sim => "sim",
            Self::Replay => "replay",
            Self::WaveshareUgv => "waveshare-ugv",
        }
    }

    pub fn is_physical(self) -> bool {
        matches!(self, Self::WaveshareUgv)
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum AcceleratorBackend {
    #[default]
    None,
    Cpu,
    Cuda,
}

impl AcceleratorBackend {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::None => "none",
            Self::Cpu => "cpu",
            Self::Cuda => "cuda",
        }
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum AgentProvider {
    #[default]
    DeterministicTest,
    #[serde(rename = "openai-compatible-http")]
    #[value(name = "openai-compatible-http")]
    OpenAiCompatibleHttp,
    LocalHttp,
}

impl AgentProvider {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::DeterministicTest => "deterministic-test",
            Self::OpenAiCompatibleHttp => "openai-compatible-http",
            Self::LocalHttp => "local-http",
        }
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, ValueEnum)]
#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum PolicyMode {
    DryRun,
    #[default]
    RequireToken,
    RequireApproval,
    Deny,
}

impl PolicyMode {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::DryRun => "dry-run",
            Self::RequireToken => "require-token",
            Self::RequireApproval => "require-approval",
            Self::Deny => "deny",
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
pub struct HarnessConfig {
    pub role: String,
    pub profile: Profile,
    pub stream_transport: StreamTransportBackend,
    pub replay_source: Option<PathBuf>,
    pub replay_speed: f64,
    pub listen: SocketAddr,
    pub allow_untokened_drive: bool,
    pub allow_physical_actuation: bool,
    pub deadman_ms: u64,
    pub soft_odometry_limit_m: f64,
    pub serial_port: String,
    pub serial_baud: u32,
    pub drive_invert: bool,
    pub drive_swap: bool,
    pub accelerator: AcceleratorBackend,
    pub require_accelerator: bool,
    pub resource_sampling: bool,
    pub agent_provider: AgentProvider,
    pub agent_model: String,
    pub agent_base_url: Option<String>,
    #[serde(skip_serializing)]
    pub agent_api_key: Option<String>,
    pub agent_timeout_ms: u64,
    pub policy_mode: PolicyMode,
}

#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct PartialHarnessConfig {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub role: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub profile: Option<Profile>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub stream_transport: Option<StreamTransportBackend>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub replay_source: Option<PathBuf>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub replay_speed: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub listen: Option<SocketAddr>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub allow_untokened_drive: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub allow_physical_actuation: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub deadman_ms: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub soft_odometry_limit_m: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub serial_port: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub serial_baud: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub drive_invert: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub drive_swap: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub accelerator: Option<AcceleratorBackend>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub require_accelerator: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub resource_sampling: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub agent_provider: Option<AgentProvider>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub agent_model: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub agent_base_url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub agent_api_key: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub agent_timeout_ms: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub policy_mode: Option<PolicyMode>,
}

#[derive(Debug, Clone)]
pub struct ConfigRequest {
    pub config_path: Option<PathBuf>,
    pub stack: Option<Profile>,
    pub stack_defaults: PartialHarnessConfig,
    pub env: BTreeMap<String, String>,
    pub cli: PartialHarnessConfig,
}

impl ConfigRequest {
    pub fn from_process(
        config_path: Option<PathBuf>,
        stack: Option<Profile>,
        cli: PartialHarnessConfig,
    ) -> Self {
        Self {
            config_path,
            stack,
            stack_defaults: PartialHarnessConfig::default(),
            env: env::vars()
                .filter(|(key, _)| key.starts_with("LEASH_"))
                .collect(),
            cli,
        }
    }

    pub fn with_stack_defaults(mut self, defaults: PartialHarnessConfig) -> Self {
        self.stack_defaults = defaults;
        self
    }
}

#[derive(Debug, Clone, Serialize)]
pub struct ResolvedHarnessConfig {
    #[serde(flatten)]
    pub config: HarnessConfig,
    pub physical: bool,
    pub physical_actuation_enabled: bool,
    pub network_bind: String,
    pub config_file: Option<String>,
    pub precedence: Vec<&'static str>,
    pub fields: Vec<ResolvedConfigField>,
}

#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct ResolvedConfigField {
    pub name: &'static str,
    pub value: Value,
    pub source: String,
    pub attention: Option<&'static str>,
}

#[derive(Debug, Clone)]
struct Resolved<T> {
    value: T,
    source: String,
}

#[derive(Debug, Clone)]
struct ConfigBuilder {
    role: Resolved<String>,
    profile: Resolved<Profile>,
    stream_transport: Resolved<StreamTransportBackend>,
    replay_source: Resolved<Option<PathBuf>>,
    replay_speed: Resolved<f64>,
    listen: Resolved<SocketAddr>,
    allow_untokened_drive: Resolved<bool>,
    allow_physical_actuation: Resolved<bool>,
    deadman_ms: Resolved<u64>,
    soft_odometry_limit_m: Resolved<f64>,
    serial_port: Resolved<String>,
    serial_baud: Resolved<u32>,
    drive_invert: Resolved<bool>,
    drive_swap: Resolved<bool>,
    accelerator: Resolved<AcceleratorBackend>,
    require_accelerator: Resolved<bool>,
    resource_sampling: Resolved<bool>,
    agent_provider: Resolved<AgentProvider>,
    agent_model: Resolved<String>,
    agent_base_url: Resolved<Option<String>>,
    agent_api_key: Resolved<Option<String>>,
    agent_timeout_ms: Resolved<u64>,
    policy_mode: Resolved<PolicyMode>,
    config_file: Option<String>,
}

impl Default for HarnessConfig {
    fn default() -> Self {
        Self {
            role: "robot".to_string(),
            profile: Profile::Sim,
            stream_transport: StreamTransportBackend::LocalPubsub,
            replay_source: None,
            replay_speed: 1.0,
            listen: SocketAddr::from_str(DEFAULT_LISTEN).expect("valid default listen address"),
            allow_untokened_drive: true,
            allow_physical_actuation: false,
            deadman_ms: 400,
            soft_odometry_limit_m: 0.0,
            serial_port: "/dev/ttyTHS1".to_string(),
            serial_baud: 115_200,
            drive_invert: false,
            drive_swap: false,
            accelerator: AcceleratorBackend::None,
            require_accelerator: false,
            resource_sampling: false,
            agent_provider: AgentProvider::DeterministicTest,
            agent_model: "deterministic-test".to_string(),
            agent_base_url: None,
            agent_api_key: None,
            agent_timeout_ms: 10_000,
            policy_mode: PolicyMode::RequireToken,
        }
    }
}

impl HarnessConfig {
    pub fn validate(&self) -> anyhow::Result<()> {
        if self.profile.is_physical()
            && !self.allow_physical_actuation
            && std::env::var("LEASH_ALLOW_PHYSICAL_ACTUATION")
                .ok()
                .as_deref()
                != Some("1")
        {
            anyhow::bail!(
                "physical profile '{}' refuses to start without LEASH_ALLOW_PHYSICAL_ACTUATION=1 or --allow-physical-actuation",
                self.profile.as_str()
            );
        }
        if self.profile == Profile::Replay && self.replay_source.is_none() {
            anyhow::bail!("profile 'replay' requires --replay-source or LEASH_REPLAY_SOURCE");
        }
        if self.replay_source.is_some() && self.profile != Profile::Replay {
            anyhow::bail!("replay_source requires profile 'replay'");
        }
        if !self.replay_speed.is_finite() || self.replay_speed <= 0.0 {
            anyhow::bail!("replay_speed must be a finite positive number");
        }
        if self.agent_timeout_ms == 0 {
            anyhow::bail!("agent_timeout_ms must be at least 1");
        }
        match self.agent_provider {
            AgentProvider::DeterministicTest => {}
            AgentProvider::OpenAiCompatibleHttp => {
                require_non_empty(
                    self.agent_base_url.as_deref(),
                    "agent provider openai-compatible-http requires LEASH_AGENT_BASE_URL or agent_base_url",
                )?;
                require_non_empty(
                    self.agent_api_key.as_deref(),
                    "agent provider openai-compatible-http requires LEASH_AGENT_API_KEY or agent_api_key",
                )?;
            }
            AgentProvider::LocalHttp => {
                require_non_empty(
                    self.agent_base_url.as_deref(),
                    "agent provider local-http requires LEASH_AGENT_BASE_URL or agent_base_url",
                )?;
            }
        }
        crate::accelerator::resolve_accelerator(self.accelerator, self.require_accelerator)?;
        Ok(())
    }
}

pub fn resolve_config(request: ConfigRequest) -> anyhow::Result<ResolvedHarnessConfig> {
    let mut builder = ConfigBuilder::default();

    if let Some(path) = resolved_config_path(request.config_path) {
        let config = read_config_file(&path)?;
        let source = format!("config-file:{}", path.display());
        builder.config_file = Some(path.display().to_string());
        builder.apply_partial(config, |_| source.clone());
    }

    if let Some(profile) = request.stack {
        builder.apply_partial(
            PartialHarnessConfig {
                profile: Some(profile),
                ..PartialHarnessConfig::default()
            },
            |_| format!("stack:{}", profile.as_str()),
        );
    }
    builder.apply_partial(request.stack_defaults, |_| "stack-default".to_string());

    builder.apply_profile_defaults();
    builder.apply_partial(env_overrides(&request.env)?, env_source);
    builder.apply_partial(request.cli, |_| "cli".to_string());
    builder.apply_profile_defaults();

    Ok(builder.finish())
}

fn resolved_config_path(explicit: Option<PathBuf>) -> Option<PathBuf> {
    if explicit.is_some() {
        return explicit;
    }
    let home = env::var_os("HOME")?;
    let path = PathBuf::from(home).join(".config/leash/config.json");
    path.exists().then_some(path)
}

fn read_config_file(path: &PathBuf) -> anyhow::Result<PartialHarnessConfig> {
    let text = fs::read_to_string(path)?;
    Ok(serde_json::from_str(&text)?)
}

fn env_overrides(env: &BTreeMap<String, String>) -> anyhow::Result<PartialHarnessConfig> {
    Ok(PartialHarnessConfig {
        role: env.get("LEASH_ROLE").cloned(),
        profile: parse_env(env, "LEASH_PROFILE", parse_profile)?,
        stream_transport: parse_env(env, "LEASH_STREAM_TRANSPORT", parse_stream_transport)?,
        replay_source: env.get("LEASH_REPLAY_SOURCE").map(PathBuf::from),
        replay_speed: parse_env(env, "LEASH_REPLAY_SPEED", parse_f64)?,
        listen: parse_env(env, "LEASH_LISTEN", parse_socket_addr)?,
        allow_untokened_drive: parse_env(env, "LEASH_ALLOW_UNTOKENED_DRIVE", parse_bool)?,
        allow_physical_actuation: parse_env(env, "LEASH_ALLOW_PHYSICAL_ACTUATION", parse_bool)?,
        deadman_ms: parse_env(env, "LEASH_DEADMAN_MS", parse_u64)?,
        soft_odometry_limit_m: parse_env(env, "LEASH_SOFT_ODOMETRY_LIMIT_M", parse_f64)?,
        serial_port: env.get("LEASH_SERIAL_PORT").cloned(),
        serial_baud: parse_env(env, "LEASH_SERIAL_BAUD", parse_u32)?,
        drive_invert: parse_env(env, "LEASH_DRIVE_INVERT", parse_bool)?,
        drive_swap: parse_env(env, "LEASH_DRIVE_SWAP", parse_bool)?,
        accelerator: parse_env(env, "LEASH_ACCELERATOR", parse_accelerator)?,
        require_accelerator: parse_env(env, "LEASH_REQUIRE_ACCELERATOR", parse_bool)?,
        resource_sampling: parse_env(env, "LEASH_RESOURCE_SAMPLING", parse_bool)?,
        agent_provider: parse_env(env, "LEASH_AGENT_PROVIDER", parse_agent_provider)?,
        agent_model: env.get("LEASH_AGENT_MODEL").cloned(),
        agent_base_url: env.get("LEASH_AGENT_BASE_URL").cloned(),
        agent_api_key: env.get("LEASH_AGENT_API_KEY").cloned(),
        agent_timeout_ms: parse_env(env, "LEASH_AGENT_TIMEOUT_MS", parse_u64)?,
        policy_mode: parse_env(env, "LEASH_POLICY_MODE", parse_policy_mode)?,
    })
}

fn parse_env<T>(
    env: &BTreeMap<String, String>,
    key: &'static str,
    parse: impl FnOnce(&str) -> anyhow::Result<T>,
) -> anyhow::Result<Option<T>> {
    env.get(key)
        .map(|value| parse(value).with_context(|| format!("parse {key}")))
        .transpose()
}

fn parse_profile(value: &str) -> anyhow::Result<Profile> {
    match value {
        "sim" => Ok(Profile::Sim),
        "replay" => Ok(Profile::Replay),
        "waveshare-ugv" => Ok(Profile::WaveshareUgv),
        _ => anyhow::bail!("expected sim, replay, or waveshare-ugv"),
    }
}

fn parse_accelerator(value: &str) -> anyhow::Result<AcceleratorBackend> {
    match value {
        "none" => Ok(AcceleratorBackend::None),
        "cpu" => Ok(AcceleratorBackend::Cpu),
        "cuda" => Ok(AcceleratorBackend::Cuda),
        _ => anyhow::bail!("expected none, cpu, or cuda"),
    }
}

fn parse_agent_provider(value: &str) -> anyhow::Result<AgentProvider> {
    match value {
        "deterministic-test" => Ok(AgentProvider::DeterministicTest),
        "openai-compatible-http" => Ok(AgentProvider::OpenAiCompatibleHttp),
        "local-http" => Ok(AgentProvider::LocalHttp),
        _ => anyhow::bail!("expected deterministic-test, openai-compatible-http, or local-http"),
    }
}

fn parse_policy_mode(value: &str) -> anyhow::Result<PolicyMode> {
    match value {
        "dry-run" => Ok(PolicyMode::DryRun),
        "require-token" => Ok(PolicyMode::RequireToken),
        "require-approval" => Ok(PolicyMode::RequireApproval),
        "deny" => Ok(PolicyMode::Deny),
        _ => anyhow::bail!("expected dry-run, require-token, require-approval, or deny"),
    }
}

fn parse_stream_transport(value: &str) -> anyhow::Result<StreamTransportBackend> {
    match value {
        "memory" => Ok(StreamTransportBackend::Memory),
        "local-pubsub" => Ok(StreamTransportBackend::LocalPubsub),
        _ => anyhow::bail!("expected memory or local-pubsub"),
    }
}

fn parse_socket_addr(value: &str) -> anyhow::Result<SocketAddr> {
    Ok(SocketAddr::from_str(value)?)
}

fn parse_bool(value: &str) -> anyhow::Result<bool> {
    match value.to_ascii_lowercase().as_str() {
        "1" | "true" | "yes" | "on" => Ok(true),
        "0" | "false" | "no" | "off" => Ok(false),
        _ => anyhow::bail!("expected true/false or 1/0"),
    }
}

fn parse_u64(value: &str) -> anyhow::Result<u64> {
    Ok(value.parse()?)
}

fn parse_u32(value: &str) -> anyhow::Result<u32> {
    Ok(value.parse()?)
}

fn parse_f64(value: &str) -> anyhow::Result<f64> {
    Ok(value.parse()?)
}

fn env_source(field: &str) -> String {
    format!("env:{}", env_var_for_field(field))
}

fn env_var_for_field(field: &str) -> &'static str {
    match field {
        "role" => "LEASH_ROLE",
        "profile" => "LEASH_PROFILE",
        "stream_transport" => "LEASH_STREAM_TRANSPORT",
        "replay_source" => "LEASH_REPLAY_SOURCE",
        "replay_speed" => "LEASH_REPLAY_SPEED",
        "listen" => "LEASH_LISTEN",
        "allow_untokened_drive" => "LEASH_ALLOW_UNTOKENED_DRIVE",
        "allow_physical_actuation" => "LEASH_ALLOW_PHYSICAL_ACTUATION",
        "deadman_ms" => "LEASH_DEADMAN_MS",
        "soft_odometry_limit_m" => "LEASH_SOFT_ODOMETRY_LIMIT_M",
        "serial_port" => "LEASH_SERIAL_PORT",
        "serial_baud" => "LEASH_SERIAL_BAUD",
        "drive_invert" => "LEASH_DRIVE_INVERT",
        "drive_swap" => "LEASH_DRIVE_SWAP",
        "accelerator" => "LEASH_ACCELERATOR",
        "require_accelerator" => "LEASH_REQUIRE_ACCELERATOR",
        "resource_sampling" => "LEASH_RESOURCE_SAMPLING",
        "agent_provider" => "LEASH_AGENT_PROVIDER",
        "agent_model" => "LEASH_AGENT_MODEL",
        "agent_base_url" => "LEASH_AGENT_BASE_URL",
        "agent_api_key" => "LEASH_AGENT_API_KEY",
        "agent_timeout_ms" => "LEASH_AGENT_TIMEOUT_MS",
        "policy_mode" => "LEASH_POLICY_MODE",
        _ => "LEASH_UNKNOWN",
    }
}

impl Default for ConfigBuilder {
    fn default() -> Self {
        let config = HarnessConfig::default();
        Self {
            role: Resolved::defaulted(config.role),
            profile: Resolved::defaulted(config.profile),
            stream_transport: Resolved::defaulted(config.stream_transport),
            replay_source: Resolved::defaulted(config.replay_source),
            replay_speed: Resolved::defaulted(config.replay_speed),
            listen: Resolved::defaulted(config.listen),
            allow_untokened_drive: Resolved::defaulted(config.allow_untokened_drive),
            allow_physical_actuation: Resolved::defaulted(config.allow_physical_actuation),
            deadman_ms: Resolved::defaulted(config.deadman_ms),
            soft_odometry_limit_m: Resolved::defaulted(config.soft_odometry_limit_m),
            serial_port: Resolved::defaulted(config.serial_port),
            serial_baud: Resolved::defaulted(config.serial_baud),
            drive_invert: Resolved::defaulted(config.drive_invert),
            drive_swap: Resolved::defaulted(config.drive_swap),
            accelerator: Resolved::defaulted(config.accelerator),
            require_accelerator: Resolved::defaulted(config.require_accelerator),
            resource_sampling: Resolved::defaulted(config.resource_sampling),
            agent_provider: Resolved::defaulted(config.agent_provider),
            agent_model: Resolved::defaulted(config.agent_model),
            agent_base_url: Resolved::defaulted(config.agent_base_url),
            agent_api_key: Resolved::defaulted(config.agent_api_key),
            agent_timeout_ms: Resolved::defaulted(config.agent_timeout_ms),
            policy_mode: Resolved::defaulted(config.policy_mode),
            config_file: None,
        }
    }
}

impl<T> Resolved<T> {
    fn defaulted(value: T) -> Self {
        Self {
            value,
            source: "default".to_string(),
        }
    }

    fn set(&mut self, value: T, source: String) {
        self.value = value;
        self.source = source;
    }
}

impl ConfigBuilder {
    fn apply_partial(
        &mut self,
        partial: PartialHarnessConfig,
        source: impl Fn(&'static str) -> String,
    ) {
        if let Some(value) = partial.role {
            self.role.set(value, source("role"));
        }
        if let Some(value) = partial.profile {
            self.profile.set(value, source("profile"));
        }
        if let Some(value) = partial.stream_transport {
            self.stream_transport.set(value, source("stream_transport"));
        }
        if let Some(value) = partial.replay_source {
            self.replay_source.set(Some(value), source("replay_source"));
        }
        if let Some(value) = partial.replay_speed {
            self.replay_speed.set(value, source("replay_speed"));
        }
        if let Some(value) = partial.listen {
            self.listen.set(value, source("listen"));
        }
        if let Some(value) = partial.allow_untokened_drive {
            self.allow_untokened_drive
                .set(value, source("allow_untokened_drive"));
        }
        if let Some(value) = partial.allow_physical_actuation {
            self.allow_physical_actuation
                .set(value, source("allow_physical_actuation"));
        }
        if let Some(value) = partial.deadman_ms {
            self.deadman_ms.set(value, source("deadman_ms"));
        }
        if let Some(value) = partial.soft_odometry_limit_m {
            self.soft_odometry_limit_m
                .set(value, source("soft_odometry_limit_m"));
        }
        if let Some(value) = partial.serial_port {
            self.serial_port.set(value, source("serial_port"));
        }
        if let Some(value) = partial.serial_baud {
            self.serial_baud.set(value, source("serial_baud"));
        }
        if let Some(value) = partial.drive_invert {
            self.drive_invert.set(value, source("drive_invert"));
        }
        if let Some(value) = partial.drive_swap {
            self.drive_swap.set(value, source("drive_swap"));
        }
        if let Some(value) = partial.accelerator {
            self.accelerator.set(value, source("accelerator"));
        }
        if let Some(value) = partial.require_accelerator {
            self.require_accelerator
                .set(value, source("require_accelerator"));
        }
        if let Some(value) = partial.resource_sampling {
            self.resource_sampling
                .set(value, source("resource_sampling"));
        }
        if let Some(value) = partial.agent_provider {
            self.agent_provider.set(value, source("agent_provider"));
        }
        if let Some(value) = partial.agent_model {
            self.agent_model.set(value, source("agent_model"));
        }
        if let Some(value) = partial.agent_base_url {
            self.agent_base_url
                .set(Some(value), source("agent_base_url"));
        }
        if let Some(value) = partial.agent_api_key {
            self.agent_api_key.set(Some(value), source("agent_api_key"));
        }
        if let Some(value) = partial.agent_timeout_ms {
            self.agent_timeout_ms.set(value, source("agent_timeout_ms"));
        }
        if let Some(value) = partial.policy_mode {
            self.policy_mode.set(value, source("policy_mode"));
        }
    }

    fn apply_profile_defaults(&mut self) {
        if self.replay_source.value.is_some() && self.profile.source == "default" {
            self.profile
                .set(Profile::Replay, "replay-source".to_string());
        }
        if self.profile.value == Profile::WaveshareUgv
            && self.allow_untokened_drive.source == "default"
        {
            self.allow_untokened_drive
                .set(false, "stack:waveshare-ugv".to_string());
        }
    }

    fn finish(self) -> ResolvedHarnessConfig {
        let config = HarnessConfig {
            role: self.role.value,
            profile: self.profile.value,
            stream_transport: self.stream_transport.value,
            replay_source: self.replay_source.value,
            replay_speed: self.replay_speed.value,
            listen: self.listen.value,
            allow_untokened_drive: self.allow_untokened_drive.value,
            allow_physical_actuation: self.allow_physical_actuation.value,
            deadman_ms: self.deadman_ms.value,
            soft_odometry_limit_m: self.soft_odometry_limit_m.value,
            serial_port: self.serial_port.value,
            serial_baud: self.serial_baud.value,
            drive_invert: self.drive_invert.value,
            drive_swap: self.drive_swap.value,
            accelerator: self.accelerator.value,
            require_accelerator: self.require_accelerator.value,
            resource_sampling: self.resource_sampling.value,
            agent_provider: self.agent_provider.value,
            agent_model: self.agent_model.value,
            agent_base_url: self.agent_base_url.value,
            agent_api_key: self.agent_api_key.value,
            agent_timeout_ms: self.agent_timeout_ms.value,
            policy_mode: self.policy_mode.value,
        };
        let physical = config.profile.is_physical();
        let physical_actuation_enabled = config.allow_physical_actuation;
        let network_bind = config.listen.to_string();
        let fields = vec![
            field("role", json!(config.role), self.role.source, None),
            field(
                "profile",
                json!(config.profile.as_str()),
                self.profile.source,
                physical.then_some("physical-profile"),
            ),
            field(
                "stream_transport",
                json!(config.stream_transport.as_str()),
                self.stream_transport.source,
                Some("module-streams"),
            ),
            field(
                "replay_source",
                json!(config
                    .replay_source
                    .as_ref()
                    .map(|path| path.display().to_string())),
                self.replay_source.source,
                config.replay_source.is_some().then_some("replay"),
            ),
            field(
                "replay_speed",
                json!(config.replay_speed),
                self.replay_speed.source,
                (config.profile == Profile::Replay).then_some("replay"),
            ),
            field(
                "listen",
                json!(config.listen.to_string()),
                self.listen.source,
                Some("network-bind"),
            ),
            field(
                "allow_untokened_drive",
                json!(config.allow_untokened_drive),
                self.allow_untokened_drive.source,
                Some("drive-auth"),
            ),
            field(
                "allow_physical_actuation",
                json!(config.allow_physical_actuation),
                self.allow_physical_actuation.source,
                Some("physical-actuation"),
            ),
            field(
                "deadman_ms",
                json!(config.deadman_ms),
                self.deadman_ms.source,
                Some("safety"),
            ),
            field(
                "soft_odometry_limit_m",
                json!(config.soft_odometry_limit_m),
                self.soft_odometry_limit_m.source,
                Some("safety"),
            ),
            field(
                "serial_port",
                json!(config.serial_port),
                self.serial_port.source,
                physical.then_some("physical-device"),
            ),
            field(
                "serial_baud",
                json!(config.serial_baud),
                self.serial_baud.source,
                physical.then_some("physical-device"),
            ),
            field(
                "drive_invert",
                json!(config.drive_invert),
                self.drive_invert.source,
                physical.then_some("physical-drive-map"),
            ),
            field(
                "drive_swap",
                json!(config.drive_swap),
                self.drive_swap.source,
                physical.then_some("physical-drive-map"),
            ),
            field(
                "accelerator",
                json!(config.accelerator.as_str()),
                self.accelerator.source,
                (config.accelerator != AcceleratorBackend::None).then_some("accelerator"),
            ),
            field(
                "require_accelerator",
                json!(config.require_accelerator),
                self.require_accelerator.source,
                config.require_accelerator.then_some("accelerator-required"),
            ),
            field(
                "resource_sampling",
                json!(config.resource_sampling),
                self.resource_sampling.source,
                config.resource_sampling.then_some("resource-monitor"),
            ),
            field(
                "agent_provider",
                json!(config.agent_provider.as_str()),
                self.agent_provider.source,
                Some("agent-model"),
            ),
            field(
                "agent_model",
                json!(config.agent_model),
                self.agent_model.source,
                Some("agent-model"),
            ),
            field(
                "agent_base_url",
                json!(config.agent_base_url),
                self.agent_base_url.source,
                matches!(
                    config.agent_provider,
                    AgentProvider::OpenAiCompatibleHttp | AgentProvider::LocalHttp
                )
                .then_some("agent-endpoint"),
            ),
            field(
                "agent_api_key",
                json!(config.agent_api_key),
                self.agent_api_key.source,
                (config.agent_provider == AgentProvider::OpenAiCompatibleHttp).then_some("secret"),
            ),
            field(
                "agent_timeout_ms",
                json!(config.agent_timeout_ms),
                self.agent_timeout_ms.source,
                Some("agent-model"),
            ),
            field(
                "policy_mode",
                json!(config.policy_mode.as_str()),
                self.policy_mode.source,
                Some("safety-policy"),
            ),
        ];

        ResolvedHarnessConfig {
            config,
            physical,
            physical_actuation_enabled,
            network_bind,
            config_file: self.config_file,
            precedence: vec![
                "default",
                "config-file",
                "stack-default",
                "environment",
                "cli",
            ],
            fields,
        }
    }
}

fn field(
    name: &'static str,
    value: Value,
    source: String,
    attention: Option<&'static str>,
) -> ResolvedConfigField {
    ResolvedConfigField {
        name,
        value: redact_value(name, value),
        source,
        attention,
    }
}

fn redact_value(name: &str, value: Value) -> Value {
    if !value.is_string() {
        return value;
    }
    let name = name.to_ascii_lowercase();
    let sensitive = ["token", "secret", "password", "key"]
        .iter()
        .any(|needle| name.contains(needle));
    if sensitive {
        json!("<redacted>")
    } else {
        value
    }
}

fn require_non_empty(value: Option<&str>, message: &'static str) -> anyhow::Result<()> {
    if value.is_some_and(|value| !value.trim().is_empty()) {
        Ok(())
    } else {
        anyhow::bail!(message)
    }
}

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

    #[test]
    fn resolves_precedence_with_sources() {
        let config_path = write_temp_config(
            "precedence",
            r#"{"role":"file-bot","deadman_ms":900,"listen":"127.0.0.1:7000"}"#,
        );
        let env = BTreeMap::from([
            ("LEASH_ROLE".to_string(), "env-bot".to_string()),
            ("LEASH_DEADMAN_MS".to_string(), "250".to_string()),
        ]);
        let resolved = resolve_config(ConfigRequest {
            config_path: Some(config_path.clone()),
            stack: None,
            stack_defaults: PartialHarnessConfig::default(),
            env,
            cli: PartialHarnessConfig {
                role: Some("cli-bot".to_string()),
                ..PartialHarnessConfig::default()
            },
        })
        .unwrap();

        assert_eq!(resolved.config.role, "cli-bot");
        assert_eq!(resolved.config.deadman_ms, 250);
        assert_eq!(resolved.config.listen.to_string(), "127.0.0.1:7000");
        assert_eq!(source_for(&resolved, "role"), "cli");
        assert_eq!(source_for(&resolved, "deadman_ms"), "env:LEASH_DEADMAN_MS");
        assert!(source_for(&resolved, "listen").starts_with("config-file:"));
        let _ = fs::remove_file(config_path);
    }

    #[test]
    fn physical_profile_defaults_to_tokened_drive_and_disabled_actuation() {
        let resolved = resolve_config(ConfigRequest {
            config_path: None,
            stack: Some(Profile::WaveshareUgv),
            stack_defaults: PartialHarnessConfig::default(),
            env: BTreeMap::new(),
            cli: PartialHarnessConfig::default(),
        })
        .unwrap();

        assert_eq!(resolved.config.profile, Profile::WaveshareUgv);
        assert!(!resolved.config.allow_untokened_drive);
        assert!(!resolved.config.allow_physical_actuation);
        assert_eq!(
            source_for(&resolved, "allow_untokened_drive"),
            "stack:waveshare-ugv"
        );
        assert_eq!(
            attention_for(&resolved, "allow_physical_actuation"),
            Some("physical-actuation")
        );
    }

    #[test]
    fn resolves_accelerator_precedence_with_sources() {
        let config_path = write_temp_config(
            "accelerator",
            r#"{"accelerator":"cpu","require_accelerator":true}"#,
        );
        let env = BTreeMap::from([("LEASH_ACCELERATOR".to_string(), "cuda".to_string())]);
        let resolved = resolve_config(ConfigRequest {
            config_path: Some(config_path.clone()),
            stack: None,
            stack_defaults: PartialHarnessConfig::default(),
            env,
            cli: PartialHarnessConfig {
                accelerator: Some(AcceleratorBackend::Cpu),
                ..PartialHarnessConfig::default()
            },
        })
        .unwrap();

        assert_eq!(resolved.config.accelerator, AcceleratorBackend::Cpu);
        assert!(resolved.config.require_accelerator);
        assert_eq!(source_for(&resolved, "accelerator"), "cli");
        assert!(source_for(&resolved, "require_accelerator").starts_with("config-file:"));
        assert_eq!(attention_for(&resolved, "accelerator"), Some("accelerator"));
        let _ = fs::remove_file(config_path);
    }

    #[test]
    fn resolves_stream_transport_from_env() {
        let env = BTreeMap::from([("LEASH_STREAM_TRANSPORT".to_string(), "memory".to_string())]);
        let resolved = resolve_config(ConfigRequest {
            config_path: None,
            stack: None,
            stack_defaults: PartialHarnessConfig::default(),
            env,
            cli: PartialHarnessConfig::default(),
        })
        .unwrap();

        assert_eq!(
            resolved.config.stream_transport,
            StreamTransportBackend::Memory
        );
        assert_eq!(
            source_for(&resolved, "stream_transport"),
            "env:LEASH_STREAM_TRANSPORT"
        );
    }

    #[test]
    fn resolves_resource_sampling_from_env_and_cli() {
        let env = BTreeMap::from([("LEASH_RESOURCE_SAMPLING".to_string(), "true".to_string())]);
        let resolved = resolve_config(ConfigRequest {
            config_path: None,
            stack: None,
            stack_defaults: PartialHarnessConfig::default(),
            env,
            cli: PartialHarnessConfig {
                resource_sampling: Some(false),
                ..PartialHarnessConfig::default()
            },
        })
        .unwrap();

        assert!(!resolved.config.resource_sampling);
        assert_eq!(source_for(&resolved, "resource_sampling"), "cli");
        assert_eq!(attention_for(&resolved, "resource_sampling"), None);
    }

    #[test]
    fn resolves_policy_mode_from_env_and_cli() {
        let env = BTreeMap::from([("LEASH_POLICY_MODE".to_string(), "deny".to_string())]);
        let resolved = resolve_config(ConfigRequest {
            config_path: None,
            stack: None,
            stack_defaults: PartialHarnessConfig::default(),
            env,
            cli: PartialHarnessConfig {
                policy_mode: Some(PolicyMode::RequireApproval),
                ..PartialHarnessConfig::default()
            },
        })
        .unwrap();

        assert_eq!(resolved.config.policy_mode, PolicyMode::RequireApproval);
        assert_eq!(source_for(&resolved, "policy_mode"), "cli");
        assert_eq!(
            value_for(&resolved, "policy_mode"),
            json!("require-approval")
        );
        assert_eq!(
            attention_for(&resolved, "policy_mode"),
            Some("safety-policy")
        );
    }

    #[test]
    fn deterministic_agent_provider_is_default_and_no_network() {
        let resolved = resolve_config(ConfigRequest {
            config_path: None,
            stack: None,
            stack_defaults: PartialHarnessConfig::default(),
            env: BTreeMap::new(),
            cli: PartialHarnessConfig::default(),
        })
        .unwrap();

        assert_eq!(
            resolved.config.agent_provider,
            AgentProvider::DeterministicTest
        );
        assert_eq!(resolved.config.agent_model, "deterministic-test");
        assert!(resolved.config.agent_base_url.is_none());
        assert!(resolved.config.agent_api_key.is_none());
        resolved.config.validate().unwrap();
    }

    #[test]
    fn hosted_agent_provider_redacts_secret_in_resolved_output() {
        let env = BTreeMap::from([
            (
                "LEASH_AGENT_PROVIDER".to_string(),
                "openai-compatible-http".to_string(),
            ),
            (
                "LEASH_AGENT_BASE_URL".to_string(),
                "https://example.test/v1".to_string(),
            ),
            (
                "LEASH_AGENT_API_KEY".to_string(),
                "super-secret".to_string(),
            ),
        ]);
        let resolved = resolve_config(ConfigRequest {
            config_path: None,
            stack: None,
            stack_defaults: PartialHarnessConfig::default(),
            env,
            cli: PartialHarnessConfig::default(),
        })
        .unwrap();

        resolved.config.validate().unwrap();
        assert_eq!(
            resolved.config.agent_provider,
            AgentProvider::OpenAiCompatibleHttp
        );
        assert_eq!(value_for(&resolved, "agent_api_key"), json!("<redacted>"));
        assert_eq!(
            source_for(&resolved, "agent_api_key"),
            "env:LEASH_AGENT_API_KEY"
        );
        assert_eq!(attention_for(&resolved, "agent_api_key"), Some("secret"));
        let output = serde_json::to_string(&resolved).unwrap();
        assert!(!output.contains("super-secret"));
    }

    #[test]
    fn hosted_agent_provider_requires_api_key() {
        let env = BTreeMap::from([
            (
                "LEASH_AGENT_PROVIDER".to_string(),
                "openai-compatible-http".to_string(),
            ),
            (
                "LEASH_AGENT_BASE_URL".to_string(),
                "https://example.test/v1".to_string(),
            ),
        ]);
        let resolved = resolve_config(ConfigRequest {
            config_path: None,
            stack: None,
            stack_defaults: PartialHarnessConfig::default(),
            env,
            cli: PartialHarnessConfig::default(),
        })
        .unwrap();

        let err = resolved.config.validate().unwrap_err().to_string();

        assert!(err.contains("LEASH_AGENT_API_KEY"));
    }

    #[test]
    fn local_agent_provider_requires_base_url() {
        let resolved = resolve_config(ConfigRequest {
            config_path: None,
            stack: None,
            stack_defaults: PartialHarnessConfig::default(),
            env: BTreeMap::from([("LEASH_AGENT_PROVIDER".to_string(), "local-http".to_string())]),
            cli: PartialHarnessConfig::default(),
        })
        .unwrap();

        let err = resolved.config.validate().unwrap_err().to_string();

        assert!(err.contains("LEASH_AGENT_BASE_URL"));
    }

    #[test]
    fn replay_source_defaults_profile_to_replay() {
        let env = BTreeMap::from([(
            "LEASH_REPLAY_SOURCE".to_string(),
            "examples/replay/sim-basic.jsonl".to_string(),
        )]);
        let resolved = resolve_config(ConfigRequest {
            config_path: None,
            stack: None,
            stack_defaults: PartialHarnessConfig::default(),
            env,
            cli: PartialHarnessConfig::default(),
        })
        .unwrap();

        assert_eq!(resolved.config.profile, Profile::Replay);
        assert_eq!(
            resolved.config.replay_source,
            Some(PathBuf::from("examples/replay/sim-basic.jsonl"))
        );
        assert_eq!(source_for(&resolved, "profile"), "replay-source");
        assert_eq!(
            source_for(&resolved, "replay_source"),
            "env:LEASH_REPLAY_SOURCE"
        );
        assert_eq!(attention_for(&resolved, "replay_source"), Some("replay"));
    }

    #[test]
    fn rejects_required_accelerator_without_backend() {
        let config = HarnessConfig {
            require_accelerator: true,
            ..HarnessConfig::default()
        };
        let err = config.validate().unwrap_err().to_string();
        assert!(err.contains("required"));
    }

    #[test]
    fn redacts_sensitive_field_names() {
        assert_eq!(
            redact_value("pilot_token", json!("abc")),
            json!("<redacted>")
        );
        assert_eq!(redact_value("api_key", json!("abc")), json!("<redacted>"));
        assert_eq!(
            redact_value("allow_untokened_drive", json!(true)),
            json!(true)
        );
        assert_eq!(redact_value("role", json!("abc")), json!("abc"));
    }

    fn source_for(resolved: &ResolvedHarnessConfig, name: &str) -> String {
        resolved
            .fields
            .iter()
            .find(|field| field.name == name)
            .unwrap()
            .source
            .clone()
    }

    fn attention_for<'a>(resolved: &'a ResolvedHarnessConfig, name: &str) -> Option<&'a str> {
        resolved
            .fields
            .iter()
            .find(|field| field.name == name)
            .unwrap()
            .attention
    }

    fn value_for(resolved: &ResolvedHarnessConfig, name: &str) -> Value {
        resolved
            .fields
            .iter()
            .find(|field| field.name == name)
            .unwrap()
            .value
            .clone()
    }

    fn write_temp_config(name: &str, body: &str) -> PathBuf {
        let path = env::temp_dir().join(format!(
            "leash-{name}-{}-{}.json",
            std::process::id(),
            crate::runtime::now_ms()
        ));
        fs::write(&path, body).unwrap();
        path
    }
}
