// 1. Check update.
// 2. Install or uninstall.

use super::{desc::Meta as PluginMeta, ipc::InstallStatus, *};
use crate::flutter;
use crate::hbbs_http::create_http_client;
use hbb_common::{allow_err, bail, log, tokio, toml};
use serde_derive::{Deserialize, Serialize};
use serde_json;
use std::{
    collections::{HashMap, HashSet},
    fs::{read_to_string, remove_dir_all, OpenOptions},
    io::Write,
    sync::{Arc, Mutex},
};

const MSG_TO_UI_PLUGIN_MANAGER_LIST: &str = "plugin_list";
const MSG_TO_UI_PLUGIN_MANAGER_INSTALL: &str = "plugin_install";
const MSG_TO_UI_PLUGIN_MANAGER_UNINSTALL: &str = "plugin_uninstall";

const IPC_PLUGIN_POSTFIX: &str = "_plugin";

#[cfg(target_os = "windows")]
const PLUGIN_PLATFORM: &str = "windows";
#[cfg(target_os = "linux")]
const PLUGIN_PLATFORM: &str = "linux";
#[cfg(target_os = "macos")]
const PLUGIN_PLATFORM: &str = "macos";

lazy_static::lazy_static! {
    static ref PLUGIN_INFO: Arc<Mutex<HashMap<String, PluginInfo>>> = Arc::new(Mutex::new(HashMap::new()));
}

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct ManagerMeta {
    pub version: String,
    pub description: String,
    pub plugins: Vec<PluginMeta>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginSource {
    pub name: String,
    pub url: String,
    pub description: String,
}

#[derive(Debug, Serialize)]
pub struct PluginInfo {
    pub source: PluginSource,
    pub meta: PluginMeta,
    pub installed_version: String,
    pub invalid_reason: String,
}

static PLUGIN_SOURCE_LOCAL: &str = "local";

fn get_plugin_source_list() -> Vec<PluginSource> {
    // Only one source for now.
    // vec![PluginSource {
    //     name: "rustdesk".to_string(),
    //     url: "https://raw.githubusercontent.com/fufesou/rustdesk-plugins/main".to_string(),
    //     description: "".to_string(),
    // }]
    vec![]
}

fn get_source_plugins() -> HashMap<String, PluginInfo> {
    let mut plugins = HashMap::new();
    for source in get_plugin_source_list().into_iter() {
        let url = format!("{}/meta.toml", source.url);
        match create_http_client().get(&url).send() {
            Ok(resp) => {
                if !resp.status().is_success() {
                    log::error!(
                        "Failed to get plugin list from '{}', status code: {}",
                        url,
                        resp.status()
                    );
                }
                if let Ok(text) = resp.text() {
                    match toml::from_str::<ManagerMeta>(&text) {
                        Ok(manager_meta) => {
                            for meta in manager_meta.plugins.iter() {
                                if !meta
                                    .platforms
                                    .to_uppercase()
                                    .contains(&PLUGIN_PLATFORM.to_uppercase())
                                {
                                    continue;
                                }
                                plugins.insert(
                                    meta.id.clone(),
                                    PluginInfo {
                                        source: source.clone(),
                                        meta: meta.clone(),
                                        installed_version: "".to_string(),
                                        invalid_reason: "".to_string(),
                                    },
                                );
                            }
                        }
                        Err(e) => log::error!("Failed to parse plugin list from '{}', {}", url, e),
                    }
                }
            }
            Err(e) => log::error!("Failed to get plugin list from '{}', {}", url, e),
        }
    }
    plugins
}

fn send_plugin_list_event(plugins: &HashMap<String, PluginInfo>) {
    let mut plugin_list = plugins.values().collect::<Vec<_>>();
    plugin_list.sort_by(|a, b| a.meta.name.cmp(&b.meta.name));
    if let Ok(plugin_list) = serde_json::to_string(&plugin_list) {
        let mut m = HashMap::new();
        m.insert("name", MSG_TO_UI_TYPE_PLUGIN_MANAGER);
        m.insert(MSG_TO_UI_PLUGIN_MANAGER_LIST, &plugin_list);
        if let Ok(event) = serde_json::to_string(&m) {
            let _res = flutter::push_global_event(flutter::APP_TYPE_MAIN, event.clone());
        }
    }
}

pub fn load_plugin_list() {
    let mut plugin_info_lock = PLUGIN_INFO.lock().unwrap();
    let mut plugins = get_source_plugins();

    // A big read lock is needed to prevent race conditions.
    // Loading plugin list may be slow.
    // Users may call uninstall plugin in the middle.
    let plugin_infos = super::plugins::get_plugin_infos();
    let plugin_infos_read_lock = plugin_infos.read().unwrap();
    for (id, info) in plugin_infos_read_lock.iter() {
        if info.uninstalled {
            continue;
        }

        if let Some(p) = plugins.get_mut(id) {
            p.installed_version = info.desc.meta().version.clone();
            p.invalid_reason = "".to_string();
        } else {
            plugins.insert(
                id.to_string(),
                PluginInfo {
                    source: PluginSource {
                        name: PLUGIN_SOURCE_LOCAL.to_string(),
                        url: PLUGIN_SOURCE_LOCAL_DIR.to_string(),
                        description: "".to_string(),
                    },
                    meta: info.desc.meta().clone(),
                    installed_version: info.desc.meta().version.clone(),
                    invalid_reason: "".to_string(),
                },
            );
        }
    }
    send_plugin_list_event(&plugins);
    *plugin_info_lock = plugins;
}

#[cfg(target_os = "windows")]
fn elevate_install(
    plugin_id: &str,
    plugin_url: &str,
    same_plugin_exists: bool,
) -> ResultType<bool> {
    // to-do: Support args with space in quotes. 'arg 1' and "arg 2"
    let args = if same_plugin_exists {
        format!("--plugin-install {}", plugin_id)
    } else {
        format!("--plugin-install {} {}", plugin_id, plugin_url)
    };
    crate::platform::elevate(&args)
}

#[cfg(target_os = "linux")]
fn elevate_install(
    plugin_id: &str,
    plugin_url: &str,
    same_plugin_exists: bool,
) -> ResultType<bool> {
    let mut args = vec!["--plugin-install", plugin_id];
    if !same_plugin_exists {
        args.push(&plugin_url);
    }
    crate::platform::elevate(args)
}

#[cfg(target_os = "macos")]
fn elevate_install(
    plugin_id: &str,
    plugin_url: &str,
    same_plugin_exists: bool,
) -> ResultType<bool> {
    let mut args = vec!["--plugin-install", plugin_id];
    if !same_plugin_exists {
        args.push(&plugin_url);
    }
    crate::platform::elevate(args, "NetExperts Support wants to install the plugin")
}

#[inline]
#[cfg(target_os = "windows")]
fn elevate_uninstall(plugin_id: &str) -> ResultType<bool> {
    crate::platform::elevate(&format!("--plugin-uninstall {}", plugin_id))
}

#[inline]
#[cfg(target_os = "linux")]
fn elevate_uninstall(plugin_id: &str) -> ResultType<bool> {
    crate::platform::elevate(vec!["--plugin-uninstall", plugin_id])
}

#[inline]
#[cfg(target_os = "macos")]
fn elevate_uninstall(plugin_id: &str) -> ResultType<bool> {
    crate::platform::elevate(
        vec!["--plugin-uninstall", plugin_id],
        "NetExperts Support wants to uninstall the plugin",
    )
}

pub fn install_plugin(id: &str) -> ResultType<()> {
    match PLUGIN_INFO.lock().unwrap().get(id) {
        Some(plugin) => {
            let mut same_plugin_exists = false;
            if let Some(version) = super::plugins::get_version(id) {
                if version == plugin.meta.version {
                    same_plugin_exists = true;
                }
            }
            let plugin_url = format!(
                "{}/plugins/{}/{}/{}_{}.zip",
                plugin.source.url,
                plugin.meta.id,
                PLUGIN_PLATFORM,
                plugin.meta.id,
                plugin.meta.version
            );
            let allowed_install = elevate_install(id, &plugin_url, same_plugin_exists)?;
            if allowed_install && same_plugin_exists {
                super::ipc::load_plugin(id)?;
                super::plugins::load_plugin(id)?;
                super::plugins::mark_uninstalled(id, false);
                push_install_event(id, "finished");
            }
            Ok(())
        }
        None => {
            bail!("Plugin not found: {}", id);
        }
    }
}

fn get_uninstalled_plugins(uninstalled_plugin_set: &HashSet<String>) -> ResultType<Vec<String>> {
    let plugins_dir = super::get_plugins_dir()?;
    let mut plugins = Vec::new();
    if plugins_dir.exists() {
        for entry in std::fs::read_dir(plugins_dir)? {
            match entry {
                Ok(entry) => {
                    let plugin_dir = entry.path();
                    if plugin_dir.is_dir() {
                        if let Some(id) = plugin_dir.file_name().and_then(|n| n.to_str()) {
                            if uninstalled_plugin_set.contains(id) {
                                plugins.push(id.to_string());
                            }
                        }
                    }
                }
                Err(e) => {
                    log::error!("Failed to read plugins dir entry, {}", e);
                }
            }
        }
    }
    Ok(plugins)
}

pub fn remove_uninstalled() -> ResultType<()> {
    let mut uninstalled_plugin_set = get_uninstall_id_set()?;
    for id in get_uninstalled_plugins(&uninstalled_plugin_set)?.iter() {
        super::config::remove(id as _);
        if let Ok(dir) = super::get_plugin_dir(id as _) {
            allow_err!(remove_dir_all(dir.clone()));
            if !dir.exists() {
                uninstalled_plugin_set.remove(id);
            }
        }
    }
    allow_err!(update_uninstall_id_set(uninstalled_plugin_set));
    Ok(())
}

pub fn uninstall_plugin(id: &str, called_by_ui: bool) {
    if called_by_ui {
        match elevate_uninstall(id) {
            Ok(true) => {
                if let Err(e) = super::ipc::uninstall_plugin(id) {
                    log::error!("Failed to uninstall plugin '{}': {}", id, e);
                    push_uninstall_event(id, "failed");
                    return;
                }
                super::plugins::unload_plugin(id);
                super::plugins::mark_uninstalled(id, true);
                super::config::remove(id);
                push_uninstall_event(id, "");
            }
            Ok(false) => {
                return;
            }
            Err(e) => {
                log::error!(
                    "Failed to uninstall plugin '{}', check permission error: {}",
                    id,
                    e
                );
                push_uninstall_event(id, "failed");
                return;
            }
        }
    }

    if super::is_server_running() {
        super::plugins::unload_plugin(&id);
    }
}

fn push_event(id: &str, r#type: &str, msg: &str) {
    let mut m = HashMap::new();
    m.insert("name", MSG_TO_UI_TYPE_PLUGIN_MANAGER);
    m.insert("id", id);
    m.insert(r#type, msg);
    if let Ok(event) = serde_json::to_string(&m) {
        let _res = flutter::push_global_event(flutter::APP_TYPE_MAIN, event.clone());
    }
}

#[inline]
fn push_uninstall_event(id: &str, msg: &str) {
    push_event(id, MSG_TO_UI_PLUGIN_MANAGER_UNINSTALL, msg);
}

#[inline]
fn push_install_event(id: &str, msg: &str) {
    push_event(id, MSG_TO_UI_PLUGIN_MANAGER_INSTALL, msg);
}

async fn handle_conn(mut stream: crate::ipc::Connection) {
    loop {
        tokio::select! {
            res = stream.next() => {
                match res {
                    Err(err) => {
                        log::trace!("plugin ipc connection closed: {}", err);
                        break;
                    }
                    Ok(Some(data)) => {
                        match &data {
                            crate::ipc::Data::Plugin(super::ipc::Plugin::InstallStatus((id, status))) => {
                                match status {
                                    InstallStatus::Downloading(n) => {
                                        push_install_event(&id, &format!("downloading-{}", n));
                                    },
                                    InstallStatus::Installing => {
                                        push_install_event(&id, "installing");
                                    }
                                    InstallStatus::Finished => {
                                        allow_err!(super::plugins::load_plugin(&id));
                                        allow_err!(super::ipc::load_plugin_async(id).await);
                                        std::thread::spawn(load_plugin_list);
                                        push_install_event(&id, "finished");
                                    }
                                    InstallStatus::FailedCreating => {
                                        push_install_event(&id, "failed-creating");
                                    }
                                    InstallStatus::FailedDownloading => {
                                        push_install_event(&id, "failed-downloading");
                                    }
                                    InstallStatus::FailedInstalling => {
                                        push_install_event(&id, "failed-installing");
                                    }
                                }
                            }
                            _ => {}
                        }
                    }
                    _ => {
                    }
                }
            }
        }
    }
}

#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[tokio::main]
pub async fn start_ipc() {
    match crate::ipc::new_listener(IPC_PLUGIN_POSTFIX).await {
        Ok(mut incoming) => {
            while let Some(result) = incoming.next().await {
                match result {
                    Ok(stream) => {
                        log::debug!("Got new connection");
                        tokio::spawn(handle_conn(crate::ipc::Connection::new(stream)));
                    }
                    Err(err) => {
                        log::error!("Couldn't get plugin client: {:?}", err);
                    }
                }
            }
        }
        Err(err) => {
            log::error!("Failed to start plugin ipc server: {}", err);
        }
    }
}

pub(super) fn get_uninstall_id_set() -> ResultType<HashSet<String>> {
    let uninstall_file_path = super::get_uninstall_file_path()?;
    if !uninstall_file_path.exists() {
        std::fs::create_dir_all(&super::get_plugins_dir()?)?;
        return Ok(HashSet::new());
    }
    let s = read_to_string(uninstall_file_path)?;
    Ok(serde_json::from_str::<HashSet<String>>(&s)?)
}

fn update_uninstall_id_set(set: HashSet<String>) -> ResultType<()> {
    let content = serde_json::to_string(&set)?;
    let file = OpenOptions::new()
        .write(true)
        .truncate(true)
        .create(true)
        .open(super::get_uninstall_file_path()?)?;
    let mut writer = std::io::BufWriter::new(file);
    writer.write_all(content.as_bytes())?;
    Ok(())
}

// install process
pub(super) mod install {
    use super::IPC_PLUGIN_POSTFIX;
    use crate::hbbs_http::create_http_client;
    use crate::{
        ipc::{connect, Data},
        plugin::ipc::{InstallStatus, Plugin},
    };
    use hbb_common::{allow_err, bail, log, tokio, ResultType};
    use std::{
        fs::File,
        io::{BufReader, BufWriter, Write},
        path::Path,
    };
    use zip::ZipArchive;

    #[tokio::main(flavor = "current_thread")]
    async fn send_install_status(id: &str, status: InstallStatus) {
        allow_err!(_send_install_status(id, status).await);
    }

    async fn _send_install_status(id: &str, status: InstallStatus) -> ResultType<()> {
        let mut c = connect(1_000, IPC_PLUGIN_POSTFIX).await?;
        c.send(&Data::Plugin(Plugin::InstallStatus((
            id.to_string(),
            status,
        ))))
        .await?;
        Ok(())
    }

    fn download_to_file(url: &str, file: File) -> ResultType<()> {
        let resp = match create_http_client().get(url).send() {
            Ok(resp) => resp,
            Err(e) => {
                bail!("get plugin from '{}', {}", url, e);
            }
        };

        if !resp.status().is_success() {
            bail!("get plugin from '{}', status code: {}", url, resp.status());
        }

        let mut writer = BufWriter::new(file);
        writer.write_all(resp.bytes()?.as_ref())?;
        Ok(())
    }

    fn download_file(id: &str, url: &str, filename: &Path) -> bool {
        let file = match File::create(filename) {
            Ok(f) => f,
            Err(e) => {
                log::error!("Failed to create plugin file: {}", e);
                send_install_status(id, InstallStatus::FailedCreating);
                return false;
            }
        };
        if let Err(e) = download_to_file(url, file) {
            log::error!("Failed to download plugin '{}', {}", id, e);
            send_install_status(id, InstallStatus::FailedDownloading);
            return false;
        }
        true
    }

    fn do_install_file(filename: &Path, target_dir: &Path) -> ResultType<()> {
        let mut zip = ZipArchive::new(BufReader::new(File::open(filename)?))?;
        for i in 0..zip.len() {
            let mut file = zip.by_index(i)?;
            let file_path = target_dir.join(file.name());
            if file.name().ends_with("/") {
                std::fs::create_dir_all(&file_path)?;
            } else {
                if let Some(p) = file_path.parent() {
                    if !p.exists() {
                        std::fs::create_dir_all(&p)?;
                    }
                }
                let mut outfile = File::create(&file_path)?;
                std::io::copy(&mut file, &mut outfile)?;
            }
        }
        Ok(())
    }

    pub fn change_uninstall_plugin(id: &str, add: bool) {
        match super::get_uninstall_id_set() {
            Ok(mut set) => {
                if add {
                    set.insert(id.to_string());
                } else {
                    set.remove(id);
                }
                if let Err(e) = super::update_uninstall_id_set(set) {
                    log::error!("Failed to write uninstall list, {}", e);
                }
            }
            Err(e) => log::error!(
                "Failed to get plugins dir, unable to read uninstall list, {}",
                e
            ),
        }
    }

    pub fn install_plugin_with_url(id: &str, url: &str) {
        log::info!("Installing plugin '{}', url: {}", id, url);
        let plugin_dir = match super::super::get_plugin_dir(id) {
            Ok(d) => d,
            Err(e) => {
                send_install_status(id, InstallStatus::FailedCreating);
                log::error!("Failed to get plugin dir: {}", e);
                return;
            }
        };
        if !plugin_dir.exists() {
            if let Err(e) = std::fs::create_dir_all(&plugin_dir) {
                send_install_status(id, InstallStatus::FailedCreating);
                log::error!("Failed to create plugin dir: {}", e);
                return;
            }
        }

        let filename = match url.rsplit('/').next() {
            Some(filename) => plugin_dir.join(filename),
            None => {
                send_install_status(id, InstallStatus::FailedDownloading);
                log::error!("Failed to download plugin file, invalid url: {}", url);
                return;
            }
        };

        let filename_to_remove = filename.clone();
        let _call_on_ret = crate::common::SimpleCallOnReturn {
            b: true,
            f: Box::new(move || {
                if let Err(e) = std::fs::remove_file(&filename_to_remove) {
                    log::error!("Failed to remove plugin file: {}", e);
                }
            }),
        };

        // download
        if !download_file(id, url, &filename) {
            return;
        }

        // install
        send_install_status(id, InstallStatus::Installing);
        if let Err(e) = do_install_file(&filename, &plugin_dir) {
            log::error!("Failed to install plugin: {}", e);
            send_install_status(id, InstallStatus::FailedInstalling);
            return;
        }

        // finished
        send_install_status(id, InstallStatus::Finished);
    }
}
