use std::str::FromStr;
use std::time::Duration;

use anyhow::{anyhow, Result};
use nostr::nips;
use nostr::prelude::*;
use serde::{Deserialize, Serialize};

use crate::bot::ons::ons1::Ons1Config;
// use crate::bot::ons::ons2::Ons2Config;
use crate::bot::ons::nip47::Nip47Config;
use crate::bot::ons::ons3::Ons3Config;
use crate::bot::ons::ons4::Ons4Config;

/// Can be specified as env variables with an OAK_ prefix
/// For example: OAK_LND_POLLING_INTERVAL_SECONDS=10 cargo run
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct StartupParamsOptional {
    /// The directory where all DB files will be stored
    /// If not specified, the DB files will be saved in the same folder from which the node is started
    pub data_dir: Option<String>,

    /// The timeout used when connecting to any HTTP endpoint.
    ///
    /// Note this also applies when calling a LNURL-pay endpoint via tor, so the value should be
    /// large enough to accommodate for that.
    pub http_request_timeout_seconds: u64,

    /// The interval in which LND will be polled for the newest block height
    pub lnd_polling_interval_seconds: u64,

    pub onion_socks5_host: Option<String>,
    pub onion_socks5_port: Option<u16>,

    /// The Nostr bot private key
    pub nostr_bot_prv_key: Option<SecretKey>,

    pub nostr_bot_owner_nip05: Option<String>,
    pub nostr_bot_owner_nprofile: Option<String>,

    /// The Nostr bot owner pubkey
    pub nostr_bot_owner_pub_key: Option<String>,
    /// The Nostr websocket endpoint (format wss://host.com)
    ///
    /// Default value. It will be overwritten during pairing.
    pub nostr_ws_endpoint: String,

    pub ons1: Ons1Config,
    // pub ons2: Ons2Config,
    pub ons3: Ons3Config,
    pub ons4: Ons4Config,
    pub nip47: Nip47Config,

    pub nostr_pow_provider_automine_p_min: Option<f64>,
    pub nostr_pow_provider_automine_t_max_millis: Option<u64>,
    pub nostr_pow_provider_automine_hashrate: Option<u64>,
    pub nostr_pow_provider_automine_baseline_price: Option<u64>,

    /// How long back in time to look for unpaid delivered PoWs
    ///
    /// The amount of unpaid delivered PoWs in this timeframe sets the minimum level of PoW required for new Client Asks.
    /// Incoming Asks below this level are ignored.
    ///
    /// This effect decays with times, so after this amount of time, an unpaid delivered PoW doesn't count anymore.
    /// After this amount of time of no unpaid delivered PoWs, all new Client Asks will be considered for automining.
    pub nostr_pow_provider_dos_watchtower_span_minutes: u32,

    pub nostr_pow_client_prvkey: Option<SecretKey>,
    pub nostr_pow_provider_prvkey: Option<SecretKey>,
}

impl StartupParamsOptional {
    /// Attempts to construct a profile from the available owner identifier, in this order:
    ///
    /// - [`nostr_bot_owner_nprofile`]
    /// - [`nostr_bot_owner_nip05`]
    /// - [`nostr_bot_owner_pub_key`] and [`nostr_ws_endpoint`]
    ///
    /// A [Profile] will be generated from the first identifier found.
    async fn try_get_bot_owner_nprofile(&self) -> Result<Option<Profile>> {
        if let Some(nprofile) = &self.nostr_bot_owner_nprofile {
            info!("Found owner nprofile");
            return Profile::from_bech32(nprofile)
                .map_err(|e| anyhow!(e))
                .map(Some);
        }

        if let Some(nip05) = &self.nostr_bot_owner_nip05 {
            info!("Found owner NIP-05");
            let profile = nips::nip05::get_profile(nip05, None).await?;
            return Ok(Some(profile));
        }

        if let Some(pubkey) = &self.nostr_bot_owner_pub_key {
            info!("Found owner pubkey");
            return Ok(Some(Profile {
                public_key: XOnlyPublicKey::from_str(pubkey)?,
                relays: vec![self.nostr_ws_endpoint.clone()],
            }));
        }

        Ok(None)
    }

    /// Attempts to construct a profile using [Self::try_get_bot_owner_nprofile].
    ///
    /// It then treats the relays as seed relays, which it queries for a NIP-65 Relay Metadata
    /// event. If found, the relay list from this event is merged with the seed relay list and
    /// an updated profile is returned.
    pub async fn try_get_bot_owner_nprofile_with_nip65_relays(&self) -> Result<Option<Profile>> {
        match self.try_get_bot_owner_nprofile().await? {
            None => Ok(None),
            Some(base_profile) => {
                // Query seed relays from the initial profile for the NIP-65 Relay List Metadata
                let mut nip65_relays = get_nip65_relays_from_profile(&base_profile).await;

                let mut merged_relay_list = base_profile.relays;
                merged_relay_list.append(&mut nip65_relays);
                merged_relay_list.sort();
                merged_relay_list.dedup();

                info!("Merged relay list: {merged_relay_list:?}");
                Ok(Some(Profile {
                    public_key: base_profile.public_key,
                    relays: merged_relay_list,
                }))
            }
        }
    }
}

/// Query relays in the arg profile (seed relays) for the NIP-65 Relay List Metadata.
async fn get_nip65_relays_from_profile(profile: &Profile) -> Vec<String> {
    info!("Querying for NIP-65 relays");

    let seed_relays: Vec<(String, Option<_>)> = profile
        .relays
        .clone()
        .into_iter()
        .map(|r| (r, None))
        .collect();

    let mut nip65_relays: Vec<String> = vec![];

    let client = nostr_sdk::Client::new(&Keys::generate());
    match client.add_relays(seed_relays).await {
        Ok(_) => {
            client.connect().await;

            match client
                .get_events_of(
                    vec![Filter::new()
                        .authors(vec![profile.public_key])
                        .kind(Kind::RelayList)],
                    Some(Duration::from_secs(5)),
                )
                .await
            {
                Ok(events) => {
                    for event in events {
                        let list = nips::nip65::extract_relay_list(&event);
                        info!("Found NIP-65 relay list metadata: {list:?}");
                        let mut list: Vec<String> =
                            list.into_iter().map(|(url, _rw)| url.to_string()).collect();
                        nip65_relays.append(&mut list);
                    }
                }
                Err(e) => warn!("Failed to lookup NIP-65 relays: {e}"),
            };
        }
        Err(e) => warn!("Failed to add relays relays: {e}"),
    };

    nip65_relays.sort();
    nip65_relays.dedup();
    nip65_relays
}

/// Default values for the optional startup params
impl Default for StartupParamsOptional {
    fn default() -> StartupParamsOptional {
        StartupParamsOptional {
            // Defaults to the current directory (path from which the executable is started)
            data_dir: None,

            http_request_timeout_seconds: 60,

            lnd_polling_interval_seconds: 60,

            onion_socks5_port: None,
            onion_socks5_host: None,

            nostr_ws_endpoint: "wss://nostr-relay.wlvs.space".to_string(),

            nostr_bot_prv_key: None,

            nostr_bot_owner_nip05: None,
            nostr_bot_owner_nprofile: None,
            nostr_bot_owner_pub_key: None,

            ons1: Ons1Config::default(),
            // ons2: Ons2Config::default(),
            ons3: Ons3Config::default(),
            ons4: Ons4Config::default(),
            nip47: Nip47Config::default(),

            nostr_pow_provider_automine_p_min: None,
            nostr_pow_provider_automine_t_max_millis: None,
            nostr_pow_provider_automine_hashrate: None,
            nostr_pow_provider_automine_baseline_price: None,

            nostr_pow_provider_dos_watchtower_span_minutes: 60,

            nostr_pow_client_prvkey: None,
            nostr_pow_provider_prvkey: None,
        }
    }
}

/// Can be specified as env variables with an OAK_ prefix
#[derive(Debug, Deserialize, Serialize)]
pub struct StartupParamsRequired {
    /// REST API endpoint for LND
    pub lnd_rest_api_url: String,

    /// Full path to LND macaroon file
    pub lnd_macaroon_path: String,

    /// Full path to LND certificate file
    pub lnd_cert_path: String,
}
