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

use crate::{GAS_PRICE_AMOUNT, mainnet};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::ops::Not;
use url::Url;

#[cfg(feature = "env")]
use crate::var_names;
#[cfg(feature = "env")]
use std::env::{VarError, var};
#[cfg(feature = "env")]
use std::ffi::OsStr;

#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct ChainDetails {
    pub bech32_account_prefix: String,
    pub mix_denom: DenomDetailsOwned,
    pub stake_denom: DenomDetailsOwned,
}

impl ChainDetails {
    pub fn mainnet() -> Self {
        ChainDetails {
            bech32_account_prefix: mainnet::BECH32_PREFIX.into(),
            mix_denom: mainnet::MIX_DENOM.into(),
            stake_denom: mainnet::STAKE_DENOM.into(),
        }
    }
}

#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct NymContracts {
    pub mixnet_contract_address: Option<String>,
    pub vesting_contract_address: Option<String>,
    #[serde(default)]
    pub performance_contract_address: Option<String>,
    #[serde(default)]
    pub network_monitors_contract_address: Option<String>,
    #[serde(default)]
    pub node_families_contract_address: Option<String>,
    pub ecash_contract_address: Option<String>,
    pub group_contract_address: Option<String>,
    pub multisig_contract_address: Option<String>,
    pub coconut_dkg_contract_address: Option<String>,
}

// I wanted to use the simpler `NetworkDetails` name, but there's a clash
// with `NetworkDetails` defined in all.rs...
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct NymNetworkDetails {
    pub network_name: String,
    pub chain_details: ChainDetails,
    pub endpoints: Vec<ValidatorDetails>,
    pub contracts: NymContracts,
    pub nym_vpn_api_url: Option<String>,
    pub nym_api_urls: Option<Vec<ApiUrl>>,
    pub nym_vpn_api_urls: Option<Vec<ApiUrl>>,
}

#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct ApiUrl {
    /// Expects a string formatted Url
    ///
    /// see https://docs.rs/url/latest/url/struct.Url.html
    pub url: String,
    /// Optional alternative equivalent hostnames. Each entry must parse as valid Host
    ///
    /// see https://docs.rs/url/latest/url/enum.Host.html
    pub front_hosts: Option<Vec<String>>,
}

impl From<Url> for ApiUrl {
    fn from(value: Url) -> Self {
        ApiUrl {
            url: value.to_string(),
            front_hosts: None,
        }
    }
}

#[derive(Copy, Clone, Debug, Serialize)]
pub struct ApiUrlConst<'a> {
    pub url: &'a str,
    pub front_hosts: Option<&'a [&'a str]>,
}

impl From<ApiUrlConst<'_>> for ApiUrl {
    fn from(value: ApiUrlConst) -> Self {
        ApiUrl {
            url: value.url.to_string(),
            front_hosts: value
                .front_hosts
                .map(|slice| slice.iter().map(|s| s.to_string()).collect()),
        }
    }
}

// by default we assume the same defaults as mainnet, i.e. same prefixes and denoms
impl Default for NymNetworkDetails {
    fn default() -> Self {
        NymNetworkDetails::new_mainnet()
    }
}

impl NymNetworkDetails {
    pub fn new_empty() -> Self {
        NymNetworkDetails {
            network_name: Default::default(),
            chain_details: ChainDetails {
                bech32_account_prefix: Default::default(),
                mix_denom: DenomDetailsOwned {
                    base: Default::default(),
                    display: Default::default(),
                    display_exponent: Default::default(),
                },
                stake_denom: DenomDetailsOwned {
                    base: Default::default(),
                    display: Default::default(),
                    display_exponent: Default::default(),
                },
            },
            endpoints: Default::default(),
            contracts: Default::default(),
            nym_vpn_api_url: Default::default(),
            nym_api_urls: Default::default(),
            nym_vpn_api_urls: Default::default(),
        }
    }

    #[cfg(feature = "env")]
    pub fn new_from_env() -> Self {
        fn get_optional_env<K: AsRef<OsStr>>(env: K) -> Option<String> {
            match var(env) {
                Ok(var) => {
                    if var.is_empty() {
                        None
                    } else {
                        Some(var)
                    }
                }
                Err(VarError::NotPresent) => None,
                err => panic!("Unable to set: {err:?}"),
            }
        }

        let nym_api = var(var_names::NYM_API).expect("nym api not set");
        let nym_api_urls = try_parse_api_urls(var_names::NYM_APIS).unwrap_or(vec![ApiUrl {
            url: nym_api.clone(),
            front_hosts: None,
        }]);
        let nym_vpn_api_urls = try_parse_api_urls(var_names::NYM_VPN_APIS);

        NymNetworkDetails::new_empty()
            .with_network_name(var(var_names::NETWORK_NAME).expect("network name not set"))
            .with_bech32_account_prefix(
                var(var_names::BECH32_PREFIX).expect("bech32 prefix not set"),
            )
            .with_mix_denom(DenomDetailsOwned {
                base: var(var_names::MIX_DENOM).expect("mix denomination base not set"),
                display: var(var_names::MIX_DENOM_DISPLAY)
                    .expect("mix denomination display not set"),
                display_exponent: var(var_names::DENOMS_EXPONENT)
                    .expect("denomination exponent not set")
                    .parse()
                    .expect("denomination exponent is not u32"),
            })
            .with_stake_denom(DenomDetailsOwned {
                base: var(var_names::STAKE_DENOM).expect("stake denomination base not set"),
                display: var(var_names::STAKE_DENOM_DISPLAY)
                    .expect("stake denomination display not set"),
                display_exponent: var(var_names::DENOMS_EXPONENT)
                    .expect("denomination exponent not set")
                    .parse()
                    .expect("denomination exponent is not u32"),
            })
            .with_additional_validator_endpoint(ValidatorDetails::new(
                var(var_names::NYXD).expect("nyxd validator not set"),
                Some(nym_api.clone()),
                get_optional_env(var_names::NYXD_WEBSOCKET),
            ))
            .with_mixnet_contract(get_optional_env(var_names::MIXNET_CONTRACT_ADDRESS))
            .with_vesting_contract(get_optional_env(var_names::VESTING_CONTRACT_ADDRESS))
            .with_node_families_contract(get_optional_env(
                var_names::NODE_FAMILIES_CONTRACT_ADDRESS,
            ))
            .with_ecash_contract(get_optional_env(var_names::ECASH_CONTRACT_ADDRESS))
            .with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS))
            .with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS))
            .with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS))
            .with_performance_contract(get_optional_env(var_names::PERFORMANCE_CONTRACT_ADDRESS))
            .with_network_monitors_contract(get_optional_env(
                var_names::NETWORK_MONITORS_CONTRACT_ADDRESS,
            ))
            .with_nym_vpn_api_url(get_optional_env(var_names::NYM_VPN_API))
            .with_nym_vpn_api_urls(nym_vpn_api_urls)
            .with_nym_api_urls(nym_api_urls)
    }

    pub fn new_mainnet() -> Self {
        fn parse_optional_str(raw: &str) -> Option<String> {
            raw.is_empty().not().then(|| raw.into())
        }

        // Consider caching this process (lazy static)
        NymNetworkDetails {
            network_name: mainnet::NETWORK_NAME.into(),
            chain_details: ChainDetails::mainnet(),
            endpoints: mainnet::validators(),
            contracts: NymContracts {
                mixnet_contract_address: parse_optional_str(mainnet::MIXNET_CONTRACT_ADDRESS),
                vesting_contract_address: parse_optional_str(mainnet::VESTING_CONTRACT_ADDRESS),
                performance_contract_address: parse_optional_str(
                    mainnet::PERFORMANCE_CONTRACT_ADDRESS,
                ),
                network_monitors_contract_address: parse_optional_str(
                    mainnet::NETWORK_MONITORS_CONTRACT_ADDRESS,
                ),
                node_families_contract_address: parse_optional_str(
                    mainnet::NODE_FAMILIES_CONTRACT_ADDRESS,
                ),
                ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS),
                group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS),
                multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS),
                coconut_dkg_contract_address: parse_optional_str(
                    mainnet::COCONUT_DKG_CONTRACT_ADDRESS,
                ),
            },
            nym_vpn_api_url: parse_optional_str(mainnet::NYM_VPN_API),
            nym_api_urls: Some(mainnet::NYM_APIS.iter().copied().map(Into::into).collect()),
            nym_vpn_api_urls: Some(
                mainnet::NYM_VPN_APIS
                    .iter()
                    .copied()
                    .map(Into::into)
                    .collect(),
            ),
        }
    }

    #[rustfmt::skip]
    #[cfg(feature = "env")]
    pub fn export_to_env(self) {
        use crate::var_names;
        use std::env::set_var;

        fn set_optional_var(var_name: &str, value: Option<String>) {
            if let Some(value) = value {
                unsafe { set_var(var_name, value) }
            }
        }
        unsafe {
            let nym_api_urls = self.nym_api_urls();
            let nym_vpn_api_urls = self.nym_vpn_api_urls();

            set_var(var_names::NETWORK_NAME, self.network_name);
            set_var(var_names::BECH32_PREFIX, self.chain_details.bech32_account_prefix);

            set_var(var_names::MIX_DENOM, self.chain_details.mix_denom.base);
            set_var(var_names::MIX_DENOM_DISPLAY, self.chain_details.mix_denom.display);

            set_var(var_names::STAKE_DENOM, self.chain_details.stake_denom.base);
            set_var(var_names::STAKE_DENOM_DISPLAY, self.chain_details.stake_denom.display);

            set_var(var_names::DENOMS_EXPONENT, self.chain_details.mix_denom.display_exponent.to_string());

            if let Some(e) = self.endpoints.first() {
                set_var(var_names::NYXD, e.nyxd_url.clone());
                set_optional_var(var_names::NYM_API, e.api_url.clone());
                set_optional_var(var_names::NYXD_WEBSOCKET, e.websocket_url.clone());
            }

            set_optional_var(var_names::MIXNET_CONTRACT_ADDRESS, self.contracts.mixnet_contract_address);
            set_optional_var(var_names::VESTING_CONTRACT_ADDRESS, self.contracts.vesting_contract_address);
            set_optional_var(var_names::NETWORK_MONITORS_CONTRACT_ADDRESS, self.contracts.network_monitors_contract_address);
            set_optional_var(var_names::NODE_FAMILIES_CONTRACT_ADDRESS, self.contracts.node_families_contract_address);
            set_optional_var(var_names::ECASH_CONTRACT_ADDRESS, self.contracts.ecash_contract_address);
            set_optional_var(var_names::GROUP_CONTRACT_ADDRESS, self.contracts.group_contract_address);
            set_optional_var(var_names::MULTISIG_CONTRACT_ADDRESS, self.contracts.multisig_contract_address);
            set_optional_var(var_names::COCONUT_DKG_CONTRACT_ADDRESS, self.contracts.coconut_dkg_contract_address);

            set_optional_var(var_names::NYM_VPN_API, self.nym_vpn_api_url);
            set_optional_var(var_names::NYM_VPN_APIS, nym_vpn_api_urls);
            set_optional_var(var_names::NYM_APIS, nym_api_urls);

        }
    }

    pub fn default_gas_price_amount(&self) -> f64 {
        GAS_PRICE_AMOUNT
    }

    #[must_use]
    pub fn with_network_name(mut self, network_name: String) -> Self {
        self.network_name = network_name;
        self
    }

    #[must_use]
    pub fn with_chain_details(mut self, chain_details: ChainDetails) -> Self {
        self.chain_details = chain_details;
        self
    }

    #[must_use]
    pub fn with_bech32_account_prefix<S: Into<String>>(mut self, prefix: S) -> Self {
        self.chain_details.bech32_account_prefix = prefix.into();
        self
    }

    #[must_use]
    pub fn with_mix_denom(mut self, mix_denom: DenomDetailsOwned) -> Self {
        self.chain_details.mix_denom = mix_denom;
        self
    }

    #[must_use]
    pub fn with_stake_denom(mut self, stake_denom: DenomDetailsOwned) -> Self {
        self.chain_details.stake_denom = stake_denom;
        self
    }

    #[must_use]
    pub fn with_base_mix_denom<S: Into<String>>(mut self, base_mix_denom: S) -> Self {
        self.chain_details.mix_denom = DenomDetailsOwned::base_only(base_mix_denom.into());
        self
    }

    #[must_use]
    pub fn with_base_stake_denom<S: Into<String>>(mut self, base_stake_denom: S) -> Self {
        self.chain_details.stake_denom = DenomDetailsOwned::base_only(base_stake_denom.into());
        self
    }

    #[must_use]
    pub fn with_additional_validator_endpoint(mut self, endpoint: ValidatorDetails) -> Self {
        self.endpoints.push(endpoint);
        self
    }

    #[must_use]
    pub fn with_validator_endpoint(mut self, endpoint: ValidatorDetails) -> Self {
        self.endpoints = vec![endpoint];
        self
    }

    #[must_use]
    pub fn with_contracts(mut self, contracts: NymContracts) -> Self {
        self.contracts = contracts;
        self
    }

    #[must_use]
    pub fn with_mixnet_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
        self.contracts.mixnet_contract_address = contract.map(Into::into);
        self
    }

    #[must_use]
    pub fn with_vesting_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
        self.contracts.vesting_contract_address = contract.map(Into::into);
        self
    }

    #[must_use]
    pub fn with_node_families_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
        self.contracts.node_families_contract_address = contract.map(Into::into);
        self
    }

    #[must_use]
    pub fn with_ecash_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
        self.contracts.ecash_contract_address = contract.map(Into::into);
        self
    }

    #[must_use]
    pub fn with_group_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
        self.contracts.group_contract_address = contract.map(Into::into);
        self
    }

    #[must_use]
    pub fn with_multisig_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
        self.contracts.multisig_contract_address = contract.map(Into::into);
        self
    }

    #[must_use]
    pub fn with_coconut_dkg_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
        self.contracts.coconut_dkg_contract_address = contract.map(Into::into);
        self
    }

    #[must_use]
    pub fn with_performance_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
        self.contracts.performance_contract_address = contract.map(Into::into);
        self
    }

    #[must_use]
    pub fn with_network_monitors_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
        self.contracts.network_monitors_contract_address = contract.map(Into::into);
        self
    }

    #[must_use]
    pub fn with_nym_vpn_api_url<S: Into<String>>(mut self, endpoint: Option<S>) -> Self {
        self.nym_vpn_api_url = endpoint.map(Into::into);
        self
    }

    pub fn set_nym_api_urls<U: Into<ApiUrl>>(&mut self, urls: Vec<U>) {
        self.nym_api_urls = Some(urls.into_iter().map(Into::into).collect());
    }

    #[must_use]
    pub fn with_nym_api_urls<U: Into<ApiUrl>>(mut self, urls: Vec<U>) -> Self {
        self.set_nym_api_urls(urls);
        self
    }

    #[must_use]
    pub fn with_nym_vpn_api_urls(mut self, urls: Option<Vec<ApiUrl>>) -> Self {
        self.nym_vpn_api_urls = urls;
        self
    }

    pub fn nym_vpn_api_url(&self) -> Option<Url> {
        self.nym_vpn_api_url.as_ref().map(|url| {
            url.parse()
                .expect("the provided nym-vpn api url is invalid!")
        })
    }

    #[cfg(feature = "env")]
    fn nym_api_urls(&self) -> Option<String> {
        serde_json::to_string(self.nym_api_urls.as_deref()?)
            .inspect_err(|e| tracing::warn!("failed to serialize nym_api_urls for env: {e}"))
            .ok()
    }

    #[cfg(feature = "env")]
    fn nym_vpn_api_urls(&self) -> Option<String> {
        serde_json::to_string(self.nym_vpn_api_urls.as_deref()?)
            .inspect_err(|e| tracing::warn!("failed to serialize nym_vpn_api_urls for env: {e}"))
            .ok()
    }
}

#[cfg(feature = "env")]
fn try_parse_api_urls(k: impl AsRef<OsStr>) -> Option<Vec<ApiUrl>> {
    let raw = var(k).ok()?;
    serde_json::from_str(&raw)
        .inspect_err(|e| tracing::warn!("failed to parse api urls from env \"{raw:?}\": {e}"))
        .ok()
}

#[derive(Debug, Copy, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct DenomDetails {
    pub base: &'static str,
    pub display: &'static str,
    // i.e. display_amount * 10^display_exponent = base_amount
    pub display_exponent: u32,
}

impl DenomDetails {
    pub const fn new(base: &'static str, display: &'static str, display_exponent: u32) -> Self {
        DenomDetails {
            base,
            display,
            display_exponent,
        }
    }
}

#[derive(Debug, Serialize, Deserialize, Hash, Clone, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct DenomDetailsOwned {
    pub base: String,
    pub display: String,
    // i.e. display_amount * 10^display_exponent = base_amount
    pub display_exponent: u32,
}

impl From<DenomDetails> for DenomDetailsOwned {
    fn from(details: DenomDetails) -> Self {
        DenomDetailsOwned {
            base: details.base.to_owned(),
            display: details.display.to_owned(),
            display_exponent: details.display_exponent,
        }
    }
}

impl DenomDetailsOwned {
    pub fn base_only(base: String) -> Self {
        DenomDetailsOwned {
            base: base.clone(),
            display: base,
            display_exponent: 0,
        }
    }
}

#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct ValidatorDetails {
    // it is assumed those values are always valid since they're being provided in our defaults file
    pub nyxd_url: String,
    //
    pub websocket_url: Option<String>,

    // Right now api_url is optional as we are not running the api reliably on all validators
    // however, later on it should be a mandatory field
    pub api_url: Option<String>,
    // TODO: I'd argue this one should also have a field like `gas_price` since its a validator-specific setting
}

impl ValidatorDetails {
    pub fn new<S: Into<String>>(nyxd_url: S, api_url: Option<S>, websocket_url: Option<S>) -> Self {
        ValidatorDetails {
            nyxd_url: nyxd_url.into(),
            websocket_url: websocket_url.map(Into::into),
            api_url: api_url.map(Into::into),
        }
    }

    pub fn new_nyxd_only<S: Into<String>>(nyxd_url: S) -> Self {
        ValidatorDetails {
            nyxd_url: nyxd_url.into(),
            websocket_url: None,
            api_url: None,
        }
    }

    pub fn nyxd_url(&self) -> Url {
        self.nyxd_url
            .parse()
            .expect("the provided nyxd url is invalid!")
    }

    pub fn api_url(&self) -> Option<Url> {
        self.api_url
            .as_ref()
            .map(|url| url.parse().expect("the provided api url is invalid!"))
    }

    pub fn websocket_url(&self) -> Option<Url> {
        self.websocket_url
            .as_ref()
            .map(|url| url.parse().expect("the provided websocket url is invalid!"))
    }
}
