use std::{
    fs,
    net::{IpAddr, Ipv4Addr, SocketAddr},
    path::PathBuf,
    time::Duration,
};

use clap::Parser;
use serde::Deserialize;
use toml;

use crate::{VERSION, error::GatewayError};

fn default_log_level() -> String {
    String::from("info")
}

fn default_grpc_port() -> u16 {
    50066
}

fn default_stats_period() -> u64 {
    30
}

fn default_ifname() -> String {
    String::from("wg0")
}

fn default_syslog_facility() -> String {
    String::from("LOG_USER")
}

fn default_adoption_timeout() -> u64 {
    10
}

fn default_syslog_socket() -> PathBuf {
    PathBuf::from("/var/run/log")
}

fn default_cert_dir() -> PathBuf {
    PathBuf::from("/etc/defguard/certs")
}

#[derive(Debug, Parser, Clone, Deserialize)]
#[clap(about = "Defguard VPN gateway service")]
#[command(version = VERSION)]
pub struct Config {
    #[arg(long, short = 'l', env = "DEFGUARD_LOG_LEVEL", default_value = "info")]
    #[serde(default = "default_log_level")]
    pub log_level: String,

    /// Gateway gRPC server port.
    #[arg(long, env = "DEFGUARD_GRPC_PORT", default_value = "50066")]
    #[serde(default = "default_grpc_port")]
    pub(crate) grpc_port: u16,

    /// Use userspace WireGuard implementation e.g. wireguard-go
    #[arg(long, short = 'u', env = "DEFGUARD_USERSPACE")]
    #[serde(default)]
    pub userspace: bool,

    /// Defines how often (in seconds) interface statistics are sent to Defguard Core.
    #[arg(long, short = 'p', env = "DEFGUARD_STATS_PERIOD", default_value = "30")]
    #[serde(default = "default_stats_period")]
    pub stats_period: u64,

    /// Network interface name (e.g. wg0)
    #[arg(long, short = 'i', env = "DEFGUARD_IFNAME", default_value = "wg0")]
    #[serde(default = "default_ifname")]
    pub ifname: String,

    /// Write process ID (PID) to this file
    #[arg(long)]
    pub pidfile: Option<PathBuf>,

    /// Log to syslog
    #[arg(long, short = 's')]
    #[serde(default)]
    pub use_syslog: bool,

    /// Syslog facility
    #[arg(long, default_value = "LOG_USER")]
    #[serde(default = "default_syslog_facility")]
    pub syslog_facility: String,

    /// Syslog socket path
    #[arg(long, default_value = "/var/run/log")]
    #[serde(default = "default_syslog_socket")]
    pub syslog_socket: PathBuf,

    /// Configuration file path
    #[arg(long = "config", short)]
    #[serde(skip)]
    config_path: Option<PathBuf>,

    /// Command to run before bringing up the interface.
    #[arg(long, env = "PRE_UP")]
    pub pre_up: Option<String>,

    /// Command to run after bringing up the interface.
    #[arg(long, env = "POST_UP")]
    pub post_up: Option<String>,

    /// Command to run before bringing down the interface.
    #[arg(long, env = "PRE_DOWN")]
    pub pre_down: Option<String>,

    /// Command to run after bringing down the interface.
    #[arg(long, env = "POST_DOWN")]
    pub post_down: Option<String>,
    /// HTTP port that will expose the REST Gateway health status endpoint.
    /// 200: Gateway is working and is connected to Core
    /// 503: Gateway is working, but is not connected to Core
    #[arg(long, env = "HEALTH_PORT")]
    pub health_port: Option<u16>,

    /// Whether the firewall should automatically apply masquerading
    #[arg(long, env = "DEFGUARD_MASQUERADE")]
    #[serde(default)]
    pub masquerade: bool,

    #[arg(long, env = "DEFGUARD_FW_PRIORITY")]
    #[serde(default)]
    pub fw_priority: Option<i32>,

    /// Whether all firewall management should be disabled
    /// Meant to be used as a workaround for incompatible hardware
    #[arg(long, env = "DEFGUARD_DISABLE_FW_MGMT")]
    #[serde(default)]
    pub disable_firewall_management: bool,

    #[arg(long, env = "DEFGUARD_HTTP_BIND_ADDRESS")]
    pub http_bind_address: Option<IpAddr>,

    #[arg(
        long,
        env = "DEFGUARD_GATEWAY_CERT_DIR",
        default_value = "/etc/defguard/certs"
    )]
    #[serde(default = "default_cert_dir")]
    pub cert_dir: PathBuf,

    /// Time limit in minutes for the auto-adoption process.
    /// After this time, gateway will reject adoption attempts until restarted.
    #[arg(
        long,
        short = 't',
        env = "DEFGUARD_ADOPTION_TIMEOUT",
        default_value = "10"
    )]
    #[serde(default = "default_adoption_timeout")]
    pub adoption_timeout: u64,

    /// On quit, clean the network interface and VPN configurations.
    #[arg(long, env = "DEFGUARD_CLEAN_ON_QUIT")]
    #[serde(default)]
    pub clean_on_quit: bool,
}

impl Config {
    #[must_use]
    pub fn stats_period(&self) -> Duration {
        Duration::from_secs(self.stats_period)
    }

    #[must_use]
    pub fn adoption_timeout(&self) -> Duration {
        Duration::from_secs(self.adoption_timeout * 60)
    }

    /// Return [`SocketAddr`] for gRPC to listen on.
    #[must_use]
    pub(crate) fn grpc_socket(&self) -> SocketAddr {
        SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), self.grpc_port)
    }
}

impl Default for Config {
    fn default() -> Self {
        Self {
            log_level: "info".into(),
            grpc_port: 50066,
            userspace: false,
            stats_period: 15,
            ifname: "wg0".into(),
            pidfile: None,
            use_syslog: false,
            syslog_facility: String::new(),
            syslog_socket: PathBuf::new(),
            config_path: None,
            pre_up: None,
            post_up: None,
            pre_down: None,
            post_down: None,
            health_port: None,
            masquerade: false,
            fw_priority: None,
            disable_firewall_management: false,
            http_bind_address: None,
            cert_dir: PathBuf::from("/etc/defguard/certs"),
            adoption_timeout: 5,
            clean_on_quit: false,
        }
    }
}

pub fn get_config() -> Result<Config, GatewayError> {
    // parse CLI arguments to get config file path
    let cli_config = Config::parse();

    // load config from file if one was specified
    if let Some(config_path) = cli_config.config_path {
        let config_toml = fs::read_to_string(config_path)
            .map_err(|err| GatewayError::InvalidConfigFile(err.to_string()))?;
        let file_config = toml::from_str(&config_toml)
            .map_err(|err| GatewayError::InvalidConfigFile(err.message().to_string()))?;
        return Ok(file_config);
    }

    Ok(cli_config)
}

#[test]
fn verify_cli() {
    use clap::CommandFactory;
    Config::command().debug_assert();
}
