use std::{
    fs::{self, File},
    io::{self, Error, ErrorKind, Write},
    path::Path,
};

use log::info;
use serde::{Deserialize, Serialize};

/// To be persisted in "chain_config_dir".
/// ref. https://pkg.go.dev/github.com/ava-labs/coreth/plugin/evm#Config
/// ref. https://github.com/ava-labs/coreth/blob/v0.8.16/plugin/evm/config.go
/// ref. https://serde.rs/container-attrs.html
///
/// If a Subnet's chain id is 2ebCneCbwthjQ1rYT41nhd7M76Hc6YmosMAQrTFhBq8qeqh6tt,
/// the config file for this chain is located at {chain-config-dir}/2ebCneCbwthjQ1rYT41nhd7M76Hc6YmosMAQrTFhBq8qeqh6tt/config.json
/// ref. https://docs.avax.network/subnets/customize-a-subnet#chain-configs
///
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Config {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub snowman_api_enabled: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub coreth_admin_api_enabled: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub coreth_admin_api_dir: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub eth_apis: Option<Vec<String>>,

    /// If not empty, it enables the profiler.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub continuous_profiler_dir: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub continuous_profiler_frequency: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub continuous_profiler_max_files: Option<i64>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub rpc_gas_cap: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rpc_tx_fee_cap: Option<f64>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub preimages_enabled: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pruning_enabled: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub snapshot_async: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub snapshot_verification_enabled: Option<bool>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub metrics_enabled: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metrics_expensive_enabled: Option<bool>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub local_txs_enabled: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub api_max_duration: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ws_cpu_refill_rate: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ws_cpu_max_stored: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub api_max_blocks_per_request: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub allow_unfinalized_queries: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub allow_unprotected_txs: Option<bool>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub keystore_directory: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub keystore_external_signer: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub keystore_insecure_unlock_allowed: Option<bool>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub remote_tx_gossip_only_enabled: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tx_regossip_frequency: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tx_regossip_max_size: Option<i32>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub log_level: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub log_json_format: Option<bool>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub offline_pruning_enabled: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub offline_pruning_bloom_filter_size: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub offline_pruning_data_directory: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_outbound_active_requests: Option<i64>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub state_sync_enabled: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub state_sync_metrics_enabled: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub state_sync_server_trie_cache: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub state_sync_ids: Option<String>,
}

pub const DEFAULT_CORETH_ADMIN_API_ENABLED: bool = true;

/// MUST BE a valid path in remote host machine.
pub const DEFAULT_PROFILE_DIR: &str = "/var/log/avalanche-profile/coreth";
pub const DEFAULT_PROFILE_FREQUENCY: i64 = 15 * 60 * 1000 * 1000 * 1000; // 15-min
pub const DEFAULT_PROFILE_MAX_FILES: i64 = 5;

pub const DEFAULT_METRICS_ENABLED: bool = true;
pub const DEFAULT_LOG_LEVEL: &str = "info";
pub const DEFAULT_LOG_JSON_FORMAT: bool = true;

/// ref. https://docs.avax.network/nodes/maintain/run-offline-pruning
pub const DEFAULT_OFFLINE_PRUNING_DATA_DIR: &str = "/data/c-chain-offline-pruning";

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

impl Config {
    /// The defaults do not match with the ones in avalanchego,
    /// as this is for avalanche-ops based deployments.
    pub fn default() -> Self {
        Self {
            snowman_api_enabled: None,
            coreth_admin_api_enabled: Some(DEFAULT_CORETH_ADMIN_API_ENABLED),
            coreth_admin_api_dir: None,

            eth_apis: Some(vec![
                "eth".to_string(),
                "eth-filter".to_string(),
                "net".to_string(),
                "web3".to_string(),
                "internal-eth".to_string(),
                "internal-blockchain".to_string(),
                "internal-transaction".to_string(),
            ]),

            continuous_profiler_dir: None,
            continuous_profiler_frequency: None,
            continuous_profiler_max_files: None,

            rpc_gas_cap: None,
            rpc_tx_fee_cap: None,

            preimages_enabled: None,
            pruning_enabled: Some(true),
            snapshot_async: None,
            snapshot_verification_enabled: None,

            metrics_enabled: Some(DEFAULT_METRICS_ENABLED),
            metrics_expensive_enabled: None,

            local_txs_enabled: None,
            api_max_duration: None,
            ws_cpu_refill_rate: None,
            ws_cpu_max_stored: None,
            api_max_blocks_per_request: None,
            allow_unfinalized_queries: None,
            allow_unprotected_txs: None,

            keystore_directory: None,
            keystore_external_signer: None,
            keystore_insecure_unlock_allowed: None,

            remote_tx_gossip_only_enabled: None,
            tx_regossip_frequency: None,
            tx_regossip_max_size: None,

            log_level: Some(String::from(DEFAULT_LOG_LEVEL)),
            log_json_format: Some(DEFAULT_LOG_JSON_FORMAT),

            offline_pruning_enabled: Some(false),
            offline_pruning_bloom_filter_size: None,
            offline_pruning_data_directory: Some(String::from(DEFAULT_OFFLINE_PRUNING_DATA_DIR)),

            max_outbound_active_requests: None,

            state_sync_enabled: Some(true),
            state_sync_metrics_enabled: None,
            state_sync_server_trie_cache: None,
            state_sync_ids: None,
        }
    }

    pub fn encode_json(&self) -> io::Result<String> {
        match serde_json::to_string(&self) {
            Ok(s) => Ok(s),
            Err(e) => Err(Error::new(
                ErrorKind::Other,
                format!("failed to serialize to JSON {}", e),
            )),
        }
    }

    /// Saves the current anchor node to disk
    /// and overwrites the file.
    pub fn sync(&self, file_path: &str) -> io::Result<()> {
        info!("syncing Config to '{}'", file_path);
        let path = Path::new(file_path);
        let parent_dir = path.parent().expect("unexpected None parent");
        fs::create_dir_all(parent_dir)?;

        let ret = serde_json::to_vec(self);
        let d = match ret {
            Ok(d) => d,
            Err(e) => {
                return Err(Error::new(
                    ErrorKind::Other,
                    format!("failed to serialize Config to YAML {}", e),
                ));
            }
        };
        let mut f = File::create(file_path)?;
        f.write_all(&d)?;

        Ok(())
    }

    pub fn load(file_path: &str) -> io::Result<Self> {
        info!("loading coreth config from {}", file_path);

        if !Path::new(file_path).exists() {
            return Err(Error::new(
                ErrorKind::NotFound,
                format!("file {} does not exists", file_path),
            ));
        }

        let f = File::open(&file_path).map_err(|e| {
            Error::new(
                ErrorKind::Other,
                format!("failed to open {} ({})", file_path, e),
            )
        })?;
        serde_json::from_reader(f)
            .map_err(|e| Error::new(ErrorKind::InvalidInput, format!("invalid JSON: {}", e)))
    }
}
