use std::path::{Path, PathBuf};
use std::sync::Arc;

use anyhow::{Context as _, Result, bail};
use fabro_config::merge::combine_files;
use fabro_types::settings::cli::CliLayer;
use fabro_types::settings::{CliSettings, SettingsLayer};
use fabro_util::printer::Printer;
use tokio::sync::OnceCell;

use crate::args::{ServerConnectionArgs, ServerTargetArgs};
use crate::server_client::ServerStoreClient;
use crate::{server_client, user_config};

#[derive(Clone, Debug)]
pub(crate) enum ServerMode {
    None,
    ByTarget {
        target_override: Option<String>,
    },
    ByStorageDir {
        target_override:      Option<String>,
        storage_dir_override: Option<PathBuf>,
    },
}

pub(crate) struct CommandContext {
    #[allow(dead_code)]
    printer:          Printer,
    cwd:              PathBuf,
    base_config_path: PathBuf,
    machine_settings: SettingsLayer,
    cli_settings:     CliSettings,
    server_mode:      ServerMode,
    server:           OnceCell<Arc<ServerStoreClient>>,
}

impl CommandContext {
    pub(crate) fn base(
        printer: Printer,
        cli_settings: CliSettings,
        cli_layer: &CliLayer,
    ) -> Result<Self> {
        Self::new(printer, ServerMode::None, cli_settings, cli_layer)
    }

    pub(crate) fn for_target(
        args: &ServerTargetArgs,
        printer: Printer,
        cli_settings: CliSettings,
        cli_layer: &CliLayer,
    ) -> Result<Self> {
        Self::new(
            printer,
            ServerMode::ByTarget {
                target_override: args.server.clone(),
            },
            cli_settings,
            cli_layer,
        )
    }

    pub(crate) fn for_connection(
        args: &ServerConnectionArgs,
        printer: Printer,
        cli_settings: CliSettings,
        cli_layer: &CliLayer,
    ) -> Result<Self> {
        Self::new(
            printer,
            ServerMode::ByStorageDir {
                target_override:      args.target.server.clone(),
                storage_dir_override: args.storage_dir.clone_path(),
            },
            cli_settings,
            cli_layer,
        )
    }

    fn new(
        printer: Printer,
        server_mode: ServerMode,
        cli_settings: CliSettings,
        cli_layer: &CliLayer,
    ) -> Result<Self> {
        let cwd = std::env::current_dir().context("Failed to get current directory")?;
        let base_config_path = user_config::active_settings_path(None);
        let disk_settings = match &server_mode {
            ServerMode::None | ServerMode::ByTarget { .. } => user_config::load_settings()?,
            ServerMode::ByStorageDir {
                storage_dir_override,
                ..
            } => user_config::load_settings_with_storage_dir(storage_dir_override.as_deref())?,
        };
        let machine_settings = combine_files(disk_settings, SettingsLayer {
            cli: Some(cli_layer.clone()),
            ..SettingsLayer::default()
        });

        Ok(Self {
            printer,
            cwd,
            base_config_path,
            machine_settings,
            cli_settings,
            server_mode,
            server: OnceCell::new(),
        })
    }

    #[allow(dead_code)]
    pub(crate) fn printer(&self) -> Printer {
        self.printer
    }

    pub(crate) fn cwd(&self) -> &Path {
        &self.cwd
    }

    pub(crate) fn base_config_path(&self) -> &Path {
        &self.base_config_path
    }

    pub(crate) fn machine_settings(&self) -> &SettingsLayer {
        &self.machine_settings
    }

    pub(crate) fn cli_settings(&self) -> &CliSettings {
        &self.cli_settings
    }

    pub(crate) async fn server(&self) -> Result<Arc<ServerStoreClient>> {
        let server_mode = self.server_mode.clone();
        let base_config_path = self.base_config_path.clone();
        let machine_settings = self.machine_settings.clone();

        let client = self
            .server
            .get_or_try_init(|| async move {
                let target = match server_mode {
                    ServerMode::None => bail!("This command context does not have server access"),
                    ServerMode::ByTarget { target_override }
                    | ServerMode::ByStorageDir {
                        target_override, ..
                    } => ServerTargetArgs {
                        server: target_override,
                    },
                };
                server_client::connect_server_with_settings(
                    &target,
                    &machine_settings,
                    &base_config_path,
                )
                .await
                .map(Arc::new)
            })
            .await?;

        Ok(Arc::clone(client))
    }
}
