use std::{
    cmp::Ordering,
    collections::{HashMap, HashSet},
    iter::FromIterator,
    str::FromStr,
    sync::Arc,
};

use iced::{Command, Subscription};
use liana::{
    commands::ListCoinsEntry,
    descriptors::LianaDescriptor,
    miniscript::bitcoin::{
        address, psbt::Psbt, secp256k1, Address, Amount, Denomination, Network, OutPoint,
    },
    spend::{SpendCreationError, MAX_FEERATE},
};

use liana_ui::{component::form, widget::Element};

use crate::{
    app::{cache::Cache, error::Error, message::Message, state::psbt, view, wallet::Wallet},
    daemon::{
        model::{remaining_sequence, Coin, CreateSpendResult, SpendTx},
        Daemon,
    },
};

/// See: https://github.com/wizardsardine/liana/blob/master/src/commands/mod.rs#L32
const DUST_OUTPUT_SATS: u64 = 5_000;

#[derive(Clone)]
pub struct TransactionDraft {
    network: Network,
    inputs: Vec<Coin>,
    recipients: Vec<Recipient>,
    generated: Option<(Psbt, Vec<String>)>,
    batch_label: Option<String>,
    labels: HashMap<String, String>,
}

impl TransactionDraft {
    pub fn new(network: Network) -> Self {
        Self {
            network,
            inputs: Vec::new(),
            recipients: Vec::new(),
            generated: None,
            batch_label: None,
            labels: HashMap::new(),
        }
    }
}

pub trait Step {
    fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message>;
    fn update(
        &mut self,
        daemon: Arc<dyn Daemon + Sync + Send>,
        cache: &Cache,
        message: Message,
    ) -> Command<Message>;
    fn apply(&self, _draft: &mut TransactionDraft) {}
    fn interrupt(&mut self) {}
    fn load(&mut self, _draft: &TransactionDraft) {}
    fn subscription(&self) -> Subscription<Message> {
        Subscription::none()
    }
}

pub struct DefineSpend {
    recipients: Vec<Recipient>,
    /// If set, this is the index of a recipient that should
    /// receive the max amount.
    send_max_to_recipient: Option<usize>,
    /// Will be `true` if coins for spend were manually selected by user.
    /// Otherwise, will be `false` (including for self-send).
    is_user_coin_selection: bool,
    is_valid: bool,
    is_duplicate: bool,

    network: Network,
    descriptor: LianaDescriptor,
    curve: secp256k1::Secp256k1<secp256k1::VerifyOnly>,
    timelock: u16,
    coins: Vec<(Coin, bool)>,
    coins_labels: HashMap<String, String>,
    batch_label: form::Value<String>,
    amount_left_to_select: Option<Amount>,
    feerate: form::Value<String>,
    generated: Option<(Psbt, Vec<String>)>,
    warning: Option<Error>,
}

impl DefineSpend {
    pub fn new(
        network: Network,
        descriptor: LianaDescriptor,
        coins: &[Coin],
        timelock: u16,
    ) -> Self {
        let coins: Vec<(Coin, bool)> = coins
            .iter()
            .filter_map(|c| {
                if c.spend_info.is_none() && !c.is_immature {
                    Some((c.clone(), false))
                } else {
                    None
                }
            })
            .collect();

        Self {
            network,
            descriptor,
            curve: secp256k1::Secp256k1::verification_only(),
            timelock,
            generated: None,
            coins,
            coins_labels: HashMap::new(),
            batch_label: form::Value::default(),
            recipients: vec![Recipient::default()],
            send_max_to_recipient: None,
            is_user_coin_selection: false, // Start with auto-selection until user edits selection.
            is_valid: false,
            is_duplicate: false,
            feerate: form::Value::default(),
            amount_left_to_select: None,
            warning: None,
        }
    }

    pub fn with_preselected_coins(mut self, preselected_coins: &[OutPoint]) -> Self {
        for (coin, selected) in &mut self.coins {
            *selected = preselected_coins.contains(&coin.outpoint);
        }
        self
    }

    pub fn with_coins_sorted(mut self, blockheight: u32) -> Self {
        self.sort_coins(blockheight);
        self
    }

    fn sort_coins(&mut self, blockheight: u32) {
        let timelock = self.timelock;
        self.coins.sort_by(|(a, a_selected), (b, b_selected)| {
            if *a_selected && !b_selected || !a_selected && *b_selected {
                b_selected.cmp(a_selected)
            } else if remaining_sequence(a, blockheight, timelock)
                == remaining_sequence(b, blockheight, timelock)
            {
                // bigger amount first
                b.amount.cmp(&a.amount)
            } else {
                // smallest blockheight (remaining_sequence) first
                a.block_height.cmp(&b.block_height)
            }
        });
    }

    pub fn self_send(mut self) -> Self {
        self.recipients = Vec::new();
        self
    }

    // If `is_redraft`, the validation of recipients will take into account
    // whether any should receive the max amount. Otherwise, all recipients
    // will be fully validated.
    fn form_values_are_valid(&self, is_redraft: bool) -> bool {
        self.feerate.valid
            && !self.feerate.value.is_empty()
            && (self.batch_label.valid || self.recipients.len() < 2)
            // Recipients will be empty for self-send.
            && self.recipients.iter().enumerate().all(|(i, r)|
            r.valid() || (is_redraft && self.send_max_to_recipient == Some(i) && r.address_valid()))
    }

    fn exists_duplicate(&self) -> bool {
        for (i, recipient) in self.recipients.iter().enumerate() {
            if !recipient.address.value.is_empty()
                && self.recipients[..i]
                    .iter()
                    .any(|r| r.address.value == recipient.address.value)
            {
                return true;
            }
        }
        false
    }

    fn check_valid(&mut self) {
        self.is_valid =
            self.form_values_are_valid(false) && self.coins.iter().any(|(_, selected)| *selected);
        self.is_duplicate = self.exists_duplicate();
    }
    /// redraft calculates the amount left to select and auto selects coins
    /// if the user did not select a coin manually
    fn redraft(&mut self, daemon: Arc<dyn Daemon + Sync + Send>) {
        if !self.form_values_are_valid(true)
            || self.exists_duplicate()
            || self.recipients.is_empty()
        {
            // The current form details are not valid to draft a spend, so remove any previously
            // calculated amount as it will no longer be valid and could be misleading, e.g. if
            // the user removes the amount from one of the recipients.
            // We can leave any coins selected as they will either be automatically updated
            // as soon as the form is valid or the user has selected these specific coins and
            // so we should not touch them.
            self.amount_left_to_select = None;
            // Remove any max amount from a recipient as it could be misleading.
            if let Some(i) = self.send_max_to_recipient {
                self.recipients
                    .get_mut(i)
                    .expect("max has been requested for this recipient so it must exist")
                    .update(
                        self.network,
                        view::CreateSpendMessage::RecipientEdited(i, "amount", "".to_string()),
                    );
            }
            return;
        }

        let destinations: HashMap<Address<address::NetworkUnchecked>, u64> = self
            .recipients
            .iter()
            .enumerate()
            .filter_map(|(i, recipient)| {
                // A recipient that receives the max should be treated as change for coin selection.
                // Note that we only give a change output if its value is above the dust
                // threshold, but a user can only send payments above the same dust threshold,
                // so using change output to determine the max amount for a recipient will
                // not prevent a value that could otherwise be entered manually by the user.
                if self.send_max_to_recipient == Some(i) {
                    None
                } else {
                    Some((
                        Address::from_str(&recipient.address.value).expect("Checked before"),
                        recipient.amount().expect("Checked before"),
                    ))
                }
            })
            .collect();

        let recipient_with_max = if let Some(i) = self.send_max_to_recipient {
            Some((
                i,
                self.recipients
                    .get_mut(i)
                    .expect("max has been requested for this recipient so it must exist"),
            ))
        } else {
            None
        };

        let outpoints = if self.is_user_coin_selection {
            let outpoints: Vec<_> = self
                .coins
                .iter()
                .filter_map(
                    |(c, selected)| {
                        if *selected {
                            Some(c.outpoint)
                        } else {
                            None
                        }
                    },
                )
                .collect();
            if outpoints.is_empty() {
                // If the user has deselected all coins, set any recipient's max amount to 0.
                if let Some((i, recipient)) = recipient_with_max {
                    recipient.update(
                        self.network,
                        view::CreateSpendMessage::RecipientEdited(i, "amount", "0".to_string()),
                    );
                }
                // Simply set the amount left to select as the total destination value. Note this
                // doesn't take account of the fee, but passing an empty list to `create_spend_tx`
                // would use auto-selection and so we settle for this approximation.
                self.amount_left_to_select = Some(Amount::from_sat(destinations.values().sum()));
                return;
            }
            outpoints
        } else if self.send_max_to_recipient.is_some() {
            // If user has not selected coins, send the max available from all coins.
            self.coins.iter().map(|(c, _)| c.outpoint).collect()
        } else {
            Vec::new() // pass empty list for auto-selection
        };

        // If sending the max to a recipient, use that recipient's address as the
        // change address.
        // Otherwise, use a fixed change address from the user's own wallet so that
        // we don't increment the change index.
        let change_address = if let Some((_, recipient)) = &recipient_with_max {
            Address::from_str(&recipient.address.value)
                .expect("Checked before")
                .as_unchecked()
                .clone()
        } else {
            self.descriptor
                .change_descriptor()
                .derive(0.into(), &self.curve)
                .address(self.network)
                .as_unchecked()
                .clone()
        };

        let feerate_vb = self.feerate.value.parse::<u64>().expect("Checked before");
        match tokio::runtime::Handle::current().block_on(async {
            daemon
                .create_spend_tx(
                    &outpoints,
                    &destinations,
                    feerate_vb,
                    Some(change_address.clone()),
                )
                .await
        }) {
            Ok(CreateSpendResult::Success { psbt, .. }) => {
                self.warning = None;
                if !self.is_user_coin_selection {
                    let selected_coins: Vec<OutPoint> = psbt
                        .unsigned_tx
                        .input
                        .iter()
                        .map(|c| c.previous_output)
                        .collect();
                    // Mark coins as selected.
                    for (coin, selected) in &mut self.coins {
                        *selected = selected_coins.contains(&coin.outpoint);
                    }
                }
                // As coin selection was successful, we can assume there is nothing left to select.
                self.amount_left_to_select = Some(Amount::from_sat(0));
                if let Some((i, recipient)) = recipient_with_max {
                    // If there's no change output, any excess must be below the dust threshold
                    // and so the max available for this recipient is 0.
                    let amount = psbt
                        .unsigned_tx
                        .output
                        .iter()
                        .find(|o| {
                            o.script_pubkey
                                == change_address.clone().assume_checked().script_pubkey()
                        })
                        .map(|change_output| change_output.value.to_btc())
                        .unwrap_or(0.0)
                        .to_string();
                    recipient.update(
                        self.network,
                        view::CreateSpendMessage::RecipientEdited(i, "amount", amount),
                    );
                }
            }
            // For coin selection error (insufficient funds), do not make any changes to
            // selected coins on screen and just show user how much is left to select.
            // User can then either:
            // - modify recipient amounts and/or feerate and let coin selection run again, or
            // - select coins manually.
            Ok(CreateSpendResult::InsufficientFunds { missing }) => {
                self.amount_left_to_select = Some(Amount::from_sat(missing));
                if let Some((i, recipient)) = recipient_with_max {
                    let amount = Amount::from_sat(if destinations.is_empty() {
                        // If there are no other recipients, then the missing value will
                        // be the amount left to select in order to create an output at the dust
                        // threshold. Therefore, set this recipient's amount to this value so
                        // that the information shown is consistent.
                        // Otherwise, there are already insufficient funds for the other
                        // recipients and so the max available for this recipient is 0.
                        DUST_OUTPUT_SATS
                    } else {
                        0
                    })
                    .to_btc()
                    .to_string();
                    recipient.update(
                        self.network,
                        view::CreateSpendMessage::RecipientEdited(i, "amount", amount),
                    );
                }
            }
            Err(e) => {
                self.warning = Some(e.into());
            }
        }
    }
}

impl Step for DefineSpend {
    fn update(
        &mut self,
        daemon: Arc<dyn Daemon + Sync + Send>,
        cache: &Cache,
        message: Message,
    ) -> Command<Message> {
        match message {
            Message::View(view::Message::CreateSpend(msg)) => {
                match msg {
                    view::CreateSpendMessage::BatchLabelEdited(label) => {
                        self.batch_label.valid = label.len() <= 100;
                        self.batch_label.value = label;
                    }
                    view::CreateSpendMessage::Clear => {
                        *self = Self::new(
                            self.network,
                            self.descriptor.clone(),
                            self.coins
                                .iter()
                                .map(|(c, _)| c.clone())
                                .collect::<Vec<ListCoinsEntry>>()
                                .as_slice(),
                            self.timelock,
                        );
                        return Command::none();
                    }
                    view::CreateSpendMessage::AddRecipient => {
                        self.recipients.push(Recipient::default());
                    }
                    view::CreateSpendMessage::DeleteRecipient(i) => {
                        self.recipients.remove(i);
                        if self.recipients.len() < 2 {
                            self.batch_label.valid = true;
                            self.batch_label.value = "".to_string();
                        }
                        if let Some(j) = self.send_max_to_recipient {
                            match j.cmp(&i) {
                                Ordering::Equal => {
                                    self.send_max_to_recipient = None;
                                }
                                Ordering::Greater => {
                                    self.send_max_to_recipient = Some(
                                        j.checked_sub(1)
                                            .expect("j must be greater than 0 in this case"),
                                    );
                                }
                                _ => {}
                            }
                        }
                    }
                    view::CreateSpendMessage::RecipientEdited(i, _, _) => {
                        self.recipients
                            .get_mut(i)
                            .unwrap()
                            .update(cache.network, msg);
                    }

                    view::CreateSpendMessage::FeerateEdited(s) => {
                        if let Ok(value) = s.parse::<u64>() {
                            self.feerate.value = s;
                            self.feerate.valid = value != 0 && value <= MAX_FEERATE;
                        } else if s.is_empty() {
                            self.feerate.value = "".to_string();
                            self.feerate.valid = true;
                        } else {
                            self.feerate.valid = false;
                        }
                        self.warning = None;
                    }
                    view::CreateSpendMessage::Generate => {
                        let inputs: Vec<OutPoint> = self
                            .coins
                            .iter()
                            .filter_map(
                                |(coin, selected)| {
                                    if *selected {
                                        Some(coin.outpoint)
                                    } else {
                                        None
                                    }
                                },
                            )
                            .collect();
                        let mut outputs: HashMap<Address<address::NetworkUnchecked>, u64> =
                            HashMap::new();
                        for recipient in &self.recipients {
                            outputs.insert(
                                Address::from_str(&recipient.address.value)
                                    .expect("Checked before"),
                                recipient.amount().expect("Checked before"),
                            );
                        }
                        let feerate_vb = self.feerate.value.parse::<u64>().unwrap_or(0);
                        self.warning = None;
                        return Command::perform(
                            async move {
                                daemon
                                    .create_spend_tx(&inputs, &outputs, feerate_vb, None)
                                    .await
                                    .map_err(|e| e.into())
                                    .and_then(|res| match res {
                                        CreateSpendResult::Success { psbt, warnings } => {
                                            Ok((psbt, warnings))
                                        }
                                        CreateSpendResult::InsufficientFunds { missing } => {
                                            Err(SpendCreationError::CoinSelection(
                                                liana::spend::InsufficientFunds { missing },
                                            )
                                            .into())
                                        }
                                    })
                            },
                            Message::Psbt,
                        );
                    }
                    view::CreateSpendMessage::SelectCoin(i) => {
                        if let Some(coin) = self.coins.get_mut(i) {
                            coin.1 = !coin.1;
                            // Once user edits selection, auto-selection can no longer be used.
                            self.is_user_coin_selection = true;
                        }
                    }
                    view::CreateSpendMessage::SendMaxToRecipient(i) => {
                        if self.recipients.get(i).is_some() {
                            if self.send_max_to_recipient == Some(i) {
                                // If already set to this recipient, then unset it.
                                self.send_max_to_recipient = None;
                            } else {
                                // Either it's set to some other recipient or not at all.
                                self.send_max_to_recipient = Some(i);
                            };
                        }
                    }
                    _ => {}
                }

                // Attempt to select coins automatically if:
                // - all form values have been added and validated
                // - not a self-send
                // - user has not yet selected coins manually
                self.redraft(daemon);
                self.check_valid();
            }
            Message::Psbt(res) => match res {
                Ok(psbt) => {
                    self.generated = Some(psbt);
                    return Command::perform(async {}, |_| Message::View(view::Message::Next));
                }
                Err(e) => self.warning = Some(e),
            },
            Message::Labels(res) => match res {
                Ok(labels) => {
                    self.coins_labels = labels;
                }
                Err(e) => self.warning = Some(e),
            },
            Message::Coins(res) => match res {
                Ok(coins) => {
                    let selected: HashSet<OutPoint> =
                        HashSet::from_iter(self.coins.iter().filter_map(|(c, selected)| {
                            if *selected {
                                Some(c.outpoint)
                            } else {
                                None
                            }
                        }));
                    self.coins = coins
                        .into_iter()
                        .filter_map(|coin| {
                            if coin.spend_info.is_none() && !coin.is_immature {
                                let selected = selected.contains(&coin.outpoint);
                                Some((coin, selected))
                            } else {
                                None
                            }
                        })
                        .collect();
                    self.sort_coins(cache.blockheight as u32);
                    // In case some selected coins are not spendable anymore and
                    // new coins make more sense to be selected. A redraft is triggered
                    // if all forms are valid (checked in the redraft method)
                    self.redraft(daemon);
                    self.check_valid();
                }
                Err(e) => self.warning = Some(e),
            },
            _ => {}
        };
        Command::none()
    }

    fn apply(&self, draft: &mut TransactionDraft) {
        draft.inputs = self
            .coins
            .iter()
            .filter_map(|(coin, selected)| if *selected { Some(coin) } else { None })
            .cloned()
            .collect();
        if let Some((psbt, _)) = &self.generated {
            draft.labels.clone_from(&self.coins_labels);
            for (i, output) in psbt.unsigned_tx.output.iter().enumerate() {
                if let Some(label) = self
                    .recipients
                    .iter()
                    .find(|recipient| {
                        !recipient.label.value.is_empty()
                            && Address::from_str(&recipient.address.value)
                                .unwrap()
                                .payload()
                                .matches_script_pubkey(&output.script_pubkey)
                            && output.value.to_sat() == recipient.amount().unwrap()
                    })
                    .map(|recipient| recipient.label.value.to_string())
                {
                    draft.labels.insert(
                        OutPoint {
                            txid: psbt.unsigned_tx.txid(),
                            vout: i as u32,
                        }
                        .to_string(),
                        label,
                    );
                }
            }
        }
        draft.recipients.clone_from(&self.recipients);
        if self.recipients.len() > 1 {
            draft.batch_label = Some(self.batch_label.value.clone());
        }
        draft.generated.clone_from(&self.generated);
    }

    fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> {
        view::spend::create_spend_tx(
            cache,
            self.recipients
                .iter()
                .enumerate()
                .map(|(i, recipient)| {
                    recipient
                        .view(i, self.send_max_to_recipient == Some(i))
                        .map(view::Message::CreateSpend)
                })
                .collect(),
            self.is_valid,
            self.is_duplicate,
            self.timelock,
            &self.coins,
            &self.coins_labels,
            &self.batch_label,
            self.amount_left_to_select.as_ref(),
            &self.feerate,
            self.warning.as_ref(),
        )
    }
}

#[derive(Default, Clone)]
struct Recipient {
    label: form::Value<String>,
    address: form::Value<String>,
    amount: form::Value<String>,
}

impl Recipient {
    fn amount(&self) -> Result<u64, Error> {
        if self.amount.value.is_empty() {
            return Err(Error::Unexpected("Amount should be non-zero".to_string()));
        }

        let amount = Amount::from_str_in(&self.amount.value, Denomination::Bitcoin)
            .map_err(|_| Error::Unexpected("cannot parse output amount".to_string()))?;

        if amount.to_sat() == 0 {
            return Err(Error::Unexpected("Amount should be non-zero".to_string()));
        }

        if amount.to_sat() < DUST_OUTPUT_SATS {
            return Err(Error::Unexpected("Amount should be non-zero".to_string()));
        }

        if let Ok(address) = Address::from_str(&self.address.value) {
            if amount <= address.payload().script_pubkey().dust_value() {
                return Err(Error::Unexpected(
                    "Amount must be superior to script dust value".to_string(),
                ));
            }
        }

        Ok(amount.to_sat())
    }

    fn address_valid(&self) -> bool {
        !self.address.value.is_empty() && self.address.valid
    }

    fn valid(&self) -> bool {
        self.address_valid()
            && !self.amount.value.is_empty()
            && self.amount.valid
            && self.label.valid
    }

    fn update(&mut self, network: Network, message: view::CreateSpendMessage) {
        match message {
            view::CreateSpendMessage::RecipientEdited(_, "address", address) => {
                self.address.value = address;
                if let Ok(address) = Address::from_str(&self.address.value) {
                    self.address.valid = address.is_valid_for_network(network);
                    if !self.amount.value.is_empty() {
                        self.amount.valid = self.amount().is_ok();
                    }
                } else if self.address.value.is_empty() {
                    // Make the error disappear if we deleted the invalid address
                    self.address.valid = true;
                } else {
                    self.address.valid = false;
                }
            }
            view::CreateSpendMessage::RecipientEdited(_, "amount", amount) => {
                self.amount.value = amount;
                if !self.amount.value.is_empty() {
                    self.amount.valid = self.amount().is_ok();
                } else {
                    // Make the error disappear if we deleted the invalid amount
                    self.amount.valid = true;
                }
            }
            view::CreateSpendMessage::RecipientEdited(_, "label", label) => {
                self.label.valid = label.len() <= 100;
                self.label.value = label;
            }
            _ => {}
        };
    }

    fn view(&self, i: usize, is_max_selected: bool) -> Element<view::CreateSpendMessage> {
        view::spend::recipient_view(i, &self.address, &self.amount, &self.label, is_max_selected)
    }
}

pub struct SaveSpend {
    wallet: Arc<Wallet>,
    spend: Option<(psbt::PsbtState, Vec<String>)>,
    curve: secp256k1::Secp256k1<secp256k1::VerifyOnly>,
}

impl SaveSpend {
    pub fn new(wallet: Arc<Wallet>) -> Self {
        Self {
            wallet,
            spend: None,
            curve: secp256k1::Secp256k1::verification_only(),
        }
    }
}

impl Step for SaveSpend {
    fn load(&mut self, draft: &TransactionDraft) {
        let (psbt, warnings) = draft.generated.clone().unwrap();
        let mut tx = SpendTx::new(
            None,
            psbt,
            draft.inputs.clone(),
            &self.wallet.main_descriptor,
            &self.curve,
            draft.network,
        );
        tx.labels.clone_from(&draft.labels);

        if tx.is_batch() {
            if let Some(label) = &draft.batch_label {
                tx.labels
                    .insert(tx.psbt.unsigned_tx.txid().to_string(), label.clone());
            }
        } else if let Some(recipient) = draft.recipients.first() {
            if !recipient.label.value.is_empty() {
                let label = recipient.label.value.clone();
                tx.labels
                    .insert(tx.psbt.unsigned_tx.txid().to_string(), label);
            }
        }

        self.spend = Some((
            psbt::PsbtState::new(self.wallet.clone(), tx, false),
            warnings,
        ));
    }

    fn interrupt(&mut self) {
        if let Some((psbt_state, _)) = &mut self.spend {
            psbt_state.interrupt()
        }
    }

    fn subscription(&self) -> Subscription<Message> {
        if let Some((psbt_state, _)) = &self.spend {
            psbt_state.subscription()
        } else {
            Subscription::none()
        }
    }

    fn update(
        &mut self,
        daemon: Arc<dyn Daemon + Sync + Send>,
        cache: &Cache,
        message: Message,
    ) -> Command<Message> {
        if let Some((psbt_state, _)) = &mut self.spend {
            psbt_state.update(daemon, cache, message)
        } else {
            Command::none()
        }
    }

    fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> {
        let (psbt_state, warnings) = self.spend.as_ref().unwrap();
        let content = view::spend::spend_view(
            cache,
            &psbt_state.tx,
            warnings,
            psbt_state.saved,
            &psbt_state.desc_policy,
            &psbt_state.wallet.keys_aliases,
            psbt_state.labels_edited.cache(),
            cache.network,
            psbt_state.warning.as_ref(),
        );
        if let Some(action) = &psbt_state.action {
            action.as_ref().view(content)
        } else {
            content
        }
    }
}
