use crate::options::structs::*;
use log::{error, info, warn};
use redis::{Client, Connection};
use std::fs::OpenOptions;
use std::io::Write;
use std::os::unix::process::CommandExt;
use std::process::Command;
use std::sync::Arc;
use std::{env, fs};
use tokio::time::Duration;

const CONFIG_PATH: &str = "settings.json";

// 4ever sync
fn load_processes(json_filename: &str) -> Option<Processes> {
    if let Ok(res) = fs::read_to_string(json_filename) {
        if let Ok(conf) = serde_json::from_str::<Processes>(&res) {
            return Some(conf);
        }
    }
    None
}

pub async fn get_actual_config() -> Option<Processes> {
    // * if no local conf -> loop and +inf getting conf from redis server
    // * if local conf -> once getting conf from redis server
    match load_processes(CONFIG_PATH) {
        Some(local_conf) => {
            info!(
                "Found local configuration, version - {}",
                &local_conf.date_of_creation
            );
            if let Some(remote_conf) =
                // TODO : rework with pubsub mech 
                once_get_remote_configuration(&format!("redis://{}/", local_conf.config_server))
            {
                return match config_comparing(&local_conf, &remote_conf) {
                    ConfigActuality::Local => {
                        info!("Local config is actual");
                        Some(local_conf)
                    }
                    ConfigActuality::Remote => {
                        info!("Pulled config is more actual. Saving changes!");
                        if save_new_config(&remote_conf, CONFIG_PATH).is_err() {
                            error!("Saving changes process failed due to unexpected error...")
                        }
                        Some(remote_conf)
                    }
                };
            }
            Some(local_conf)
        }
        None => {
            warn!("No local valid conf was found. Trying to pull remote one...");
            let mut conn = get_connection_watcher(&open_watcher("redis://localhost/"));
            let remote_config = get_remote_conf_watcher(&mut conn).await;
            if let Some(conf) = remote_config {
                info!("Config {} was pulled from Redis-Server. Starting...", &conf.date_of_creation);
                let _ = save_new_config(&conf, CONFIG_PATH);
                return Some(conf);
            }
            None
        }
    }
}

async fn get_remote_conf_watcher(conn : &mut Connection) -> Option<Processes> {
    let mut conn = conn.as_pubsub();
    let cont = crate::utils::get_container_id();
    loop { 
        match cont {
            Some(ref cont) => {
                let cont = cont.trim();
                if conn.subscribe(cont).is_err() {
                    // todo : delay
                    continue;
                }
                match conn.get_message() {
                    Ok(msg) => {
                        let msg: Result<String, redis::RedisError> = msg.get_payload();
                        if let Ok(payload) = msg {
                            if let Some(remote) = parse_extern_config(&payload) {
                                return Some(remote)
                            }
                            else {
                                error!("Pulled invalid config, cannot start. Waiting for remote conf...");
                            }
                        } else {
                            error!("Cannot get Redis message payload. Waiting for remote conf...");
                        }
                        // todo : delay
                        continue;
                    },
                    Err(_) => {
                        // todo : delay
                        continue;
                    },
                }
            },
            None => {
                error!("Cannot get container id. Returning");
                break
            },
        }
    }
    None
} 
// ! once iter exec
// ! only for situation when local isn't None (no need to fck redis server)
fn once_get_remote_configuration(serv_info: &str) -> Option<Processes> {
    let cont = crate::utils::get_container_id();
    match Client::open(serv_info) {
        Ok(client) => {
            match client.get_connection() {
                Ok(mut conn) => {
                    let mut conn =  conn.as_pubsub();
                    match conn.subscribe(cont) {
                        Ok(_) => {
                            if conn.set_read_timeout(Some(Duration::from_millis(100))).is_err() {
                                error!("Cannot set reading pubsub timeout and pull remote config");
                                return None;
                            }
                            match conn.get_message() {
                                Ok(msg) => {
                                    info!("Pulled config from Redis Server");
                                    let get_payload: Result<String, redis::RedisError> = msg.get_payload();
                                    match get_payload {
                                        Ok(payload) => {
                                            let remote = parse_extern_config(&payload);
                                            if remote.is_none() {
                                                error!("Pulled config is invalid. Check it in Redis Server");
                                            }
                                            return remote;
                                        },
                                        Err(_) => {
                                            error!("Cannot extract payload from new message. Check Redis Server state");
                                            return None;
                                        },
                                    }
                                },
                                Err(_) => {
                                    warn!("Cannot get config from Redis Server. Empty channel");
                                    return None;
                                },
                            }
                        },
                        Err(_) => {
                            error!("Redis subscription process failed. Check Redis configuration!");
                            return None;
                        }
                    }
                }
                Err(_) => {
                    error!("Redis connection attempt is failed. Check Redis configuration!");
                    None
                }
            }
        }
        Err(_) => {
            error!("Redis-Client opening attempt is failed. Check network configuration!");
            None
        }
    }
}

// ! watchers

fn open_watcher(serv_info: &str) -> Client {
    loop {
        match Client::open(serv_info) {
            Ok(redis) => {
                info!("Successfully opened Redis-Client");
                return redis;
            }
            Err(_) => {
                error!("Redis-Client opening attempt is failed. Check network configuration! Retrying...");
                std::thread::sleep(Duration::from_secs(4));
            }
        }
    }
}

fn get_connection_watcher(client: &Client) -> Connection {
    loop {
        match client.get_connection() {
            Ok(conn) => {
                info!("Successfully got Redis connection object");
                return conn;
            }
            Err(_) => {
                error!(
                    "Redis connection attempt is failed. Check Redis configuration! Retrying..."
                );
                std::thread::sleep(Duration::from_secs(4));
            }
        }
    }
}

fn restart_main_thread() -> std::io::Result<()> {
    let current_exe = env::current_exe()?;
    Command::new(current_exe).exec();
    Ok(())
}
pub async fn subscribe_config_stream(actual_prcs: Arc<Processes>) -> Result<(), CustomError> {
    if let Ok(client) = Client::open(format!("redis://{}/", &actual_prcs.config_server)) {
        if let Ok(mut conn) = client.get_connection() {
            match crate::utils::get_container_id() {
                Some(channel_name) => {
                    let channel_name = channel_name.trim();
                    let mut pubsub = conn.as_pubsub();
                    if pubsub.subscribe(&channel_name).is_ok() {
                        info!("Runner subscribed on config update publishing in channel {}", &channel_name);
                        loop {
                            if let Ok(msg) = pubsub.get_message() {
                                info!("New config was pulled from Redis Server");
                                let get_remote_config: Result<String, redis::RedisError> = msg.get_payload();
                                match get_remote_config {
                                    Ok(payload) => {
                                        if let Some(remote_config) = parse_extern_config(&payload) {
                                            match config_comparing(&actual_prcs, &remote_config) {
                                                ConfigActuality::Remote => {
                                                    warn!("Pulled config is actual. Saving and restarting...");
                                                    if save_new_config(&remote_config, CONFIG_PATH).is_err() {
                                                        error!("Error with saving new config to {}. Stopping sub mechanism...", &CONFIG_PATH);
                                                        return Err(CustomError::Fatal);
                                                    }
                                                    if restart_main_thread().is_err() {
                                                        error!("Error with restarting Runner. Stopping sub mechanism...");
                                                        return Err(CustomError::Fatal);
                                                    }
                                                }
                                                _ => continue,
                                            }
                                        }
                                        else {
                                            error!("Invalid conig was pulled");
                                        }
                                    },
                                    Err(_) => {
                                        error!("Cannot extract new config from message");
                                        break;
                                    },
                                }
                            }
                            tokio::time::sleep(tokio::time::Duration::from_secs(30)).await;
                        }
                    } else {
                        error!("Cannot subscribe channel {}. Check Redis Server status", &channel_name);
                    }
                }, 
                None => {
                    error!("Cannot get channel name");
                }
            }
        }
    }
    error!("Error with subscribing Redis stream on update. Working only with selected config...");
    Err(CustomError::Fatal)
}

fn config_comparing(local: &Processes, remote: &Processes) -> ConfigActuality {
    let local_date: u64 = local.date_of_creation.parse().unwrap();
    let remote_date: u64 = remote.date_of_creation.parse().unwrap();

    match local_date.cmp(&remote_date) {
        std::cmp::Ordering::Equal | std::cmp::Ordering::Greater => ConfigActuality::Local,
        std::cmp::Ordering::Less => ConfigActuality::Remote,
    }
}

// ! TEMPORARILY DEPRECATED !
// fn native_date_from_millis(mls: &str) -> Option<chrono::DateTime<Utc>> {
//     match mls.parse::<i64>(){
//         Ok(val) => return chrono::DateTime::from_timestamp_millis(val),
//         Err(_) => return None,
//     }
// }

fn save_new_config(config: &Processes, config_file: &str) -> Result<(), CustomError> {
    match serde_json::to_string_pretty(&config) {
        // Ok(st) => match fs::write(config_file, st) {
        //     Ok(_) => Ok(()),
        //     Err(_) => Err(CustomError::Fatal),
        // },
        Ok(st) => {
            let file = OpenOptions::new()
                .read(true)
                .write(true)
                .create(true)
                .truncate(false)
                .open(config_file);
            match file {
                Ok(fs) => {
                    let mut writer = fs;
                    match writeln!(writer, "{}", st) {
                        Ok(_) => Ok(()),
                        Err(_) => Err(CustomError::Fatal),
                    }
                }
                Err(_) => return Err(CustomError::Fatal),
            }
        }
        Err(_) => Err(CustomError::Fatal),
    }
}

fn parse_extern_config(json_string: &str) -> Option<Processes> {
    if let Ok(des) = serde_json::from_str::<Processes>(json_string) {
        return Some(des);
    }
    None
}

// unit tests
#[cfg(test)]
mod config_unittests {
    use super::*;
    #[test]
    fn parsing_valid_conf() {
        assert!(load_processes("tests/examples/settings.json").is_some());
    }
    #[test]
    fn parsing_invalid_conf() {
        assert!(load_processes("tests/examples/invalid_config.json").is_none());
    }
    #[test]
    fn configuration_comparing() {
        // old one (kinda local)
        let a = Processes {
            date_of_creation: String::from("1"),
            config_server: String::new(),
            processes: vec![],
        };
        // new one (kinda remote)
        let b = Processes {
            date_of_creation: String::from("2"),
            config_server: String::new(),
            processes: vec![],
        };

        assert_eq!(config_comparing(&a, &b), ConfigActuality::Remote);
    }
    // TODO : strange output
    // #[test]
    // fn get_actual_config_mechanism() {
    //     assert!(get_actual_config().is_some())
    // }
    #[test]
    fn save_config() {
        let a = Processes {
            date_of_creation: String::from("1"),
            config_server: String::new(),
            processes: vec![],
        };
        assert!(save_new_config(&a, "tests/examples/save-conf.json").is_ok());
    }

    #[test]
    fn save_to_zero_file() {
        let a = Processes {
            date_of_creation: String::from("1"),
            config_server: String::new(),
            processes: vec![],
        };
        assert!(save_new_config(&a, "tests/examples/none.json").is_ok());
    }
}
