use std::fs::File;
use std::io::{self, BufRead, BufReader};
use std::iter::empty;
use std::net::{SocketAddr, ToSocketAddrs};
use std::num::NonZeroU16;

use eyre::WrapErr;

use self::ResolversError::*;
use super::{ChoosePort, PortChooser, Resolvers};

error!(ResolversError);
#[derive(Debug, displaydoc::Display)]
#[prefix_enum_doc_attributes]
/// failed to gather default resolvers
enum ResolversError {
    /// error opening /etc/resolv.conf
    OpenResolvConf,
    /// error reading from /etc/resolv.conf
    ReadResolvConf,
    /// malformed nameserver line
    NameserverLine,
    /// error converting to SocketAddr
    SocketAddrs,
}

pub(crate) struct UnixResolvers {
    port: NonZeroU16,
    lines: Box<dyn Iterator<Item = io::Result<String>>>,
    addresses: Box<dyn Iterator<Item = SocketAddr>>,
}

impl UnixResolvers {
    fn with_source(port: PortChooser<NonZeroU16>, source: impl BufRead + 'static) -> Self {
        let lines = Box::new(source.lines());
        let addresses = Box::new(empty());

        Self {
            port: port.choose(None),
            lines,
            addresses,
        }
    }
}

impl Resolvers for UnixResolvers {
    fn new(port: PortChooser<NonZeroU16>) -> eyre::Result<Self> {
        let source = File::open("/etc/resolv.conf").wrap_err_with(|| OpenResolvConf)?;

        Ok(UnixResolvers::with_source(port, BufReader::new(source)))
    }
}

impl Iterator for UnixResolvers {
    type Item = eyre::Result<SocketAddr>;

    fn next(&mut self) -> Option<Self::Item> {
        while let Some(address) = self.addresses.next() {
            return Some(Ok(address));
        }

        while let Some(result) = self.lines.next() {
            let line = itry!(result, ReadResolvConf);
            let line = line
                .split(|c| c == '#' || c == ';')
                .next()
                .expect("str::split never returns an empty iterator");
            let mut words = line.trim().split_whitespace();

            if words.next() == Some("nameserver") {
                match words.next() {
                    None => return Some(Err(NameserverLine.into())),
                    Some(host) => {
                        let result = (host, self.port.into()).to_socket_addrs();
                        self.addresses = Box::new(itry!(result, SocketAddrs));
                        return self.next();
                    }
                }
            }
        }

        None
    }
}

pub(crate) fn resolvers(port: PortChooser<NonZeroU16>) -> eyre::Result<impl Resolvers> {
    UnixResolvers::new(port)
}

#[cfg(test)]
mod test {
    use std::io::Cursor;
    use std::net::{Ipv6Addr, SocketAddr, SocketAddrV6};
    use std::num::NonZeroU16;
    use std::str::FromStr;

    use assert_matches::assert_matches;

    use crate::platform::PortChooser;

    use super::UnixResolvers;

    #[test]
    fn port() -> eyre::Result<()> {
        let override_port = NonZeroU16::new(5353);
        let default_port = NonZeroU16::new(53).expect("is not zero");
        let port = PortChooser::new(override_port, default_port);
        assert_matches!(UnixResolvers::with_source(port, Cursor::new("nameserver 127.0.0.1")).next(),
                        Some(Ok(x)) if x == SocketAddr::from_str("127.0.0.1:5353")?);

        Ok(())
    }

    #[test]
    fn comment() -> eyre::Result<()> {
        let default_port = NonZeroU16::new(53).expect("is not zero");
        let port = PortChooser::new(None, default_port);
        let mut resolvers = UnixResolvers::with_source(
            port,
            Cursor::new(
                "nameserver 192.0.2.13#
                 nameserver 192.0.2.42;
                 nameserver 192.0.2.69",
            ),
        );

        assert_matches!(resolvers.next(), Some(Ok(x)) if x == SocketAddr::from_str("192.0.2.13:53")?);
        assert_matches!(resolvers.next(), Some(Ok(x)) if x == SocketAddr::from_str("192.0.2.42:53")?);
        assert_matches!(resolvers.next(), Some(Ok(x)) if x == SocketAddr::from_str("192.0.2.69:53")?);

        Ok(())
    }

    #[test]
    fn zone() -> eyre::Result<()> {
        let default_port = NonZeroU16::new(53).expect("is not zero");
        let port = PortChooser::new(None, default_port);
        let mut resolvers = UnixResolvers::with_source(port, Cursor::new("nameserver fe80::%13"));
        let expected = SocketAddrV6::new(Ipv6Addr::from_str("fe80::")?, 53, 0, 13);

        assert_matches!(resolvers.next(), Some(Ok(SocketAddr::V6(x))) if x == expected);

        Ok(())
    }
}
