// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0

//! Collection of initialization steps used by client implementations

use crate::client::base_client::storage::helpers::{
    has_gateway_details, load_active_gateway_details, load_client_keys, load_gateway_details,
    store_gateway_details, update_stored_published_data_gateway,
};
use crate::client::key_manager::ClientKeys;
use crate::client::key_manager::persistence::KeyStore;
use crate::error::ClientCoreError;
use crate::init::helpers::{
    choose_gateway_by_latency, get_specified_gateway, uniformly_random_gateway,
};
use crate::init::types::{
    GatewaySelectionSpecification, GatewaySetup, InitialisationResult, SelectedGateway,
};
use nym_client_core_gateways_storage::{GatewayDetails, GatewayRegistration};
use nym_client_core_gateways_storage::{GatewayPublishedData, GatewaysDetailsStore};
use nym_gateway_client::client::InitGatewayClient;
use nym_topology::node::RoutingNode;
use rand::rngs::OsRng;
use rand::{CryptoRng, RngCore};
use serde::Serialize;
#[cfg(unix)]
use std::{os::fd::RawFd, sync::Arc};

pub mod helpers;
pub mod types;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) mod websockets;

// helpers for error wrapping

pub async fn generate_new_client_keys<K, R>(
    rng: &mut R,
    key_store: &K,
) -> Result<(), ClientCoreError>
where
    R: RngCore + CryptoRng,
    K: KeyStore,
    K::StorageError: Send + Sync + 'static,
{
    ClientKeys::generate_new(rng)
        .persist_keys(key_store)
        .await
        .map_err(|source| ClientCoreError::KeyStoreError {
            source: Box::new(source),
        })
}

async fn setup_new_gateway<K, D>(
    key_store: &K,
    details_store: &D,
    selection_specification: GatewaySelectionSpecification,
    available_gateways: Vec<RoutingNode>,
    #[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
) -> Result<InitialisationResult, ClientCoreError>
where
    K: KeyStore,
    D: GatewaysDetailsStore,
    K::StorageError: Send + Sync + 'static,
    D::StorageError: Send + Sync + 'static,
{
    tracing::trace!("Setting up new gateway");

    // if we're setting up new gateway, we must have had generated long-term client keys before
    let client_keys = load_client_keys(key_store).await?;

    let mut rng = OsRng;

    let selected_gateway = match selection_specification {
        GatewaySelectionSpecification::UniformRemote {
            must_use_tls,
            no_hostname,
        } => {
            let gateway = uniformly_random_gateway(&mut rng, &available_gateways, must_use_tls)?;
            SelectedGateway::from_topology_node(gateway, must_use_tls, no_hostname)?
        }
        GatewaySelectionSpecification::RemoteByLatency {
            must_use_tls,
            no_hostname,
        } => {
            let gateway =
                choose_gateway_by_latency(&mut rng, &available_gateways, must_use_tls).await?;
            SelectedGateway::from_topology_node(gateway, must_use_tls, no_hostname)?
        }
        GatewaySelectionSpecification::Specified {
            must_use_tls,
            no_hostname,
            identity,
        } => {
            let gateway = get_specified_gateway(&identity, &available_gateways, must_use_tls)?;
            SelectedGateway::from_topology_node(gateway, must_use_tls, no_hostname)?
        }
        GatewaySelectionSpecification::Custom {
            gateway_identity,
            additional_data,
        } => SelectedGateway::custom(gateway_identity, additional_data)?,
    };

    // check if we already have details associated with this particular gateway
    // and if so, see if we can overwrite it
    let selected_id = selected_gateway.gateway_id().to_base58_string();
    if has_gateway_details(details_store, &selected_id).await? {
        return Err(ClientCoreError::AlreadyRegistered {
            gateway_id: selected_id,
        });
    }

    let (gateway_details, authenticated_ephemeral_client) = match selected_gateway {
        SelectedGateway::Remote {
            gateway_id,

            gateway_listeners,
        } => {
            // if we're using a 'normal' gateway setup, do register
            let our_identity = client_keys.identity_keypair();

            let registration = helpers::register_with_gateway(
                gateway_id,
                gateway_listeners.clone(),
                our_identity,
                #[cfg(unix)]
                connection_fd_callback,
            )
            .await?;
            (
                GatewayDetails::new_remote(
                    gateway_id,
                    registration.shared_keys,
                    GatewayPublishedData::new(gateway_listeners),
                ),
                Some(registration.authenticated_ephemeral_client),
            )
        }
        SelectedGateway::Custom {
            gateway_id,
            additional_data,
        } => (
            GatewayDetails::new_custom(gateway_id, additional_data),
            None,
        ),
    };

    let gateway_registration = gateway_details.into();

    // persist gateway details
    store_gateway_details(details_store, &gateway_registration).await?;

    Ok(InitialisationResult {
        gateway_registration,
        client_keys,
        authenticated_ephemeral_client,
    })
}

pub async fn refresh_gateway_published_data<D>(
    details_store: &D,
    registration: GatewayRegistration,
    available_gateways: Vec<RoutingNode>,
    must_use_tls: bool,
    no_hostname: bool,
) -> Result<(), ClientCoreError>
where
    D: GatewaysDetailsStore,
    D::StorageError: Send + Sync + 'static,
{
    let gateway_id = registration.gateway_id().to_base58_string();
    tracing::trace!("Updating gateway details : {gateway_id}");

    let gateway = get_specified_gateway(&gateway_id, &available_gateways, must_use_tls)?;
    let selected_gateway = SelectedGateway::from_topology_node(gateway, must_use_tls, no_hostname)?;

    let new_gateway_listeners = match selected_gateway {
        SelectedGateway::Remote {
            gateway_listeners, ..
        } => gateway_listeners,
        SelectedGateway::Custom { .. } => {
            // this should not happen, as `from_topology_node` returns a Remote
            Err(ClientCoreError::UnexpectedCustomGatewaySelection)?
        }
    };

    let new_published_data = GatewayPublishedData::new(new_gateway_listeners);

    // update gateway details
    update_stored_published_data_gateway(
        details_store,
        &registration.gateway_id(),
        &new_published_data,
    )
    .await?;

    Ok(())
}

async fn use_loaded_gateway_details<K, D>(
    key_store: &K,
    details_store: &D,
    gateway_id: Option<String>,
) -> Result<InitialisationResult, ClientCoreError>
where
    K: KeyStore,
    D: GatewaysDetailsStore,
    K::StorageError: Send + Sync + 'static,
    D::StorageError: Send + Sync + 'static,
{
    let loaded_details = if let Some(gateway_id) = gateway_id {
        load_gateway_details(details_store, &gateway_id).await?
    } else {
        load_active_gateway_details(details_store)
            .await?
            .registration
            .ok_or(ClientCoreError::NoActiveGatewaySet)?
    };

    let loaded_keys = load_client_keys(key_store).await?;

    // no need to persist anything as we got everything from the storage
    Ok(InitialisationResult::new_loaded(
        loaded_details,
        loaded_keys,
    ))
}

fn reuse_gateway_connection(
    authenticated_ephemeral_client: InitGatewayClient,
    gateway_registration: GatewayRegistration,
    client_keys: ClientKeys,
) -> InitialisationResult {
    InitialisationResult {
        gateway_registration,
        client_keys,
        authenticated_ephemeral_client: Some(authenticated_ephemeral_client),
    }
}

pub async fn setup_gateway<K, D>(
    setup: GatewaySetup,
    key_store: &K,
    details_store: &D,
) -> Result<InitialisationResult, ClientCoreError>
where
    K: KeyStore,
    D: GatewaysDetailsStore,
    K::StorageError: Send + Sync + 'static,
    D::StorageError: Send + Sync + 'static,
{
    tracing::debug!("Setting up gateway");
    match setup {
        GatewaySetup::MustLoad { gateway_id } => {
            tracing::debug!("GatewaySetup::MustLoad with id: {gateway_id:?}");
            use_loaded_gateway_details(key_store, details_store, gateway_id).await
        }
        GatewaySetup::New {
            specification,
            available_gateways,
            #[cfg(unix)]
            connection_fd_callback,
        } => {
            tracing::debug!("GatewaySetup::New with spec: {specification:?}");
            setup_new_gateway(
                key_store,
                details_store,
                specification,
                available_gateways,
                #[cfg(unix)]
                connection_fd_callback,
            )
            .await
        }
        GatewaySetup::ReuseConnection {
            authenticated_ephemeral_client,
            gateway_details,
            client_keys: managed_keys,
        } => {
            tracing::debug!("GatewaySetup::ReuseConnection");
            Ok(reuse_gateway_connection(
                *authenticated_ephemeral_client,
                *gateway_details,
                managed_keys,
            ))
        }
    }
}

pub fn output_to_json<T: Serialize>(init_results: &T, output_file: &str) {
    match std::fs::File::create(output_file) {
        Ok(file) => match serde_json::to_writer_pretty(file, init_results) {
            Ok(_) => println!("Saved: {output_file}"),
            Err(err) => eprintln!("Could not save {output_file}: {err}"),
        },
        Err(err) => eprintln!("Could not save {output_file}: {err}"),
    }
}
