// Copyright 2020 The Exonum Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Helper crate for secure and convenient configuration of the Exonum nodes.
//!
//! `exonum-cli` supports multi-stage configuration process made with safety in mind. It involves
//! 4 steps (or stages) and allows to configure and run multiple blockchain nodes without
//! need in exchanging private keys between administrators.
//!
//! # How to Run the Network
//!
//! 1. Generate common (template) part of the nodes configuration using `generate-template` command.
//!   Generated `.toml` file must be spread among all the nodes and must be used in the following
//!   configuration step.
//! 2. Generate public and secret (private) parts of the node configuration using `generate-config`
//!   command. At this step, Exonum will generate master key from which consensus and service
//!   validator keys are derived. Master key is stored in the encrypted file. Consensus secret key
//!   is used for communications between the nodes, while service secret key is used
//!   mainly to sign transactions generated by the node. Both secret keys may be encrypted with a
//!   password. The public part of the node configuration must be spread among all nodes, while the
//!   secret part must be only accessible by the node administrator only.
//! 3. Generate final node configuration using `finalize` command. Exonum combines secret part of
//!   the node configuration with public configurations of every other node, producing a single
//!   configuration file with all the necessary node and network settings.
//! 4. Use `run` command and provide it with final node configuration file produced at the previous
//!   step. If the secret keys are protected with passwords, the user need to enter the password.
//!   Running node will automatically connect to other nodes in the network using IP addresses from
//!   public parts of the node configurations.
//!
//! ## Additional Commands
//!
//! `exonum-cli` also supports additional CLI commands for performing maintenance actions by node
//! administrators and easier debugging.
//!
//! - `run-dev` command automatically generates network configuration with a single node and runs
//!   it. This command can be useful for fast testing of the services during development process.
//! - `maintenance` command allows to clear node's consensus messages with `clear-cache`, and
//!   restart node's service migration script with `restart-migration`.
//!
//! ## How to Extend Parameters
//!
//! `exonum-cli` allows to extend the list of the parameters for any command and even add new CLI
//! commands with arbitrary behavior. To do so, you need to implement a structure with a list of
//! additional parameters and use `flatten` macro attribute of [`serde`][serde] and
//! [`structopt`][structopt] libraries.
//!
//! ```
//! use exonum_cli::command::{Run, ExonumCommand};
//! use serde::{Deserialize, Serialize};
//! use structopt::StructOpt;
//!
//! #[derive(Serialize, Deserialize, StructOpt)]
//! struct MyRunCommand {
//!     #[serde(flatten)]
//!     #[structopt(flatten)]
//!     inner: Run,
//!
//!     /// My awesome parameter.
//!     #[structopt(name = "secret", long, default_value = "0")]
//!     secret_number: i32,
//! }
//!
//! // Usage. We use `StructOpt::from_iter` for testing purposes;
//! // the real app should use `StructOpt::from_args`.
//! let command = MyRunCommand::from_iter(vec![
//!     "executable",
//!     "-c", "./node.toml",
//!     "--db-path", "./db",
//!     "--secret", "42",
//! ]);
//! assert_eq!(command.secret_number, 42);
//! # drop(|| -> anyhow::Result<()> {
//! command.inner.execute()?;
//! # Ok(())
//! # });
//! ```
//!
//! You can also create own list of commands by implementing an enum with a similar principle:
//!
//! ```
//! use exonum_cli::command::Run;
//! use structopt::StructOpt;
//!
//! // `MyRunCommand` defined as in the previous example...
//! # #[derive(StructOpt)] pub struct MyRunCommand {}
//!
//! #[derive(StructOpt)]
//! pub enum MyCommands {
//!     #[structopt(name = "run")]
//!     DefaultRun(Run),
//!     #[structopt(name = "my-run")]
//!     MyAwesomeRun(MyRunCommand),
//! }
//! ```
//!
//! While implementing custom behavior for your commands, you may use
//! [`StandardResult`](./command/enum.StandardResult.html) enum for
//! accessing node configuration files created and filled by the standard Exonum commands.
//!
//! [serde]: https://crates.io/crates/serde
//! [structopt]: https://crates.io/crates/structopt

#![warn(
    missing_debug_implementations,
    missing_docs,
    unsafe_code,
    bare_trait_objects
)]
#![warn(clippy::pedantic, clippy::nursery)]
#![allow(
    // Next `cast_*` lints don't give alternatives.
    clippy::cast_possible_wrap, clippy::cast_possible_truncation, clippy::cast_sign_loss,
    // Next lints produce too much noise/false positives.
    clippy::module_name_repetitions, clippy::similar_names, clippy::must_use_candidate,
    clippy::pub_enum_variant_names,
    // '... may panic' lints.
    clippy::indexing_slicing,
    // Too much work to fix.
    clippy::missing_errors_doc, clippy::missing_const_for_fn
)]

pub use crate::{
    config_manager::DefaultConfigManager,
    io::{load_config_file, save_config_file},
};
pub use exonum_rust_runtime::spec::Spec;
pub use structopt;

use exonum::{
    blockchain::config::{GenesisConfig, GenesisConfigBuilder},
    merkledb::RocksDB,
    runtime::{RuntimeInstance, WellKnownRuntime},
};
use exonum_explorer_service::ExplorerFactory;
use exonum_node::{Node, NodeBuilder as CoreNodeBuilder};
use exonum_rust_runtime::{spec::Deploy, RustRuntimeBuilder};
use exonum_supervisor::{Supervisor, SupervisorConfig};
use exonum_system_api::SystemApiPlugin;
use structopt::StructOpt;
use tempfile::TempDir;

use std::{env, ffi::OsString, iter, path::PathBuf};

use crate::command::{Command, ExonumCommand, NodeRunConfig, StandardResult};

pub mod command;
pub mod config;
mod io;
pub mod password;

mod config_manager;

/// Rust-specific node builder used for constructing a node with a list
/// of provided services.
#[derive(Debug)]
pub struct NodeBuilder {
    rust_runtime: RustRuntimeBuilder,
    external_runtimes: Vec<RuntimeInstance>,
    genesis_config: GenesisConfigBuilder,
    args: Option<Vec<OsString>>,
    temp_dir: Option<TempDir>,
}

impl Default for NodeBuilder {
    fn default() -> Self {
        Self::new()
    }
}

impl NodeBuilder {
    /// Creates a new builder.
    pub fn new() -> Self {
        Self {
            genesis_config: GenesisConfigBuilder::default(),
            rust_runtime: RustRuntimeBuilder::new(),
            external_runtimes: vec![],
            args: None,
            temp_dir: None,
        }
    }

    /// Creates a new builder with the provided command-line arguments. The path
    /// to the current executable **does not** need to be specified as the first argument.
    #[doc(hidden)] // unstable
    pub fn with_args<I>(args: I) -> Self
    where
        I: IntoIterator,
        I::Item: Into<OsString>,
    {
        let mut this = Self::new();
        let executable = env::current_exe().map_or_else(|_| "node".into(), PathBuf::into_os_string);
        let all_args = iter::once(executable)
            .chain(args.into_iter().map(Into::into))
            .collect();
        this.args = Some(all_args);
        this
    }

    /// Creates a single-node development network with default settings. The node stores
    /// its data in a temporary directory, which is automatically removed when the node is stopped.
    ///
    /// # Return value
    ///
    /// Returns an error if the temporary directory cannot be created.
    pub fn development_node() -> anyhow::Result<Self> {
        let temp_dir = TempDir::new()?;
        let mut this = Self::with_args(vec![
            OsString::from("run-dev"),
            OsString::from("--blockchain-path"),
            temp_dir.path().into(),
        ]);
        this.temp_dir = Some(temp_dir);
        Ok(this)
    }

    /// Adds a deploy spec to this builder. The spec may contain artifacts and service instances
    /// to deploy at the blockchain start.
    pub fn with(mut self, spec: impl Deploy) -> Self {
        spec.deploy(&mut self.genesis_config, &mut self.rust_runtime);
        self
    }

    /// Adds a new `Runtime` to the list of available runtimes.
    ///
    /// Note that you don't have to add the Rust runtime, since it is included by default.
    pub fn with_external_runtime(mut self, runtime: impl WellKnownRuntime) -> Self {
        self.external_runtimes.push(runtime.into());
        self
    }

    /// Executes a command received from the command line.
    ///
    /// # Return value
    ///
    /// Returns:
    ///
    /// - `Ok(Some(_))` if the command lead to the node creation
    /// - `Ok(None)` if the command executed successfully and did not lead to node creation
    /// - `Err(_)` if an error occurred during command execution
    #[doc(hidden)] // unstable
    pub fn execute_command(mut self) -> anyhow::Result<Option<Node>> {
        let command = if let Some(args) = self.args {
            Command::from_iter(args)
        } else {
            Command::from_args()
        };

        if let StandardResult::Run(run_config) = command.execute()? {
            // Deploy "default" services (supervisor and the explorer).
            let supervisor = Self::supervisor_service(&run_config);
            supervisor.deploy(&mut self.genesis_config, &mut self.rust_runtime);
            Spec::new(ExplorerFactory)
                .with_default_instance()
                .deploy(&mut self.genesis_config, &mut self.rust_runtime);

            let genesis_config = Self::genesis_config(&run_config, self.genesis_config);
            let db_options = &run_config.node_config.private_config.database;
            let database = RocksDB::open(run_config.db_path, db_options)?;

            let node_config_path = run_config.node_config_path.to_string_lossy();
            let config_manager = DefaultConfigManager::new(node_config_path.into_owned());
            let rust_runtime = self.rust_runtime;

            let node_config = run_config.node_config.into();
            let node_keys = run_config.node_keys;
            let sync_policy = run_config.sync_policy.as_path().display().to_string();
            let scoring_flag = run_config.scoring_flag.as_path().display().to_string();
            let model_name = run_config.model_name.as_path().display().to_string();

            let mut node_builder = CoreNodeBuilder::new(database, node_config, node_keys, sync_policy, scoring_flag, model_name)
                .with_genesis_config(genesis_config)
                .with_config_manager(config_manager)
                .with_plugin(SystemApiPlugin)
                .with_runtime_fn(|channel| rust_runtime.build(channel.endpoints_sender()));
            for runtime in self.external_runtimes {
                node_builder = node_builder.with_runtime(runtime);
            }
            Ok(Some(node_builder.build()))
        } else {
            Ok(None)
        }
    }

    /// Configures the node using parameters provided by user from stdin and then runs it.
    pub async fn run(mut self) -> anyhow::Result<()> {
        // Store temporary directory until the node is done.
        let _temp_dir = self.temp_dir.take();
        if let Some(node) = self.execute_command()? {
            node.run().await
        } else {
            Ok(())
        }
    }

    fn genesis_config(run_config: &NodeRunConfig, builder: GenesisConfigBuilder) -> GenesisConfig {
        // Add builtin services to genesis config.
        let mut config = builder.build();
        // Override consensus config.
        config.consensus_config = run_config.node_config.public_config.consensus.clone();
        config
    }

    fn supervisor_service(run_config: &NodeRunConfig) -> impl Deploy {
        let mode = run_config
            .node_config
            .public_config
            .general
            .supervisor_mode
            .clone();
        Supervisor::builtin_instance(SupervisorConfig::new(mode))
    }
}
