use crate::wallets::plotnft_utils::{
    get_plotnft_by_launcher_id, submit_next_state_spend_bundle,
    submit_next_state_spend_bundle_with_key, PlotNFTWallet,
};
use crate::wallets::Wallet;
use bip39::Mnemonic;
use blst::min_pk::SecretKey;
use dg_xch_clients::api::full_node::FullnodeAPI;
use dg_xch_clients::api::pool::{DefaultPoolClient, PoolClient};
use dg_xch_clients::rpc::full_node::FullnodeClient;
use dg_xch_core::blockchain::coin_spend::CoinSpend;
use dg_xch_core::blockchain::sized_bytes::{Bytes32, Bytes48};
use dg_xch_core::consensus::constants::ConsensusConstants;
use dg_xch_core::constants::{FARMING_TO_POOL, POOL_PROTOCOL_VERSION, SELF_POOLING};
use dg_xch_core::plots::PlotNft;
use dg_xch_core::pool::PoolState;
use dg_xch_core::protocols::pool::GetPoolInfoResponse;
use dg_xch_core::traits::SizedBytes;
use dg_xch_keys::{
    encode_puzzle_hash, fingerprint, key_from_mnemonic_str, master_sk_to_farmer_sk,
    master_sk_to_pool_sk, master_sk_to_wallet_sk, BLS_SPEC_NUMBER, CHIA_BLOCKCHAIN_NUMBER,
    FARMER_PATH, POOL_PATH,
};
use dg_xch_puzzles::p2_delegated_puzzle_or_hidden_puzzle::{
    calculate_synthetic_secret_key, puzzle_hash_for_pk, DEFAULT_HIDDEN_PUZZLE_HASH,
};
use log::{debug, error, info};
use std::collections::{HashMap, HashSet};
use std::io::{Error, ErrorKind};
use std::ops::Add;
use std::sync::Arc;
use std::time::{Duration, Instant};

pub fn create_cold_wallet() -> Result<(), Error> {
    let mnemonic = Mnemonic::generate(24)
        .map_err(|e| Error::new(ErrorKind::InvalidInput, format!("{e:?}")))?;
    let master_secret_key = key_from_mnemonic_str(&mnemonic.to_string())?;
    let master_public_key = master_secret_key.sk_to_pk();
    let fp = fingerprint(&master_public_key);
    info!("Fingerprint: {fp}");
    info!("Mnemonic Phrase: {}", &mnemonic.to_string());
    info!("Master SK: {}", Bytes32::new(master_secret_key.to_bytes()));
    info!(
        "Master public key (m): {}",
        Bytes48::from(master_public_key.to_bytes())
    );
    info!(
        "Farmer public key (m/{BLS_SPEC_NUMBER}/{CHIA_BLOCKCHAIN_NUMBER}/{FARMER_PATH}/0): {}",
        Bytes48::from(
            master_sk_to_farmer_sk(&master_secret_key)?
                .sk_to_pk()
                .to_bytes()
        )
    );
    info!(
        "Pool public key (m/{BLS_SPEC_NUMBER}/{CHIA_BLOCKCHAIN_NUMBER}/{POOL_PATH}/0: {}",
        Bytes48::from(
            master_sk_to_pool_sk(&master_secret_key)?
                .sk_to_pk()
                .to_bytes()
        )
    );
    info!("First 3 Wallet addresses");
    for i in 0..3 {
        let wallet_sk = master_sk_to_wallet_sk(&master_secret_key, i)
            .map_err(|e| Error::new(ErrorKind::InvalidInput, format!("MasterKey: {e:?}")))?;
        let address = encode_puzzle_hash(
            &puzzle_hash_for_pk(Bytes48::from(wallet_sk.sk_to_pk().to_bytes()))?,
            "xch",
        )?;
        info!("Index: {i}, Address: {address}");
    }
    Ok(())
}

pub fn keys_for_coinspends(
    coin_spends: &[CoinSpend],
    master_sk: &SecretKey,
    max_pub_keys: u32,
) -> Result<HashMap<Bytes48, SecretKey>, Error> {
    let mut key_cache: HashMap<Bytes48, SecretKey> = HashMap::new();
    let mut puz_key_cache: HashSet<Bytes32> = HashSet::new();
    let mut last_key_index = 0;
    for c in coin_spends {
        if puz_key_cache.contains(&c.coin.puzzle_hash) {
            continue;
        }
        for ki in last_key_index..max_pub_keys {
            let sec_key = master_sk_to_wallet_sk(master_sk, ki)?;
            let pub_key = sec_key.sk_to_pk();
            let puz_hash = puzzle_hash_for_pk(pub_key.into())?;
            let synthetic_secret_key =
                calculate_synthetic_secret_key(&sec_key, *DEFAULT_HIDDEN_PUZZLE_HASH)?;
            info!("MasterSK: {master_sk:?}");
            info!("WalletSK: {sec_key:?}");
            info!("SyntheticSK: {synthetic_secret_key:?}");
            key_cache.insert(pub_key.into(), synthetic_secret_key.clone());
            puz_key_cache.insert(puz_hash);
            if c.coin.puzzle_hash == puz_hash {
                last_key_index = ki;
                break;
            }
        }
    }
    Ok(key_cache)
}

pub async fn migrate_plot_nft(
    client: Arc<FullnodeClient>,
    target_pool: &str,
    launcher_id: Bytes32,
    owner_ph: Bytes32,
    mnemonic: &str,
    constants: Arc<ConsensusConstants>,
    fee: u64,
) -> Result<(), Error> {
    let pool_url = if target_pool.trim().is_empty() {
        None
    } else {
        Some(if target_pool.starts_with("https://") {
            target_pool.to_string()
        } else {
            format!("https://{target_pool}")
        })
    };
    let pool_wallet =
        PlotNFTWallet::new(key_from_mnemonic_str(mnemonic)?, client.as_ref(), constants)?;
    info!("Searching for PlotNFT with LauncherID: {launcher_id}");
    if let Some(mut plot_nft) =
        get_plotnft_by_launcher_id(client.clone(), launcher_id, None).await?
    {
        info!("Checking if PlotNFT needs migration");
        if plot_nft.pool_state.pool_url != pool_url || plot_nft.pool_state.state != FARMING_TO_POOL
        {
            info!("Starting Migration");
            let target_pool_state =
                create_and_validate_target_state(pool_url.clone(), owner_ph, &plot_nft).await?;
            if plot_nft.pool_state.state == FARMING_TO_POOL {
                info!("Creating Leaving Pool Spend");
                if fee > 0 && !pool_wallet.sync().await? {
                    error!("Failed to Sync Wallet");
                    return Err(Error::other("Failed to Sync"));
                }
                submit_next_state_spend_bundle(
                    client.clone(),
                    &pool_wallet,
                    &plot_nft,
                    &target_pool_state,
                    fee,
                )
                .await?;
                info!(
                    "Waiting for PlotNFT State to be Buried for Leaving {}",
                    plot_nft
                        .pool_state
                        .pool_url
                        .as_ref()
                        .unwrap_or(&String::from("None"))
                );
                wait_for_plot_nft_ready_state(
                    client.clone(),
                    launcher_id,
                    Some(plot_nft.singleton_coin.coin.name()),
                )
                .await;
                info!("Reloading PlotNFT Info");
                plot_nft = get_plotnft_by_launcher_id(
                    client.clone(),
                    launcher_id,
                    Some(plot_nft.singleton_coin.coin.name()),
                )
                .await?
                .ok_or_else(|| {
                    error!("Failed to reload plot_nft after first spend");
                    Error::other("Failed to reload plot_nft after first spend")
                })?;
            }
            info!("Creating Farming to Pool Spend");
            if fee > 0 && !pool_wallet.sync().await? {
                error!("Failed to Sync Wallet");
                return Err(Error::other("Failed to Sync"));
            }
            submit_next_state_spend_bundle(
                client.clone(),
                &pool_wallet,
                &plot_nft,
                &target_pool_state,
                fee,
            )
            .await?;
            info!(
                "Waiting for PlotNFT State to be Buried for Joining {}",
                pool_url.unwrap_or(String::from("Self Pooling"))
            );
            wait_for_plot_nft_ready_state(
                client,
                launcher_id,
                Some(plot_nft.singleton_coin.coin.name()),
            )
            .await;
        } else {
            info!("PlotNFT Already on Selected Pool");
        }
    } else {
        info!("No PlotNFT Found");
    }
    Ok(())
}
pub async fn migrate_plot_nft_with_owner_key(
    client: Arc<FullnodeClient>,
    target_pool: &str,
    launcher_id: Bytes32,
    owner_ph: Bytes32,
    owner_key: &SecretKey,
) -> Result<(), Error> {
    let pool_url = if target_pool.trim().is_empty() {
        None
    } else {
        Some(if target_pool.starts_with("https://") {
            target_pool.to_string()
        } else {
            format!("https://{target_pool}")
        })
    };
    info!("Searching for PlotNFT with LauncherID: {launcher_id}");
    if let Some(mut plot_nft) =
        get_plotnft_by_launcher_id(client.clone(), launcher_id, None).await?
    {
        info!("Checking if PlotNFT needs migration");
        if plot_nft.pool_state.pool_url != pool_url || plot_nft.pool_state.state != FARMING_TO_POOL
        {
            info!("Starting Migration");
            let target_pool_state =
                create_and_validate_target_state(pool_url.clone(), owner_ph, &plot_nft).await?;
            if plot_nft.pool_state.state == FARMING_TO_POOL {
                info!("Creating Leaving Pool Spend");
                submit_next_state_spend_bundle_with_key(
                    client.clone(),
                    owner_key,
                    &plot_nft,
                    &target_pool_state,
                    &ConsensusConstants::default(),
                )
                .await?;
                info!(
                    "Waiting for PlotNFT State to be Buried for Leaving {}",
                    plot_nft
                        .pool_state
                        .pool_url
                        .as_ref()
                        .unwrap_or(&String::from("None"))
                );
                wait_for_plot_nft_ready_state(
                    client.clone(),
                    launcher_id,
                    Some(plot_nft.singleton_coin.coin.name()),
                )
                .await;
                info!("Reloading PlotNFT Info");
                plot_nft = get_plotnft_by_launcher_id(
                    client.clone(),
                    launcher_id,
                    Some(plot_nft.singleton_coin.coin.name()),
                )
                .await?
                .ok_or_else(|| {
                    error!("Failed to reload plot_nft after first spend");
                    Error::other("Failed to reload plot_nft after first spend")
                })?;
            }
            info!("Creating Farming to Pool Spend");
            submit_next_state_spend_bundle_with_key(
                client.clone(),
                owner_key,
                &plot_nft,
                &target_pool_state,
                &ConsensusConstants::default(),
            )
            .await?;
            info!(
                "Waiting for PlotNFT State to be Buried for Joining {}",
                pool_url.unwrap_or(String::from("Self Pooling"))
            );
            wait_for_num_blocks(client.clone(), 20, 600).await;
        } else {
            info!("PlotNFT Already on Selected Pool");
        }
    } else {
        info!("No PlotNFT Found");
    }
    Ok(())
}

async fn wait_for_plot_nft_ready_state(
    client: Arc<FullnodeClient>,
    launcher_id: Bytes32,
    last_known_coin_name: Option<Bytes32>,
) {
    loop {
        match get_plotnft_ready_state(client.clone(), launcher_id, last_known_coin_name).await {
            Ok(is_ready) => {
                if is_ready {
                    break;
                }
                tokio::time::sleep(Duration::from_secs(60)).await;
            }
            Err(e) => {
                error!("Error Checking PlotNFT State, Waiting and Trying again. {e:?}");
                tokio::time::sleep(Duration::from_secs(30)).await;
            }
        }
    }
}

async fn wait_for_num_blocks(client: Arc<FullnodeClient>, height: u32, timeout_seconds: u64) {
    let mut start_height = None;
    let end_time = Instant::now().add(Duration::from_secs(timeout_seconds));
    loop {
        let now = Instant::now();
        if now >= end_time {
            break;
        }
        match client.get_blockchain_state().await {
            Ok(state) => {
                if let Some(peak) = state.peak {
                    if let Some(start) = start_height {
                        if peak.height > start + height {
                            break;
                        }
                        info!("Waiting for {} more blocks", start + height - peak.height);
                        tokio::time::sleep(std::cmp::min(
                            end_time.duration_since(now),
                            Duration::from_secs(10),
                        ))
                        .await;
                    } else {
                        start_height = Some(peak.height);
                    }
                }
            }
            Err(e) => {
                error!("Error Checking PlotNFT State, Waiting and Trying again. {e:?}");
            }
        }
    }
}

async fn create_and_validate_target_state(
    pool_url: Option<String>,
    target_puzzle_hash: Bytes32,
    plot_nft: &PlotNft,
) -> Result<PoolState, Error> {
    let target_pool_state = if let Some(pool_url) = pool_url {
        let pool_info = get_pool_info(&pool_url).await?;
        PoolState {
            owner_pubkey: plot_nft.pool_state.owner_pubkey,
            pool_url: Some(pool_url),
            relative_lock_height: pool_info.relative_lock_height,
            state: FARMING_TO_POOL, //# Farming to Pool
            target_puzzle_hash: pool_info.target_puzzle_hash,
            version: 1,
        }
    } else {
        PoolState {
            owner_pubkey: plot_nft.pool_state.owner_pubkey,
            pool_url: None,
            relative_lock_height: 0,
            state: SELF_POOLING, //# Self pooling
            target_puzzle_hash,
            version: 1,
        }
    };
    if plot_nft.pool_state == target_pool_state {
        let error_message = format!(
            "Current State equal to Target State: {:?}",
            &target_pool_state
        );
        error!("{error_message}");
        return Err(Error::new(ErrorKind::InvalidData, error_message));
    }
    info!(
        "Targeting State: {}",
        serde_json::to_string_pretty(&target_pool_state).unwrap_or_default()
    );
    Ok(target_pool_state)
}

async fn get_pool_info(pool_url: &str) -> Result<GetPoolInfoResponse, Error> {
    let pool_info = DefaultPoolClient::new()
        .get_pool_info(pool_url)
        .await
        .map_err(|e| Error::other(format!("Failed to load pool info: {e:?}")))?;
    validate_pool_info(&pool_info)?;
    Ok(pool_info)
}

fn validate_pool_info(pool_info: &GetPoolInfoResponse) -> Result<(), Error> {
    if pool_info.relative_lock_height > 1000 {
        let error_message = "Relative lock height too high for this pool, cannot join";
        error!("{error_message}");
        Err(Error::new(ErrorKind::InvalidData, error_message))
    } else if pool_info.protocol_version != POOL_PROTOCOL_VERSION {
        let error_message = format!(
            "Incorrect version: {}, should be {POOL_PROTOCOL_VERSION}",
            pool_info.protocol_version
        );
        error!("{error_message}");
        Err(Error::new(ErrorKind::InvalidData, error_message))
    } else {
        Ok(())
    }
}

pub async fn get_plotnft_ready_state(
    client: Arc<FullnodeClient>,
    launcher_id: Bytes32,
    last_known_coin_name: Option<Bytes32>,
) -> Result<bool, Error> {
    let mut peak = None;
    while peak.is_none() {
        peak = client.get_blockchain_state().await?.peak;
        tokio::time::sleep(Duration::from_secs(5)).await;
    }
    let peak = peak.unwrap();
    match get_plotnft_by_launcher_id(client, launcher_id, last_known_coin_name).await? {
        None => {
            error!("Failed to find PlotNFT with LauncherID: {launcher_id}");
            Ok(false)
        }
        Some(plotnft) => {
            debug!("Found PlotNFT: {}", plotnft.launcher_id);
            let test_height = plotnft.pool_state.relative_lock_height
                + 2
                + plotnft.singleton_coin.confirmed_block_index;
            info!(
                "Ready to move {}: {}, current_height: {}, target_height {}",
                plotnft.launcher_id,
                peak.height >= test_height,
                peak.height,
                test_height
            );
            Ok(peak.height >= test_height)
        }
    }
}
