mod config;
mod leases;

pub use config::*;
pub use leases::*;

/// The standard port for dhcpv4 servers defined in the RFC.
pub const SERVER_PORT: u16 = 67;
/// This is an arbitrary port number above 1024 that can be used when testing the server.
/// Port numbers below 1024 are privileged ports that require root access to bind to.
/// See: <https://www.w3.org/Daemon/User/Installation/PrivilegedPorts.html>
pub const TEST_SERVER_PORT: u16 = 8067;
/// Resolves to `TEST_SERVER_PORT` for debug builds and `SERVER_PORT` for release builds
pub const RESOLVED_SERVER_PORT: u16 = if cfg!(debug_assertions) {
    TEST_SERVER_PORT
} else {
    SERVER_PORT
};

use crate::v4::error::Result;
use crate::v4::message::*;
use std::net::Ipv4Addr;
use std::net::SocketAddrV4;
use tracing::{debug, error, info, warn};

/// Ties together a ServerConfig, Socket, and Leases in order to handle incoming Client Messages,
/// and construct server-specific response Messages.
///
/// Used by the server binary and integration tests.
///
/// Requires the `v4_server` feature, which is enabled by default.
#[derive(Debug)]
pub struct Server {
    /// A UdpSocket that understands how to use `Message` to communicate with the Client
    pub socket: Socket,
    /// Direct access to the ipv4 address passed to the `UdpSocket`.
    ///
    /// A server with multiple network addresses may use any of its addresses.
    pub ip: Ipv4Addr,
    /// Configuration read from toe-beans.toml
    pub config: InnerServerConfig,
    /// Manages the leasing logic of a range of ip addresses
    pub leases: Leases,
}

impl Server {
    /// Setup everything necessary for a Server to listen for incoming Messages.
    ///
    /// This fn is relatively "slow" and may panic, but that is okay
    /// because it is called before the `listen` or `listen_once` fns.
    pub fn new(config: ServerConfig) -> Self {
        let leases = Leases::new(config.lease_config);
        let socket = Socket::new(config.socket_config);
        let ip = config
            .server
            .server_address
            .unwrap_or_else(|| socket.get_ip());

        if config.server.rapid_commit {
            debug!("Rapid commit is enabled on server");
        }

        Self {
            socket,
            ip,
            leases,
            config: config.server,
        }
    }

    /// A server will send its response to different locations in different scenarios.
    fn reply(&self, response: Message, port: u16) {
        let reply_result = if !response.giaddr.is_unspecified() {
            // received request from relay agent so send response to relay agent.
            self.socket
                .unicast(&response, SocketAddrV4::new(response.giaddr, port))
        } else {
            self.socket.broadcast(&response, port)
        };

        match reply_result {
            Ok(_) => {}
            Err(error) => {
                error!("{error}");
            }
        };
    }

    /// Will wait for one Message on the UdpSocket and send a response based on it.
    ///
    /// To handle more than one Message, use `listen`.
    pub fn listen_once(&mut self) {
        let (decoded, src) = match self.socket.receive::<Message>() {
            Ok(values) => values,
            Err(error) => {
                error!("{error}");
                return;
            }
        };

        let message_type = match decoded.find_option(53) {
            Some(MessageOptions::MessageType(message_type)) => message_type,
            _ => {
                error!("Request does not have option 53");
                return;
            }
        };

        let response = match message_type {
            MessageTypes::Discover => 'discover: {
                debug!("Received Discover message");
                if self.config.rapid_commit {
                    match decoded.find_option(80) {
                        Some(MessageOptions::RapidCommit) => {
                            debug!(
                                "Discover message has rapid commit option. Will send Ack instead of Offer"
                            );
                            let mac_address = decoded.get_mac_address();

                            let ip = match self.leases.ack(mac_address) {
                                Ok(ip) => ip,
                                Err(error) => {
                                    error!("{}", error);
                                    return;
                                }
                            };
                            let mut message = self.ack(decoded, ip);
                            message.add_option(MessageOptions::RapidCommit);
                            break 'discover message;
                        }
                        _ => {
                            // Discover message did not send a rapid commit option or it wasnt valid. Will send Offer
                        }
                    };
                }

                match self.offer(decoded) {
                    Ok(message) => message,
                    Err(error) => {
                        error!("{}", error);
                        return;
                    }
                }
            }
            MessageTypes::Request => 'request: {
                debug!("Received Request message");
                let mac_address = decoded.get_mac_address();

                // "If the DHCPREQUEST message contains a 'server identifier' option, the message is in response to a DHCPOFFER message."
                // "Otherwise, the message is a request to verify or extend an existing lease."
                match decoded.find_option(54) {
                    Some(MessageOptions::ServerIdentifier(address_option)) => {
                        let server_identifier = address_option.inner();
                        if server_identifier != self.ip {
                            debug!(
                                "Ignoring client broadcast of DHCP Request for {} which is not this server ({}).",
                                server_identifier, self.ip
                            );

                            match self.leases.release(mac_address) {
                                Ok(_) => {
                                    debug!("Removed IP address previously offered to this client")
                                }
                                Err(error) => error!("{}", error),
                            }

                            return;
                        }

                        debug!("client has selected this server for a lease");
                        let ip = match self.leases.ack(mac_address) {
                            Ok(ip) => ip,
                            Err(error) => {
                                error!("{}", error);
                                return;
                            }
                        };
                        self.ack(decoded, ip)
                    }
                    _ => {
                        if decoded.ciaddr.is_unspecified() {
                            // no server identifier, and no ciaddr = init-reboot (verify)
                            debug!("client is attempting to verify lease");

                            let ip = match decoded.find_option(50) {
                                Some(MessageOptions::RequestedIp(address_option)) => {
                                    address_option.inner()
                                }
                                _ => {
                                    // If a server receives a DHCPREQUEST message with an invalid 'requested IP address',
                                    // the server SHOULD respond to the client with a DHCPNAK message
                                    break 'request self.nak(decoded, "Request does not have required option 50 (requested ip address)".to_string());
                                }
                            };

                            match self.leases.verify_lease(mac_address, &ip) {
                                Ok(_) => self.ack(decoded, ip),
                                Err(error) => {
                                    if error == "No IP address lease found with that owner" {
                                        // If the DHCP server has no record of this client, then it MUST remain silent,
                                        // This is necessary for peaceful coexistence of non-communicating DHCP servers on the same wire.
                                        debug!("{}. Not sending response", error);
                                        return;
                                    } else {
                                        // Client's notion of ip address is wrong or lease expired
                                        debug!("Lease not verified: {}", error);
                                        self.nak(decoded, error.to_string())
                                    }
                                }
                            }
                        } else {
                            // no server identifier, and a ciaddr = renew/rebind (extend)
                            debug!("client is attempting to renew/rebind lease");
                            match self.leases.extend(mac_address) {
                                Ok(_) => {
                                    // TODO The server SHOULD return T1 and T2,
                                    // and their values SHOULD be adjusted from their original values
                                    // to take account of the time remaining on the lease.
                                    let ip = decoded.ciaddr;
                                    self.ack(decoded, ip)
                                }
                                Err(error) => {
                                    debug!("Lease not extended: {}", error);
                                    self.nak(decoded, error.to_string())
                                }
                            }
                        }
                    }
                }
            }
            MessageTypes::Release => {
                debug!("Received Release message");
                if let Err(error) = self.leases.release(decoded.get_mac_address()) {
                    error!("{}", error);
                }
                return;
            }
            MessageTypes::Decline => {
                debug!("Received Decline message");

                // RFC 2131 says to mark the network address as not available,
                // but I'm concerned that could be a vector for easy ip address exhaustion.
                // For now, I'll release the address as there is no sense in keeping it acked.
                if let Err(error) = self.leases.release(decoded.get_mac_address()) {
                    error!("{}", error);
                }
                return;
            }
            MessageTypes::Inform => {
                debug!("Received Inform message");
                self.ack_inform(decoded)
            }
            MessageTypes::LeaseQuery
            | MessageTypes::BulkLeaseQuery
            | MessageTypes::ActiveLeaseQuery
            | MessageTypes::Tls => {
                warn!("Client sent a message type that is known but not yet handled");
                return;
            }
            _ => {
                error!("Client sent unknown message type");
                return;
            }
        };

        self.reply(response, src.port());
    }

    /// Calls `listen_once` in a loop forever.
    pub fn listen(&mut self) -> ! {
        info!("Listening for DHCP requests");
        loop {
            self.listen_once();
        }
    }

    /// If a request message has a parameter request list,
    /// each requested parameter is added to the response message
    /// if the parameter's value is configured on the server.
    fn handle_parameter_requests(&self, request_message: &Message) -> Vec<MessageOptions> {
        match request_message.find_option(55) {
            Some(MessageOptions::RequestedOptions(list)) => {
                // list is a Vec<u8> of dhcp option numbers
                list.iter()
                    .filter_map(|parameter_request| {
                        self.config.parameters.get(parameter_request).cloned()
                    })
                    .collect()
            }
            _ => vec![],
        }
    }

    /// Sent from the server to client following a Discover message
    pub fn offer(&mut self, message: Message) -> Result<Message> {
        debug!("Sending Offer message");

        // The client may suggest values for the network address
        // and lease time in the DHCPDISCOVER message. The client may include
        // the 'requested IP address' option to suggest that a particular IP
        // address be assigned
        let maybe_requested_ip = match message.find_option(50) {
            Some(MessageOptions::RequestedIp(address_option)) => Some(address_option.inner()),
            _ => None,
        };
        let mac_address = message.get_mac_address();
        let yiaddr = self.leases.offer(mac_address, maybe_requested_ip)?;

        let mut parameters = self.handle_parameter_requests(&message);
        parameters.push(MessageOptions::MessageType(MessageTypes::Offer)); // must
        parameters.push(MessageOptions::LeaseTime(TimeOption::new(
            self.leases.get_config().lease_time.as_client(),
        ))); // must
        parameters.push(MessageOptions::ServerIdentifier(AddressOption::new(
            self.ip,
        ))); // must, rfc2132

        Ok(Message {
            op: Ops::Reply,
            htype: HTypes::Ethernet,
            hlen: 6,
            hops: 0,
            xid: message.xid,
            secs: 0,
            flags: message.flags,
            ciaddr: Ipv4Addr::UNSPECIFIED, // field not used
            yiaddr,
            siaddr: self.ip,
            giaddr: message.giaddr,
            chaddr: message.chaddr,
            sname: SName::EMPTY,
            file: File::EMPTY,
            magic: MAGIC,
            options: parameters.into(),
        })
    }

    /// Sent from the server to client following a Request message
    ///
    /// Acks behave differently depending on if they are sent in response to a DHCP Request or DHCP Inform.
    /// For example, a DHCP Request must send lease time and a DHCP Inform must not.
    ///
    /// Any configuration parameters in the DHCPACK message SHOULD NOT
    /// conflict with those in the earlier DHCPOFFER message to which the
    /// client is responding.
    pub fn ack(&mut self, message: Message, yiaddr: Ipv4Addr) -> Message {
        debug!("Sending Ack message");

        let mut parameters = self.handle_parameter_requests(&message);
        parameters.push(MessageOptions::MessageType(MessageTypes::Ack)); // must
        parameters.push(MessageOptions::LeaseTime(TimeOption::new(
            self.leases.get_config().lease_time.as_client(),
        ))); // must
        parameters.push(MessageOptions::ServerIdentifier(AddressOption::new(
            self.ip,
        ))); // must

        Message {
            op: Ops::Reply,
            htype: HTypes::Ethernet,
            hlen: 6,
            hops: 0,
            xid: message.xid,
            secs: 0,
            flags: message.flags,
            ciaddr: message.ciaddr,
            yiaddr,
            siaddr: self.ip, // ip address of next bootstrap server
            giaddr: message.giaddr,
            chaddr: message.chaddr,
            sname: SName::EMPTY,
            file: File::EMPTY,
            magic: MAGIC,
            options: parameters.into(),
        }
    }

    /// Sent from server to client in response to an Inform message
    ///
    /// Acks behave differently depending on if they are sent in response to a DHCP Request or DHCP Inform.
    /// For example, a DHCP Request must send lease time and a DHCP Inform must not.
    pub fn ack_inform(&self, message: Message) -> Message {
        debug!("Sending Ack message (in response to an inform)");

        let mut parameters = self.handle_parameter_requests(&message);
        parameters.push(MessageOptions::MessageType(MessageTypes::Ack)); // must
        parameters.push(MessageOptions::ServerIdentifier(AddressOption::new(
            self.ip,
        ))); // must

        Message {
            op: Ops::Reply,
            htype: HTypes::Ethernet,
            hlen: 6,
            hops: 0,
            xid: message.xid,
            secs: 0,
            flags: message.flags,
            ciaddr: message.ciaddr,
            yiaddr: Ipv4Addr::UNSPECIFIED,
            siaddr: self.ip, // ip address of next bootstrap server
            giaddr: message.giaddr,
            chaddr: message.chaddr,
            sname: SName::EMPTY,
            file: File::EMPTY,
            magic: MAGIC,
            options: parameters.into(),
        }
    }

    /// Sent from the server to client following a Request message
    pub fn nak(&self, message: Message, error: String) -> Message {
        debug!("Sending Nak message");

        let mut response = Message {
            op: Ops::Reply,
            htype: HTypes::Ethernet,
            hlen: 6,
            hops: 0,
            xid: message.xid,
            secs: 0,
            flags: message.flags,
            ciaddr: Ipv4Addr::UNSPECIFIED,
            yiaddr: Ipv4Addr::UNSPECIFIED,
            siaddr: Ipv4Addr::UNSPECIFIED,
            giaddr: message.giaddr,
            chaddr: message.chaddr,
            sname: SName::EMPTY, // this field not used
            file: File::EMPTY,   // this field not used
            magic: MAGIC,
            options: vec![
                MessageOptions::MessageType(MessageTypes::Nak), // must
                MessageOptions::ServerIdentifier(AddressOption::new(self.ip)), // must
            ]
            .into(),
        };

        match StringOption::new(error) {
            Ok(converted) => {
                response.add_option(MessageOptions::Message(converted)); // should
            }
            Err(_) => {
                error!("Sending Nak without error message option because option's value is empty");
            }
        };

        response
    }
}
