Documentation
use std::{
    borrow::Cow,
    fmt,
    path::{Path, PathBuf},
};

use anyhow::Context;
pub use lexe_common::{env::DeployEnv, ln::network::Network};

use crate::{
    types::auth::{CredentialsRef, UserPk},
    unstable::SDK_USER_AGENT,
};

// --- Structs --- //
//
// - WalletEnv (`wallet_env`)
// - WalletEnvConfig (`env_config`)
// - WalletUserConfig (`user_config`)
// - WalletEnvDbConfig (`env_db_config`)
// - WalletUserDbConfig (`user_db_config`)

/// A wallet environment, e.g. "prod-mainnet-sgx".
///
/// Disambiguates persisted state and secrets across environments.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct WalletEnv {
    /// Prod, staging, or dev.
    pub deploy_env: DeployEnv,
    /// The Bitcoin network: mainnet, testnet, or regtest.
    pub network: Network,
    /// Whether our node should be running in a real SGX enclave.
    /// Set to [`true`] for prod and staging.
    pub use_sgx: bool,
}

/// A configuration for a wallet environment.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WalletEnvConfig {
    /// The wallet environment.
    pub wallet_env: WalletEnv,
    // NOTE(unstable): Fields should stay pub(crate) until API is more mature
    pub(crate) gateway_url: Cow<'static, str>,
    pub(crate) user_agent: Cow<'static, str>,
}

/// A wallet configuration for a specific user and wallet environment.
#[derive(Clone)]
pub struct WalletUserConfig {
    /// The user public key.
    pub user_pk: UserPk,
    /// The configuration for the wallet environment.
    pub env_config: WalletEnvConfig,
}

/// Database directory configuration for a specific wallet environment.
#[derive(Clone)]
pub struct WalletEnvDbConfig {
    // NOTE(unstable): Fields should stay pub(crate) until API is more mature.
    /// The base data directory for all Lexe-related data.
    /// Holds data for different app environments and users.
    ///
    /// `<lexe_data_dir>`
    pub(crate) lexe_data_dir: PathBuf,
    /// Database directory for a specific wallet environment.
    /// Holds data for all users within that environment.
    ///
    /// `<lexe_data_dir>/<deploy_env>-<network>-<use_sgx>`
    pub(crate) env_db_dir: PathBuf,
}

/// Database directory configuration for a specific user and wallet environment.
#[derive(Clone)]
pub struct WalletUserDbConfig {
    // NOTE(unstable): Fields should stay pub(crate) until API is more mature.
    /// Environment-level database configuration.
    pub(crate) env_db_config: WalletEnvDbConfig,
    /// The user public key.
    pub(crate) user_pk: UserPk,
    /// Database directory for a specific user.
    /// Contains user-specific data like payments, settings, etc.
    ///
    /// `<lexe_data_dir>/<deploy_env>-<network>-<use_sgx>/<user_pk>`
    pub(crate) user_db_dir: PathBuf,
}

// --- impl WalletEnv --- //

impl WalletEnv {
    /// Bitcoin mainnet environment (prod, SGX).
    pub fn mainnet() -> Self {
        Self {
            deploy_env: DeployEnv::Prod,
            network: Network::Mainnet,
            use_sgx: true,
        }
    }

    /// Bitcoin testnet3 environment (staging, SGX).
    pub fn testnet3() -> Self {
        Self {
            deploy_env: DeployEnv::Staging,
            network: Network::Testnet3,
            use_sgx: true,
        }
    }

    /// Regtest environment (dev, configurable SGX).
    pub fn regtest(use_sgx: bool) -> Self {
        Self {
            deploy_env: DeployEnv::Dev,
            network: Network::Regtest,
            use_sgx,
        }
    }

    /// Returns the path to the seedphrase file for this environment.
    ///
    /// - Mainnet (prod): `<data_dir>/seedphrase.txt`
    /// - Other environments: `<data_dir>/seedphrase.<wallet_env>.txt`
    pub fn seedphrase_path(&self, data_dir: &Path) -> PathBuf {
        let filename = if self.deploy_env == DeployEnv::Prod {
            Cow::Borrowed("seedphrase.txt")
        } else {
            Cow::Owned(format!("seedphrase.{self}.txt"))
        };
        data_dir.join(filename.as_ref())
    }
}

impl fmt::Display for WalletEnv {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let deploy_env = self.deploy_env.as_str();
        let network = self.network.as_str();
        let sgx = if self.use_sgx { "sgx" } else { "dbg" };
        write!(f, "{deploy_env}-{network}-{sgx}")
    }
}

// --- impl WalletEnvConfig --- //

impl WalletEnvConfig {
    /// Standard wallet environment configuration for Bitcoin mainnet.
    pub fn mainnet() -> Self {
        let wallet_env = WalletEnv::mainnet();
        Self {
            gateway_url: wallet_env.deploy_env.gateway_url(None),
            user_agent: Cow::Borrowed(*SDK_USER_AGENT),
            wallet_env,
        }
    }

    /// Standard wallet environment configuration for Bitcoin testnet3.
    pub fn testnet3() -> Self {
        let wallet_env = WalletEnv::testnet3();
        Self {
            gateway_url: wallet_env.deploy_env.gateway_url(None),
            user_agent: Cow::Borrowed(*SDK_USER_AGENT),
            wallet_env,
        }
    }

    /// Regtest configuration for local development and testing.
    ///
    /// - `use_sgx`: Whether to use SGX enclaves.
    /// - `gateway_url`: Custom gateway URL. If `None`, uses the default dev
    ///   URL.
    pub fn regtest(
        use_sgx: bool,
        gateway_url: Option<impl Into<Cow<'static, str>>>,
    ) -> Self {
        let wallet_env = WalletEnv::regtest(use_sgx);
        let gateway_url = gateway_url.map(Into::into);
        Self {
            gateway_url: wallet_env.deploy_env.gateway_url(gateway_url),
            user_agent: Cow::Borrowed(*SDK_USER_AGENT),
            wallet_env,
        }
    }

    /// Construct a [`WalletEnvConfig`].
    #[cfg(feature = "unstable")]
    pub fn new(
        wallet_env: WalletEnv,
        gateway_url: Cow<'static, str>,
        user_agent: Cow<'static, str>,
    ) -> Self {
        Self {
            wallet_env,
            gateway_url,
            user_agent,
        }
    }

    /// Get a [`WalletEnvConfig`] for production.
    //
    // This is unstable because "Deploy environment" is an internal concept and
    // should not be exposed to SDK users.
    #[cfg(feature = "unstable")]
    pub fn prod() -> Self {
        Self::mainnet()
    }

    /// Get a [`WalletEnvConfig`] for staging.
    //
    // This is unstable because "Deploy environment" is an internal concept and
    // should not be exposed to SDK users.
    #[cfg(feature = "unstable")]
    pub fn staging() -> Self {
        Self::testnet3()
    }

    /// Get a [`WalletEnvConfig`] for dev/testing.
    //
    // This is unstable because "Deploy environment" is an internal concept and
    // should not be exposed to SDK users.
    #[cfg(feature = "unstable")]
    pub fn dev(
        use_sgx: bool,
        gateway_url: Option<impl Into<Cow<'static, str>>>,
    ) -> Self {
        Self::regtest(use_sgx, gateway_url)
    }

    /// The gateway URL.
    #[cfg(feature = "unstable")]
    pub fn gateway_url(&self) -> &str {
        &self.gateway_url
    }

    /// The user agent string.
    #[cfg(feature = "unstable")]
    pub fn user_agent(&self) -> &str {
        &self.user_agent
    }

    /// Returns the path to the seedphrase file for this environment.
    pub fn seedphrase_path(&self, data_dir: &Path) -> PathBuf {
        self.wallet_env.seedphrase_path(data_dir)
    }
}

// --- impl WalletEnvDbConfig --- //

impl WalletEnvDbConfig {
    /// Construct a new [`WalletEnvDbConfig`] from the wallet environment and
    /// base data directory.
    pub fn new(wallet_env: WalletEnv, lexe_data_dir: PathBuf) -> Self {
        let env_db_dir = lexe_data_dir.join(wallet_env.to_string());
        Self {
            lexe_data_dir,
            env_db_dir,
        }
    }

    /// The top-level, root, base data directory for Lexe-related data.
    pub fn lexe_data_dir(&self) -> &PathBuf {
        &self.lexe_data_dir
    }

    /// The database directory for this wallet environment.
    pub fn env_db_dir(&self) -> &PathBuf {
        &self.env_db_dir
    }
}

// --- impl WalletUserDbConfig --- //

impl WalletUserDbConfig {
    /// Construct a new [`WalletUserDbConfig`] from the environment database
    /// config and user public key.
    pub fn new(env_db_config: WalletEnvDbConfig, user_pk: UserPk) -> Self {
        let user_db_dir = env_db_config.env_db_dir.join(user_pk.to_string());
        Self {
            env_db_config,
            user_pk,
            user_db_dir,
        }
    }

    /// Construct a new [`WalletUserDbConfig`] from credentials and the
    /// environment database config.
    pub fn from_credentials(
        credentials: CredentialsRef<'_>,
        env_db_config: WalletEnvDbConfig,
    ) -> anyhow::Result<Self> {
        // Is `Some(_)` if the credentials were created by `node-v0.8.11+`.
        let user_pk = credentials.user_pk().context(
            "Client credentials are out of date. \
             Please create a new one from within the Lexe wallet app.",
        )?;
        Ok(Self::new(env_db_config, user_pk))
    }

    /// The environment-level database configuration.
    pub fn env_db_config(&self) -> &WalletEnvDbConfig {
        &self.env_db_config
    }

    /// The user public key.
    pub fn user_pk(&self) -> UserPk {
        self.user_pk
    }

    /// The top-level, root, base data directory for Lexe-related data.
    ///
    /// `<lexe_data_dir>`
    pub fn lexe_data_dir(&self) -> &PathBuf {
        self.env_db_config.lexe_data_dir()
    }

    /// The database directory for this wallet environment.
    ///
    /// `<lexe_data_dir>/<deploy_env>-<network>-<use_sgx>`
    pub fn env_db_dir(&self) -> &PathBuf {
        self.env_db_config.env_db_dir()
    }

    /// The user-specific database directory.
    ///
    /// `<lexe_data_dir>/<deploy_env>-<network>-<use_sgx>/<user_pk>`
    pub fn user_db_dir(&self) -> &PathBuf {
        &self.user_db_dir
    }

    /// Payment records and history.
    ///
    /// `<lexe_data_dir>/<deploy_env>-<network>-<use_sgx>/<user_pk>/payments_db`
    // Unstable
    pub(crate) fn payments_db_dir(&self) -> PathBuf {
        self.user_db_dir.join("payments_db")
    }

    // --- Old dirs --- //

    /// Old payment database directories that may need cleanup after migration.
    pub(crate) fn old_payment_db_dirs(&self) -> [PathBuf; 1] {
        [
            // BasicPaymentV1
            self.user_db_dir.join("payment_db"),
            // Add more here as needed
        ]
    }

    /// Old provision database directory that may need cleanup.
    pub(crate) fn old_provision_db_dir(&self) -> PathBuf {
        self.user_db_dir.join("provision_db")
    }
}

#[cfg(test)]
mod test {
    use std::str::FromStr;

    use super::*;

    /// Ensure SDK_USER_AGENT parses correctly and has the expected format.
    #[test]
    fn test_sdk_user_agent() {
        let user_agent: &str = &SDK_USER_AGENT;

        // Should match: "lexe/<semver> node/<semver>"
        let (sdk_part, node_part) = user_agent
            .split_once(" node/")
            .expect("Missing ' node/' separator");

        // Validate sdk part: "lexe/<version>"
        let sdk_version_str = sdk_part
            .strip_prefix("lexe/")
            .expect("Missing 'lexe/' prefix");
        let _sdk_version = semver::Version::from_str(sdk_version_str)
            .expect("Invalid SDK semver version");

        // Validate node version
        let _node_version = semver::Version::from_str(node_part)
            .expect("Invalid node semver version");
    }
}