use crate::config::find_codex_home;
use crate::config::is_builtin_permission_profile_name;
use crate::config::reject_unknown_builtin_permission_profile;
use crate::config::resolve_permission_profile;
use crate::exec_policy::ExecPolicyError;
use crate::exec_policy::format_exec_policy_error_with_source;
use crate::exec_policy::load_exec_policy;
use anyhow::Context;
use anyhow::Result;
use codex_app_server_protocol::ConfigLayerSource;
use codex_config::CONFIG_TOML_FILE;
use codex_config::ConfigLayerStack;
use codex_config::ConfigLayerStackOrdering;
use codex_config::LoaderOverrides;
use codex_config::loader::load_config_layers_state;
use codex_config::merge_toml_values;
use codex_config::permissions_toml::NetworkMitmActionToml;
use codex_config::permissions_toml::NetworkMitmHookToml;
use codex_config::permissions_toml::NetworkMitmToml;
use codex_config::permissions_toml::NetworkToml;
use codex_config::permissions_toml::PermissionsToml;
use codex_config::permissions_toml::overlay_network_domain_permissions;
use codex_exec_server::LOCAL_FS;
use codex_network_proxy::ConfigReloader;
use codex_network_proxy::ConfigReloaderFuture;
use codex_network_proxy::ConfigState;
use codex_network_proxy::NetworkMode;
use codex_network_proxy::NetworkProxyConfig;
use codex_network_proxy::NetworkProxyConstraintError;
use codex_network_proxy::NetworkProxyConstraints;
use codex_network_proxy::NetworkProxyState;
use codex_network_proxy::build_config_state;
use codex_network_proxy::normalize_host;
use codex_network_proxy::validate_policy_against_constraints;
use codex_utils_absolute_path::AbsolutePathBuf;
use indexmap::IndexMap;
use serde::Deserialize;
use std::sync::Arc;
use tokio::sync::RwLock;

pub async fn build_network_proxy_state() -> Result<NetworkProxyState> {
    let (state, reloader) = build_network_proxy_state_and_reloader().await?;
    Ok(NetworkProxyState::with_reloader(state, Arc::new(reloader)))
}

pub async fn build_network_proxy_state_and_reloader() -> Result<(ConfigState, MtimeConfigReloader)>
{
    let (state, layer_mtimes) = build_config_state_with_mtimes().await?;
    Ok((state, MtimeConfigReloader::new(layer_mtimes)))
}

async fn build_config_state_with_mtimes() -> Result<(ConfigState, Vec<LayerMtime>)> {
    let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;
    let cli_overrides = Vec::new();
    let overrides = LoaderOverrides::default();
    let config_layer_stack = load_config_layers_state(
        LOCAL_FS.as_ref(),
        &codex_home,
        /*cwd*/ None,
        &cli_overrides,
        overrides,
        &codex_config::NoopThreadConfigLoader,
    )
    .await
    .context("failed to load Codex config")?;

    let (exec_policy, warning) = match load_exec_policy(&config_layer_stack).await {
        Ok(policy) => (policy, None),
        Err(err @ ExecPolicyError::ParsePolicy { .. }) => {
            (codex_execpolicy::Policy::empty(), Some(err))
        }
        Err(err) => return Err(err.into()),
    };
    if let Some(err) = warning.as_ref() {
        tracing::warn!(
            "failed to parse execpolicy while building network proxy state: {}",
            format_exec_policy_error_with_source(err)
        );
    }

    let config = config_from_layers(&config_layer_stack, &exec_policy)?;

    let constraints = enforce_trusted_constraints(&config_layer_stack, &config)?;
    let layer_mtimes = collect_layer_mtimes(&config_layer_stack);
    let state = build_config_state(config, constraints)?;
    Ok((state, layer_mtimes))
}

fn collect_layer_mtimes(stack: &ConfigLayerStack) -> Vec<LayerMtime> {
    stack
        .get_layers(
            ConfigLayerStackOrdering::LowestPrecedenceFirst,
            /*include_disabled*/ false,
        )
        .iter()
        .filter_map(|layer| {
            let path = match &layer.name {
                ConfigLayerSource::System { file } => Some(file.clone()),
                ConfigLayerSource::User { file, .. } => Some(file.clone()),
                ConfigLayerSource::Project { dot_codex_folder } => {
                    Some(dot_codex_folder.join(CONFIG_TOML_FILE))
                }
                ConfigLayerSource::LegacyManagedConfigTomlFromFile { file } => Some(file.clone()),
                _ => None,
            };
            path.map(LayerMtime::new)
        })
        .collect()
}

fn enforce_trusted_constraints(
    layers: &ConfigLayerStack,
    config: &NetworkProxyConfig,
) -> Result<NetworkProxyConstraints> {
    let constraints = network_constraints_from_trusted_layers(layers)?;
    validate_policy_against_constraints(config, &constraints)
        .map_err(NetworkProxyConstraintError::into_anyhow)
        .context("network proxy constraints")?;
    Ok(constraints)
}

fn network_constraints_from_trusted_layers(
    layers: &ConfigLayerStack,
) -> Result<NetworkProxyConstraints> {
    let mut constraints = NetworkProxyConstraints::default();
    let mut merged = toml::Value::Table(toml::map::Map::new());
    for layer in layers.get_layers(
        ConfigLayerStackOrdering::LowestPrecedenceFirst,
        /*include_disabled*/ false,
    ) {
        if is_user_controlled_layer(&layer.name) {
            continue;
        }

        merge_toml_values(&mut merged, &layer.config);
    }

    let parsed = network_tables_from_toml(&merged)?;
    if let Some(network) = selected_network_from_tables(parsed)? {
        apply_network_constraints(network, &mut constraints);
    }
    Ok(constraints)
}

fn apply_network_constraints(network: NetworkToml, constraints: &mut NetworkProxyConstraints) {
    if let Some(enabled) = network.enabled {
        constraints.enabled = Some(enabled);
    }
    if let Some(mode) = network.mode {
        constraints.mode = Some(mode);
    }
    if let Some(allow_upstream_proxy) = network.allow_upstream_proxy {
        constraints.allow_upstream_proxy = Some(allow_upstream_proxy);
    }
    if let Some(dangerously_allow_non_loopback_proxy) = network.dangerously_allow_non_loopback_proxy
    {
        constraints.dangerously_allow_non_loopback_proxy =
            Some(dangerously_allow_non_loopback_proxy);
    }
    if let Some(dangerously_allow_all_unix_sockets) = network.dangerously_allow_all_unix_sockets {
        constraints.dangerously_allow_all_unix_sockets = Some(dangerously_allow_all_unix_sockets);
    }
    if let Some(domains) = network.domains.as_ref() {
        let mut config = NetworkProxyConfig::default();
        if let Some(allowed_domains) = constraints.allowed_domains.take() {
            config.network.set_allowed_domains(allowed_domains);
        }
        if let Some(denied_domains) = constraints.denied_domains.take() {
            config.network.set_denied_domains(denied_domains);
        }
        overlay_network_domain_permissions(&mut config, domains);
        constraints.allowed_domains = config.network.allowed_domains();
        constraints.denied_domains = config.network.denied_domains();
    }
    if let Some(unix_sockets) = network.unix_sockets.as_ref() {
        let allow_unix_sockets = unix_sockets.allow_unix_sockets();
        constraints.allow_unix_sockets = Some(allow_unix_sockets);
    }
    if let Some(allow_local_binding) = network.allow_local_binding {
        constraints.allow_local_binding = Some(allow_local_binding);
    }
}

#[derive(Debug, Clone, Default, Deserialize)]
struct NetworkTablesToml {
    default_permissions: Option<String>,
    permissions: Option<PermissionsToml>,
}

fn network_tables_from_toml(value: &toml::Value) -> Result<NetworkTablesToml> {
    value
        .clone()
        .try_into()
        .context("failed to deserialize network tables from config")
}

fn selected_network_from_tables(parsed: NetworkTablesToml) -> Result<Option<NetworkToml>> {
    let Some(default_permissions) = parsed.default_permissions else {
        return Ok(None);
    };
    if is_builtin_permission_profile_name(&default_permissions) {
        return Ok(None);
    }
    reject_unknown_builtin_permission_profile(&default_permissions)?;

    let permissions = parsed
        .permissions
        .context("default_permissions requires a `[permissions]` table for network settings")?;
    let profile = resolve_permission_profile(&permissions, &default_permissions)
        .map_err(anyhow::Error::from)?;
    Ok(profile.network)
}

#[cfg(test)]
fn apply_network_tables(config: &mut NetworkProxyConfig, parsed: NetworkTablesToml) -> Result<()> {
    if let Some(network) = selected_network_from_tables(parsed)? {
        network.apply_to_network_proxy_config(config);
    }
    Ok(())
}

#[derive(Default)]
struct NetworkConfigAccumulator {
    config: NetworkProxyConfig,
    mitm_hooks: IndexMap<String, NetworkMitmHookToml>,
    mitm_actions: IndexMap<String, NetworkMitmActionToml>,
}

impl NetworkConfigAccumulator {
    fn apply_network_tables(&mut self, parsed: NetworkTablesToml) -> Result<()> {
        if let Some(network) = selected_network_from_tables(parsed)? {
            self.apply_network(network);
        }
        Ok(())
    }

    fn apply_network(&mut self, mut network: NetworkToml) {
        let mitm = network.mitm.take();
        network.apply_to_network_proxy_config(&mut self.config);

        if let Some(mitm) = mitm {
            if let Some(actions) = mitm.actions {
                self.mitm_actions.extend(actions);
            }
            if let Some(hooks) = mitm.hooks {
                self.mitm_hooks.extend(hooks);
            }
        }
    }

    fn finish(mut self) -> Result<NetworkProxyConfig> {
        if !self.mitm_hooks.is_empty() {
            let actions = self.mitm_actions;
            let mitm = NetworkMitmToml {
                hooks: Some(self.mitm_hooks),
                actions: Some(actions.clone()),
            };
            mitm.validate_action_references(&actions)
                .map_err(anyhow::Error::msg)?;
            self.config.network.mitm_hooks = mitm.to_runtime_hooks(Some(&actions));
        }

        self.config.network.mitm = self.config.network.mode == NetworkMode::Limited
            || !self.config.network.mitm_hooks.is_empty();
        Ok(self.config)
    }
}

fn config_from_layers(
    layers: &ConfigLayerStack,
    exec_policy: &codex_execpolicy::Policy,
) -> Result<NetworkProxyConfig> {
    let mut merged = toml::Value::Table(toml::map::Map::new());
    for layer in layers.get_layers(
        ConfigLayerStackOrdering::LowestPrecedenceFirst,
        /*include_disabled*/ false,
    ) {
        merge_toml_values(&mut merged, &layer.config);
    }
    let parsed = network_tables_from_toml(&merged)?;
    let mut accumulator = NetworkConfigAccumulator::default();
    accumulator.apply_network_tables(parsed)?;
    let mut config = accumulator.finish()?;
    apply_exec_policy_network_rules(&mut config, exec_policy);
    Ok(config)
}

fn apply_exec_policy_network_rules(
    config: &mut NetworkProxyConfig,
    exec_policy: &codex_execpolicy::Policy,
) {
    let (allowed_domains, denied_domains) = exec_policy.compiled_network_domains();
    for host in allowed_domains {
        upsert_network_domain(
            config,
            host,
            codex_network_proxy::NetworkDomainPermission::Allow,
        );
    }
    for host in denied_domains {
        upsert_network_domain(
            config,
            host,
            codex_network_proxy::NetworkDomainPermission::Deny,
        );
    }
}

fn upsert_network_domain(
    config: &mut NetworkProxyConfig,
    host: String,
    permission: codex_network_proxy::NetworkDomainPermission,
) {
    config
        .network
        .upsert_domain_permission(host, permission, normalize_host);
}

fn is_user_controlled_layer(layer: &ConfigLayerSource) -> bool {
    matches!(
        layer,
        ConfigLayerSource::User { .. }
            | ConfigLayerSource::Project { .. }
            | ConfigLayerSource::SessionFlags
    )
}

#[derive(Clone)]
struct LayerMtime {
    path: AbsolutePathBuf,
    mtime: Option<std::time::SystemTime>,
}

impl LayerMtime {
    fn new(path: AbsolutePathBuf) -> Self {
        let mtime = path.metadata().and_then(|m| m.modified()).ok();
        Self { path, mtime }
    }
}

pub struct MtimeConfigReloader {
    layer_mtimes: RwLock<Vec<LayerMtime>>,
}

impl MtimeConfigReloader {
    fn new(layer_mtimes: Vec<LayerMtime>) -> Self {
        Self {
            layer_mtimes: RwLock::new(layer_mtimes),
        }
    }

    async fn needs_reload(&self) -> bool {
        let guard = self.layer_mtimes.read().await;
        guard.iter().any(|layer| {
            let metadata = std::fs::metadata(&layer.path).ok();
            match (metadata.and_then(|m| m.modified().ok()), layer.mtime) {
                (Some(new_mtime), Some(old_mtime)) => new_mtime > old_mtime,
                (Some(_), None) => true,
                (None, Some(_)) => true,
                (None, None) => false,
            }
        })
    }

    async fn maybe_reload(&self) -> Result<Option<ConfigState>> {
        if !self.needs_reload().await {
            return Ok(None);
        }

        let (state, layer_mtimes) = build_config_state_with_mtimes().await?;
        let mut guard = self.layer_mtimes.write().await;
        *guard = layer_mtimes;
        Ok(Some(state))
    }

    async fn reload_now(&self) -> Result<ConfigState> {
        let (state, layer_mtimes) = build_config_state_with_mtimes().await?;
        let mut guard = self.layer_mtimes.write().await;
        *guard = layer_mtimes;
        Ok(state)
    }
}

impl ConfigReloader for MtimeConfigReloader {
    fn source_label(&self) -> String {
        "config layers".to_string()
    }

    fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option<ConfigState>> {
        Box::pin(MtimeConfigReloader::maybe_reload(self))
    }

    fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState> {
        Box::pin(MtimeConfigReloader::reload_now(self))
    }
}

#[cfg(test)]
#[path = "network_proxy_loader_tests.rs"]
mod tests;
