use crate::{
    args,
    monero,
    utils,
};
use clap::Parser;
use log::{
    debug,
    info,
    warn,
};
use serde::{
    Deserialize,
    Serialize,
};
use std::{
    env,
    fs,
    process::Command,
    time::Duration,
};

#[derive(Debug, Deserialize, Serialize)]
pub struct HttpProxyStatus {
    pub open: bool,
}

#[derive(Debug, Serialize, PartialEq)]
pub enum ProxyStatus {
    Opening,
    Open,
}

impl ProxyStatus {
    pub fn value(&self) -> String {
        match *self {
            ProxyStatus::Opening => String::from("opening\n"),
            ProxyStatus::Open => String::from("open\n"),
        }
    }
}

#[derive(Serialize, Deserialize, Debug)]
struct Tunnel {
    // http proxy tunnel wont have this field
    dest: Option<String>,
    port: String,
}

#[derive(Serialize, Deserialize, Debug)]
struct Tunnels {
    tunnels: Vec<Tunnel>,
}

impl Default for Tunnels {
    fn default() -> Self {
        Tunnels {
            tunnels: Vec::new(),
        }
    }
}

/// Looks for the `tunnels-config.json` at /home/$USER/.i2p-zero/config/
///
/// and attempts to extract the app and http proxy tunnel information.
async fn find_tunnels() {
    let app_port = utils::get_app_port();
    let file_path = format!(
        "/home/{}/.i2p-zero/config/tunnels.json",
        env::var("USER").unwrap_or(String::from("user"))
    );
    let contents = fs::read_to_string(file_path).unwrap_or(utils::empty_string());
    debug!("i2p tunnels: {}", contents);
    let has_app_tunnel = contents.contains(&format!("{}", app_port));
    let proxy_port = get_i2p_proxy_port();
    let tx_proxy_port = monero::get_daemon_port();
    let has_http_tunnel = contents.contains(&proxy_port);
    let has_tx_proxy_tunnel = contents.contains(&format!("{}", &tx_proxy_port));
    if !has_app_tunnel || !has_http_tunnel {
        tokio::time::sleep(Duration::new(120, 0)).await;
    }
    if !has_app_tunnel {
        debug!("creating app tunnel");
        create_tunnel();
    }
    if !has_http_tunnel {
        debug!("creating http tunnel");
        create_http_proxy();
    }
    if !has_tx_proxy_tunnel && !utils::is_using_remote_node() {
        create_tx_proxy_tunnel();
    }
}

/// Called on application startup for i2p tunnel creation,
///
/// proxy tunnel, etc. Logs proxy status every minute.
pub async fn start() {
    info!("starting i2p-zero");
    let args = args::Args::parse();
    let path = args.i2p_zero_dir;
    let output = Command::new(format!("{}/router/bin/i2p-zero", path)).spawn();
    match output {
        Ok(child) => debug!("{:?}", child.stdout),
        _ => {
            warn!("i2p-zero not installed, manual tunnel creation required");
            ()
        }
    }
    find_tunnels().await;
    {
        tokio::spawn(async move {
            let tick: std::sync::mpsc::Receiver<()> = schedule_recv::periodic_ms(600000);
            loop {
                tick.recv().unwrap();
                check_connection().await;
            }
        });
    }
}

/// Create an i2p tunnel for the NEVEKO application
fn create_tunnel() {
    info!("creating tunnel");
    let args = args::Args::parse();
    let path = args.i2p_zero_dir;
    let output = Command::new(format!("{}/router/bin/tunnel-control.sh", path))
        .args([
            "server.create",
            "127.0.0.1",
            &format!("{}", utils::get_app_port()),
        ])
        .spawn()
        .expect("i2p-zero failed to create a tunnel");
    debug!("{:?}", output.stdout);
}

/// Create an i2p tunnel for the monero tx proxy
fn create_tx_proxy_tunnel() {
    info!("creating monerod tx proxy tunnel");
    let args = args::Args::parse();
    let path = args.i2p_zero_dir;
    let output = Command::new(format!("{}/router/bin/tunnel-control.sh", path))
        .args([
            "server.create",
            "127.0.0.1",
            &format!("{}", monero::get_daemon_port()),
        ])
        .spawn()
        .expect("i2p-zero failed to create a tunnel");
    debug!("{:?}", output.stdout);
}

/// Extract i2p port from command line arg
fn get_i2p_proxy_port() -> String {
    let proxy_host = utils::get_i2p_http_proxy();
    let values = proxy_host.split(":");
    let mut v: Vec<String> = values.map(|s| String::from(s)).collect();
    let port = v.remove(2);
    port
}

/// Create the http proxy if it doesn't exist
fn create_http_proxy() {
    let args = args::Args::parse();
    let path = args.i2p_zero_dir;
    info!("creating http proxy");
    let port = get_i2p_proxy_port();
    let output = Command::new(format!("{}/router/bin/tunnel-control.sh", path))
        .args(["http.create", &port])
        .spawn()
        .expect("i2p-zero failed to create a http proxy");
    debug!("{:?}", output.stdout);
}

/// This is the `dest` value of the app i2p tunnels
///
/// in `tunnels-config.json`.
/// 
/// `port` - the port of the tunnel (e.g. `utils::get_app_port()`)
pub fn get_destination(port: Option<u16>) -> String {
    let file_path = format!(
        "/home/{}/.i2p-zero/config/tunnels.json",
        env::var("USER").unwrap_or(String::from("user"))
    );
    // Don't panic if i2p-zero isn't installed
    let contents = match fs::read_to_string(file_path) {
        Ok(file) => file,
        _ => utils::empty_string(),
    };
    if contents != utils::empty_string() {
        let input = format!(r#"{contents}"#);
        let j: Tunnels = serde_json::from_str(&input).unwrap_or(Default::default());
        let mut destination: String = utils::empty_string();
        let tunnels: Vec<Tunnel> = j.tunnels;
        for tunnel in tunnels {
            if tunnel.port == format!("{}", port.unwrap_or(utils::get_app_port())) {
                destination = tunnel.dest.unwrap_or(utils::empty_string());
            }
        }
        return destination;
    }
    utils::empty_string()
}

/// Ping the i2p-zero http proxy `tunnel-control http.state <port>`
pub async fn check_connection() -> ProxyStatus {
    let args = args::Args::parse();
    let path = args.i2p_zero_dir;
    let port = get_i2p_proxy_port();
    let output = Command::new(format!("{}/router/bin/tunnel-control.sh", path))
        .args(["http.state", &port])
        .output()
        .expect("check i2p connection failed");
    let str_status = String::from_utf8(output.stdout).unwrap();
    if str_status == ProxyStatus::Open.value() {
        debug!("http proxy is open");
        ProxyStatus::Open
    } else {
        debug!("http proxy is opening");
        ProxyStatus::Opening
    }
}