//! Lexe SDK API request and response types.

use anyhow::Context;
use lexe_api::{
    models::command,
    types::{
        bounded_string::BoundedString,
        invoice::Invoice,
        payments::{
            ClientPaymentId, PaymentCreatedIndex, PaymentHash, PaymentSecret,
        },
    },
};
use lexe_common::{ln::amount::Amount, ppm::Ppm, time::TimestampMs};
use lexe_payment_uri::PaymentMethod;
use serde::{Deserialize, Serialize};

use crate::types::{
    auth::{Measurement, NodePk, UserPk},
    bitcoin::Offer,
    payment::Payment,
};

/// Information about a Lexe node.
// Simple version of `lexe_api::models::command::NodeInfo`.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NodeInfo {
    /// The node's current semver version, e.g. `0.6.9`.
    pub version: semver::Version,
    /// The hex-encoded SGX 'measurement' of the current node.
    /// The measurement is the hash of the enclave binary.
    pub measurement: Measurement,
    /// The hex-encoded ed25519 user public key used to identify a Lexe user.
    /// The user keypair is derived from the root seed.
    pub user_pk: UserPk,
    /// The hex-encoded secp256k1 Lightning node public key; the `node_id`.
    pub node_pk: NodePk,

    /// The sum of our `lightning_balance` and our `onchain_balance`, in sats.
    pub balance: Amount,

    /// Total Lightning balance in sats, summed over all of our channels.
    pub lightning_balance: Amount,
    /// An estimated upper bound, in sats, on how much of our Lightning balance
    /// we can send to most recipients on the Lightning Network, accounting for
    /// Lightning limits such as our channel reserve, pending HTLCs, fees, etc.
    /// You should usually be able to spend this amount.
    // User-facing name for `LightningBalance::sendable`
    pub lightning_sendable_balance: Amount,
    /// A hard upper bound on how much of our Lightning balance can be spent
    /// right now, in sats. This is always >= `lightning_sendable_balance`.
    /// Generally it is only possible to spend exactly this amount if the
    /// recipient is a Lexe user.
    // User-facing name for `LightningBalance::max_sendable`
    pub lightning_max_sendable_balance: Amount,

    /// Total on-chain balance in sats, including unconfirmed funds.
    // `OnchainBalance::total`
    pub onchain_balance: Amount,
    /// Trusted on-chain balance in sats, including only confirmed funds and
    /// unconfirmed outputs originating from our own wallet.
    // Equivalent to BDK's `trusted_spendable`, but with a better name.
    pub onchain_trusted_balance: Amount,

    /// The total number of Lightning channels.
    pub num_channels: usize,
    /// The number of channels which are currently usable, i.e. `channel_ready`
    /// messages have been exchanged and the channel peer is online.
    /// Is always less than or equal to `num_channels`.
    pub num_usable_channels: usize,
}

impl From<command::NodeInfo> for NodeInfo {
    fn from(info: command::NodeInfo) -> Self {
        let lightning_balance = info.lightning_balance.total();
        let onchain_balance = Amount::try_from(info.onchain_balance.total())
            .expect("We're unreasonably rich!");
        let onchain_trusted_balance =
            Amount::try_from(info.onchain_balance.trusted_spendable())
                .expect("We're unreasonably rich!");
        let balance = lightning_balance.saturating_add(onchain_balance);

        Self {
            version: info.version,
            measurement: Measurement::from_unstable(info.measurement),
            user_pk: UserPk::from_unstable(info.user_pk),
            node_pk: NodePk::from_unstable(info.node_pk),

            balance,

            lightning_balance,
            lightning_sendable_balance: info.lightning_balance.sendable,
            lightning_max_sendable_balance: info.lightning_balance.max_sendable,
            onchain_balance,
            onchain_trusted_balance,
            num_channels: info.num_channels,
            num_usable_channels: info.num_usable_channels,
        }
    }
}

/// A request to analyze the contents of a Bitcoin or Lightning payment string.
/// Reveals all payment methods encoded in the string, and gives payment-related
/// details on each. See [`PayableDetails`] for more info.
pub struct AnalyzeRequest {
    /// The Bitcoin or Lightning payment string to analyze.
    pub payable: String,
}

/// Describes basic information for a payable string.
pub struct PayableDetails {
    /// The payable string encoding the payment method.
    pub payable: String,
    /// The deserialized payment method.
    pub method: PaymentMethod,

    /// The description encoded in the `payable`, if any.
    pub description: Option<String>,

    /// The amount that should be paid to the `payable`; if `None`, the payer
    /// should specify an amount to pay.
    ///
    /// This will be `None` if `min_amount` or `max_amount` are specified.
    pub amount: Option<Amount>,
    /// The minimum amount that can be paid to the `payable`.
    ///
    /// This will be `None` if `amount` is specified.
    pub min_amount: Option<Amount>,
    /// The maximum amount that can be paid to the `payable`.
    ///
    /// This will be `None` if `amount` is specified.
    pub max_amount: Option<Amount>,

    /// The payable expiration time, in milliseconds since the UNIX epoch.
    pub expires_at: Option<TimestampMs>,
}

/// The response to a string analysis request.
pub struct AnalyzeResponse {
    // TODO: kind: PaymentUri
    /// The valid payment routes encoded in the analyzed string, ordered
    /// by most recommended payment route first, and least recommended payment
    /// route last.
    pub payables: Vec<PayableDetails>,
}

/// A catch-all request to pay a Bitcoin or Lightning payment string.
///
/// The following encodings are supported:
///   - BIP 321 URI: `bitcoin:bc1...`
///   - Lightning URI: `lightning:ln...`
///   - BOLT 11 invoice: `lnbc1...`
///   - BOLT 12 offer: `lno1...`
///   - Onchain bitcoin address: `bc1...`
///   - Human Bitcoin Address: `₿satoshi@lexe.app`
///   - Lightning Address: `satoshi@lexe.app`
///   - LNURL: `lnurl1...` or `lnurlp://domain.com/path`
///
/// See [`PaymentMethod`] for more details on supported payment methods.
///
/// If there exist multiple encoded payment methods, the best recommended
/// payment method will be chosen.
pub struct PayRequest {
    /// The string we will pay.
    pub payable: String,

    /// The amount we will attempt to pay.
    /// If the payable specifies an amount, this field is optional.
    pub amount: Option<Amount>,

    /// An optional message to send to the recipient, supported if sending to:
    ///
    /// - BOLT 12 offers
    /// - Human Bitcoin Addresses which point to an offer
    /// - LNURL recipients whose wallets accept LUD-12 comments
    /// - Lightning Addresses to a wallet that accepts LUD-12 comments
    ///
    /// If the payable doesn't support payer notes; this note will be ignored.
    ///
    /// If provided, it must be non-empty and no longer than 200 chars / 512
    /// UTF-8 bytes.
    pub payer_note: Option<String>,

    /// An optional personal note for this payment.
    ///
    /// The receiver will not see this note.
    ///
    /// If provided, it must be non-empty and no longer than 200 chars /
    /// 512 UTF-8 bytes.
    pub note: Option<String>,
}

/// The response to a general pay request.
pub struct PayResponse {
    /// Identifier for this outbound payment.
    pub index: PaymentCreatedIndex,
    /// When we tried to pay, in milliseconds since the UNIX epoch.
    pub created_at: TimestampMs,
}

/// A request to create a BOLT 11 invoice.
#[derive(Default, Serialize, Deserialize)]
pub struct CreateInvoiceRequest {
    /// The expiration, in seconds, to encode into the invoice.
    /// If no duration is provided, the expiration time defaults to 86400
    /// (1 day).
    pub expiration_secs: Option<u32>,

    /// Optionally include an amount, in sats, to encode into the invoice.
    /// If no amount is provided, the sender will specify how much to pay.
    pub amount: Option<Amount>,

    /// The description to be encoded into the invoice.
    /// The sender will see this description when they scan the invoice.
    // If `None`, the `description` field inside the invoice will be an empty
    // string (""), as lightning _requires_ a description (or description
    // hash) to be set.
    pub description: Option<String>,

    /// The partner's user_pk, if the partner is setting the fee for this
    /// payment instead of using Lexe's default fees.
    ///
    /// This must be set in order for `partner_prop_fee` and `partner_base_fee`
    /// to take effect.
    // Added in `node-v0.9.6`
    #[serde(default)]
    pub partner_pk: Option<UserPk>,

    /// The partner-chosen proportional fee to charge on this payment.
    /// If `partner_pk` is set, this must be set to [`Some`].
    ///
    /// Minimum: 5000 ppm (`LSP_USERNODE_SKIM_FEE_PPM`)
    /// Maximum: 500,000 ppm (50%)
    // Added in `node-v0.9.6`
    #[serde(default)]
    pub partner_prop_fee: Option<Ppm>,

    /// The partner-chosen base fee to charge on this payment.
    ///
    /// If this is set, the invoice `amount` must also be set.
    // Added in `node-v0.9.6`
    #[serde(default)]
    pub partner_base_fee: Option<Amount>,
}

/// The response to a BOLT 11 invoice request.
#[derive(Serialize, Deserialize)]
pub struct CreateInvoiceResponse {
    /// Identifier for this inbound invoice payment.
    pub index: PaymentCreatedIndex,
    /// The BOLT 11 invoice.
    pub invoice: Invoice,
    /// The description encoded in the invoice, if one was provided.
    pub description: Option<String>,
    /// The amount encoded in the invoice, if there was one.
    /// Returning `None` means we created an amountless invoice.
    pub amount: Option<Amount>,
    /// The invoice creation time, in milliseconds since the UNIX epoch.
    pub created_at: TimestampMs,
    /// The invoice expiration time, in milliseconds since the UNIX epoch.
    pub expires_at: TimestampMs,
    /// The hex-encoded payment hash of the invoice.
    pub payment_hash: PaymentHash,
    /// The payment secret of the invoice.
    pub payment_secret: PaymentSecret,
}

impl CreateInvoiceResponse {
    /// Build a [`CreateInvoiceResponse`] from an index and invoice.
    pub fn new(index: PaymentCreatedIndex, invoice: Invoice) -> Self {
        let description = invoice.description_str().map(|s| s.to_owned());
        let amount_sats = invoice.amount();
        let created_at = invoice.saturating_created_at();
        let expires_at = invoice.saturating_expires_at();
        let payment_hash = invoice.payment_hash();
        let payment_secret = invoice.payment_secret();

        Self {
            index,
            invoice,
            description,
            amount: amount_sats,
            created_at,
            expires_at,
            payment_hash,
            payment_secret,
        }
    }
}

impl TryFrom<CreateInvoiceRequest> for command::CreateInvoiceRequest {
    type Error = anyhow::Error;

    fn try_from(req: CreateInvoiceRequest) -> anyhow::Result<Self> {
        /// The default expiration we use if none is provided.
        const DEFAULT_EXPIRATION_SECS: u32 = 60 * 60 * 24; // 1 day

        Ok(Self {
            expiry_secs: req.expiration_secs.unwrap_or(DEFAULT_EXPIRATION_SECS),
            amount: req.amount,
            description: req.description,
            // TODO(maurice): Add description_hash if we really need it.
            description_hash: None,
            payer_note: None,
            partner_pk: req.partner_pk.map(|pk| pk.unstable()),
            partner_prop_fee: req.partner_prop_fee,
            partner_base_fee: req.partner_base_fee,
        })
    }
}

/// A request to pay a BOLT 11 invoice.
#[derive(Serialize, Deserialize)]
pub struct PayInvoiceRequest {
    /// The invoice we want to pay.
    pub invoice: Invoice,
    /// Specifies the amount we will pay if the invoice to be paid is
    /// amountless. This field must be set if the invoice is amountless.
    pub fallback_amount: Option<Amount>,
    /// An optional personal note for this payment.
    /// The receiver will not see this note.
    /// If provided, it must be non-empty and no longer than 200 chars /
    /// 512 UTF-8 bytes.
    pub note: Option<String>,
}

impl TryFrom<PayInvoiceRequest> for command::PayInvoiceRequest {
    type Error = anyhow::Error;

    fn try_from(req: PayInvoiceRequest) -> anyhow::Result<Self> {
        Ok(Self {
            invoice: req.invoice,
            fallback_amount: req.fallback_amount,
            note: req
                .note
                .map(BoundedString::new)
                .transpose()
                .context("Invalid personal note")?,
            payer_note: None,
        })
    }
}

/// The response to a request to pay a BOLT 11 invoice.
#[derive(Serialize, Deserialize)]
pub struct PayInvoiceResponse {
    /// Identifier for this outbound invoice payment.
    pub index: PaymentCreatedIndex,
    /// When we tried to pay this invoice, in milliseconds since the UNIX
    /// epoch.
    pub created_at: TimestampMs,
}

/// A request to create a BOLT 12 offer to receive Lightning payments.
///
/// Unlike invoices, offers are reusable: multiple payments can be made to
/// it, including from multiple payers.
#[derive(Default, Serialize, Deserialize)]
pub struct CreateOfferRequest {
    /// An optional description to encode into the offer.
    ///
    /// The sender will see this description when they scan the offer.
    ///
    /// If provided, it must be non-empty and no longer than 200 chars /
    /// 512 UTF-8 bytes.
    pub description: Option<String>,

    /// An optional minimum payment size for payments to this offer.
    /// If not set, the payer can send any amount.
    pub min_amount: Option<Amount>,

    /// An optional expiration for the offer, in seconds from now.
    pub expiration_secs: Option<u32>,
}

impl TryFrom<CreateOfferRequest> for command::CreateOfferRequest {
    type Error = anyhow::Error;

    fn try_from(req: CreateOfferRequest) -> anyhow::Result<Self> {
        let description = req
            .description
            .map(BoundedString::new)
            .transpose()
            .context("Invalid description")?;

        Ok(Self {
            description,
            min_amount: req.min_amount,
            expiry_secs: req.expiration_secs,
            max_quantity: None,
            issuer: None,
        })
    }
}

/// The response to a BOLT 12 offer creation request.
#[derive(Serialize, Deserialize)]
pub struct CreateOfferResponse {
    /// The BOLT 12 offer.
    pub offer: Offer,
}

/// A request to pay a BOLT 12 offer over Lightning.
#[derive(Serialize, Deserialize)]
pub struct PayOfferRequest {
    /// The offer we want to pay.
    pub offer: Offer,
    /// The amount we will pay. If the offer specifies a minimum amount,
    /// this value must satisfy that minimum.
    pub amount: Amount,
    /// An optional personal note for this payment.
    /// The receiver will not see this note.
    /// If provided, it must be non-empty and no longer than 200 chars /
    /// 512 UTF-8 bytes.
    pub note: Option<String>,
    /// An optional note that is included in the BOLT 12 invoice request and
    /// visible to the recipient. If provided, it must be non-empty and no
    /// longer than 200 chars / 512 UTF-8 bytes.
    pub payer_note: Option<String>,
}

impl PayOfferRequest {
    /// Build a [`command::PayOfferRequest`] from this SDK request.
    pub(crate) fn into_unstable(
        self,
        cid: ClientPaymentId,
    ) -> anyhow::Result<command::PayOfferRequest> {
        Ok(command::PayOfferRequest {
            cid,
            offer: self.offer,
            amount: self.amount,
            note: self
                .note
                .map(BoundedString::new)
                .transpose()
                .context("Invalid personal note")?,
            payer_note: self
                .payer_note
                .map(BoundedString::new)
                .transpose()
                .context("Invalid payer note")?,
        })
    }
}

/// The response to a request to pay a BOLT 12 offer.
#[derive(Serialize, Deserialize)]
pub struct PayOfferResponse {
    /// Identifier for this outbound offer payment.
    pub index: PaymentCreatedIndex,
    /// When we tried to pay this offer, in milliseconds since the UNIX epoch.
    pub created_at: TimestampMs,
}

/// A request to update the personal note on an existing payment.
/// Pass `None` to clear the note.
#[derive(Serialize, Deserialize)]
pub struct UpdatePaymentNoteRequest {
    /// Identifier for the payment to be updated.
    pub index: PaymentCreatedIndex,
    /// The updated note, or `None` to clear.
    /// If provided, it must be non-empty and no longer than 200 chars /
    /// 512 UTF-8 bytes.
    pub note: Option<String>,
}

impl TryFrom<UpdatePaymentNoteRequest> for command::UpdatePaymentNote {
    type Error = anyhow::Error;

    fn try_from(sdk: UpdatePaymentNoteRequest) -> anyhow::Result<Self> {
        Ok(Self {
            index: sdk.index,
            note: sdk
                .note
                .map(BoundedString::new)
                .transpose()
                .context("Invalid note")?,
        })
    }
}

/// A request to get information about a payment by its index.
#[derive(Serialize, Deserialize)]
pub struct GetPaymentRequest {
    /// Identifier for this payment.
    pub index: PaymentCreatedIndex,
}

/// A response to a request to get information about a payment by its index.
#[derive(Serialize, Deserialize)]
pub struct GetPaymentResponse {
    /// Information about this payment, if it exists.
    pub payment: Option<Payment>,
}

/// Response from listing payments.
#[derive(Serialize, Deserialize)]
pub struct ListPaymentsResponse {
    /// Payments in the requested page.
    pub payments: Vec<Payment>,
    /// Cursor for fetching the next page. `None` when there are no more
    /// results. Pass this as the `after` argument to get the next page.
    pub next_index: Option<PaymentCreatedIndex>,
}

/// Summary of changes from a payment sync operation.
#[derive(Debug)]
pub struct PaymentSyncSummary {
    /// Number of new payments added to the local database.
    pub num_new: usize,
    /// Number of existing payments that were updated.
    pub num_updated: usize,
}
