#![deny(unsafe_code)]
#![cfg_attr(ci_, deny(warnings))]

#[allow(unused_macros)]
macro_rules! error {
    ($name:ident $(($source:ident($x:expr) $(, $other:ident)*))? $(, $variant:ident)*) => {
        impl ::std::error::Error for $name {
            fn source(&self) -> Option<&(dyn ::std::error::Error + 'static)> {
                match self {
                    $($name($source $(, $other)*) => Some($x),)?
                    $($name::$variant(s) => Some(s),)*

                    #[allow(unreachable_patterns)]
                    _ => None,
                }
            }
        }
    };
}

#[macro_use]
mod cli;
#[macro_use]
mod highlight;
pub mod platform;

use std::convert::TryInto;
use std::fmt;
use std::io::{stderr, stdin, stdout, Read, Write};
use std::iter::once;
use std::net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs};
use std::num::NonZeroU16;
use std::str::FromStr;
use std::time::Duration;

use pledge::{pledge, pledge_promises};
use rand::prelude::*;
use tokio::{net::UdpSocket, time};

use nonymous::emit::message::MessageBuilder;
use nonymous::emit::name::{NameBuilder, NameError};
use nonymous::emit::{Buffer, Builder, NewBuilder};
use nonymous::view::{Message, View};
use nonymous::zone::{Plain, Position, Pretty, QuestionPosition, RecordPosition};

use crate::cli::{ColorOption, Query, Request};
use crate::platform::{
    enable_virtual_terminal_processing, expect_pledge, expect_unveil, sigpipe_default, spawn_man,
    ChoosePort, PortChooser,
};

#[tokio::main]
async fn main() {
    if let Err(error) = bore().await {
        eprintln!("Fatal error: {:?}", error);
        std::process::exit(1);
    }
}

fn pretty(w: &mut dyn fmt::Write, p: &mut Position, s: &str) -> fmt::Result {
    match p {
        Position {
            question: Some(QuestionPosition::Qname),
            ..
        } => hwrite!(w, true, Bold, "{}", s),
        Position {
            question: Some(QuestionPosition::Qtype),
            ..
        } => hwrite!(w, true, Accent, "{}", s),
        Position {
            record: Some(RecordPosition::Rdata),
            ..
        } => hwrite!(w, true, Bold, "{}", s),
        Position {
            record: Some(RecordPosition::Type),
            ..
        } => hwrite!(w, true, Accent, "{}", s),
        _ => hwrite!(w, true, Dimmed, "{}", s),
    }
}

fn pretty_print(color: bool, source: &[u8]) -> eyre::Result<()> {
    let (message, rest) = Message::view(source, 0..source.len())?;
    if color {
        println!("{}", Pretty(&message, &pretty));
    } else {
        println!("{}", Plain(&message));
    }

    if rest.len() > 0 {
        hprintln!(color, Dimmed, ";; {} octets after message", rest.len());
        println!();
    }

    Ok(())
}

async fn bore() -> eyre::Result<()> {
    sigpipe_default();
    enable_virtual_terminal_processing();

    let arguments = self::cli::app().get_matches();

    if arguments.is_present("man") {
        if !spawn_man().await {
            println!("{}", include_str!("../doc/bore.1.txt"));
        }

        return Ok(());
    }

    expect_unveil("/etc/resolv.conf", "r");

    // empty execpromises
    expect_pledge(pledge![Stdio Inet Dns Rpath,]);

    let color = value_t_default_exit!(arguments, "color", ColorOption).should();
    let override_port = arguments.value_of("port").map(|x| x.parse()).transpose()?;
    let default_port = NonZeroU16::new(53).expect("is not zero");
    let port = PortChooser::new(override_port, default_port);

    let host_option = arguments.value_of("host");
    let reverse = arguments.is_present("reverse");
    let first = arguments.value_of("name");
    let second = arguments.value_of("type");
    let third = arguments.value_of("@host");
    let positionals = [first, second, third];
    let positionals = positionals.iter().filter_map(|x| x.map(|x| x.into()));
    let request = Request::new(positionals, host_option)?;
    let params = Query::new(request, reverse)?;
    let qclass = arguments.value_of("class").expect("has Arg::default_value");

    let mut query = vec![];

    if arguments.is_present("decode") {
        expect_pledge(pledge_promises![Stdio]);

        let mut source = vec![];
        stdin().lock().read_to_end(&mut source)?;
        pretty_print(color, &source)?;
        return Ok(());
    }

    let remotes = if let Some(host) = host_option {
        expect_pledge(pledge_promises![Stdio Inet Dns]);

        custom_remotes(port, host)
    } else {
        expect_pledge(pledge_promises![Stdio Inet Rpath]);

        default_remotes(port)
    }?;

    if arguments.is_present("show-resolvers") {
        expect_pledge(pledge_promises![Stdio Rpath]);

        use self::platform::Resolvers;
        self::platform::resolvers(port)?.show(&mut stdout())?;

        print!("effective resolver order (");
        if let Some(port) = override_port {
            print!("--port {}", port);
        } else {
            print!("no --port");
        }
        println!("):");
        for remote in remotes {
            println!("\t{}", remote?);
        }

        return Ok(());
    }

    expect_pledge(pledge_promises![Stdio Inet]);

    if arguments.is_present("load") {
        stdin().lock().read_to_end(&mut query)?;
    } else {
        // unbound(8) requires RD (no cache snooping) by default
        query = MessageBuilder::new(query.try_into()?)?
            .id(random())
            .rd(true)
            .question()?
            .qname()?
            .handle_qname(params.qname, reverse)?
            .finish()?
            .finish(params.qtype, qclass.parse()?)?
            .into_ar()
            .extension()?
            .finish()?
            .finish()
            .finish();
    }

    if arguments.is_present("encode") {
        expect_pledge(pledge_promises![Stdio]);

        stdout().lock().write_all(&query)?;
        return Ok(());
    }

    let timeout = arguments
        .value_of("timeout")
        .expect("should never fail (has Arg::default_value)")
        .parse()
        .map(Duration::from_secs)?;

    let ecolor = color && !arguments.is_present("dump");
    let mut esink: Box<dyn Write> = if arguments.is_present("dump") {
        Box::new(stderr())
    } else {
        Box::new(stdout())
    };

    for remote in remotes {
        let remote = remote?;
        let mut response = [0u8; 4096];
        let result = time::timeout(timeout, send(remote, &query, &mut response)).await;

        match result {
            Ok(Ok((length, local))) => {
                if arguments.is_present("dump") {
                    stdout().lock().write_all(&response[..length])?;
                    return Ok(());
                } else {
                    hprintln!(color, Dimmed, ";; {} ({})", remote, local);
                    pretty_print(color, &response[..length])?;
                    return Ok(());
                }
            }
            Ok(Err(error)) => {
                hwriteln!(esink, ecolor, Failure, ";; {} error: {}", remote, error)?;
            }
            Err(_) => {
                hwriteln!(esink, ecolor, Failure, ";; {} timed out", remote)?;
            }
        }
    }

    Ok(())
}

trait NameBuilderExt: Sized {
    fn handle_qname(self, qname: &str, reverse: bool) -> Result<Self, QnameError>;
}

error!(QnameError);
#[derive(Debug, displaydoc::Display)]
#[prefix_enum_doc_attributes]
/// failed to build QNAME
enum QnameError {
    /// error while emitting name
    Name(NameError),

    /// error while parsing IP address
    ParseIp(AddrParseError),
}

impl From<NameError> for QnameError {
    fn from(inner: NameError) -> Self {
        Self::Name(inner)
    }
}

impl From<AddrParseError> for QnameError {
    fn from(inner: AddrParseError) -> Self {
        Self::ParseIp(inner)
    }
}

impl<B: Buffer, P: Builder<B>> NameBuilderExt for NameBuilder<B, P> {
    fn handle_qname(mut self, qname: &str, reverse: bool) -> Result<Self, QnameError> {
        if reverse {
            match IpAddr::from_str(qname).map_err(QnameError::ParseIp)? {
                IpAddr::V4(x) => {
                    for octet in x.octets().iter().rev() {
                        self = self.label(octet.to_string().as_bytes())?;
                    }
                    self = self.label(b"in-addr")?;
                    self = self.label(b"arpa")?;
                }
                IpAddr::V6(x) => {
                    for octet in x.octets().iter().rev() {
                        self = self.label(format!("{:x}", octet >> 0 & 0xF).as_bytes())?;
                        self = self.label(format!("{:x}", octet >> 4 & 0xF).as_bytes())?;
                    }
                    self = self.label(b"ip6")?;
                    self = self.label(b"arpa")?;
                }
            }
        } else {
            self = self
                .labels(qualify(qname).as_bytes())
                .map_err(QnameError::Name)?;
        }

        Ok(self)
    }
}

// FIXME there must be a better place for this
fn qualify(name: impl AsRef<str>) -> String {
    let name = name.as_ref();

    if name == "." {
        "".into()
    } else if !name.ends_with(".") || name.ends_with(r"\.") {
        [name, "."].join("").into()
    } else {
        name.into()
    }
}

async fn send(
    remote: SocketAddr,
    query: &[u8],
    response: &mut [u8],
) -> Result<(usize, SocketAddr), tokio::io::Error> {
    let local = match remote {
        SocketAddr::V4(_) => SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0)),
        SocketAddr::V6(_) => SocketAddr::from((Ipv6Addr::UNSPECIFIED, 0)),
    };

    let socket = UdpSocket::bind(&local).await?;

    // RFC 2181 § 4
    socket.connect(&remote).await?;

    socket.send(&query).await?;

    Ok((socket.recv(response).await?, socket.local_addr()?))
}

fn custom_remotes(
    port: PortChooser<NonZeroU16>,
    host: &str,
) -> eyre::Result<Box<dyn Iterator<Item = eyre::Result<SocketAddr>>>> {
    Ok(Box::new(
        (host, port.choose_or_zero(None)).to_socket_addrs()?.map(Ok),
    ))
}

fn default_remotes(
    port: PortChooser<NonZeroU16>,
) -> eyre::Result<Box<dyn Iterator<Item = eyre::Result<SocketAddr>>>> {
    let mut result = self::platform::resolvers(port)?.peekable();

    if result.peek().is_some() {
        return Ok(Box::new(result));
    }

    let port = port.choose_or_zero(None);
    let ip6 = SocketAddr::from((Ipv6Addr::LOCALHOST, port));
    let ip4 = SocketAddr::from((Ipv4Addr::LOCALHOST, port));

    Ok(Box::new(once(ip6).chain(once(ip4)).map(Ok)))
}
