#![allow(dead_code)]

use std::{
    net::{IpAddr, SocketAddr},
    path::PathBuf,
    process::ExitCode,
    sync::{Arc, atomic::AtomicBool},
};

use crate::{
    ShellType,
    common::{
        config::{
            ConfigFileControl, ConfigLoader, ConsoleLoggerConfig, EncryptionAlgorithm,
            FileLoggerConfig, LoggingConfigLoader, NetworkIdentity, PeerConfig, PortForwardConfig,
            TomlConfigLoader, VpnPortalConfig, load_config_from_file, process_secure_mode_cfg,
        },
        constants::EASYTIER_VERSION,
        log,
    },
    defer,
    instance_manager::NetworkInstanceManager,
    launcher::add_proxy_network_to_config,
    proto::common::{CompressionAlgoPb, SecureModeConfig},
    rpc_service::ApiRpcServer,
    utils::panic::setup_panic_handler,
    web_client,
};
use anyhow::Context;
use cidr::IpCidr;
use clap::{CommandFactory, Parser};
use rust_i18n::t;
use strum::VariantArray;
use tokio::io::AsyncReadExt;

use crate::tunnel::IpScheme;
#[cfg(feature = "jemalloc-prof")]
use jemalloc_ctl::{Access as _, AsName as _, epoch, stats};

#[cfg(target_os = "windows")]
windows_service::define_windows_service!(ffi_service_main, win_service_main);

fn set_prof_active(_active: bool) {
    #[cfg(feature = "jemalloc-prof")]
    {
        const PROF_ACTIVE: &[u8] = b"prof.active\0";
        let name = PROF_ACTIVE.name();
        name.write(_active).expect("Should succeed to set prof");
    }
}

fn get_dump_profile_path(cur_allocated: usize, suffix: &str) -> String {
    format!(
        "profile-{}-{}.{}",
        cur_allocated,
        chrono::Local::now().format("%Y-%m-%d-%H-%M-%S"),
        suffix
    )
}

fn dump_profile(_cur_allocated: usize) {
    #[cfg(feature = "jemalloc-prof")]
    {
        const PROF_DUMP: &[u8] = b"prof.dump\0";
        static mut PROF_DUMP_FILE_NAME: [u8; 128] = [0; 128];
        let file_name_str = get_dump_profile_path(_cur_allocated, "out");
        // copy file name to PROF_DUMP
        let file_name = file_name_str.as_bytes();
        let len = file_name.len();
        if len > 127 {
            panic!("file name too long");
        }
        unsafe {
            PROF_DUMP_FILE_NAME[..len].copy_from_slice(file_name);
            // set the last byte to 0
            PROF_DUMP_FILE_NAME[len] = 0;

            let name = PROF_DUMP.name();
            name.write(&PROF_DUMP_FILE_NAME[..len + 1])
                .expect("Should succeed to dump profile");
            println!("dump profile to: {}", file_name_str);
        }
    }
}

#[derive(Parser, Debug)]
#[command(name = "easytier-core", author, version = EASYTIER_VERSION , about, long_about = None)]
struct Cli {
    #[arg(
        short = 'w',
        long,
        env = "ET_CONFIG_SERVER",
        help = t!("core_clap.config_server").to_string()
    )]
    config_server: Option<String>,

    #[arg(
        long,
        env = "ET_MACHINE_ID",
        help = t!("core_clap.machine_id").to_string()
    )]
    machine_id: Option<String>,

    #[arg(
        short,
        long,
        env = "ET_CONFIG_FILE",
        value_delimiter = ',',
        help = t!("core_clap.config_file").to_string(),
        num_args = 1..,
    )]
    config_file: Option<Vec<PathBuf>>,

    #[arg(
        long,
        env = "ET_CONFIG_DIR",
        help = t!("core_clap.config_dir").to_string()
    )]
    config_dir: Option<PathBuf>,

    #[command(flatten)]
    network_options: NetworkOptions,

    #[command(flatten)]
    logging_options: LoggingOptions,

    #[command(flatten)]
    rpc_portal_options: RpcPortalOptions,

    #[clap(long, help = t!("core_clap.generate_completions").to_string())]
    gen_autocomplete: Option<ShellType>,

    #[clap(long, help = t!("core_clap.check_config").to_string())]
    check_config: bool,

    #[clap(long, help = t!("core_clap.daemon").to_string())]
    daemon: bool,

    #[clap(long, help = t!("core_clap.disable_env_parsing").to_string())]
    disable_env_parsing: bool,
}

#[derive(Parser, Debug, Default, PartialEq, Eq)]
struct NetworkOptions {
    #[arg(
        long,
        env = "ET_NETWORK_NAME",
        help = t!("core_clap.network_name").to_string(),
    )]
    network_name: Option<String>,

    #[arg(
        long,
        env = "ET_NETWORK_SECRET",
        help = t!("core_clap.network_secret").to_string(),
    )]
    network_secret: Option<String>,

    #[arg(
        short,
        long,
        env = "ET_IPV4",
        help = t!("core_clap.ipv4").to_string()
    )]
    ipv4: Option<String>,

    #[arg(
        long,
        env = "ET_IPV6",
        help = t!("core_clap.ipv6").to_string()
    )]
    ipv6: Option<String>,

    #[arg(
        short,
        long,
        env = "ET_DHCP",
        help = t!("core_clap.dhcp").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    dhcp: Option<bool>,

    #[arg(
        short,
        long,
        env = "ET_PEERS",
        value_delimiter = ',',
        help = t!("core_clap.peers").to_string(),
        num_args = 0..
    )]
    peers: Vec<String>,

    #[arg(
        short,
        long,
        env = "ET_EXTERNAL_NODE",
        help = t!("core_clap.external_node").to_string()
    )]
    external_node: Option<String>,

    #[arg(
        short = 'n',
        long,
        env = "ET_PROXY_NETWORKS",
        value_delimiter = ',',
        help = t!("core_clap.proxy_networks").to_string()
    )]
    proxy_networks: Vec<String>,

    #[arg(
        short,
        long,
        env = "ET_LISTENERS",
        value_delimiter = ',',
        help = t!("core_clap.listeners").to_string(),
        num_args = 0..
    )]
    listeners: Vec<String>,

    #[arg(
        long,
        env = "ET_MAPPED_LISTENERS",
        value_delimiter = ',',
        help = t!("core_clap.mapped_listeners").to_string(),
        num_args = 0..
    )]
    mapped_listeners: Vec<String>,

    #[arg(
        long,
        env = "ET_NO_LISTENER",
        help = t!("core_clap.no_listener").to_string(),
        default_value = "false",
    )]
    no_listener: bool,

    #[arg(
        long,
        env = "ET_HOSTNAME",
        help = t!("core_clap.hostname").to_string()
    )]
    hostname: Option<String>,

    #[arg(
        short = 'm',
        long,
        env = "ET_INSTANCE_NAME",
        help = t!("core_clap.instance_name").to_string(),
    )]
    instance_name: Option<String>,

    #[arg(
        long,
        env = "ET_VPN_PORTAL",
        help = t!("core_clap.vpn_portal").to_string()
    )]
    vpn_portal: Option<String>,

    #[arg(
        long,
        env = "ET_DEFAULT_PROTOCOL",
        help = t!("core_clap.default_protocol").to_string()
    )]
    default_protocol: Option<String>,

    #[arg(
        short = 'u',
        long,
        env = "ET_DISABLE_ENCRYPTION",
        help = t!("core_clap.disable_encryption").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    disable_encryption: Option<bool>,

    #[arg(
        long,
        env = "ET_ENCRYPTION_ALGORITHM",
        help = t!("core_clap.encryption_algorithm").to_string(),
        value_enum,
    )]
    encryption_algorithm: Option<EncryptionAlgorithm>,

    #[arg(
        long,
        env = "ET_MULTI_THREAD",
        help = t!("core_clap.multi_thread").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    multi_thread: Option<bool>,

    #[arg(
        long,
        env = "ET_MULTI_THREAD_COUNT",
        help = t!("core_clap.multi_thread_count").to_string(),
    )]
    multi_thread_count: Option<u32>,

    #[arg(
        long,
        env = "ET_DISABLE_IPV6",
        help = t!("core_clap.disable_ipv6").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    disable_ipv6: Option<bool>,

    #[arg(
        long,
        env = "ET_DEV_NAME",
        help = t!("core_clap.dev_name").to_string()
    )]
    dev_name: Option<String>,

    #[arg(
        long,
        env = "ET_MTU",
        help = t!("core_clap.mtu").to_string()
    )]
    mtu: Option<u16>,

    #[arg(
        long,
        env = "ET_LATENCY_FIRST",
        help = t!("core_clap.latency_first").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    latency_first: Option<bool>,

    #[arg(
        long,
        env = "ET_EXIT_NODES",
        value_delimiter = ',',
        help = t!("core_clap.exit_nodes").to_string(),
        num_args = 0..
    )]
    exit_nodes: Vec<IpAddr>,

    #[arg(
        long,
        env = "ET_ENABLE_EXIT_NODE",
        help = t!("core_clap.enable_exit_node").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    enable_exit_node: Option<bool>,

    #[arg(
        long,
        env = "ET_PROXY_FORWARD_BY_SYSTEM",
        help = t!("core_clap.proxy_forward_by_system").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    proxy_forward_by_system: Option<bool>,

    #[arg(
        long,
        env = "ET_NO_TUN",
        help = t!("core_clap.no_tun").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    no_tun: Option<bool>,

    #[arg(
        long,
        env = "ET_USE_SMOLTCP",
        help = t!("core_clap.use_smoltcp").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    use_smoltcp: Option<bool>,

    #[arg(
        long,
        env = "ET_MANUAL_ROUTES",
        value_delimiter = ',',
        help = t!("core_clap.manual_routes").to_string(),
        num_args = 0..
    )]
    manual_routes: Option<Vec<String>>,

    // if not in relay_network_whitelist:
    // for foreign virtual network, will refuse the incoming connection
    // for local virtual network, will refuse to relay tun packets
    #[arg(
        long,
        env = "ET_RELAY_NETWORK_WHITELIST",
        value_delimiter = ',',
        help = t!("core_clap.relay_network_whitelist").to_string(),
        num_args = 0..
    )]
    relay_network_whitelist: Option<Vec<String>>,

    #[arg(
        long,
        env = "ET_P2P_ONLY",
        help = t!("core_clap.p2p_only").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    p2p_only: Option<bool>,

    #[arg(
        long,
        env = "ET_LAZY_P2P",
        help = t!("core_clap.lazy_p2p").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    lazy_p2p: Option<bool>,

    #[arg(
        long,
        env = "ET_DISABLE_P2P",
        help = t!("core_clap.disable_p2p").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    disable_p2p: Option<bool>,

    #[arg(
        long,
        env = "ET_DISABLE_UDP_HOLE_PUNCHING",
        help = t!("core_clap.disable_udp_hole_punching").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    disable_udp_hole_punching: Option<bool>,

    #[arg(
        long,
        env = "ET_DISABLE_TCP_HOLE_PUNCHING",
        help = t!("core_clap.disable_tcp_hole_punching").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    disable_tcp_hole_punching: Option<bool>,

    #[arg(
        long,
        env = "ET_DISABLE_SYM_HOLE_PUNCHING",
        help = t!("core_clap.disable_sym_hole_punching").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    disable_sym_hole_punching: Option<bool>,

    #[arg(
        long,
        env = "ET_RELAY_ALL_PEER_RPC",
        help = t!("core_clap.relay_all_peer_rpc").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    relay_all_peer_rpc: Option<bool>,

    #[arg(
        long,
        env = "ET_NEED_P2P",
        help = t!("core_clap.need_p2p").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    need_p2p: Option<bool>,

    #[cfg(feature = "socks5")]
    #[arg(
        long,
        env = "ET_SOCKS5",
        help = t!("core_clap.socks5").to_string()
    )]
    socks5: Option<u16>,

    #[arg(
        long,
        env = "ET_COMPRESSION",
        help = t!("core_clap.compression").to_string(),
    )]
    compression: Option<String>,

    #[arg(
        long,
        env = "ET_BIND_DEVICE",
        help = t!("core_clap.bind_device").to_string()
    )]
    bind_device: Option<bool>,

    #[arg(
        long,
        env = "ET_ENABLE_KCP_PROXY",
        help = t!("core_clap.enable_kcp_proxy").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    enable_kcp_proxy: Option<bool>,

    #[arg(
        long,
        env = "ET_DISABLE_KCP_INPUT",
        help = t!("core_clap.disable_kcp_input").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    disable_kcp_input: Option<bool>,

    #[arg(
        long,
        env = "ET_ENABLE_QUIC_PROXY",
        help = t!("core_clap.enable_quic_proxy").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    enable_quic_proxy: Option<bool>,

    #[arg(
        long,
        env = "ET_DISABLE_QUIC_INPUT",
        help = t!("core_clap.disable_quic_input").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    disable_quic_input: Option<bool>,

    #[arg(
        long,
        env = "ET_PORT_FORWARD",
        value_delimiter = ',',
        help = t!("core_clap.port_forward").to_string(),
        num_args = 1..
    )]
    port_forward: Vec<url::Url>,

    #[arg(
        long,
        env = "ET_ACCEPT_DNS",
        help = t!("core_clap.accept_dns").to_string(),
    )]
    accept_dns: Option<bool>,

    #[arg(
        long = "tld-dns-zone",
        env = "ET_TLD_DNS_ZONE",
        help = t!("core_clap.tld_dns_zone").to_string())]
    tld_dns_zone: Option<String>,

    #[arg(
        long,
        env = "ET_PRIVATE_MODE",
        help = t!("core_clap.private_mode").to_string(),
    )]
    private_mode: Option<bool>,

    #[arg(
        long,
        env = "ET_FOREIGN_RELAY_BPS_LIMIT",
        help = t!("core_clap.foreign_relay_bps_limit").to_string(),
    )]
    foreign_relay_bps_limit: Option<u64>,

    #[arg(
        long,
        env = "ET_INSTANCE_RECV_BPS_LIMIT",
        help = t!("core_clap.instance_recv_bps_limit").to_string(),
    )]
    instance_recv_bps_limit: Option<u64>,

    #[arg(
        long,
        value_delimiter = ',',
        help = t!("core_clap.tcp_whitelist").to_string(),
        num_args = 0..
    )]
    tcp_whitelist: Vec<String>,

    #[arg(
        long,
        value_delimiter = ',',
        help = t!("core_clap.udp_whitelist").to_string(),
        num_args = 0..
    )]
    udp_whitelist: Vec<String>,

    #[arg(
        long,
        env = "ET_DISABLE_RELAY_KCP",
        help = t!("core_clap.disable_relay_kcp").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    disable_relay_kcp: Option<bool>,

    #[arg(
        long,
        env = "ET_DISABLE_RELAY_QUIC",
        help = t!("core_clap.disable_relay_quic").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    disable_relay_quic: Option<bool>,

    #[arg(
        long,
        env = "ET_ENABLE_RELAY_FOREIGN_NETWORK_KCP",
        help = t!("core_clap.enable_relay_foreign_network_kcp").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    enable_relay_foreign_network_kcp: Option<bool>,

    #[arg(
        long,
        env = "ET_ENABLE_RELAY_FOREIGN_NETWORK_QUIC",
        help = t!("core_clap.enable_relay_foreign_network_quic").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    enable_relay_foreign_network_quic: Option<bool>,

    #[arg(
        long,
        env = "ET_STUN_SERVERS",
        value_delimiter = ',',
        help = t!("core_clap.stun_servers").to_string(),
        num_args = 0..
    )]
    stun_servers: Option<Vec<String>>,

    #[arg(
        long,
        env = "ET_STUN_SERVERS_V6",
        value_delimiter = ',',
        help = t!("core_clap.stun_servers_v6").to_string(),
        num_args = 0..
    )]
    stun_servers_v6: Option<Vec<String>>,

    #[arg(
        long,
        env = "ET_SECURE_MODE",
        help = t!("core_clap.secure_mode").to_string(),
        num_args = 0..=1,
        default_missing_value = "true"
    )]
    secure_mode: Option<bool>,

    #[arg(
        long,
        env = "ET_LOCAL_PRIVATE_KEY",
        help = t!("core_clap.local_private_key").to_string()
    )]
    local_private_key: Option<String>,

    #[arg(
        long,
        env = "ET_LOCAL_PUBLIC_KEY",
        help = t!("core_clap.local_public_key").to_string()
    )]
    local_public_key: Option<String>,

    #[arg(
        long,
        env = "ET_CREDENTIAL",
        help = t!("core_clap.credential").to_string()
    )]
    credential: Option<String>,

    #[arg(
        long,
        env = "ET_CREDENTIAL_FILE",
        help = t!("core_clap.credential_file").to_string()
    )]
    credential_file: Option<PathBuf>,
}

#[derive(Parser, Debug)]
struct LoggingOptions {
    #[arg(
        long,
        env = "ET_CONSOLE_LOG_LEVEL",
        help = t!("core_clap.console_log_level").to_string()
    )]
    console_log_level: Option<String>,

    #[arg(
        long,
        env = "ET_FILE_LOG_LEVEL",
        help = t!("core_clap.file_log_level").to_string()
    )]
    file_log_level: Option<String>,

    #[arg(
        long,
        env = "ET_FILE_LOG_DIR",
        help = t!("core_clap.file_log_dir").to_string()
    )]
    file_log_dir: Option<String>,

    #[arg(
        long,
        env = "ET_FILE_LOG_SIZE",
        help = t!("core_clap.file_log_size_mb").to_string()
    )]
    file_log_size: Option<u64>,

    #[arg(
        long,
        env = "ET_FILE_LOG_COUNT",
        help = t!("core_clap.file_log_count").to_string()
    )]
    file_log_count: Option<usize>,
}

#[derive(Parser, Debug)]
struct RpcPortalOptions {
    #[arg(
        short,
        long,
        env = "ET_RPC_PORTAL",
        help = t!("core_clap.rpc_portal").to_string(),
    )]
    rpc_portal: Option<String>,

    #[arg(
        long,
        env = "ET_RPC_PORTAL_WHITELIST",
        value_delimiter = ',',
        help = t!("core_clap.rpc_portal_whitelist").to_string(),
    )]
    rpc_portal_whitelist: Option<Vec<IpCidr>>,
}

impl Cli {
    fn parse_listeners(no_listener: bool, listeners: Vec<String>) -> anyhow::Result<Vec<String>> {
        if no_listener || listeners.is_empty() {
            return Ok(vec![]);
        }

        if listeners.len() == 1
            && let Ok(port) = listeners[0].parse::<u16>()
        {
            let listeners = IpScheme::VARIANTS
                .iter()
                .map(|proto| {
                    format!(
                        "{}://0.0.0.0:{}",
                        proto,
                        if port == 0 {
                            0
                        } else {
                            port + proto.port_offset()
                        }
                    )
                })
                .collect();
            return Ok(listeners);
        }

        listeners
            .into_iter()
            .map(|l| {
                let l = l
                    .parse::<url::Url>()
                    .or_else(|_| url::Url::parse(&format!("{}:", l)))?;

                if l.has_authority() {
                    return Ok(l.to_string());
                }

                let scheme: IpScheme = l.scheme().parse()?;
                let port = {
                    let port = l.path();
                    if port.is_empty() {
                        11010 + scheme.port_offset()
                    } else {
                        port.parse::<u16>()
                            .with_context(|| format!("invalid port: {}", port))?
                    }
                };
                Ok(format!("{}://0.0.0.0:{}", scheme, port))
            })
            .collect()
    }
}

impl NetworkOptions {
    fn can_merge(
        &self,
        cfg: &TomlConfigLoader,
        source: ConfigSource,
        explicit_config_file_count: usize,
        config_dir_file_count: usize,
    ) -> bool {
        if (*self) == NetworkOptions::default() {
            return false;
        }

        if source == ConfigSource::CliConfigFile
            && explicit_config_file_count == 1
            && config_dir_file_count == 0
        {
            return true;
        }

        let Some(network_name) = &self.network_name else {
            return false;
        };

        if source == ConfigSource::ConfigDir {
            return cfg.get_network_identity().network_name == *network_name;
        }

        cfg.get_network_identity().network_name == *network_name
    }

    fn merge_into(&self, cfg: &TomlConfigLoader) -> anyhow::Result<()> {
        if self.hostname.is_some() {
            cfg.set_hostname(self.hostname.clone());
        }

        let old_ns = cfg.get_network_identity();
        let network_name = self.network_name.clone().unwrap_or(old_ns.network_name);

        if self.credential.is_some() {
            // Credential mode: no network_secret, authenticate via credential keypair
            cfg.set_network_identity(NetworkIdentity::new_credential(network_name));
        } else {
            let network_secret = self
                .network_secret
                .clone()
                .unwrap_or(old_ns.network_secret.unwrap_or_default());
            cfg.set_network_identity(NetworkIdentity::new(network_name, network_secret));
        }

        if let Some(dhcp) = self.dhcp {
            cfg.set_dhcp(dhcp);
        }

        if let Some(ipv4) = &self.ipv4 {
            cfg.set_ipv4(Some(ipv4.parse().with_context(|| {
                format!("failed to parse ipv4 address: {}", ipv4)
            })?))
        }

        if let Some(ipv6) = &self.ipv6 {
            cfg.set_ipv6(Some(ipv6.parse().with_context(|| {
                format!("failed to parse ipv6 address: {}", ipv6)
            })?))
        }

        if !self.peers.is_empty() {
            let mut peers = cfg.get_peers();
            peers.reserve(peers.len() + self.peers.len());
            for p in &self.peers {
                peers.push(PeerConfig {
                    uri: p
                        .parse()
                        .with_context(|| format!("failed to parse peer uri: {}", p))?,
                    peer_public_key: None,
                });
            }
            cfg.set_peers(peers);
        }

        if self.no_listener || !self.listeners.is_empty() {
            cfg.set_listeners(
                Cli::parse_listeners(self.no_listener, self.listeners.clone())
                    .with_context(|| format!("failed to parse listeners: {:?}", self.listeners))?
                    .into_iter()
                    .map(|s| s.parse().unwrap())
                    .collect(),
            );
        } else if cfg.get_listeners().is_none() {
            cfg.set_listeners(
                Cli::parse_listeners(false, vec!["11010".to_string()])?
                    .into_iter()
                    .map(|s| s.parse().unwrap())
                    .collect(),
            );
        }

        if !self.mapped_listeners.is_empty() {
            let mut errs = Vec::new();
            cfg.set_mapped_listeners(Some(
                self.mapped_listeners
                    .iter()
                    .map(|s| {
                        s.parse()
                            .with_context(|| format!("mapped listener is not a valid url: {}", s))
                            .unwrap()
                    })
                    .map(|s: url::Url| {
                        if s.port().is_none() {
                            errs.push(anyhow::anyhow!("mapped listener port is missing: {}", s));
                        }
                        s
                    })
                    .collect::<Vec<_>>(),
            ));
            if !errs.is_empty() {
                return Err(anyhow::anyhow!(
                    "{}",
                    errs.iter()
                        .map(|x| format!("{}", x))
                        .collect::<Vec<_>>()
                        .join("\n")
                ));
            }
        }

        for n in self.proxy_networks.iter() {
            add_proxy_network_to_config(n, cfg)?;
        }

        if let Some(external_nodes) = self.external_node.as_ref() {
            let mut old_peers = cfg.get_peers();
            old_peers.push(PeerConfig {
                uri: external_nodes.parse().with_context(|| {
                    format!("failed to parse external node uri: {}", external_nodes)
                })?,
                peer_public_key: None,
            });
            cfg.set_peers(old_peers);
        }

        if let Some(inst_name) = &self.instance_name {
            cfg.set_inst_name(inst_name.clone());
        }

        if let Some(vpn_portal) = self.vpn_portal.as_ref() {
            let url: url::Url = vpn_portal
                .parse()
                .with_context(|| format!("failed to parse vpn portal url: {}", vpn_portal))?;
            let host = url
                .host_str()
                .ok_or_else(|| anyhow::anyhow!("vpn portal url missing host"))?;
            let port = url
                .port()
                .ok_or_else(|| anyhow::anyhow!("vpn portal url missing port"))?;
            let client_cidr = url.path()[1..].parse().with_context(|| {
                format!("failed to parse vpn portal client cidr: {}", url.path())
            })?;
            let wireguard_listen: SocketAddr = format!("{}:{}", host, port).parse().unwrap();
            cfg.set_vpn_portal_config(VpnPortalConfig {
                wireguard_listen,
                client_cidr,
            });
        }

        if let Some(manual_routes) = self.manual_routes.as_ref() {
            let mut routes = Vec::<cidr::Ipv4Cidr>::with_capacity(manual_routes.len());
            for r in manual_routes {
                routes.push(
                    r.parse()
                        .with_context(|| format!("failed to parse route: {}", r))?,
                );
            }
            cfg.set_routes(Some(routes));
        }

        #[cfg(feature = "socks5")]
        if let Some(socks5_proxy) = self.socks5 {
            cfg.set_socks5_portal(Some(
                format!("socks5://0.0.0.0:{}", socks5_proxy)
                    .parse()
                    .unwrap(),
            ));
        }

        for port_forward in self.port_forward.iter() {
            let example_str = ", example: udp://0.0.0.0:12345/10.126.126.1:12345";

            let bind_addr = format!(
                "{}:{}",
                port_forward.host_str().expect("local bind host is missing"),
                port_forward.port().expect("local bind port is missing")
            )
            .parse()
            .unwrap_or_else(|_| panic!("failed to parse local bind addr {}", example_str));

            let dst_addr = port_forward
                .path_segments()
                .unwrap_or_else(|| panic!("remote destination addr is missing {}", example_str))
                .next()
                .unwrap_or_else(|| panic!("remote destination addr is missing {}", example_str))
                .to_string()
                .parse()
                .unwrap_or_else(|_| {
                    panic!("failed to parse remote destination addr {}", example_str)
                });

            let port_forward_item = PortForwardConfig {
                bind_addr,
                dst_addr,
                proto: port_forward.scheme().to_string(),
            };

            let mut old = cfg.get_port_forwards();
            old.push(port_forward_item);
            cfg.set_port_forwards(old);
        }

        if let Some(ref credential_file) = self.credential_file {
            cfg.set_credential_file(Some(credential_file.clone()));
        }

        if let Some(ref credential_secret) = self.credential {
            // --credential implies --secure-mode and sets the credential private key
            let c = SecureModeConfig {
                enabled: true,
                local_private_key: Some(credential_secret.clone()),
                local_public_key: None,
            };
            cfg.set_secure_mode(Some(process_secure_mode_cfg(c)?));
        } else if let Some(secure_mode) = self.secure_mode
            && secure_mode
        {
            let c = SecureModeConfig {
                enabled: secure_mode,
                local_private_key: self.local_private_key.clone(),
                local_public_key: self.local_public_key.clone(),
            };
            cfg.set_secure_mode(Some(process_secure_mode_cfg(c)?));
        }

        let mut f = cfg.get_flags();
        if let Some(default_protocol) = &self.default_protocol {
            f.default_protocol = default_protocol.clone()
        };
        if let Some(v) = self.disable_encryption {
            f.enable_encryption = !v;
        }
        if let Some(algorithm) = &self.encryption_algorithm {
            f.encryption_algorithm = algorithm.to_string();
        }
        if let Some(v) = self.disable_ipv6 {
            f.enable_ipv6 = !v;
        }
        f.latency_first = self.latency_first.unwrap_or(f.latency_first);
        if let Some(dev_name) = &self.dev_name {
            f.dev_name = dev_name.clone()
        }
        if let Some(mtu) = self.mtu {
            f.mtu = mtu as u32;
        }
        f.enable_exit_node = self.enable_exit_node.unwrap_or(f.enable_exit_node);
        f.proxy_forward_by_system = self
            .proxy_forward_by_system
            .unwrap_or(f.proxy_forward_by_system);
        f.no_tun = self.no_tun.unwrap_or(f.no_tun) || cfg!(not(feature = "tun"));
        f.use_smoltcp = self.use_smoltcp.unwrap_or(f.use_smoltcp);
        if let Some(wl) = self.relay_network_whitelist.as_ref() {
            f.relay_network_whitelist = wl.join(" ");
        }
        f.disable_p2p = self.disable_p2p.unwrap_or(f.disable_p2p);
        f.p2p_only = self.p2p_only.unwrap_or(f.p2p_only);
        f.lazy_p2p = self.lazy_p2p.unwrap_or(f.lazy_p2p);
        f.disable_tcp_hole_punching = self
            .disable_tcp_hole_punching
            .unwrap_or(f.disable_tcp_hole_punching);
        f.disable_udp_hole_punching = self
            .disable_udp_hole_punching
            .unwrap_or(f.disable_udp_hole_punching);
        f.relay_all_peer_rpc = self.relay_all_peer_rpc.unwrap_or(f.relay_all_peer_rpc);
        f.need_p2p = self.need_p2p.unwrap_or(f.need_p2p);
        f.multi_thread = self.multi_thread.unwrap_or(f.multi_thread);
        if let Some(compression) = &self.compression {
            f.data_compress_algo = match compression.as_str() {
                "none" => CompressionAlgoPb::None,
                "zstd" => CompressionAlgoPb::Zstd,
                _ => panic!(
                    "unknown compression algorithm: {}, supported: none, zstd",
                    compression
                ),
            }
            .into();
        }
        f.bind_device = self.bind_device.unwrap_or(f.bind_device);
        f.enable_kcp_proxy = self.enable_kcp_proxy.unwrap_or(f.enable_kcp_proxy);
        f.disable_kcp_input = self.disable_kcp_input.unwrap_or(f.disable_kcp_input);
        f.enable_quic_proxy = self.enable_quic_proxy.unwrap_or(f.enable_quic_proxy);
        f.disable_quic_input = self.disable_quic_input.unwrap_or(f.disable_quic_input);
        f.accept_dns = self.accept_dns.unwrap_or(f.accept_dns);
        f.private_mode = self.private_mode.unwrap_or(f.private_mode);
        f.foreign_relay_bps_limit = self
            .foreign_relay_bps_limit
            .unwrap_or(f.foreign_relay_bps_limit);
        f.instance_recv_bps_limit = self
            .instance_recv_bps_limit
            .unwrap_or(f.instance_recv_bps_limit);
        f.multi_thread_count = self.multi_thread_count.unwrap_or(f.multi_thread_count);
        f.disable_relay_kcp = self.disable_relay_kcp.unwrap_or(f.disable_relay_kcp);
        f.disable_relay_quic = self.disable_relay_quic.unwrap_or(f.disable_relay_quic);
        f.enable_relay_foreign_network_kcp = self
            .enable_relay_foreign_network_kcp
            .unwrap_or(f.enable_relay_foreign_network_kcp);
        f.enable_relay_foreign_network_quic = self
            .enable_relay_foreign_network_quic
            .unwrap_or(f.enable_relay_foreign_network_quic);
        f.disable_sym_hole_punching = self.disable_sym_hole_punching.unwrap_or(false);
        // Configure tld_dns_zone: use provided value if set
        if let Some(tld_dns_zone) = &self.tld_dns_zone {
            f.tld_dns_zone = tld_dns_zone.clone();
        }
        cfg.set_flags(f);

        if !self.exit_nodes.is_empty() {
            cfg.set_exit_nodes(self.exit_nodes.clone());
        }

        let mut old_tcp_whitelist = cfg.get_tcp_whitelist();
        old_tcp_whitelist.extend(self.tcp_whitelist.clone());
        cfg.set_tcp_whitelist(old_tcp_whitelist);

        let mut old_udp_whitelist = cfg.get_udp_whitelist();
        old_udp_whitelist.extend(self.udp_whitelist.clone());
        cfg.set_udp_whitelist(old_udp_whitelist);

        if let Some(stun_servers) = &self.stun_servers {
            let mut old_stun_servers = cfg.get_stun_servers().unwrap_or_default();
            old_stun_servers.extend(stun_servers.iter().cloned());
            cfg.set_stun_servers(Some(old_stun_servers));
        }

        if let Some(stun_servers_v6) = &self.stun_servers_v6 {
            let mut old_stun_servers_v6 = cfg.get_stun_servers_v6().unwrap_or_default();
            old_stun_servers_v6.extend(stun_servers_v6.iter().cloned());
            cfg.set_stun_servers_v6(Some(old_stun_servers_v6));
        }
        Ok(())
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ConfigSource {
    CliConfigFile,
    ConfigDir,
}

impl LoggingConfigLoader for &LoggingOptions {
    fn get_console_logger_config(&self) -> ConsoleLoggerConfig {
        ConsoleLoggerConfig {
            level: self.console_log_level.clone(),
        }
    }

    fn get_file_logger_config(&self) -> FileLoggerConfig {
        FileLoggerConfig {
            level: self.file_log_level.clone(),
            dir: self.file_log_dir.clone(),
            file: None,
            size_mb: self.file_log_size,
            count: self.file_log_count,
        }
    }
}

#[cfg(target_os = "windows")]
fn win_service_set_work_dir(service_name: &std::ffi::OsString) -> anyhow::Result<()> {
    use crate::common::constants::WIN_SERVICE_WORK_DIR_REG_KEY;
    use winreg::{RegKey, enums::*};

    let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
    let key = hklm.open_subkey_with_flags(WIN_SERVICE_WORK_DIR_REG_KEY, KEY_READ)?;
    let dir_pat_str = key.get_value::<std::ffi::OsString, _>(service_name)?;
    let dir_path = std::fs::canonicalize(dir_pat_str)?;

    std::env::set_current_dir(dir_path)?;

    Ok(())
}

#[cfg(target_os = "windows")]
fn win_service_event_loop(
    stop_notify: std::sync::Arc<tokio::sync::Notify>,
    cli: Cli,
    status_handle: windows_service::service_control_handler::ServiceStatusHandle,
) {
    use std::time::Duration;
    use tokio::runtime::Runtime;
    use windows_service::service::*;

    let normal_status = ServiceStatus {
        service_type: ServiceType::OWN_PROCESS,
        current_state: ServiceState::Running,
        controls_accepted: ServiceControlAccept::STOP,
        exit_code: ServiceExitCode::Win32(0),
        checkpoint: 0,
        wait_hint: Duration::default(),
        process_id: None,
    };
    let error_status = ServiceStatus {
        service_type: ServiceType::OWN_PROCESS,
        current_state: ServiceState::Stopped,
        controls_accepted: ServiceControlAccept::empty(),
        exit_code: ServiceExitCode::ServiceSpecific(1u32),
        checkpoint: 0,
        wait_hint: Duration::default(),
        process_id: None,
    };

    std::thread::spawn(move || {
        let rt = Runtime::new().unwrap();
        rt.block_on(async move {
            tokio::select! {
                res = run_main(cli) => {
                    match res {
                        Ok(_) => {
                            status_handle.set_service_status(normal_status).unwrap();
                            std::process::exit(0);
                        }
                        Err(error) => {
                            status_handle.set_service_status(error_status).unwrap();
                            log::error!(?error);
                        }
                    }
                },
                _ = stop_notify.notified() => {
                    _ = status_handle.set_service_status(normal_status);
                    std::process::exit(0);
                }
            }
        });
    });
}

fn parse_cli() -> Cli {
    let mut cli = Cli::parse();
    // for --stun-servers="", we want vec![], but clap will give vec![""], hack for that
    if let Some(stun_servers) = &mut cli.network_options.stun_servers {
        stun_servers.retain(|s| !s.trim().is_empty());
    }
    if let Some(stun_servers_v6) = &mut cli.network_options.stun_servers_v6 {
        stun_servers_v6.retain(|s| !s.trim().is_empty());
    }
    cli
}

#[cfg(target_os = "windows")]
fn win_service_main(arg: Vec<std::ffi::OsString>) {
    use std::{sync::Arc, time::Duration};
    use tokio::sync::Notify;
    use windows_service::{service::*, service_control_handler::*};

    _ = win_service_set_work_dir(&arg[0]);

    let cli = parse_cli();

    let stop_notify_send = Arc::new(Notify::new());
    let stop_notify_recv = Arc::clone(&stop_notify_send);
    let event_handler = move |control_event| -> ServiceControlHandlerResult {
        match control_event {
            ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
            ServiceControl::Stop => {
                stop_notify_send.notify_one();
                ServiceControlHandlerResult::NoError
            }
            _ => ServiceControlHandlerResult::NotImplemented,
        }
    };
    let status_handle = register(String::new(), event_handler).expect("register service fail");
    let next_status = ServiceStatus {
        service_type: ServiceType::OWN_PROCESS,
        current_state: ServiceState::Running,
        controls_accepted: ServiceControlAccept::STOP,
        exit_code: ServiceExitCode::Win32(0),
        checkpoint: 0,
        wait_hint: Duration::default(),
        process_id: None,
    };
    status_handle
        .set_service_status(next_status)
        .expect("set service status fail");

    win_service_event_loop(stop_notify_recv, cli, status_handle);
}

async fn run_main(cli: Cli) -> anyhow::Result<()> {
    defer!(dump_profile(0););
    log::init(&cli.logging_options, true)?;

    let manager = Arc::new(NetworkInstanceManager::new().with_config_path(cli.config_dir.clone()));

    let _rpc_server = ApiRpcServer::new(
        cli.rpc_portal_options.rpc_portal,
        cli.rpc_portal_options.rpc_portal_whitelist,
        manager.clone(),
    )?
    .serve()
    .await?;

    let _web_client = if let Some(config_server_url_s) = cli.config_server.as_ref() {
        let wc = web_client::run_web_client(
            config_server_url_s,
            cli.machine_id.clone(),
            cli.network_options.hostname.clone(),
            cli.network_options.secure_mode.unwrap_or(false),
            manager.clone(),
            None,
        )
        .await
        .inspect(|_| {
            log::info!(
                server = config_server_url_s,
                "Web client started successfully...",
            );

            log::info!("Official config website: https://easytier.cn/web");
        })?;

        Some(wc)
    } else {
        None
    };

    let _daemon_guard = if cli.daemon {
        Some(manager.register_daemon())
    } else {
        None
    };

    let explicit_config_file_count = cli.config_file.as_ref().map_or(0, |files| files.len());
    let mut config_dir_file_count = 0;
    let mut config_files = if let Some(v) = cli.config_file {
        v.iter()
            .cloned()
            .map(|path| (path, ConfigSource::CliConfigFile))
            .collect()
    } else {
        vec![]
    };
    if let Some(config_dir) = cli.config_dir.as_ref() {
        if !config_dir.is_dir() {
            anyhow::bail!("config_dir {} is not a directory", config_dir.display());
        }

        for entry in std::fs::read_dir(config_dir)? {
            let entry = entry?;
            let path = entry.path();
            if !path.is_file() {
                continue;
            }
            let Some(ext) = path.extension() else {
                continue;
            };
            if ext != "toml" {
                continue;
            }
            config_dir_file_count += 1;
            config_files.push((path, ConfigSource::ConfigDir));
        }
    }
    let config_file_count = config_files.len();
    let mut crate_cli_network = {
        if cli.daemon {
            false
        } else if config_file_count == 0 && cli.config_server.is_none() {
            true
        } else {
            cli.network_options.network_name.is_some()
        }
    };
    for (config_file, source) in config_files {
        let (cfg, mut control) = load_config_from_file(
            &config_file,
            cli.config_dir.as_ref(),
            cli.disable_env_parsing,
        )
        .await?;

        if cli.network_options.can_merge(
            &cfg,
            source,
            explicit_config_file_count,
            config_dir_file_count,
        ) {
            cli.network_options
                .merge_into(&cfg)
                .with_context(|| format!("failed to merge config from cli: {:?}", config_file))?;
            crate_cli_network = false;
            control.set_read_only(true);
            control.set_no_delete(true);
        }

        log::info!(
            "\
            Starting easytier from config file {:?}({:?}) with config:\n\
            ############### TOML ###############\n\
            {}\n\
            -----------------------------------\n\
            ",
            config_file,
            control.permission,
            cfg.dump()
        );
        manager.run_network_instance(cfg, true, control)?;
    }

    if crate_cli_network {
        let cfg = TomlConfigLoader::default();
        cli.network_options
            .merge_into(&cfg)
            .with_context(|| "failed to create config from cli".to_string())?;
        log::info!(
            "\
            Starting easytier from cli with config:\n\
            ############### TOML ###############\n\
            {}\n\
            -----------------------------------\n\
            ",
            cfg.dump()
        );
        manager.run_network_instance(cfg, true, ConfigFileControl::STATIC_CONFIG)?;
    }

    #[cfg(unix)]
    let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
    #[cfg(unix)]
    let sigterm = sigterm.recv();
    #[cfg(not(unix))]
    let sigterm = std::future::pending::<()>();

    tokio::select! {
        _ = manager.wait() => {
            let infos = manager.collect_network_infos().await?;
            if infos
                .into_values()
                .filter_map(|info| info.error_msg).next().is_some() {
                return Err(anyhow::anyhow!("some instances stopped with errors"));
            }
        }
        _ = tokio::signal::ctrl_c() => {
            log::info!("ctrl-c received, exiting...");
        }

        _ = sigterm, if cfg!(unix) => {
            log::warn!("terminate signal received, exiting...");
        }
    }
    Ok(())
}

fn memory_monitor(_force_dump: Arc<AtomicBool>) {
    #[cfg(feature = "jemalloc-prof")]
    {
        let mut last_peak_size = 0;
        let e = epoch::mib().unwrap();
        let allocated_stats = stats::allocated::mib().unwrap();

        loop {
            e.advance().unwrap();
            let new_heap_size = allocated_stats.read().unwrap();

            log::debug!("heap size: {} bytes", new_heap_size);

            // dump every 75MB
            if (last_peak_size > 0
                && new_heap_size > last_peak_size
                && new_heap_size - last_peak_size > 10 * 1024 * 1024)
                || _force_dump.load(std::sync::atomic::Ordering::Relaxed)
            {
                log::debug!(
                    "heap size increased: {} bytes",
                    new_heap_size - last_peak_size,
                );
                dump_profile(new_heap_size);
                last_peak_size = new_heap_size;
                if _force_dump.load(std::sync::atomic::Ordering::Relaxed) {
                    // also dump whole jemalloc stats
                    use jemalloc_ctl::stats_print::stats_print;
                    let tmp_file = get_dump_profile_path(new_heap_size, "stats");
                    let mut file = std::fs::File::create(tmp_file).unwrap();
                    let _ = stats_print(&mut file, Default::default());
                    _force_dump.store(false, std::sync::atomic::Ordering::Relaxed);
                }
            }

            if last_peak_size == 0 {
                last_peak_size = new_heap_size;
            }

            std::thread::sleep(std::time::Duration::from_secs(5));
        }
    }
}

pub async fn main() -> ExitCode {
    let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"));
    rust_i18n::set_locale(&locale);
    setup_panic_handler();

    #[cfg(target_os = "windows")]
    match windows_service::service_dispatcher::start(String::new(), ffi_service_main) {
        Ok(_) => std::thread::park(),
        Err(e) => {
            let should_panic = if let windows_service::Error::Winapi(ref io_error) = e {
                io_error.raw_os_error() != Some(0x427) // ERROR_FAILED_SERVICE_CONTROLLER_CONNECT
            } else {
                true
            };

            if should_panic {
                panic!("SCM start an error: {}", e);
            }
        }
    };

    set_prof_active(true);
    // register a signal handler to set force dump when signal usr1 is received
    let force_dump = Arc::new(AtomicBool::new(false));
    #[cfg(all(feature = "jemalloc-prof", not(target_os = "windows")))]
    {
        let force_dump_clone = force_dump.clone();
        let mut sigusr1 =
            tokio::signal::unix::signal(tokio::signal::unix::SignalKind::user_defined1()).unwrap();
        tokio::task::spawn(async move {
            while sigusr1.recv().await.is_some() {
                force_dump_clone.store(true, std::sync::atomic::Ordering::Relaxed);
            }
        });
    }
    let _monitor = std::thread::spawn(move || memory_monitor(force_dump));

    let cli = parse_cli();

    if let Some(shell) = cli.gen_autocomplete {
        let mut cmd = Cli::command();
        if let Some(shell) = shell.to_shell() {
            crate::print_completions(shell, &mut cmd, "easytier-core");
        } else {
            // Handle Nushell
            crate::print_nushell_completions(&mut cmd, "easytier-core");
        }
        return ExitCode::SUCCESS;
    }

    // Verify configurations
    if cli.check_config {
        if let Err(error) = validate_config(&cli).await {
            log::error!(?error, "Config validation failed");
            return ExitCode::FAILURE;
        } else {
            return ExitCode::SUCCESS;
        }
    }

    let mut ret_code = 0;

    if let Err(error) = run_main(cli).await {
        log::error!(?error);
        ret_code = 1;
    }

    log::info!("Stopping easytier...");
    set_prof_active(false);

    ExitCode::from(ret_code)
}

async fn validate_config(cli: &Cli) -> anyhow::Result<()> {
    // Check if a config file is provided
    let config_files = cli
        .config_file
        .as_ref()
        .ok_or_else(|| anyhow::anyhow!("--config-file is required when using --check-config"))?;

    for config_file in config_files {
        if config_file == &PathBuf::from("-") {
            let mut stdin = String::new();
            _ = tokio::io::stdin().read_to_string(&mut stdin).await?;
            TomlConfigLoader::new_from_str(stdin.as_str())
                .with_context(|| "config source: stdin")?;
        } else {
            TomlConfigLoader::new(config_file)
                .with_context(|| format!("config source: {:?}", config_file))?;
        };
    }

    Ok(())
}
