use std::net::IpAddr;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};

use futures::FutureExt;
use minecraft_protocol::data::server_status::ServerStatus;
use tokio::process::Command;
use tokio::sync::watch;
#[cfg(feature = "rcon")]
use tokio::sync::Semaphore;
use tokio::sync::{Mutex, RwLock, RwLockReadGuard};
use tokio::time;

use crate::config::{Config, Server as ConfigServer};
use crate::mc::ban::{BannedIp, BannedIps};
use crate::mc::whitelist::Whitelist;
use crate::os;
use crate::proto::packets::play::join_game::JoinGameData;

/// Server cooldown after the process quit.
/// Used to give it some more time to quit forgotten threads, such as for RCON.
const SERVER_QUIT_COOLDOWN: Duration = Duration::from_millis(2500);

/// RCON cooldown. Required period between RCON invocations.
///
/// The Minecraft RCON implementation is very broken and brittle, this is used in the hopes to
/// improve reliability.
#[cfg(feature = "rcon")]
const RCON_COOLDOWN: Duration = Duration::from_secs(15);

/// Exit code when SIGTERM is received on Unix.
#[cfg(unix)]
const UNIX_EXIT_SIGTERM: i32 = 130;

/// Shared server state.
#[derive(Debug)]
pub struct Server {
    /// Server state.
    ///
    /// Matches `State`, utilzes AtomicU8 for better performance.
    state: AtomicU8,

    /// State watch sender, broadcast state changes.
    state_watch_sender: watch::Sender<State>,

    /// State watch receiver, subscribe to state changes.
    state_watch_receiver: watch::Receiver<State>,

    /// Server process PID.
    ///
    /// Set if a server process is running.
    pid: Mutex<Option<u32>>,

    /// Last known server status.
    ///
    /// Will remain set once known, not cleared if server goes offline.
    status: RwLock<Option<ServerStatus>>,

    /// Last active time.
    ///
    /// The last time there was activity on the server. Also set at the moment the server comes
    /// online.
    last_active: RwLock<Option<Instant>>,

    /// Force server to stay online until.
    keep_online_until: RwLock<Option<Instant>>,

    /// Time to force kill the server process at.
    ///
    /// Used as starting/stopping timeout.
    kill_at: RwLock<Option<Instant>>,

    /// List of banned IPs.
    banned_ips: RwLock<BannedIps>,

    /// Whitelist if enabled.
    whitelist: RwLock<Option<Whitelist>>,

    /// Lock for exclusive RCON operations.
    #[cfg(feature = "rcon")]
    rcon_lock: Semaphore,

    /// Last time server was stopped over RCON.
    #[cfg(feature = "rcon")]
    rcon_last_stop: Mutex<Option<Instant>>,

    /// Probed join game data.
    pub probed_join_game: RwLock<Option<JoinGameData>>,

    /// Forge payload.
    ///
    /// Sent to clients when they connect to lobby. Recorded from server by probe.
    pub forge_payload: RwLock<Vec<Vec<u8>>>,
}

impl Server {
    /// Get current state.
    pub fn state(&self) -> State {
        State::from_u8(self.state.load(Ordering::Relaxed))
    }

    /// Get state receiver to subscribe on server state changes.
    pub fn state_receiver(&self) -> watch::Receiver<State> {
        self.state_watch_receiver.clone()
    }

    /// Set a new state.
    ///
    /// This updates various other internal things depending on how the state changes.
    ///
    /// Returns false if the state didn't change, in which case nothing happens.
    async fn update_state(&self, state: State, config: &Config) -> bool {
        self.update_state_from(None, state, config).await
    }

    /// Set new state, from a current state.
    ///
    /// This updates various other internal things depending on how the state changes.
    ///
    /// Returns false if current state didn't match `from` or if nothing changed.
    async fn update_state_from(&self, from: Option<State>, new: State, config: &Config) -> bool {
        // Atomically swap state to new, return if from doesn't match
        let old = State::from_u8(match from {
            Some(from) => match self.state.compare_exchange(
                from.to_u8(),
                new.to_u8(),
                Ordering::Relaxed,
                Ordering::Relaxed,
            ) {
                Ok(old) => old,
                Err(_) => return false,
            },
            None => self.state.swap(new.to_u8(), Ordering::Relaxed),
        });

        // State must be changed
        if old == new {
            return false;
        }

        trace!("Change server state from {:?} to {:?}", old, new);

        // Broadcast change
        let _ = self.state_watch_sender.send(new);

        // Update kill at time for starting/stopping state
        *self.kill_at.write().await = match new {
            State::Starting if config.server.start_timeout > 0 => {
                Some(Instant::now() + Duration::from_secs(config.server.start_timeout as u64))
            }
            State::Stopping if config.server.stop_timeout > 0 => {
                Some(Instant::now() + Duration::from_secs(config.server.stop_timeout as u64))
            }
            _ => None,
        };

        // Online/offline messages
        match new {
            State::Started => info!(target: "lazymc::monitor", "Server is now online"),
            State::Stopped => info!(target: "lazymc::monitor", "Server is now sleeping"),
            _ => {}
        }

        // If Starting -> Started, update active time and keep it online for configured time
        if old == State::Starting && new == State::Started {
            self.update_last_active().await;
            self.keep_online_for(Some(config.time.min_online_time))
                .await;
        }

        true
    }

    /// Update status as obtained from the server.
    ///
    /// This updates various other internal things depending on the current state and the given
    /// status.
    pub async fn update_status(&self, config: &Config, status: Option<ServerStatus>) {
        // Update state based on curren
        match (self.state(), &status) {
            (State::Stopped | State::Starting, Some(_)) => {
                self.update_state(State::Started, config).await;
            }
            (State::Started, None) => {
                self.update_state(State::Stopped, config).await;
            }
            _ => {}
        }

        // Update last status if known
        if let Some(status) = status {
            // Update last active time if there are online players
            if status.players.online > 0 {
                self.update_last_active().await;
            }

            self.status.write().await.replace(status);
        }
    }

    /// Try to start the server.
    ///
    /// Does nothing if currently not in stopped state.
    pub async fn start(config: Arc<Config>, server: Arc<Server>, username: Option<String>) -> bool {
        // Must set state from stopped to starting
        if !server
            .update_state_from(Some(State::Stopped), State::Starting, &config)
            .await
        {
            return false;
        }

        // Log starting message
        match username {
            Some(username) => info!(target: "lazymc", "Starting server for '{}'...", username),
            None => info!(target: "lazymc", "Starting server..."),
        }

        // TODO add config option for this
        if let Some(pid) = *server.pid.lock().await {
            return os::unfreeze(pid);
        }

        // Spawn server in new task
        Self::spawn_server_task(config, server);
        true
    }

    /// Spawn the server task.
    ///
    /// This should not be called directly.
    fn spawn_server_task(config: Arc<Config>, server: Arc<Server>) {
        tokio::spawn(invoke_server_cmd(config, server).map(|_| ()));
    }

    /// Stop running server.
    ///
    /// This will attempt to stop the server with all available methods.
    #[allow(unused_variables)]
    pub async fn stop(&self, config: &Config) -> bool {
        // Try to stop through RCON if started
        #[cfg(feature = "rcon")]
        if self.state() == State::Started && stop_server_rcon(config, self).await {
            return true;
        }

        // Try to stop through signal
        #[cfg(unix)]
        if stop_server_signal(config, self).await {
            return true;
        }

        warn!(target: "lazymc", "Failed to stop server, no more suitable stopping method to use");
        false
    }

    /// Force kill running server.
    ///
    /// This requires the server PID to be known.
    pub async fn force_kill(&self) -> bool {
        if let Some(pid) = *self.pid.lock().await {
            return os::force_kill(pid);
        }
        false
    }

    /// Decide whether the server should sleep.
    ///
    /// Always returns false if it is currently not online.
    pub async fn should_sleep(&self, config: &Config) -> bool {
        // Server must be online
        if self.state() != State::Started {
            return false;
        }

        // Never sleep if players are online
        let players_online = self
            .status
            .read()
            .await
            .as_ref()
            .map(|status| status.players.online > 0)
            .unwrap_or(false);
        if players_online {
            trace!(target: "lazymc", "Not sleeping because players are online");
            return false;
        }

        // Don't sleep when keep online until isn't expired
        let keep_online = self
            .keep_online_until
            .read()
            .await
            .map(|i| i >= Instant::now())
            .unwrap_or(false);
        if keep_online {
            trace!(target: "lazymc", "Not sleeping because of keep online");
            return false;
        }

        // Last active time must have passed sleep threshold
        if let Some(last_idle) = self.last_active.read().await.as_ref() {
            return last_idle.elapsed() >= Duration::from_secs(config.time.sleep_after as u64);
        }

        false
    }

    /// Decide whether to force kill the server process.
    pub async fn should_kill(&self) -> bool {
        self.kill_at
            .read()
            .await
            .map(|t| t <= Instant::now())
            .unwrap_or(false)
    }

    /// Read last known server status.
    pub async fn status(&self) -> RwLockReadGuard<'_, Option<ServerStatus>> {
        self.status.read().await
    }

    /// Update the last active time.
    async fn update_last_active(&self) {
        self.last_active.write().await.replace(Instant::now());
    }

    /// Force the server to be online for the given number of seconds.
    async fn keep_online_for(&self, duration: Option<u32>) {
        *self.keep_online_until.write().await = duration
            .filter(|d| *d > 0)
            .map(|d| Instant::now() + Duration::from_secs(d as u64));
    }

    /// Check whether the given IP is banned.
    ///
    /// This uses the latest known `banned-ips.json` contents if known.
    /// If this feature is disabled, this will always return false.
    pub async fn is_banned_ip(&self, ip: &IpAddr) -> bool {
        self.banned_ips.read().await.is_banned(ip)
    }

    /// Get user ban entry.
    pub async fn ban_entry(&self, ip: &IpAddr) -> Option<BannedIp> {
        self.banned_ips.read().await.get(ip)
    }

    /// Check whether the given IP is banned.
    ///
    /// This uses the latest known `banned-ips.json` contents if known.
    /// If this feature is disabled, this will always return false.
    pub fn is_banned_ip_blocking(&self, ip: &IpAddr) -> bool {
        futures::executor::block_on(async { self.is_banned_ip(ip).await })
    }

    /// Check whether the given username is whitelisted.
    ///
    /// Returns `true` if no whitelist is currently used.
    pub async fn is_whitelisted(&self, username: &str) -> bool {
        self.whitelist
            .read()
            .await
            .as_ref()
            .map(|w| w.is_whitelisted(username))
            .unwrap_or(true)
    }

    /// Update the list of banned IPs.
    pub async fn set_banned_ips(&self, ips: BannedIps) {
        *self.banned_ips.write().await = ips;
    }

    /// Update the list of banned IPs.
    pub fn set_banned_ips_blocking(&self, ips: BannedIps) {
        futures::executor::block_on(async { self.set_banned_ips(ips).await })
    }

    /// Update the whitelist.
    pub async fn set_whitelist(&self, whitelist: Option<Whitelist>) {
        *self.whitelist.write().await = whitelist;
    }

    /// Update the whitelist.
    pub fn set_whitelist_blocking(&self, whitelist: Option<Whitelist>) {
        futures::executor::block_on(async { self.set_whitelist(whitelist).await })
    }
}

impl Default for Server {
    fn default() -> Self {
        let (state_watch_sender, state_watch_receiver) = watch::channel(State::Stopped);

        Self {
            state: AtomicU8::new(State::Stopped.to_u8()),
            state_watch_sender,
            state_watch_receiver,
            pid: Default::default(),
            status: Default::default(),
            last_active: Default::default(),
            keep_online_until: Default::default(),
            kill_at: Default::default(),
            banned_ips: Default::default(),
            whitelist: Default::default(),
            #[cfg(feature = "rcon")]
            rcon_lock: Semaphore::new(1),
            #[cfg(feature = "rcon")]
            rcon_last_stop: Default::default(),
            probed_join_game: Default::default(),
            forge_payload: Default::default(),
        }
    }
}

/// Server state.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum State {
    /// Server is stopped.
    Stopped,

    /// Server is starting.
    Starting,

    /// Server is online and responding.
    Started,

    /// Server is stopping.
    Stopping,
}

impl State {
    /// From u8, panics if invalid.
    pub fn from_u8(state: u8) -> Self {
        match state {
            0 => Self::Stopped,
            1 => Self::Starting,
            2 => Self::Started,
            3 => Self::Stopping,
            _ => panic!("invalid State u8"),
        }
    }

    /// To u8.
    pub fn to_u8(self) -> u8 {
        match self {
            Self::Stopped => 0,
            Self::Starting => 1,
            Self::Started => 2,
            Self::Stopping => 3,
        }
    }
}

/// Invoke server command, store PID and wait for it to quit.
pub async fn invoke_server_cmd(
    config: Arc<Config>,
    state: Arc<Server>,
) -> Result<(), Box<dyn std::error::Error>> {
    // Configure command
    let args = shlex::split(&config.server.command).expect("invalid server command");
    let mut cmd = Command::new(&args[0]);
    cmd.args(args.iter().skip(1));
    cmd.kill_on_drop(true);

    // Set working directory
    if let Some(ref dir) = ConfigServer::server_directory(&config) {
        cmd.current_dir(dir);
    }

    // Spawn process
    let mut child = match cmd.spawn() {
        Ok(child) => child,
        Err(err) => {
            error!(target: "lazymc", "Failed to start server process through command");
            return Err(err.into());
        }
    };

    // Remember PID
    state
        .pid
        .lock()
        .await
        .replace(child.id().expect("unknown server PID"));

    // Wait for process to exit, handle status
    let crashed = match child.wait().await {
        Ok(status) if status.success() => {
            debug!(target: "lazymc", "Server process stopped successfully ({})", status);
            false
        }
        #[cfg(unix)]
        Ok(status) if status.code() == Some(UNIX_EXIT_SIGTERM) => {
            debug!(target: "lazymc", "Server process stopped successfully by SIGTERM ({})", status);
            false
        }
        Ok(status) => {
            warn!(target: "lazymc", "Server process stopped with error code ({})", status);
            state.state() == State::Started
        }
        Err(err) => {
            error!(target: "lazymc", "Failed to wait for server process to quit: {}", err);
            error!(target: "lazymc", "Assuming server quit, cleaning up...");
            false
        }
    };

    // Forget server PID
    state.pid.lock().await.take();

    // Give server a little more time to quit forgotten threads
    time::sleep(SERVER_QUIT_COOLDOWN).await;

    // Set server state to stopped
    state.update_state(State::Stopped, &config).await;

    // Restart on crash
    if crashed && config.server.wake_on_crash {
        warn!(target: "lazymc", "Server crashed, restarting...");
        Server::start(config, state, None).await;
    }

    Ok(())
}

/// Stop server through RCON.
#[cfg(feature = "rcon")]
async fn stop_server_rcon(config: &Config, server: &Server) -> bool {
    use crate::mc::rcon::Rcon;

    // RCON must be enabled
    if !config.rcon.enabled {
        trace!(target: "lazymc", "Not using RCON to stop server, disabled in config");
        return false;
    }

    // Grab RCON lock
    let rcon_lock = server.rcon_lock.acquire().await.unwrap();

    // Ensure RCON has cooled down
    let rcon_cooled_down = server
        .rcon_last_stop
        .lock()
        .await
        .map(|t| t.elapsed() >= RCON_COOLDOWN)
        .unwrap_or(true);
    if !rcon_cooled_down {
        debug!(target: "lazymc", "Not using RCON to stop server, in cooldown, used too recently");
        return false;
    }

    // Create RCON client
    let mut rcon = match Rcon::connect_config(config).await {
        Ok(rcon) => rcon,
        Err(err) => {
            error!(target: "lazymc", "Failed to RCON server to sleep: {}", err);
            return false;
        }
    };

    // Invoke stop
    if let Err(err) = rcon.cmd("stop").await {
        error!(target: "lazymc", "Failed to invoke stop through RCON: {}", err);
        return false;
    }

    // Set server to stopping state, update last RCON time
    server.rcon_last_stop.lock().await.replace(Instant::now());
    server.update_state(State::Stopping, config).await;

    // Gracefully close connection
    rcon.close().await;

    drop(rcon_lock);

    true
}

/// Stop server by sending SIGTERM signal.
///
/// Only available on Unix.
#[cfg(unix)]
async fn stop_server_signal(config: &Config, server: &Server) -> bool {
    // Grab PID
    let pid = match *server.pid.lock().await {
        Some(pid) => pid,
        None => {
            debug!(target: "lazymc", "Could not send stop signal to server process, PID unknown");
            return false;
        }
    };

    // Send kill signal
    // TODO uncomment this and add a config option
    /*if !crate::os::kill_gracefully(pid) {
        error!(target: "lazymc", "Failed to send stop signal to server process");
        return false;
    }*/
    if !os::freeze(pid) {
        error!(target: "lazymc", "Failed to send freeze signal to server process.");
    }

    // Update from starting/started to stopping
    server
        .update_state_from(Some(State::Starting), State::Stopping, config)
        .await;
    server
        .update_state_from(Some(State::Started), State::Stopping, config)
        .await;

    true
}
