use std::env::var_os;

use atty::{self, Stream};
use clap::{arg_enum, App, Arg};

use nonymous::core::Type;

macro_rules! value_t_default_exit {
    ($m:ident, $v:expr, $t:ty) => {{
        use ::clap::value_t;
        match ::clap::value_t!($m, $v, $t) {
            Err(::clap::Error {
                kind: ::clap::ErrorKind::ArgumentNotFound,
                ..
            }) => <$t as ::core::default::Default>::default(),
            Err(other) => other.exit(),
            Ok(other) => other,
        }
    }};
}

arg_enum! {
    #[derive(Debug)]
    #[allow(non_camel_case_types)]
    pub enum ColorOption {
        auto,
        never,
        always,
    }
}

impl ColorOption {
    pub fn should(&self) -> bool {
        match self {
            Self::never => return false,
            Self::always => return true,
            Self::auto => (),
        }

        if !atty::is(Stream::Stdout) {
            return false;
        }

        let missing_term_is_meaningful = cfg!(unix);

        if var_os("TERM").map_or(missing_term_is_meaningful, |x| x == "dumb") {
            return false;
        }

        // “preference” environment variables not yet enabled, because i’m not yet convinced that
        // either of these conventions is accepted widely enough to commit to permanent support

        #[cfg(any())]
        if var("CLICOLOR_FORCE").as_ref().map(|x| &**x).unwrap_or("0") != "0" {
            return true;
        }

        #[cfg(any())]
        if let Ok("0") = var("CLICOLOR").as_ref().map(|x| &**x) {
            return false;
        }

        #[cfg(any())]
        if var("NO_COLOR").is_ok() {
            return false;
        }

        return true;
    }
}

impl Default for ColorOption {
    fn default() -> Self {
        Self::auto
    }
}

#[rustfmt::skip]
pub fn app<'b>() -> App<'static, 'b> {
    App::new("bore")
        .version(env!("CARGO_PKG_VERSION"))
        .author(env!("CARGO_PKG_AUTHORS"))
        .about(r#"
Sends a DNS query and prints the response in zone format.

The default <type> is "PTR" if --reverse is given, or "A" if a <name>
is given, or "NS" otherwise. If no <host> is given, bore uses your
system's resolvers.

Note that your platform's native resolution probably supports non-DNS
sources like a hosts(5) file, whereas bore sends DNS queries only.
"#)
        .usage("bore [OPTIONS] [@host] [name] [type]")
        .help_message("Print this help")
        .version_message("Print version details")
        .arg(Arg::from_usage("--encode 'Write raw query to stdout'").display_order(0).conflicts_with_all(&modes_minus("encode")))
        .arg(Arg::from_usage("--load 'Read raw query from stdin'").display_order(1))
        .arg(Arg::from_usage("--dump 'Write raw response to stdout'").display_order(2))
        .arg(Arg::from_usage("--decode 'Read raw response from stdin'").display_order(3).conflicts_with_all(&modes_minus("decode")))
        .arg(Arg::from_usage("--show-resolvers 'Print resolver details'").display_order(4).conflicts_with_all(&modes_minus("show-resolvers")))
        .arg(Arg::from_usage("-x --reverse 'Make reverse DNS query (name required)'").display_order(5))
        .arg(Arg::from_usage("--host [host] 'Host to send query'").display_order(6))
        .arg(Arg::from_usage("--port [port] 'Port to send query'").display_order(7))
        .arg(Arg::from_usage("--timeout [seconds] 'Time to wait for each host'").display_order(8).default_value("5"))
        .arg(Arg::from_usage("--class [class] 'Question class'").display_order(9).default_value("IN"))
        .arg(Arg::from_usage("[color] --color [when] 'When to use colours or other text styles\n'")
            .display_order(10)
            .case_insensitive(true)
            .possible_values(&ColorOption::variants()))
        .arg(Arg::from_usage("--man 'Show the full manual page (more help)'"))
        .arg(Arg::from_usage("[@host] 'Host to send query (same as --host)'"))
        .arg(Arg::from_usage("[name] 'Question name [default: .]'"))
        .arg(Arg::from_usage("[type] 'Question type [see above]'"))
}

#[derive(Debug)]
pub enum Positional<'a> {
    Host(&'a str),
    Qname(&'a str),
    Qtype(&'a str),
}

impl<'a> From<&'a str> for Positional<'a> {
    fn from(arg: &'a str) -> Self {
        if arg.starts_with("@") {
            Self::Host(&arg[1..])
        } else if arg.contains(&['.', ':'][..]) {
            Self::Qname(arg)
        } else {
            Self::Qtype(arg)
        }
    }
}

#[derive(Debug)]
pub struct Request<'a> {
    pub host: Option<&'a str>,
    pub qname: Option<&'a str>,
    pub qtype: Option<Type>,
}

impl<'a> Request<'a> {
    pub fn new(
        positionals: impl IntoIterator<Item = Positional<'a>>,
        host_option: Option<&'a str>,
    ) -> eyre::Result<Self> {
        let mut result = Request {
            qname: None,
            qtype: None,
            host: host_option,
        };

        for positional in positionals {
            match positional {
                Positional::Host(host) => {
                    if host_option.is_some() {
                        eyre::bail!(
                            "positional argument ‘@host’ must not be given with option ‘--host’"
                        );
                    } else if result.host.replace(host).is_some() {
                        eyre::bail!(
                            r#"positional argument ‘@host’ must not be given more than once
       Hint: to query a name starting with ‘@’, escape it like ‘\@...’"#
                        );
                    }
                }
                Positional::Qname(qname) => {
                    if result.qname.replace(qname).is_some() {
                        eyre::bail!(
                            r#"positional argument ‘name’ must not be given more than once
       Hint: bore does not support multiple queries in a single command"#
                        );
                    }
                }
                Positional::Qtype(qtype) => {
                    let qtype = qtype.parse().map_err(|_| {
                        eyre::eyre!(
                            r#"unrecognised query type: {}
       Hint: to query a top-level domain, add a dot like ‘tld.’
       Hint: you can query types numerically, like ‘TYPE65534’"#,
                            qtype
                        )
                    })?;
                    if result.qtype.replace(qtype).is_some() {
                        eyre::bail!(
                            r#"positional argument ‘type’ must not be given more than once
       Hint: to query a top-level domain, add a dot like ‘tld.’"#
                        );
                    }
                }
            }
        }

        Ok(result)
    }
}

#[derive(Debug)]
pub struct Query<'a> {
    pub host: Option<&'a str>,
    pub qname: &'a str,
    pub qtype: Type,
}

impl<'a> Query<'a> {
    pub fn new(request: Request<'a>, reverse: bool) -> eyre::Result<Self> {
        let Request { host, qname, qtype } = request;

        if reverse && qname.is_none() {
            eyre::bail!(
                r#"option ‘-x’ (‘--reverse’) requires positional argument ‘name’
       Hint: add a positional argument with an IPv4 or IPv6 address"#
            );
        }

        let (qname, qtype) = if qname.is_none() && qtype.is_none() {
            (".", "NS".parse()?)
        } else if reverse {
            (qname.unwrap_or("."), qtype.unwrap_or("PTR".parse()?))
        } else {
            (qname.unwrap_or("."), qtype.unwrap_or("A".parse()?))
        };

        Ok(Query { host, qname, qtype })
    }
}

fn modes_minus(name: &'static str) -> Vec<&'static str> {
    return ["encode", "load", "dump", "decode", "show-resolvers"]
        .iter()
        .by_ref()
        .map(|&x| x)
        .filter(|&x| x != name)
        .collect();
}

#[cfg(test)]
mod test {
    use assert_matches::assert_matches;

    use super::Positional;
    use super::Query;
    use super::Request;

    #[rustfmt::skip]
    #[test]
    fn positionals() {
        assert_matches!(Positional::from("@"), Positional::Host(""));
        assert_matches!(Positional::from("@."), Positional::Host("."));
        assert_matches!(Positional::from("@ai"), Positional::Host("ai"));
        assert_matches!(Positional::from(""), Positional::Qtype(""));
        assert_matches!(Positional::from("soa"), Positional::Qtype("soa"));
        assert_matches!(Positional::from("mx"), Positional::Qtype("mx")); // (!)
        assert_matches!(Positional::from("ai"), Positional::Qtype("ai")); // (!)
        assert_matches!(Positional::from("soa."), Positional::Qname("soa."));
        assert_matches!(Positional::from("mx."), Positional::Qname("mx."));
        assert_matches!(Positional::from("ai."), Positional::Qname("ai."));
    }

    #[rustfmt::skip]
    #[test]
    fn request() {
        assert_matches!(Request::new(["@.", "@."].iter().map(|&x| x.into()), None), // bore @. @.
                        Err(x) if x.to_string().starts_with("positional argument ‘@host’ must not be given more than once\n"));
        assert_matches!(Request::new([".", "."].iter().map(|&x| x.into()), None), // bore . .
                        Err(x) if x.to_string().starts_with("positional argument ‘name’ must not be given more than once\n"));
        assert_matches!(Request::new(["mx", "md"].iter().map(|&x| x.into()), None), // bore mx md
                        Err(x) if x.to_string().starts_with("positional argument ‘type’ must not be given more than once\n"));
        assert_matches!(Request::new(["ai"].iter().map(|&x| x.into()), None), // bore ai
                        Err(x) if x.to_string().starts_with("unrecognised query type: ai\n"));
        assert_matches!(Request::new(["@."].iter().map(|&x| x.into()), Some(".")), // bore @.
                        Err(x) if x.to_string() == "positional argument ‘@host’ must not be given with option ‘--host’");
    }

    #[rustfmt::skip]
    #[test]
    fn query() -> eyre::Result<()> {
        assert_matches!(Query::new(Request::new(["@."].iter().map(|&x| x.into()), None)?, true), // bore -x @.
                        Err(x) if x.to_string().starts_with("option ‘-x’ (‘--reverse’) requires positional argument ‘name’\n"));
        assert_matches!(Query::new(Request::new(["ptr"].iter().map(|&x| x.into()), None)?, true), // bore -x ptr
                        Err(x) if x.to_string().starts_with("option ‘-x’ (‘--reverse’) requires positional argument ‘name’\n"));

        Ok(())
    }
}
