use std::{
    collections::{HashMap, HashSet},
    fmt::Display,
    path::Path,
    sync::{
        Arc, Mutex,
        mpsc::{self, Receiver},
    },
};

use filthy_rich::PresenceClient;
use iced::Task;
use notify::Watcher;
use ql_core::{
    GenericProgress, Instance, InstanceKind, IntoIoError, IntoStringError, IoError, JsonFileError,
    LAUNCHER_DIR, LAUNCHER_VERSION_NAME, LaunchedProcess, Progress, err,
    file_utils::{self, exists},
    read_log::LogLine,
};
use ql_instances::auth::{AccountData, AccountType, ms::CLIENT_ID};
use tokio::process::ChildStdin;

use crate::{
    config::{LauncherConfig, SIDEBAR_WIDTH},
    message_update::PresenceConnectionState,
    stylesheet::styles::LauncherTheme,
};

mod images;
mod menu;
mod message;
pub use images::ImageState;
pub use menu::*;
pub use message::*;

pub const OFFLINE_ACCOUNT_NAME: &str = "(Offline)";
pub const NEW_ACCOUNT_NAME: &str = "+ Add Account";

pub const ADD_JAR_NAME: &str = "+ Add JAR";
pub const REMOVE_JAR_NAME: &str = "- Remove Selected";
pub const OPEN_FOLDER_JAR_NAME: &str = "> Open Folder";
pub const NONE_JAR_NAME: &str = "(None)";

type Res<T = ()> = Result<T, String>;

pub struct InstanceLog {
    pub log: Vec<String>,
    pub has_crashed: bool,
    pub command: String,
}

pub struct Launcher {
    pub state: State,
    pub selected_instance: Option<Instance>,
    pub config: LauncherConfig,
    pub theme: LauncherTheme,
    pub images: ImageState,

    pub is_log_open: bool,
    pub log_scroll: isize,
    pub tick_timer: usize,
    pub is_launching_game: bool,

    pub discord_ipc_client: Option<PresenceClient>,
    pub discord_connection_state: Arc<Mutex<PresenceConnectionState>>,

    pub java_recv: Option<ProgressBar<GenericProgress>>,
    pub custom_jar: Option<CustomJarState>,
    /// See [`AutoSaveKind`]
    pub autosave: HashSet<AutoSaveKind>,

    pub accounts: HashMap<String, AccountData>,
    pub accounts_dropdown: Vec<String>,
    pub account_selected: String,

    pub client_list: Option<Vec<String>>,
    pub server_list: Option<Vec<String>>,
    pub client_watcher: Option<DirWatcher>,
    pub server_watcher: Option<DirWatcher>,

    pub processes: HashMap<Instance, GameProcess>,
    pub logs: HashMap<Instance, InstanceLog>,

    pub window_state: WindowState,
    pub keys_pressed: HashSet<iced::keyboard::Key>,
    pub modifiers_pressed: iced::keyboard::Modifiers,
}

/// Used to temporarily "block" auto-saving something,
/// or indicate it was already saved.
///
/// On the [`Launcher`] struct,
///
/// - Use `self.autosave.remove(n)`
///   to indicate a change was made
/// - Use `self.autosave.insert(n)`
///   to indicate it was saved, and doesn't need saving again
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum AutoSaveKind {
    LauncherConfig,
    InstanceConfig,
    Jarmods,
}

pub struct WindowState {
    pub size: (f32, f32),
    pub mouse_pos: (f32, f32),
    pub is_maximized: bool,
}

pub struct CustomJarState {
    pub choices: Vec<String>,
    pub watcher: DirWatcher,
}

impl CustomJarState {
    pub fn load() -> Task<Message> {
        Task::perform(load_custom_jars(), |n| {
            EditInstanceMessage::CustomJarLoaded(n.strerr()).into()
        })
    }
}

pub struct DirWatcher {
    recv: Receiver<notify::Event>,
    _watcher: notify::RecommendedWatcher,
}

impl DirWatcher {
    pub fn has_changed(&self) -> bool {
        let mut has_changed = false;
        while let Ok(_event) = self.recv.try_recv() {
            has_changed = true;
        }
        has_changed
    }
}

pub struct GameProcess {
    pub child: LaunchedProcess,
    pub receiver: Option<Receiver<LogLine>>,
    pub server_input: Option<(ChildStdin, bool)>,
}

impl Launcher {
    pub fn load_new(
        is_new_user: bool,
        config: Result<LauncherConfig, JsonFileError>,
    ) -> Result<Self, JsonFileError> {
        if let Err(err) = file_utils::get_launcher_dir() {
            err!("Could not get launcher dir (This is a bug):");
            return Ok(Self::with_error(format!(
                "Could not get launcher dir: {err}"
            )));
        }

        let mut config = config?;
        let theme = config.c_theme();
        let (window_width, window_height) = config.c_window_size();

        let mut launch = MenuLaunch::default();
        launch.resize_sidebar(SIDEBAR_WIDTH);
        let launch = State::Launch(launch);

        // The version field was added in 0.3
        let version = config.version.as_deref().unwrap_or("0.3.0");

        let state = if is_new_user {
            State::Welcome(MenuWelcome::P1InitialScreen)
        } else if version == LAUNCHER_VERSION_NAME {
            launch
        } else {
            if let Err(err) = migration(version) {
                err!(no_log, "{err}");
            }
            config.version = Some(LAUNCHER_VERSION_NAME.to_owned());
            State::ChangeLog
        };

        let (accounts, accounts_dropdown, account_selected) = load_accounts(&mut config);

        let persistent = config.c_persistent();
        let selected_instance = persistent
            .selected_instance
            .as_ref()
            .filter(|_| persistent.selected_remembered)
            .map(|n| {
                Instance::new(
                    n,
                    persistent
                        .selected_instance_kind
                        .unwrap_or(ql_core::InstanceKind::Client),
                )
            });

        Ok(Self {
            selected_instance,
            state,
            config,
            theme,
            accounts,
            accounts_dropdown,

            window_state: WindowState {
                size: (window_width, window_height),
                mouse_pos: (0.0, 0.0),
                is_maximized: false,
            },
            account_selected,

            client_list: None,
            server_list: None,
            client_watcher: None,
            server_watcher: None,
            java_recv: None,
            custom_jar: None,

            logs: HashMap::new(),
            processes: HashMap::new(),

            keys_pressed: HashSet::new(),

            is_log_open: false,
            is_launching_game: false,

            discord_ipc_client: None,
            discord_connection_state: Arc::new(Mutex::new(PresenceConnectionState::Uninitialized)),

            log_scroll: 0,
            tick_timer: 0,

            autosave: HashSet::new(),
            images: ImageState::default(),
            modifiers_pressed: iced::keyboard::Modifiers::empty(),
        })
    }

    pub fn with_error(error: impl Display) -> Self {
        let error = error.to_string();
        let launcher_dir = if error.contains("Could not get launcher dir") {
            None
        } else {
            Some(LAUNCHER_DIR.clone())
        };

        let (config, theme) = launcher_dir
            .as_ref()
            .and_then(|_| {
                match LauncherConfig::load_s().map(|n| {
                    let theme = n.c_theme();
                    (n, theme)
                }) {
                    Ok(n) => Some(n),
                    Err(err) => {
                        err!("Error loading config: {err}");
                        None
                    }
                }
            })
            .unwrap_or((LauncherConfig::default(), LauncherTheme::default()));

        let (window_width, window_height) = config.c_window_size();

        Self {
            config,
            theme,

            state: State::Error { error },

            java_recv: None,
            client_list: None,
            server_list: None,
            client_watcher: None,
            server_watcher: None,
            selected_instance: None,
            custom_jar: None,

            is_log_open: false,
            is_launching_game: false,

            log_scroll: 0,
            tick_timer: 0,

            discord_ipc_client: None,
            discord_connection_state: Arc::new(Mutex::new(PresenceConnectionState::Uninitialized)),

            logs: HashMap::new(),
            processes: HashMap::new(),
            accounts: HashMap::new(),
            keys_pressed: HashSet::new(),

            images: ImageState::default(),
            window_state: WindowState {
                size: (window_width, window_height),
                mouse_pos: (0.0, 0.0),
                is_maximized: false,
            },
            autosave: HashSet::new(),
            accounts_dropdown: vec![OFFLINE_ACCOUNT_NAME.to_owned(), NEW_ACCOUNT_NAME.to_owned()],
            account_selected: OFFLINE_ACCOUNT_NAME.to_owned(),
            modifiers_pressed: iced::keyboard::Modifiers::empty(),
        }
    }

    pub fn instance(&self) -> &Instance {
        self.selected_instance.as_ref().unwrap()
    }

    #[allow(clippy::needless_pass_by_value)]
    pub fn set_error(&mut self, error: impl ToString) {
        let error = error.to_string().replace(CLIENT_ID, "[CLIENT ID]");
        err!("{error}");
        self.state = State::Error { error }
    }

    pub fn go_to_main_menu(&mut self, message: Option<InfoMessage>) -> Task<Message> {
        let mut menu_launch = MenuLaunch::new(message);
        menu_launch.resize_sidebar(SIDEBAR_WIDTH);
        let t = if let Some(inst) = &self.selected_instance {
            menu_launch.reload_notes(inst.clone())
        } else {
            Task::none()
        };
        self.state = State::Launch(menu_launch);

        t
    }
}

fn load_accounts(
    config: &mut LauncherConfig,
) -> (HashMap<String, AccountData>, Vec<String>, String) {
    let mut accounts = HashMap::new();

    let mut accounts_dropdown = vec![OFFLINE_ACCOUNT_NAME.to_owned(), NEW_ACCOUNT_NAME.to_owned()];

    let mut accounts_to_remove = Vec::new();

    for (username, account) in config.accounts.iter_mut().flatten() {
        load_account(
            &mut accounts,
            &mut accounts_dropdown,
            &mut accounts_to_remove,
            username,
            account,
        );
    }

    if let Some(accounts) = &mut config.accounts {
        for rem in accounts_to_remove {
            accounts.remove(&rem);
        }
    }

    let selected_account = config.account_selected.clone().unwrap_or(
        accounts_dropdown
            .first()
            .cloned()
            .unwrap_or_else(|| OFFLINE_ACCOUNT_NAME.to_owned()),
    );
    (accounts, accounts_dropdown, selected_account)
}

fn load_account(
    accounts: &mut HashMap<String, AccountData>,
    accounts_dropdown: &mut Vec<String>,
    accounts_to_remove: &mut Vec<String>,
    username: &str,
    account: &mut crate::config::ConfigAccount,
) {
    let account_type = if username.ends_with(" (elyby)") {
        AccountType::ElyBy
    } else if username.ends_with(" (littleskin)") {
        AccountType::LittleSkin
    } else {
        account.account_type.unwrap_or_default()
    };

    let keyring_username = account.get_keyring_identifier(username);
    let refresh_token =
        ql_instances::auth::read_refresh_token(keyring_username, account_type).strerr();

    let keyring_username = account.get_keyring_identifier(username);

    match refresh_token {
        Ok(refresh_token) => {
            accounts_dropdown.insert(0, username.to_owned());
            accounts.insert(
                username.to_owned(),
                AccountData {
                    access_token: None,
                    uuid: account.uuid.clone(),
                    refresh_token,
                    needs_refresh: true,
                    account_type,

                    username: keyring_username.to_owned(),
                    nice_username: account
                        .username_nice
                        .clone()
                        .unwrap_or_else(|| username.to_owned()),
                },
            );
        }
        Err(err) => {
            err!(
                "Could not load account: {err}\nUsername: {keyring_username}, Account Type: {}",
                account_type.to_string()
            );
            accounts_to_remove.push(username.to_owned());
        }
    }
}

pub async fn get_entries(kind: InstanceKind) -> Res<(Vec<String>, InstanceKind)> {
    let dir_path = kind.get_root_directory();
    if !exists(&dir_path).await {
        tokio::fs::create_dir_all(&dir_path)
            .await
            .path(&dir_path)
            .strerr()?;
        return Ok((Vec::new(), kind));
    }

    Ok((
        file_utils::read_filenames_from_dir(&dir_path)
            .await
            .strerr()?
            .into_iter()
            .filter(|n| !n.is_file)
            .map(|n| n.name)
            .collect(),
        kind,
    ))
}

pub struct ProgressBar<T: Progress> {
    pub num: f32,
    pub message: Option<String>,
    pub receiver: Receiver<T>,
    pub progress: T,
}

impl<T: Default + Progress> ProgressBar<T> {
    pub fn with_recv(receiver: Receiver<T>) -> Self {
        Self {
            num: 0.0,
            message: None,
            receiver,
            progress: T::default(),
        }
    }

    pub fn with_recv_and_msg(receiver: Receiver<T>, msg: String) -> Self {
        Self {
            num: 0.0,
            message: Some(msg),
            receiver,
            progress: T::default(),
        }
    }
}

impl<T: Progress> ProgressBar<T> {
    pub fn tick(&mut self) -> bool {
        let mut has_ticked = false;
        while let Ok(progress) = self.receiver.try_recv() {
            self.num = progress.get_num();
            self.message = progress.get_message();
            self.progress = progress;
            has_ticked = true;
        }
        has_ticked
    }
}

pub async fn load_custom_jars() -> Result<Vec<String>, IoError> {
    let names = file_utils::read_filenames_from_dir(LAUNCHER_DIR.join("custom_jars")).await?;
    let mut list: Vec<String> = names
        .into_iter()
        .filter(|n| n.is_file)
        .map(|n| n.name)
        .collect();

    list.insert(0, NONE_JAR_NAME.to_owned());
    list.push(ADD_JAR_NAME.to_owned());
    list.push(REMOVE_JAR_NAME.to_owned());
    list.push(OPEN_FOLDER_JAR_NAME.to_owned());

    Ok(list)
}

pub fn dir_watch<P: AsRef<Path>>(path: P) -> notify::Result<DirWatcher> {
    let (tx, recv) = mpsc::channel();

    // `notify` runs callbacks in its own thread.
    let mut watcher: notify::RecommendedWatcher = notify::recommended_watcher(move |res| {
        if let Ok(event) = res {
            _ = tx.send(event);
        }
    })?;
    let path = path.as_ref();
    watcher.watch(path, notify::RecursiveMode::NonRecursive)?;

    Ok(DirWatcher {
        recv,
        _watcher: watcher,
    })
}

fn migration(version: &str) -> Result<(), String> {
    fn ver(major: u64, minor: u64, patch: u64) -> semver::Version {
        semver::Version {
            major,
            minor,
            patch,
            pre: semver::Prerelease::default(),
            build: semver::BuildMetadata::default(),
        }
    }

    let version = version.strip_prefix("v").unwrap_or(version);
    let version = semver::Version::parse(version).strerr()?;

    if version <= ver(0, 4, 2) && (cfg!(target_os = "windows") || cfg!(target_os = "macos")) {
        // Mojang sneakily updated their Java 8 to fix certs.
        // Let's redownload it.
        let java_dir = LAUNCHER_DIR.join("java_installs/java_8");
        if java_dir.is_dir() {
            std::fs::remove_dir_all(&java_dir)
                .path(&java_dir)
                .strerr()?;
        }
    }
    Ok(())
}
