use crate::amount::{AsSats, ToAmount};
use crate::data_store::CreatedInvoice;
use crate::errors::Result;
use crate::locker::Locker;
use crate::node_config::WithTimezone;
use crate::support::Support;
use crate::util::unix_timestamp_to_system_time;
use crate::{
    fill_payout_fee, filter_out_and_log_corrupted_activities,
    filter_out_and_log_corrupted_payments, Activity, ChannelCloseInfo, ChannelCloseState,
    IncomingPaymentInfo, InvoiceDetails, ListActivitiesResponse, OutgoingPaymentInfo, PaymentInfo,
    PaymentState, ReverseSwapInfo, RuntimeErrorCode, SwapInfo,
};
use breez_sdk_core::{
    parse_invoice, ClosedChannelPaymentDetails, ListPaymentsRequest, PaymentDetails, PaymentStatus,
    PaymentTypeFilter,
};
use perro::{invalid_input, permanent_failure, MapToError, OptionToError};
use std::cmp::{min, Reverse};
use std::collections::HashSet;
use std::sync::Arc;
use std::time::SystemTime;
pub struct Activities {
    support: Arc<Support>,
}
impl Activities {
    pub(crate) fn new(support: Arc<Support>) -> Self {
        Self { support }
    }
    pub fn list(&self, number_of_completed_activities: u32) -> Result<ListActivitiesResponse> {
        const LEEWAY_FOR_PENDING_PAYMENTS: u32 = 30;
        let list_payments_request = ListPaymentsRequest {
            filters: Some(vec![
                PaymentTypeFilter::Sent,
                PaymentTypeFilter::Received,
                PaymentTypeFilter::ClosedChannel,
            ]),
            metadata_filters: None,
            from_timestamp: None,
            to_timestamp: None,
            include_failures: Some(true),
            limit: Some(number_of_completed_activities + LEEWAY_FOR_PENDING_PAYMENTS),
            offset: None,
        };
        let breez_activities = self
            .support
            .rt
            .handle()
            .block_on(self.support.sdk.list_payments(list_payments_request))
            .map_to_runtime_error(RuntimeErrorCode::NodeUnavailable, "Failed to list payments")?
            .into_iter()
            .map(|p| self.activity_from_breez_payment(p))
            .filter_map(filter_out_and_log_corrupted_activities)
            .collect::<Vec<_>>();
        let created_invoices = self
            .support
            .data_store
            .lock_unwrap()
            .retrieve_created_invoices(number_of_completed_activities)?;
        let number_of_created_invoices = created_invoices.len();
        let mut activities = self.multiplex_activities(breez_activities, created_invoices);
        activities.sort_by_cached_key(|m| Reverse(m.get_time()));
        let look_for_pending = LEEWAY_FOR_PENDING_PAYMENTS as usize + number_of_created_invoices;
        let mut tail_activities = activities.split_off(min(look_for_pending, activities.len()));
        let head_activities = activities;
        let (mut pending_activities, mut completed_activities): (Vec<_>, Vec<_>) =
            head_activities.into_iter().partition(Activity::is_pending);
        tail_activities.retain(|m| !m.is_pending());
        completed_activities.append(&mut tail_activities);
        completed_activities.truncate(number_of_completed_activities as usize);
        if let Some(in_progress_swap) = self
            .support
            .rt
            .handle()
            .block_on(self.support.sdk.in_progress_swap())
            .map_to_runtime_error(
                RuntimeErrorCode::NodeUnavailable,
                "Failed to get in-progress swap",
            )?
        {
            let created_at = unix_timestamp_to_system_time(in_progress_swap.created_at as u64)
                .with_timezone(
                    self.support
                        .user_preferences
                        .lock_unwrap()
                        .clone()
                        .timezone_config,
                );
            pending_activities.push(Activity::Swap {
                incoming_payment_info: None,
                swap_info: SwapInfo {
                    bitcoin_address: in_progress_swap.bitcoin_address,
                    created_at,
                    paid_amount: (in_progress_swap.unconfirmed_sats
                        + in_progress_swap.confirmed_sats)
                        .as_sats()
                        .to_amount_down(&self.support.get_exchange_rate()),
                },
            })
        }
        pending_activities.sort_by_cached_key(|m| Reverse(m.get_time()));
        Ok(ListActivitiesResponse {
            pending_activities,
            completed_activities,
        })
    }
    pub fn get(&self, hash: String) -> Result<Activity> {
        if let Some(activity) = self
            .support
            .rt
            .handle()
            .block_on(self.support.sdk.payment_by_hash(hash.clone()))
            .map_to_runtime_error(
                RuntimeErrorCode::NodeUnavailable,
                "Failed to get payment by hash",
            )?
            .map(|p| self.activity_from_breez_payment(p))
        {
            return activity;
        }
        let invoice = self
            .support
            .data_store
            .lock_unwrap()
            .retrieve_created_invoice_by_hash(&hash)?;
        if let Some(invoice) = invoice {
            let incoming_payment_info = self.payment_from_created_invoice(&invoice);
            Ok(Activity::IncomingPayment {
                incoming_payment_info: incoming_payment_info?,
            })
        } else {
            invalid_input!("No activity with provided hash was found")
        }
    }
    pub fn get_by_reverse_swap(&self, reverse_swap_id: String) -> Result<Option<Activity>> {
        const LEEWAY_FOR_REVERSE_SWAPS: u32 = 30;
        let list_payments_request = ListPaymentsRequest {
            filters: Some(vec![PaymentTypeFilter::Sent]),
            metadata_filters: None,
            from_timestamp: None,
            to_timestamp: None,
            include_failures: Some(false),
            limit: Some(LEEWAY_FOR_REVERSE_SWAPS),
            offset: None,
        };
        let is_swap_with_id = |p: &breez_sdk_core::Payment| {
            if let breez_sdk_core::PaymentDetails::Ln { ref data } = p.details {
                if let Some(ref swap_info) = data.reverse_swap_info {
                    return swap_info.id == reverse_swap_id;
                }
            }
            false
        };
        self.support
            .rt
            .handle()
            .block_on(self.support.sdk.list_payments(list_payments_request))
            .map_to_runtime_error(RuntimeErrorCode::NodeUnavailable, "Failed to list payments")?
            .into_iter()
            .find(is_swap_with_id)
            .map(|p| self.activity_from_breez_payment(p))
            .transpose()
    }
    pub fn get_incoming_payment(&self, hash: String) -> Result<IncomingPaymentInfo> {
        match self.get(hash)? {
            Activity::IncomingPayment {
                incoming_payment_info,
            } => Ok(incoming_payment_info),
            Activity::OfferClaim {
                incoming_payment_info,
                ..
            } => Ok(incoming_payment_info),
            Activity::Swap {
                incoming_payment_info,
                ..
            } => incoming_payment_info
                .ok_or(invalid_input("Swap activity without incoming payment info")),
            Activity::ReverseSwap { .. }
            | Activity::OutgoingPayment { .. }
            | Activity::ChannelClose { .. } => invalid_input!("Activity not incoming payment"),
        }
    }
    pub fn get_outgoing_payment(&self, hash: String) -> Result<OutgoingPaymentInfo> {
        match self.get(hash)? {
            Activity::OutgoingPayment {
                outgoing_payment_info,
            } => Ok(outgoing_payment_info),
            Activity::ReverseSwap {
                outgoing_payment_info,
                ..
            } => Ok(outgoing_payment_info),
            Activity::OfferClaim { .. }
            | Activity::IncomingPayment { .. }
            | Activity::ChannelClose { .. }
            | Activity::Swap { .. } => invalid_input!("Activity not incoming payment"),
        }
    }
    pub fn set_personal_note(&self, payment_hash: String, note: String) -> Result<()> {
        let note = Some(note.trim().to_string()).filter(|s| !s.is_empty());
        self.support
            .data_store
            .lock_unwrap()
            .update_personal_note(&payment_hash, note.as_deref())
    }
    pub(crate) fn activity_from_breez_payment(
        &self,
        breez_payment: breez_sdk_core::Payment,
    ) -> Result<Activity> {
        match &breez_payment.details {
            PaymentDetails::Ln { .. } => self.activity_from_breez_ln_payment(breez_payment),
            PaymentDetails::ClosedChannel { data } => {
                self.activity_from_breez_closed_channel_payment(&breez_payment, data)
            }
        }
    }
    fn multiplex_activities(
        &self,
        breez_activities: Vec<Activity>,
        local_created_invoices: Vec<CreatedInvoice>,
    ) -> Vec<Activity> {
        let breez_payment_hashes: HashSet<_> = breez_activities
            .iter()
            .filter_map(|m| m.get_payment_info().map(|p| p.hash.clone()))
            .collect();
        let mut activities = local_created_invoices
            .into_iter()
            .filter(|i| !breez_payment_hashes.contains(i.hash.as_str()))
            .map(|i| self.payment_from_created_invoice(&i))
            .filter_map(filter_out_and_log_corrupted_payments)
            .map(|p| Activity::IncomingPayment {
                incoming_payment_info: p,
            })
            .collect::<Vec<_>>();
        activities.extend(breez_activities);
        activities
    }
    pub(crate) fn activity_from_breez_ln_payment(
        &self,
        breez_payment: breez_sdk_core::Payment,
    ) -> Result<Activity> {
        let payment_details = match breez_payment.details {
            PaymentDetails::Ln { ref data } => data,
            PaymentDetails::ClosedChannel { .. } => {
                invalid_input!("PaymentInfo cannot be created from channel close")
            }
        };
        let local_payment_data = self
            .support
            .data_store
            .lock_unwrap()
            .retrieve_payment_info(&payment_details.payment_hash)?;
        let (exchange_rate, tz_config, personal_note, offer, received_on, received_lnurl_comment) =
            match local_payment_data {
                Some(data) => (
                    data.exchange_rate
                        .or_else(|| self.support.get_exchange_rate()),
                    data.user_preferences.map(|u| u.timezone_config).unwrap_or(
                        self.support
                            .user_preferences
                            .lock_unwrap()
                            .timezone_config
                            .clone(),
                    ),
                    data.personal_note,
                    data.offer,
                    data.received_on,
                    data.received_lnurl_comment,
                ),
                None => (
                    self.support.get_exchange_rate(),
                    self.support
                        .user_preferences
                        .lock_unwrap()
                        .timezone_config
                        .clone(),
                    None,
                    None,
                    None,
                    None,
                ),
            };
        if let Some(offer) = offer {
            let incoming_payment_info = IncomingPaymentInfo::new(
                breez_payment,
                &exchange_rate,
                tz_config,
                personal_note,
                received_on,
                received_lnurl_comment,
                &self
                    .support
                    .node_config
                    .remote_services_config
                    .lipa_lightning_domain,
            )?;
            let offer = fill_payout_fee(
                offer,
                incoming_payment_info.requested_amount.sats.as_msats(),
                &exchange_rate,
            );
            Ok(Activity::OfferClaim {
                incoming_payment_info,
                offer,
            })
        } else if let Some(ref s) = payment_details.swap_info {
            let swap_info = SwapInfo {
                bitcoin_address: s.bitcoin_address.clone(),
                created_at: unix_timestamp_to_system_time(s.created_at as u64)
                    .with_timezone(tz_config.clone()),
                paid_amount: s.paid_msat.as_msats().to_amount_down(&exchange_rate),
            };
            let incoming_payment_info = IncomingPaymentInfo::new(
                breez_payment,
                &exchange_rate,
                tz_config,
                personal_note,
                received_on,
                received_lnurl_comment,
                &self
                    .support
                    .node_config
                    .remote_services_config
                    .lipa_lightning_domain,
            )?;
            Ok(Activity::Swap {
                incoming_payment_info: Some(incoming_payment_info),
                swap_info,
            })
        } else if let Some(ref s) = payment_details.reverse_swap_info {
            let reverse_swap_info = ReverseSwapInfo {
                paid_onchain_amount: s.onchain_amount_sat.as_sats().to_amount_up(&exchange_rate),
                swap_fees_amount: (breez_payment.amount_msat
                    - s.onchain_amount_sat.as_sats().msats)
                    .as_msats()
                    .to_amount_up(&exchange_rate),
                claim_txid: s.claim_txid.clone(),
                status: s.status,
            };
            let outgoing_payment_info = OutgoingPaymentInfo::new(
                breez_payment,
                &exchange_rate,
                tz_config,
                personal_note,
                &self
                    .support
                    .node_config
                    .remote_services_config
                    .lipa_lightning_domain,
            )?;
            Ok(Activity::ReverseSwap {
                outgoing_payment_info,
                reverse_swap_info,
            })
        } else if breez_payment.payment_type == breez_sdk_core::PaymentType::Received {
            let incoming_payment_info = IncomingPaymentInfo::new(
                breez_payment,
                &exchange_rate,
                tz_config,
                personal_note,
                received_on,
                received_lnurl_comment,
                &self
                    .support
                    .node_config
                    .remote_services_config
                    .lipa_lightning_domain,
            )?;
            Ok(Activity::IncomingPayment {
                incoming_payment_info,
            })
        } else if breez_payment.payment_type == breez_sdk_core::PaymentType::Sent {
            let outgoing_payment_info = OutgoingPaymentInfo::new(
                breez_payment,
                &exchange_rate,
                tz_config,
                personal_note,
                &self
                    .support
                    .node_config
                    .remote_services_config
                    .lipa_lightning_domain,
            )?;
            Ok(Activity::OutgoingPayment {
                outgoing_payment_info,
            })
        } else {
            permanent_failure!("Unreachable code")
        }
    }
    pub(crate) fn activity_from_breez_closed_channel_payment(
        &self,
        breez_payment: &breez_sdk_core::Payment,
        details: &ClosedChannelPaymentDetails,
    ) -> Result<Activity> {
        let amount = breez_payment
            .amount_msat
            .as_msats()
            .to_amount_up(&self.support.get_exchange_rate());
        let user_preferences = self.support.user_preferences.lock_unwrap();
        let time = unix_timestamp_to_system_time(breez_payment.payment_time as u64)
            .with_timezone(user_preferences.timezone_config.clone());
        let (closed_at, state) = match breez_payment.status {
            PaymentStatus::Pending => (None, ChannelCloseState::Pending),
            PaymentStatus::Complete => (Some(time), ChannelCloseState::Confirmed),
            PaymentStatus::Failed => {
                permanent_failure!("A channel close Breez Payment has status *Failed*");
            }
        };
        let closing_tx_id = details.closing_txid.clone().unwrap_or_default();
        Ok(Activity::ChannelClose {
            channel_close_info: ChannelCloseInfo {
                amount,
                state,
                closed_at,
                closing_tx_id,
            },
        })
    }
    fn payment_from_created_invoice(
        &self,
        created_invoice: &CreatedInvoice,
    ) -> Result<IncomingPaymentInfo> {
        let invoice =
            parse_invoice(created_invoice.invoice.as_str()).map_to_permanent_failure(format!(
                "Invalid invoice obtained from local db: {}",
                created_invoice.invoice
            ))?;
        let invoice_details = InvoiceDetails::from_ln_invoice(invoice.clone(), &None);
        let payment_state = if SystemTime::now() > invoice_details.expiry_timestamp {
            PaymentState::InvoiceExpired
        } else {
            PaymentState::Created
        };
        let local_payment_data = self
            .support
            .data_store
            .lock_unwrap()
            .retrieve_payment_info(&invoice_details.payment_hash)?
            .ok_or_permanent_failure("Locally created invoice doesn't have local payment data")?;
        let invoice_details =
            InvoiceDetails::from_ln_invoice(invoice, &local_payment_data.exchange_rate);
        let timezone_config = local_payment_data
            .user_preferences
            .map(|u| u.timezone_config)
            .unwrap_or(
                self.support
                    .user_preferences
                    .lock_unwrap()
                    .timezone_config
                    .clone(),
            );
        let time = invoice_details
            .creation_timestamp
            .with_timezone(timezone_config);
        let lsp_fees = created_invoice
            .channel_opening_fees
            .unwrap_or_default()
            .as_msats()
            .to_amount_up(&local_payment_data.exchange_rate);
        let requested_amount = invoice_details
            .amount
            .clone()
            .ok_or_permanent_failure("Locally created invoice doesn't include an amount")?
            .sats
            .as_sats()
            .to_amount_down(&local_payment_data.exchange_rate);
        let amount = requested_amount.clone().sats - lsp_fees.sats;
        let amount = amount
            .as_sats()
            .to_amount_down(&local_payment_data.exchange_rate);
        let personal_note = local_payment_data.personal_note;
        let payment_info = PaymentInfo {
            payment_state,
            hash: invoice_details.payment_hash.clone(),
            amount,
            invoice_details: invoice_details.clone(),
            created_at: time,
            description: invoice_details.description,
            preimage: None,
            personal_note,
        };
        let incoming_payment_info = IncomingPaymentInfo {
            payment_info,
            requested_amount,
            lsp_fees,
            received_on: None,
            received_lnurl_comment: None,
        };
        Ok(incoming_payment_info)
    }
}