use super::*;

fn canonicalize_json(value: &mut serde_json::Value) {
    match value {
        serde_json::Value::Object(map) => {
            let mut pairs: Vec<(String, serde_json::Value)> =
                std::mem::take(map).into_iter().collect();
            pairs.sort_by(|a, b| a.0.cmp(&b.0));
            for (_, item) in pairs.iter_mut() {
                canonicalize_json(item);
            }
            for (key, item) in pairs {
                map.insert(key, item);
            }
        }
        serde_json::Value::Array(items) => {
            for item in items {
                canonicalize_json(item);
            }
        }
        _ => {}
    }
}

pub(super) fn config_equal(lhs: &ProxyConfig, rhs: &ProxyConfig) -> bool {
    let mut left = match serde_json::to_value(lhs) {
        Ok(value) => value,
        Err(_) => return false,
    };
    let mut right = match serde_json::to_value(rhs) {
        Ok(value) => value,
        Err(_) => return false,
    };
    canonicalize_json(&mut left);
    canonicalize_json(&mut right);
    left == right
}

fn listeners_equal(
    lhs: &[crate::config::ListenerConfig],
    rhs: &[crate::config::ListenerConfig],
) -> bool {
    serde_json::to_value(lhs).ok() == serde_json::to_value(rhs).ok()
}

/// Warns when the requested snapshot contains fields that require restart.
pub(super) fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: bool) {
    let mut warned = false;
    if old.server.port != new.server.port {
        warned = true;
        warn!(
            "config reload: server.port changed ({} → {}); restart required",
            old.server.port, new.server.port
        );
    }
    if old.server.api.enabled != new.server.api.enabled
        || old.server.api.listen != new.server.api.listen
        || old.server.api.whitelist != new.server.api.whitelist
        || old.server.api.gray_action != new.server.api.gray_action
        || old.server.api.auth_header != new.server.api.auth_header
        || old.server.api.request_body_limit_bytes != new.server.api.request_body_limit_bytes
        || old.server.api.minimal_runtime_enabled != new.server.api.minimal_runtime_enabled
        || old.server.api.minimal_runtime_cache_ttl_ms
            != new.server.api.minimal_runtime_cache_ttl_ms
        || old.server.api.runtime_edge_enabled != new.server.api.runtime_edge_enabled
        || old.server.api.runtime_edge_cache_ttl_ms != new.server.api.runtime_edge_cache_ttl_ms
        || old.server.api.runtime_edge_top_n != new.server.api.runtime_edge_top_n
        || old.server.api.runtime_edge_events_capacity
            != new.server.api.runtime_edge_events_capacity
        || old.server.api.read_only != new.server.api.read_only
    {
        warned = true;
        warn!("config reload: server.api changed; restart required");
    }
    if old.server.proxy_protocol != new.server.proxy_protocol
        || !listeners_equal(&old.server.listeners, &new.server.listeners)
        || old.server.listen_backlog != new.server.listen_backlog
        || old.server.listen_addr_ipv4 != new.server.listen_addr_ipv4
        || old.server.listen_addr_ipv6 != new.server.listen_addr_ipv6
        || old.server.listen_tcp != new.server.listen_tcp
        || old.server.client_mss != new.server.client_mss
        || old.server.listen_unix_sock != new.server.listen_unix_sock
        || old.server.listen_unix_sock_perm != new.server.listen_unix_sock_perm
    {
        warned = true;
        warn!("config reload: server listener settings changed; restart required");
    }
    if old.web.decoy_fasttrack_mode != new.web.decoy_fasttrack_mode {
        warned = true;
        warn!("config reload: web.decoy_fasttrack_mode changed; restart required");
    }
    if old.censorship.tls_domain != new.censorship.tls_domain
        || old.censorship.tls_domains != new.censorship.tls_domains
        || old.censorship.tls_fetch_scope != new.censorship.tls_fetch_scope
        || old.censorship.mask != new.censorship.mask
        || old.censorship.mask_dynamic != new.censorship.mask_dynamic
        || old.censorship.mask_host != new.censorship.mask_host
        || old.censorship.mask_port != new.censorship.mask_port
        || old.censorship.exclusive_mask != new.censorship.exclusive_mask
        || old.censorship.mask_unix_sock != new.censorship.mask_unix_sock
        || old.censorship.fake_cert_len != new.censorship.fake_cert_len
        || old.censorship.tls_emulation != new.censorship.tls_emulation
        || old.censorship.tls_front_dir != new.censorship.tls_front_dir
        || old.censorship.server_hello_delay_min_ms != new.censorship.server_hello_delay_min_ms
        || old.censorship.server_hello_delay_max_ms != new.censorship.server_hello_delay_max_ms
        || old.censorship.tls_new_session_tickets != new.censorship.tls_new_session_tickets
        || old.censorship.serverhello_compact != new.censorship.serverhello_compact
        || old.censorship.tls_full_cert_ttl_secs != new.censorship.tls_full_cert_ttl_secs
        || old.censorship.alpn_enforce != new.censorship.alpn_enforce
        || old.censorship.mask_proxy_protocol != new.censorship.mask_proxy_protocol
        || old.censorship.mask_shape_hardening != new.censorship.mask_shape_hardening
        || old.censorship.mask_shape_bucket_floor_bytes
            != new.censorship.mask_shape_bucket_floor_bytes
        || old.censorship.mask_shape_bucket_cap_bytes != new.censorship.mask_shape_bucket_cap_bytes
        || old.censorship.mask_shape_above_cap_blur != new.censorship.mask_shape_above_cap_blur
        || old.censorship.mask_shape_above_cap_blur_max_bytes
            != new.censorship.mask_shape_above_cap_blur_max_bytes
        || old.censorship.mask_relay_max_bytes != new.censorship.mask_relay_max_bytes
        || old.censorship.mask_relay_timeout_ms != new.censorship.mask_relay_timeout_ms
        || old.censorship.mask_relay_idle_timeout_ms != new.censorship.mask_relay_idle_timeout_ms
        || old.censorship.mask_classifier_prefetch_timeout_ms
            != new.censorship.mask_classifier_prefetch_timeout_ms
        || old.censorship.mask_timing_normalization_enabled
            != new.censorship.mask_timing_normalization_enabled
        || old.censorship.mask_timing_normalization_floor_ms
            != new.censorship.mask_timing_normalization_floor_ms
        || old.censorship.mask_timing_normalization_ceiling_ms
            != new.censorship.mask_timing_normalization_ceiling_ms
    {
        warned = true;
        warn!("config reload: censorship settings changed; restart required");
    }
    if old.censorship.tls_domain != new.censorship.tls_domain {
        warned = true;
        warn!(
            "config reload: censorship.tls_domain changed ('{}' → '{}'); restart required",
            old.censorship.tls_domain, new.censorship.tls_domain
        );
    }
    if old.network.ipv4 != new.network.ipv4 || old.network.ipv6 != new.network.ipv6 {
        warned = true;
        warn!("config reload: network.ipv4/ipv6 changed; restart required");
    }
    if old.network.prefer != new.network.prefer
        || old.network.multipath != new.network.multipath
        || old.network.stun_use != new.network.stun_use
        || old.network.stun_servers != new.network.stun_servers
        || old.network.stun_tcp_fallback != new.network.stun_tcp_fallback
        || old.network.http_ip_detect_urls != new.network.http_ip_detect_urls
        || old.network.cache_public_ip_path != new.network.cache_public_ip_path
    {
        warned = true;
        warn!("config reload: non-hot network settings changed; restart required");
    }
    if old.general.use_middle_proxy != new.general.use_middle_proxy {
        warned = true;
        warn!("config reload: use_middle_proxy changed; restart required");
    }
    if old.general.stun_nat_probe_concurrency != new.general.stun_nat_probe_concurrency {
        warned = true;
        warn!("config reload: general.stun_nat_probe_concurrency changed; restart required");
    }
    if old.general.middle_proxy_pool_size != new.general.middle_proxy_pool_size {
        warned = true;
        warn!("config reload: general.middle_proxy_pool_size changed; restart required");
    }
    if old.general.me_route_no_writer_mode != new.general.me_route_no_writer_mode
        || old.general.me_route_no_writer_wait_ms != new.general.me_route_no_writer_wait_ms
        || old.general.me_route_hybrid_max_wait_ms != new.general.me_route_hybrid_max_wait_ms
        || old.general.me_route_blocking_send_timeout_ms
            != new.general.me_route_blocking_send_timeout_ms
        || old.general.me_route_inline_recovery_attempts
            != new.general.me_route_inline_recovery_attempts
        || old.general.me_route_inline_recovery_wait_ms
            != new.general.me_route_inline_recovery_wait_ms
    {
        warned = true;
        warn!("config reload: general.me_route_no_writer_* changed; restart required");
    }
    if old.general.unknown_dc_log_path != new.general.unknown_dc_log_path
        || old.general.unknown_dc_file_log_enabled != new.general.unknown_dc_file_log_enabled
    {
        warned = true;
        warn!("config reload: general.unknown_dc_* changed; restart required");
    }
    if old.general.me_init_retry_attempts != new.general.me_init_retry_attempts {
        warned = true;
        warn!("config reload: general.me_init_retry_attempts changed; restart required");
    }
    if old.general.me2dc_fallback != new.general.me2dc_fallback
        || old.general.me2dc_fast != new.general.me2dc_fast
    {
        warned = true;
        warn!("config reload: general.me2dc_fallback/me2dc_fast changed; restart required");
    }
    if old.general.proxy_config_v4_cache_path != new.general.proxy_config_v4_cache_path
        || old.general.proxy_config_v6_cache_path != new.general.proxy_config_v6_cache_path
    {
        warned = true;
        warn!("config reload: general.proxy_config_*_cache_path changed; restart required");
    }
    if old.general.me_keepalive_enabled != new.general.me_keepalive_enabled
        || old.general.me_keepalive_interval_secs != new.general.me_keepalive_interval_secs
        || old.general.me_keepalive_jitter_secs != new.general.me_keepalive_jitter_secs
        || old.general.me_keepalive_payload_random != new.general.me_keepalive_payload_random
    {
        warned = true;
        warn!("config reload: general.me_keepalive_* changed; restart required");
    }
    if old.general.upstream_connect_retry_attempts != new.general.upstream_connect_retry_attempts
        || old.general.upstream_connect_retry_backoff_ms
            != new.general.upstream_connect_retry_backoff_ms
        || old.general.tg_connect != new.general.tg_connect
        || old.general.upstream_unhealthy_fail_threshold
            != new.general.upstream_unhealthy_fail_threshold
        || old.general.upstream_connect_failfast_hard_errors
            != new.general.upstream_connect_failfast_hard_errors
        || old.general.rpc_proxy_req_every != new.general.rpc_proxy_req_every
    {
        warned = true;
        warn!("config reload: general.upstream_* changed; restart required");
    }
    if non_hot_changed && !warned {
        warn!("config reload: one or more non-hot fields changed; restart required");
    }
}

/// Resolve the public host for link generation — mirrors the logic in main.rs.
///
/// Priority:
/// 1. `[general.links] public_host` — explicit override in config
/// 2. `detected_ip_v4` — from STUN/interface probe at startup
/// 3. `detected_ip_v6` — fallback
/// 4. `"UNKNOWN"` — warn the user to set `public_host`

/// Which top-level config sections changed and whether any require a restart.
#[derive(Debug, Default, Clone, serde::Serialize)]
pub struct ChangeClassification {
    pub changed: Vec<String>,
    pub restart_required: bool,
}

/// Classify old->new using Telemt's OWN reload rule: overlay the hot fields and
/// see if anything non-hot remains different. This guarantees `restart_required`
/// matches actual runtime behavior and never drifts as new fields are added.
pub fn classify_config_changes(old: &ProxyConfig, new: &ProxyConfig) -> ChangeClassification {
    let applied = overlay_hot_fields(old, new);
    let restart_required = !config_equal(&applied, new);
    ChangeClassification {
        changed: changed_sections(old, new),
        restart_required,
    }
}

/// Top-level config sections whose canonical serialized form differs between
/// old and new. Uses the same serialize+canonicalize path as `config_equal`.
fn changed_sections(old: &ProxyConfig, new: &ProxyConfig) -> Vec<String> {
    let mut lhs = serde_json::to_value(old).unwrap_or(serde_json::Value::Null);
    let mut rhs = serde_json::to_value(new).unwrap_or(serde_json::Value::Null);
    canonicalize_json(&mut lhs);
    canonicalize_json(&mut rhs);

    let mut out = Vec::new();
    if let (Some(lo), Some(ro)) = (lhs.as_object(), rhs.as_object()) {
        let mut keys: std::collections::BTreeSet<&String> = lo.keys().collect();
        keys.extend(ro.keys());
        for key in keys {
            if lo.get(key) != ro.get(key) {
                out.push(key.clone());
            }
        }
    }
    out
}
