use crate::{Error, Result};
use socks5_impl::protocol::UserKey;
use tproxy_config::IpCidr;

#[cfg(target_os = "linux")]
use std::ffi::OsString;

use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
use std::str::FromStr;

#[macro_export]
macro_rules! version_info {
    () => {
        concat!(env!("CARGO_PKG_VERSION"), " (", env!("GIT_HASH"), " ", env!("BUILD_TIME"), ")")
    };
}

fn about_info() -> &'static str {
    concat!("Tunnel interface to proxy.\nVersion: ", version_info!())
}

#[derive(Debug, Clone, clap::Parser, serde::Serialize, serde::Deserialize)]
#[command(author, version = version_info!(), about = about_info(), long_about = None)]
pub struct Args {
    /// Proxy URL in the form proto://[username[:password]@]host:port,
    /// where proto is one of socks4, socks5, http.
    /// Username and password are encoded in percent encoding. For example:
    /// socks5://myname:pass%40word@127.0.0.1:1080
    #[arg(short, long, value_parser = |s: &str| ArgProxy::try_from(s), value_name = "URL")]
    pub proxy: ArgProxy,

    /// Name of the tun interface, such as tun0, utun4, etc.
    /// If this option is not provided, the OS will generate a random one.
    #[arg(short, long, value_name = "name", value_parser = validate_tun)]
    #[cfg_attr(unix, arg(conflicts_with = "tun_fd"))]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tun: Option<String>,

    /// File descriptor of the tun interface
    #[cfg(unix)]
    #[arg(long, value_name = "fd", conflicts_with = "tun")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tun_fd: Option<i32>,

    /// Set whether to close the received raw file descriptor on drop or not.
    /// This setting is dependent on [tun_fd].
    #[cfg(unix)]
    #[arg(long, value_name = "true or false", conflicts_with = "tun", requires = "tun_fd")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub close_fd_on_drop: Option<bool>,

    /// Create a tun interface in a newly created unprivileged namespace
    /// while maintaining proxy connectivity via the global network namespace.
    #[cfg(target_os = "linux")]
    #[arg(long)]
    pub unshare: bool,

    /// Create a pidfile of `unshare` process when using `--unshare`.
    #[cfg(target_os = "linux")]
    #[arg(long)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub unshare_pidfile: Option<String>,

    /// File descriptor for UNIX datagram socket meant to transfer
    /// network sockets from global namespace to the new one.
    /// See `unshare(1)`, `namespaces(7)`, `sendmsg(2)`, `unix(7)`.
    #[cfg(target_os = "linux")]
    #[arg(long, value_name = "fd", hide(true))]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub socket_transfer_fd: Option<i32>,

    /// Specify a command to run with root-like capabilities in the new namespace
    /// when using `--unshare`.
    /// This could be useful to start additional daemons, e.g. `openvpn` instance.
    #[cfg(target_os = "linux")]
    #[arg(requires = "unshare")]
    pub admin_command: Vec<OsString>,

    /// IPv6 enabled
    #[arg(short = '6', long)]
    pub ipv6_enabled: bool,

    /// Routing and system setup, which decides whether to setup the routing and system configuration.
    /// This option requires root-like privileges on every platform.
    /// It is very important on Linux, see `capabilities(7)`.
    #[arg(short, long)]
    pub setup: bool,

    /// DNS handling strategy
    #[arg(short, long, value_name = "strategy", value_enum, default_value = "direct")]
    pub dns: ArgDns,

    /// DNS resolver address
    #[arg(long, value_name = "IP", default_value = "8.8.8.8")]
    pub dns_addr: IpAddr,

    /// IP address pool to be used by virtual DNS in CIDR notation.
    #[arg(long, value_name = "CIDR", default_value = "198.18.0.0/15")]
    pub virtual_dns_pool: IpCidr,

    /// IPs used in routing setup which should bypass the tunnel,
    /// in the form of IP or IP/CIDR. Multiple IPs can be specified,
    /// e.g. --bypass 3.4.5.0/24 --bypass 5.6.7.8
    #[arg(short, long, value_name = "IP/CIDR")]
    pub bypass: Vec<IpCidr>,

    /// TCP timeout in seconds
    #[arg(long, value_name = "seconds", default_value = "600")]
    pub tcp_timeout: u64,

    /// UDP timeout in seconds
    #[arg(long, value_name = "seconds", default_value = "10")]
    pub udp_timeout: u64,

    /// Verbosity level
    #[arg(short, long, value_name = "level", value_enum, default_value = "info")]
    pub verbosity: ArgVerbosity,

    /// Daemonize for unix family or run as Windows service
    #[arg(long)]
    pub daemonize: bool,

    /// Exit immediately when fatal error occurs, useful for running as a service
    #[arg(long)]
    pub exit_on_fatal_error: bool,

    /// Maximum number of sessions to be handled concurrently
    #[arg(long, value_name = "number", default_value = "200")]
    pub max_sessions: usize,

    /// UDP gateway server address, forwards UDP packets via specified TCP server
    #[cfg(feature = "udpgw")]
    #[arg(long, value_name = "IP:PORT")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub udpgw_server: Option<SocketAddr>,

    /// Max connections for the UDP gateway, default value is 5
    #[cfg(feature = "udpgw")]
    #[arg(long, value_name = "number", requires = "udpgw_server")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub udpgw_connections: Option<usize>,

    /// Keepalive interval in seconds for the UDP gateway, default value is 30
    #[cfg(feature = "udpgw")]
    #[arg(long, value_name = "seconds", requires = "udpgw_server")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub udpgw_keepalive: Option<u64>,
}

fn validate_tun(p: &str) -> Result<String> {
    #[cfg(target_os = "macos")]
    if p.len() <= 4 || &p[..4] != "utun" {
        return Err(Error::from("Invalid tun interface name, please use utunX"));
    }
    Ok(p.to_string())
}

impl Default for Args {
    fn default() -> Self {
        #[cfg(target_os = "linux")]
        let setup = false;
        #[cfg(not(target_os = "linux"))]
        let setup = true;
        Args {
            proxy: ArgProxy::default(),
            tun: None,
            #[cfg(unix)]
            tun_fd: None,
            #[cfg(unix)]
            close_fd_on_drop: None,
            #[cfg(target_os = "linux")]
            unshare: false,
            #[cfg(target_os = "linux")]
            unshare_pidfile: None,
            #[cfg(target_os = "linux")]
            socket_transfer_fd: None,
            #[cfg(target_os = "linux")]
            admin_command: Vec::new(),
            ipv6_enabled: false,
            setup,
            dns: ArgDns::default(),
            dns_addr: "8.8.8.8".parse().unwrap(),
            bypass: vec![],
            tcp_timeout: 600,
            udp_timeout: 10,
            verbosity: ArgVerbosity::Info,
            virtual_dns_pool: IpCidr::from_str("198.18.0.0/15").unwrap(),
            daemonize: false,
            exit_on_fatal_error: false,
            max_sessions: 200,
            #[cfg(feature = "udpgw")]
            udpgw_server: None,
            #[cfg(feature = "udpgw")]
            udpgw_connections: None,
            #[cfg(feature = "udpgw")]
            udpgw_keepalive: None,
        }
    }
}

impl Args {
    #[allow(clippy::let_and_return)]
    pub fn parse_args() -> Self {
        let args = <Self as ::clap::Parser>::parse();
        #[cfg(target_os = "linux")]
        if !args.setup && args.tun.is_none() {
            eprintln!("Missing required argument, '--tun' must present when '--setup' is not used.");
            std::process::exit(-1);
        }
        args
    }

    pub fn proxy(&mut self, proxy: ArgProxy) -> &mut Self {
        self.proxy = proxy;
        self
    }

    pub fn dns(&mut self, dns: ArgDns) -> &mut Self {
        self.dns = dns;
        self
    }

    #[cfg(feature = "udpgw")]
    pub fn udpgw_server(&mut self, udpgw: SocketAddr) -> &mut Self {
        self.udpgw_server = Some(udpgw);
        self
    }

    #[cfg(feature = "udpgw")]
    pub fn udpgw_connections(&mut self, udpgw_connections: usize) -> &mut Self {
        self.udpgw_connections = Some(udpgw_connections);
        self
    }

    #[cfg(unix)]
    pub fn tun_fd(&mut self, tun_fd: Option<i32>) -> &mut Self {
        self.tun_fd = tun_fd;
        self
    }

    #[cfg(unix)]
    pub fn close_fd_on_drop(&mut self, close_fd_on_drop: bool) -> &mut Self {
        self.close_fd_on_drop = Some(close_fd_on_drop);
        self
    }

    pub fn verbosity(&mut self, verbosity: ArgVerbosity) -> &mut Self {
        self.verbosity = verbosity;
        self
    }

    pub fn tun(&mut self, tun: String) -> &mut Self {
        self.tun = Some(tun);
        self
    }

    pub fn dns_addr(&mut self, dns_addr: IpAddr) -> &mut Self {
        self.dns_addr = dns_addr;
        self
    }

    pub fn bypass(&mut self, bypass: IpCidr) -> &mut Self {
        self.bypass.push(bypass);
        self
    }

    pub fn ipv6_enabled(&mut self, ipv6_enabled: bool) -> &mut Self {
        self.ipv6_enabled = ipv6_enabled;
        self
    }

    pub fn setup(&mut self, setup: bool) -> &mut Self {
        self.setup = setup;
        self
    }
}

#[repr(C)]
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum, serde::Serialize, serde::Deserialize)]
pub enum ArgVerbosity {
    Off = 0,
    Error,
    Warn,
    #[default]
    Info,
    Debug,
    Trace,
}

#[cfg(target_os = "android")]
impl TryFrom<jni::sys::jint> for ArgVerbosity {
    type Error = Error;
    fn try_from(value: jni::sys::jint) -> Result<Self> {
        match value {
            0 => Ok(ArgVerbosity::Off),
            1 => Ok(ArgVerbosity::Error),
            2 => Ok(ArgVerbosity::Warn),
            3 => Ok(ArgVerbosity::Info),
            4 => Ok(ArgVerbosity::Debug),
            5 => Ok(ArgVerbosity::Trace),
            _ => Err(Error::from("Invalid verbosity level")),
        }
    }
}

impl From<ArgVerbosity> for log::LevelFilter {
    fn from(verbosity: ArgVerbosity) -> Self {
        match verbosity {
            ArgVerbosity::Off => log::LevelFilter::Off,
            ArgVerbosity::Error => log::LevelFilter::Error,
            ArgVerbosity::Warn => log::LevelFilter::Warn,
            ArgVerbosity::Info => log::LevelFilter::Info,
            ArgVerbosity::Debug => log::LevelFilter::Debug,
            ArgVerbosity::Trace => log::LevelFilter::Trace,
        }
    }
}

impl From<log::Level> for ArgVerbosity {
    fn from(level: log::Level) -> Self {
        match level {
            log::Level::Error => ArgVerbosity::Error,
            log::Level::Warn => ArgVerbosity::Warn,
            log::Level::Info => ArgVerbosity::Info,
            log::Level::Debug => ArgVerbosity::Debug,
            log::Level::Trace => ArgVerbosity::Trace,
        }
    }
}

impl std::fmt::Display for ArgVerbosity {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            ArgVerbosity::Off => write!(f, "off"),
            ArgVerbosity::Error => write!(f, "error"),
            ArgVerbosity::Warn => write!(f, "warn"),
            ArgVerbosity::Info => write!(f, "info"),
            ArgVerbosity::Debug => write!(f, "debug"),
            ArgVerbosity::Trace => write!(f, "trace"),
        }
    }
}

/// DNS query handling strategy
/// - Virtual: Use a virtual DNS server to handle DNS queries, also known as Fake-IP mode
/// - OverTcp: Use TCP to send DNS queries to the DNS server
/// - Direct: Do not handle DNS by relying on DNS server bypassing
#[repr(C)]
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum, serde::Serialize, serde::Deserialize)]
pub enum ArgDns {
    Virtual = 0,
    OverTcp,
    #[default]
    Direct,
}

#[cfg(target_os = "android")]
impl TryFrom<jni::sys::jint> for ArgDns {
    type Error = Error;
    fn try_from(value: jni::sys::jint) -> Result<Self> {
        match value {
            0 => Ok(ArgDns::Virtual),
            1 => Ok(ArgDns::OverTcp),
            2 => Ok(ArgDns::Direct),
            _ => Err(Error::from("Invalid DNS strategy")),
        }
    }
}

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct ArgProxy {
    pub proxy_type: ProxyType,
    pub addr: SocketAddr,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub credentials: Option<UserKey>,
}

impl Default for ArgProxy {
    fn default() -> Self {
        ArgProxy {
            proxy_type: ProxyType::Socks5,
            addr: "127.0.0.1:1080".parse().unwrap(),
            credentials: None,
        }
    }
}

impl std::fmt::Display for ArgProxy {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let auth = match &self.credentials {
            Some(creds) => format!("{creds}"),
            None => "".to_owned(),
        };
        if auth.is_empty() {
            write!(f, "{}://{}", &self.proxy_type, &self.addr)
        } else {
            write!(f, "{}://{}@{}", &self.proxy_type, auth, &self.addr)
        }
    }
}

impl TryFrom<&str> for ArgProxy {
    type Error = Error;
    fn try_from(s: &str) -> Result<Self> {
        if s == "none" {
            return Ok(ArgProxy {
                proxy_type: ProxyType::None,
                addr: "0.0.0.0:0".parse().unwrap(),
                credentials: None,
            });
        }

        let e = format!("`{s}` is not a valid proxy URL");
        let url = url::Url::parse(s).map_err(|_| Error::from(&e))?;
        let e = format!("`{s}` does not contain a host");
        let host = url.host_str().ok_or(Error::from(e))?;

        let e = format!("`{s}` does not contain a port");
        let port = url.port_or_known_default().ok_or(Error::from(&e))?;

        let e2 = format!("`{host}` does not resolve to a usable IP address");
        let addr = (host, port).to_socket_addrs()?.next().ok_or(Error::from(&e2))?;

        let credentials = if url.username() == "" && url.password().is_none() {
            None
        } else {
            use percent_encoding::percent_decode;
            let username = percent_decode(url.username().as_bytes()).decode_utf8()?;
            let password = percent_decode(url.password().unwrap_or("").as_bytes()).decode_utf8()?;
            Some(UserKey::new(username, password))
        };

        let proxy_type = url.scheme().to_ascii_lowercase().as_str().try_into()?;

        Ok(ArgProxy {
            proxy_type,
            addr,
            credentials,
        })
    }
}

#[repr(C)]
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Default, serde::Serialize, serde::Deserialize)]
pub enum ProxyType {
    Http = 0,
    Socks4,
    #[default]
    Socks5,
    None,
}

impl TryFrom<&str> for ProxyType {
    type Error = Error;
    fn try_from(value: &str) -> Result<Self> {
        match value {
            "http" => Ok(ProxyType::Http),
            "socks4" => Ok(ProxyType::Socks4),
            "socks5" => Ok(ProxyType::Socks5),
            "none" => Ok(ProxyType::None),
            scheme => Err(Error::from(&format!("`{scheme}` is an invalid proxy type"))),
        }
    }
}

impl std::fmt::Display for ProxyType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ProxyType::Socks4 => write!(f, "socks4"),
            ProxyType::Socks5 => write!(f, "socks5"),
            ProxyType::Http => write!(f, "http"),
            ProxyType::None => write!(f, "none"),
        }
    }
}
