//! ## ActivityManager
//!
//! `activity_manager` is the module which provides run methods and handling for activities

use std::env;
use std::path::PathBuf;
use std::time::Duration;

use remotefs_ssh::SshKeyStorage as SshKeyStorageTrait;
use ssh2_config::SshConfig;

use crate::cli::{Remote, RemoteArgs};
use crate::filetransfer::{
    FileTransferParams, FileTransferProtocol, HostBridgeParams, ProtocolParams,
};
use crate::host::{HostError, HostErrorType};
use crate::system::bookmarks_client::BookmarksClient;
use crate::system::config_client::ConfigClient;
use crate::system::environment;
use crate::system::sshkey_storage::SshKeyStorage;
use crate::system::theme_provider::ThemeProvider;
use crate::ui::activities::auth::AuthActivity;
use crate::ui::activities::filetransfer::FileTransferActivity;
use crate::ui::activities::setup::SetupActivity;
use crate::ui::activities::{Activity, ExitReason};
use crate::ui::context::Context;
use crate::utils::{fmt, ssh as ssh_utils, tty};

/// NextActivity identifies the next identity to run once the current has ended
pub enum NextActivity {
    Authentication,
    FileTransfer,
    SetupActivity,
}

pub enum Host {
    HostBridge,
    Remote,
}

pub enum HostParams {
    HostBridge(HostBridgeParams),
    Remote(FileTransferParams),
}

/// The activity manager takes care of running activities and handling them until the application has ended
pub struct ActivityManager {
    context: Option<Context>,
    ticks: Duration,
}

impl ActivityManager {
    /// Initializes a new Activity Manager
    pub fn new(ticks: Duration, keyring: bool) -> Result<ActivityManager, HostError> {
        // Prepare Context
        // Initialize configuration client
        let (config_client, error_config): (ConfigClient, Option<String>) =
            match Self::init_config_client() {
                Ok(cli) => (cli, None),
                Err(err) => {
                    error!("Failed to initialize config client: {}", err);
                    (ConfigClient::degraded(), Some(err))
                }
            };
        let (bookmarks_client, error_bookmark) = match Self::init_bookmarks_client(keyring) {
            Ok(cli) => (cli, None),
            Err(err) => (None, Some(err)),
        };
        let error = error_config.or(error_bookmark);
        let theme_provider: ThemeProvider = Self::init_theme_provider();
        let ssh_config = config_client
            .get_ssh_config()
            .map(ssh_utils::parse_ssh2_config)
            .transpose()
            .map_err(|err| HostError::from(HostErrorType::InvalidSshConfig(err)))?;

        let ctx: Context = Context::new(
            bookmarks_client,
            config_client,
            theme_provider,
            ssh_config,
            error,
        );
        Ok(ActivityManager {
            context: Some(ctx),
            ticks,
        })
    }

    /// Configure remote args
    pub fn configure_remote_args(&mut self, remote_args: RemoteArgs) -> Result<(), String> {
        // Set for host bridge
        match remote_args.host_bridge {
            Remote::Bookmark(params) => self.resolve_bookmark_name(
                Host::HostBridge,
                &params.name,
                params.password.as_deref(),
            ),
            Remote::Host(host_params) => {
                let params = apply_ssh_config_to_omitted_cli_parameters(
                    host_params.file_transfer_params,
                    host_params.port_explicit,
                    self.context_ref()?.ssh_config(),
                );
                self.set_host_params(
                    HostParams::HostBridge(HostBridgeParams::Remote(
                        params.protocol,
                        params.params,
                    )),
                    host_params.password.as_deref(),
                )
            }
            Remote::None => {
                // local dir is remote_args.local_dir if set, otherwise current dir
                let local_dir = remote_args
                    .local_dir
                    .map(Ok)
                    .unwrap_or_else(env::current_dir)
                    .map_err(|err| format!("Could not resolve current directory: {err}"))?;
                debug!("host bridge is None, setting local dir to {:?}", local_dir,);

                self.set_host_params(
                    HostParams::HostBridge(HostBridgeParams::Localhost(local_dir)),
                    None,
                )
            }
        }?;

        // set remote
        match remote_args.remote {
            Remote::Bookmark(params) => {
                self.resolve_bookmark_name(Host::Remote, &params.name, params.password.as_deref())
            }
            Remote::Host(host_params) => self.set_host_params(
                HostParams::Remote(apply_ssh_config_to_omitted_cli_parameters(
                    host_params.file_transfer_params,
                    host_params.port_explicit,
                    self.context_ref()?.ssh_config(),
                )),
                host_params.password.as_deref(),
            ),
            Remote::None => Ok(()),
        }
    }

    /// Set file transfer params
    pub fn set_host_params(
        &mut self,
        host: HostParams,
        password: Option<&str>,
    ) -> Result<(), String> {
        let (remote_local_path, remote_remote_path) = match &host {
            HostParams::Remote(params) => (params.local_path.clone(), params.remote_path.clone()),
            _ => (None, None),
        };

        let mut remote_params = match &host {
            HostParams::HostBridge(HostBridgeParams::Remote(protocol, protocol_params)) => {
                Some((*protocol, protocol_params.clone()))
            }
            HostParams::HostBridge(HostBridgeParams::Localhost(_)) => None,
            HostParams::Remote(ft_params) => Some((ft_params.protocol, ft_params.params.clone())),
        };

        // Put params into the context
        if let Some((protocol, params)) = remote_params.as_mut() {
            self.resolve_password_for_protocol_params(*protocol, params, password)?;
        }

        match host {
            HostParams::HostBridge(HostBridgeParams::Localhost(path)) => {
                self.context_mut()?
                    .set_host_bridge_params(HostBridgeParams::Localhost(path));
            }
            HostParams::HostBridge(HostBridgeParams::Remote(_, _)) => {
                let (protocol, params) = remote_params.ok_or_else(|| {
                    String::from("Missing remote parameters for host bridge configuration")
                })?;
                self.context_mut()?
                    .set_host_bridge_params(HostBridgeParams::Remote(protocol, params));
            }
            HostParams::Remote(_) => {
                let (protocol, params) = remote_params
                    .ok_or_else(|| String::from("Missing remote parameters for remote host"))?;
                let params = FileTransferParams {
                    local_path: remote_local_path,
                    remote_path: remote_remote_path,
                    protocol,
                    params,
                };
                self.context_mut()?.set_remote_params(params);
            }
        }
        Ok(())
    }

    fn resolve_password_for_protocol_params(
        &mut self,
        protocol: FileTransferProtocol,
        params: &mut ProtocolParams,
        password: Option<&str>,
    ) -> Result<(), String> {
        // Set password if provided
        if params.password_missing() {
            if let Some(password) = password {
                params.set_default_secret(password.to_string());
            } else if matches!(
                protocol,
                FileTransferProtocol::Scp | FileTransferProtocol::Sftp,
            ) && params.generic_params().is_some()
            {
                // * if protocol is SCP or SFTP check whether a SSH key is registered for this remote, in case not ask password
                let storage = SshKeyStorage::from(self.context_ref()?.config());
                let generic_params = params.generic_params().ok_or_else(|| {
                    String::from("Missing generic parameters for SSH password resolution")
                })?;
                let username = generic_params
                    .username
                    .clone()
                    .map(Ok)
                    .unwrap_or_else(whoami::username)
                    .map_err(|err| format!("Could not get current username: {err}"))?;

                if storage
                    .resolve(&generic_params.address, &username)
                    .is_none()
                {
                    debug!(
                        "storage could not find any suitable key for {}... prompting for password",
                        generic_params.address
                    );
                    self.prompt_password(params)?;
                } else {
                    debug!(
                        "a key is already set for {}; password is not required",
                        generic_params.address
                    );
                }
            } else {
                self.prompt_password(params)?;
            }
        }

        Ok(())
    }

    /// Prompt user for password to set into params.
    fn prompt_password(&mut self, params: &mut ProtocolParams) -> Result<(), String> {
        let ctx = self.context_mut()?;
        let prompt = format!("Password for {}: ", params.host_name());

        match tty::read_secret_from_tty(ctx.terminal(), prompt) {
            Err(err) => Err(format!("Could not read password: {err}")),
            Ok(Some(secret)) => {
                debug!(
                    "Read password from tty: {}",
                    fmt::shadow_password(secret.as_str())
                );
                params.set_default_secret(secret);
                Ok(())
            }
            Ok(None) => Ok(()),
        }
    }

    /// Resolve provided bookmark name and set it as file transfer params.
    /// Returns error if bookmark is not found
    pub fn resolve_bookmark_name(
        &mut self,
        host: Host,
        bookmark_name: &str,
        password: Option<&str>,
    ) -> Result<(), String> {
        if let Some(bookmarks_client) = self.context_mut()?.bookmarks_client_mut() {
            let params = match bookmarks_client.get_bookmark(bookmark_name) {
                None => {
                    return Err(format!(
                        r#"Could not resolve bookmark name: "{bookmark_name}" no such bookmark"#
                    ));
                }
                Some(params) => params,
            };

            let params = match host {
                Host::Remote => HostParams::Remote(params),
                Host::HostBridge => {
                    HostParams::HostBridge(HostBridgeParams::Remote(params.protocol, params.params))
                }
            };

            self.set_host_params(params, password)
        } else {
            Err(String::from(
                "Could not resolve bookmark name: bookmarks client not initialized",
            ))
        }
    }

    fn context_mut(&mut self) -> Result<&mut Context, String> {
        self.context
            .as_mut()
            .ok_or_else(|| String::from("Activity manager context is not initialized"))
    }

    fn context_ref(&self) -> Result<&Context, String> {
        self.context
            .as_ref()
            .ok_or_else(|| String::from("Activity manager context is not initialized"))
    }

    ///
    /// Loop for activity manager. You need to provide the activity to start with
    /// Returns the exitcode
    pub fn run(&mut self, launch_activity: NextActivity) {
        let mut current_activity: Option<NextActivity> = Some(launch_activity);
        loop {
            current_activity = match current_activity {
                Some(activity) => match activity {
                    NextActivity::Authentication => self.run_authentication(),
                    NextActivity::FileTransfer => self.run_filetransfer(),
                    NextActivity::SetupActivity => self.run_setup(),
                },
                None => break, // Exit
            }
        }
        // Drop context
        drop(self.context.take());
    }

    // -- Activity Loops

    /// Loop for Authentication activity.
    /// Returns when activity terminates.
    /// Returns the next activity to run
    fn run_authentication(&mut self) -> Option<NextActivity> {
        info!("Starting AuthActivity...");
        // Prepare activity
        let mut activity: AuthActivity = AuthActivity::new(self.ticks);
        // Prepare result
        let result: Option<NextActivity>;
        // Get context
        let ctx: Context = match self.context.take() {
            Some(ctx) => ctx,
            None => {
                error!("Failed to start AuthActivity: context is None");
                return None;
            }
        };
        // Create activity
        activity.on_create(ctx);
        loop {
            // Draw activity
            activity.on_draw();
            // Check if has to be terminated
            if let Some(exit_reason) = activity.will_umount() {
                match exit_reason {
                    ExitReason::Quit => {
                        info!("AuthActivity terminated due to 'Quit'");
                        result = None;
                        break;
                    }
                    ExitReason::EnterSetup => {
                        // User requested activity
                        info!("AuthActivity terminated due to 'EnterSetup'");
                        result = Some(NextActivity::SetupActivity);
                        break;
                    }
                    ExitReason::Connect => {
                        // User submitted, set next activity
                        info!("AuthActivity terminated due to 'Connect'");
                        result = Some(NextActivity::FileTransfer);
                        break;
                    }
                    _ => { /* Nothing to do */ }
                }
            }
        }
        // Destroy activity
        self.context = activity.on_destroy();
        info!("AuthActivity destroyed");
        result
    }

    /// Loop for FileTransfer activity.
    /// Returns when activity terminates.
    /// Returns the next activity to run
    fn run_filetransfer(&mut self) -> Option<NextActivity> {
        info!("Starting FileTransferActivity");
        // Get context
        let mut ctx: Context = match self.context.take() {
            Some(ctx) => ctx,
            None => {
                error!("Failed to start FileTransferActivity: context is None");
                return None;
            }
        };

        let host_bridge_params = match ctx.host_bridge_params() {
            Some(params) => params.clone(),
            None => {
                error!("Failed to start FileTransferActivity: host bridge params is None");
                return None;
            }
        };

        // If ft params is None, return None
        let remote_params: &FileTransferParams = match ctx.remote_params() {
            Some(ft_params) => ft_params,
            None => {
                error!("Failed to start FileTransferActivity: file transfer params is None");
                return None;
            }
        };

        // try to setup activity
        let mut activity =
            match FileTransferActivity::new(host_bridge_params, remote_params, self.ticks) {
                Ok(activity) => activity,
                Err(err) => {
                    error!("Failed to start FileTransferActivity: {}", err);
                    ctx.set_error(err);
                    self.context = Some(ctx);
                    // Return to authentication
                    return Some(NextActivity::Authentication);
                }
            };
        // Prepare result
        let result: Option<NextActivity>;
        // Create activity
        activity.on_create(ctx);
        loop {
            // Draw activity
            activity.on_draw();
            // Check if has to be terminated
            if let Some(exit_reason) = activity.will_umount() {
                match exit_reason {
                    ExitReason::Quit => {
                        info!("FileTransferActivity terminated due to 'Quit'");
                        result = None;
                        break;
                    }
                    ExitReason::Disconnect => {
                        // User disconnected, set next activity to authentication
                        info!("FileTransferActivity terminated due to 'Authentication'");
                        result = Some(NextActivity::Authentication);
                        break;
                    }
                    _ => { /* Nothing to do */ }
                }
            }
        }
        // Destroy activity
        self.context = activity.on_destroy();
        result
    }

    /// `SetupActivity` run loop.
    /// Returns when activity terminates.
    /// Returns the next activity to run
    fn run_setup(&mut self) -> Option<NextActivity> {
        // Prepare activity
        let mut activity: SetupActivity = SetupActivity::new(self.ticks);
        // Get context
        let ctx: Context = match self.context.take() {
            Some(ctx) => ctx,
            None => {
                error!("Failed to start SetupActivity: context is None");
                return None;
            }
        };
        // Create activity
        activity.on_create(ctx);
        loop {
            // Draw activity
            activity.on_draw();
            // Check if activity has terminated
            if let Some(ExitReason::Quit) = activity.will_umount() {
                info!("SetupActivity terminated due to 'Quit'");
                break;
            }
        }
        // Destroy activity
        self.context = activity.on_destroy();
        // This activity always returns to AuthActivity
        Some(NextActivity::Authentication)
    }

    // -- misc

    fn init_bookmarks_client(keyring: bool) -> Result<Option<BookmarksClient>, String> {
        crate::support::bookmarks_client(keyring)
    }

    /// Initialize configuration client
    fn init_config_client() -> Result<ConfigClient, String> {
        // Get config dir
        match environment::init_config_dir() {
            Ok(config_dir) => {
                match config_dir {
                    Some(config_dir) => {
                        // Get config client paths
                        let (config_path, ssh_dir): (PathBuf, PathBuf) =
                            environment::get_config_paths(config_dir.as_path());
                        match ConfigClient::new(config_path.as_path(), ssh_dir.as_path()) {
                            Ok(cli) => Ok(cli),
                            Err(err) => Err(format!("Could not read configuration: {err}")),
                        }
                    }
                    None => Err(String::from(
                        "Your system doesn't provide a configuration directory",
                    )),
                }
            }
            Err(err) => Err(format!(
                "Could not initialize configuration directory: {err}"
            )),
        }
    }

    fn init_theme_provider() -> ThemeProvider {
        match environment::init_config_dir() {
            Ok(config_dir) => {
                match config_dir {
                    Some(config_dir) => {
                        // Get config client paths
                        let theme_path: PathBuf = environment::get_theme_path(config_dir.as_path());
                        match ThemeProvider::new(theme_path.as_path()) {
                            Ok(provider) => provider,
                            Err(err) => {
                                error!(
                                    "Could not initialize theme provider with file '{}': {}; using theme provider in degraded mode",
                                    theme_path.display(),
                                    err
                                );
                                ThemeProvider::degraded()
                            }
                        }
                    }
                    None => {
                        error!(
                            "This system doesn't provide a configuration directory; using theme provider in degraded mode"
                        );
                        ThemeProvider::degraded()
                    }
                }
            }
            Err(err) => {
                error!(
                    "Could not initialize configuration directory: {}; using theme provider in degraded mode",
                    err
                );
                ThemeProvider::degraded()
            }
        }
    }
}

/// Applies SSH configuration values only to CLI parameters omitted by the user.
fn apply_ssh_config_to_omitted_cli_parameters(
    mut file_transfer_params: FileTransferParams,
    port_explicit: bool,
    ssh_config: Option<&SshConfig>,
) -> FileTransferParams {
    if !matches!(
        file_transfer_params.protocol,
        FileTransferProtocol::Scp | FileTransferProtocol::Sftp,
    ) {
        return file_transfer_params;
    }

    if let ProtocolParams::Generic(params) = &mut file_transfer_params.params {
        let resolved = ssh_utils::resolve_ssh_host_params(ssh_config, params.address.as_str());
        if !port_explicit {
            params.port = resolved.port;
        }
        if params.username.is_none() {
            params.username = resolved.username;
        }
    }

    file_transfer_params
}

#[cfg(test)]
mod test {
    use pretty_assertions::assert_eq;

    use super::apply_ssh_config_to_omitted_cli_parameters;
    use crate::filetransfer::params::GenericProtocolParams;
    use crate::filetransfer::{FileTransferParams, FileTransferProtocol, ProtocolParams};
    use crate::utils::ssh::parse_ssh2_config;
    use crate::utils::test_helpers;

    #[test]
    fn should_apply_ssh_config_to_omitted_cli_port_and_username() {
        let config = ssh_config();
        let params = ssh_params(22, None);

        let resolved = apply_ssh_config_to_omitted_cli_parameters(params, false, Some(&config));
        let resolved = resolved.params.generic_params().unwrap();

        assert_eq!(resolved.port, 2222);
        assert_eq!(resolved.username.as_deref(), Some("configured-user"));
    }

    #[test]
    fn should_preserve_explicit_cli_port_over_ssh_config() {
        let config = ssh_config();
        let params = ssh_params(22, None);

        let resolved = apply_ssh_config_to_omitted_cli_parameters(params, true, Some(&config));
        let resolved = resolved.params.generic_params().unwrap();

        assert_eq!(resolved.port, 22);
        assert_eq!(resolved.username.as_deref(), Some("configured-user"));
    }

    #[test]
    fn should_preserve_explicit_cli_username_over_ssh_config() {
        let config = ssh_config();
        let params = ssh_params(22, Some("cli-user"));

        let resolved = apply_ssh_config_to_omitted_cli_parameters(params, false, Some(&config));
        let resolved = resolved.params.generic_params().unwrap();

        assert_eq!(resolved.port, 2222);
        assert_eq!(resolved.username.as_deref(), Some("cli-user"));
    }

    #[test]
    fn should_apply_only_omitted_cli_ssh_parameters() {
        let config = ssh_config();
        let params = ssh_params(2200, Some("cli-user"));

        let resolved = apply_ssh_config_to_omitted_cli_parameters(params, true, Some(&config));
        let resolved = resolved.params.generic_params().unwrap();

        assert_eq!(resolved.port, 2200);
        assert_eq!(resolved.username.as_deref(), Some("cli-user"));
    }

    #[test]
    fn should_default_omitted_cli_ssh_port_without_configuration() {
        let params = ssh_params(22, None);

        let resolved = apply_ssh_config_to_omitted_cli_parameters(params, false, None);

        assert_eq!(resolved.params.generic_params().unwrap().port, 22);
    }

    #[test]
    fn should_leave_non_ssh_cli_parameters_unchanged() {
        let params = FileTransferParams::new(
            FileTransferProtocol::Ftp(false),
            ProtocolParams::Generic(
                GenericProtocolParams::default()
                    .address("configured-host")
                    .port(21)
                    .username(Some("ftp-user")),
            ),
        );

        let resolved =
            apply_ssh_config_to_omitted_cli_parameters(params, false, Some(&ssh_config()));
        let resolved = resolved.params.generic_params().unwrap();

        assert_eq!(resolved.port, 21);
        assert_eq!(resolved.username.as_deref(), Some("ftp-user"));
    }

    fn ssh_params(port: u16, username: Option<&str>) -> FileTransferParams {
        FileTransferParams::new(
            FileTransferProtocol::Scp,
            ProtocolParams::Generic(
                GenericProtocolParams::default()
                    .address("configured-host")
                    .port(port)
                    .username(username),
            ),
        )
    }

    fn ssh_config() -> ssh2_config::SshConfig {
        let config_file = test_helpers::create_sample_file_with_content(
            "Host configured-host\n    Port 2222\n    User configured-user\n",
        );

        parse_ssh2_config(&config_file.path().to_string_lossy())
            .expect("test SSH configuration should parse")
    }
}
