//!
//! Primitives for declaring transaction payment destinations.
//!

use crate::imports::*;
use kaspa_consensus_client::{TransactionOutput, TransactionOutputInner};
use kaspa_txscript::pay_to_address_script;

#[wasm_bindgen(typescript_custom_section)]
const TS_PAYMENT_OUTPUTS: &'static str = r#"
/**
 * 
 * Defines a single payment output.
 * 
 * @see {@link IGeneratorSettingsObject}, {@link Generator}
 * @category Wallet SDK
 */
export interface IPaymentOutput {
    /**
     * Destination address. The address prefix must match the network
     * you are transacting on (e.g. `kaspa:` for mainnet, `kaspatest:` for testnet, etc).
     */
    address: Address | string;
    /**
     * Output amount in SOMPI.
     */
    amount: bigint;
}
"#;

#[wasm_bindgen]
extern "C" {
    /// WASM (TypeScript) type representing a single payment output (`IPaymentOutput`).
    /// @category Wallet SDK
    #[wasm_bindgen(typescript_type = "IPaymentOutput")]
    pub type IPaymentOutput;
    /// WASM (TypeScript) type representing multiple payment outputs (`IPaymentOutput[]`).
    /// @category Wallet SDK
    #[wasm_bindgen(typescript_type = "IPaymentOutput[]")]
    pub type IPaymentOutputArray;
}

/// A Rust data structure representing a payment destination.
/// A payment destination is used to signal Generator where to send the funds.
/// The destination can be a change address or a set of [`PaymentOutput`].
#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub enum PaymentDestination {
    Change,
    PaymentOutputs(PaymentOutputs),
}

impl PaymentDestination {
    pub fn amount(&self) -> Option<u64> {
        match self {
            Self::Change => None,
            Self::PaymentOutputs(payment_outputs) => Some(payment_outputs.amount()),
        }
    }
}

/// A Rust data structure representing a single payment
/// output containing a destination address and amount.
///
/// @category Wallet SDK
#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, CastFromJs)]
#[cfg_attr(feature = "py-sdk", pyclass)]
#[wasm_bindgen(inspectable)]
pub struct PaymentOutput {
    #[wasm_bindgen(getter_with_clone)]
    pub address: Address,
    pub amount: u64,
}

impl TryCastFromJs for PaymentOutput {
    type Error = Error;
    fn try_cast_from<'a, R>(value: &'a R) -> Result<Cast<'a, Self>, Self::Error>
    where
        R: AsRef<JsValue> + 'a,
    {
        Self::resolve(value, || {
            if let Some(array) = value.as_ref().dyn_ref::<Array>() {
                let length = array.length();
                if length != 2 {
                    Err(Error::Custom("Invalid payment output".to_string()))
                } else {
                    let address = Address::try_owned_from(array.get(0))?;
                    let amount = array.get(1).try_as_u64()?;
                    Ok(Self { address, amount })
                }
            } else if let Some(object) = Object::try_from(value.as_ref()) {
                let address = object.cast_into::<Address>("address")?;
                let amount = object.get_u64("amount")?;
                Ok(Self { address, amount })
            } else {
                Err(Error::Custom("Invalid payment output".to_string()))
            }
        })
    }
}

#[cfg(feature = "py-sdk")]
impl TryFrom<&Bound<'_, PyDict>> for PaymentOutput {
    type Error = PyErr;
    fn try_from(value: &Bound<PyDict>) -> PyResult<Self> {
        let address_value = value.get_item("address")?.ok_or_else(|| PyException::new_err("Key `address` not present"))?;

        let address = if let Ok(address) = address_value.extract::<Address>() {
            address
        } else if let Ok(s) = address_value.extract::<String>() {
            Address::try_from(s).map_err(|err| PyException::new_err(format!("{}", err)))?
        } else {
            return Err(PyException::new_err("Addresses must be either an Address instance or a string"));
        };

        let amount: u64 = value.get_item("amount")?.ok_or_else(|| PyException::new_err("Key `amount` not present"))?.extract()?;

        Ok(PaymentOutput::new(address, amount))
    }
}

#[wasm_bindgen]
impl PaymentOutput {
    #[wasm_bindgen(constructor)]
    pub fn new(address: Address, amount: u64) -> Self {
        Self { address, amount }
    }
}

#[cfg(feature = "py-sdk")]
#[pymethods]
impl PaymentOutput {
    #[new]
    pub fn new_py(address: Address, amount: u64) -> Self {
        Self { address, amount }
    }
}

impl From<PaymentOutput> for TransactionOutput {
    fn from(value: PaymentOutput) -> Self {
        Self::new_with_inner(TransactionOutputInner { script_public_key: pay_to_address_script(&value.address), value: value.amount })
    }
}

impl From<PaymentOutput> for PaymentDestination {
    fn from(output: PaymentOutput) -> Self {
        Self::PaymentOutputs(PaymentOutputs { outputs: vec![output] })
    }
}

/// @category Wallet SDK
#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, CastFromJs)]
#[cfg_attr(feature = "py-sdk", pyclass)]
#[wasm_bindgen]
pub struct PaymentOutputs {
    #[wasm_bindgen(skip)]
    pub outputs: Vec<PaymentOutput>,
}

impl PaymentOutputs {
    pub fn amount(&self) -> u64 {
        self.outputs.iter().map(|payment_output| payment_output.amount).sum()
    }

    pub fn iter(&self) -> impl Iterator<Item = &PaymentOutput> {
        self.outputs.iter()
    }
}

impl From<PaymentOutputs> for PaymentDestination {
    fn from(outputs: PaymentOutputs) -> Self {
        Self::PaymentOutputs(outputs)
    }
}

#[wasm_bindgen]
impl PaymentOutputs {
    #[wasm_bindgen(constructor)]
    pub fn constructor(output_array: IPaymentOutputArray) -> crate::result::Result<PaymentOutputs> {
        let mut outputs = vec![];
        let iterator = js_sys::try_iter(&output_array)?.ok_or("need to pass iterable JS values!")?;
        for x in iterator {
            // outputs.push((x?).try_into_cast()?);
            outputs.push(PaymentOutput::try_owned_from(x?)?);
        }

        Ok(Self { outputs })
    }
}

#[cfg(feature = "py-sdk")]
#[pymethods]
impl PaymentOutputs {
    #[new]
    pub fn constructor_py(output_array: Vec<PaymentOutput>) -> PyResult<PaymentOutputs> {
        Ok(Self { outputs: output_array })
    }
}

impl TryCastFromJs for PaymentOutputs {
    type Error = Error;
    fn try_cast_from<'a, R>(value: &'a R) -> Result<Cast<'a, Self>, Self::Error>
    where
        R: AsRef<JsValue> + 'a,
    {
        Self::resolve(value, || {
            let outputs = if let Some(output_array) = value.as_ref().dyn_ref::<js_sys::Array>() {
                let vec = output_array.to_vec();
                vec.into_iter().map(PaymentOutput::try_owned_from).collect::<Result<Vec<_>, _>>()?
            } else if let Some(object) = value.as_ref().dyn_ref::<js_sys::Object>() {
                Object::entries(object).iter().map(PaymentOutput::try_owned_from).collect::<Result<Vec<_>, _>>()?
            } else if let Some(map) = value.as_ref().dyn_ref::<js_sys::Map>() {
                map.entries().into_iter().flat_map(|v| v.map(PaymentOutput::try_owned_from)).collect::<Result<Vec<_>, _>>()?
            } else {
                return Err(Error::Custom("payment outputs must be an array or an object".to_string()));
            };

            Ok(Self { outputs })
        })
    }
}

#[cfg(feature = "py-sdk")]
impl TryFrom<Vec<&Bound<'_, PyDict>>> for PaymentOutputs {
    type Error = PyErr;
    fn try_from(value: Vec<&Bound<PyDict>>) -> PyResult<Self> {
        let outputs: Vec<PaymentOutput> = value.iter().map(|utxo| PaymentOutput::try_from(*utxo)).collect::<Result<Vec<_>, _>>()?;
        Ok(PaymentOutputs { outputs })
    }
}

impl From<PaymentOutputs> for Vec<TransactionOutput> {
    fn from(value: PaymentOutputs) -> Self {
        value.outputs.into_iter().map(TransactionOutput::from).collect()
    }
}

impl From<(Address, u64)> for PaymentOutputs {
    fn from((address, amount): (Address, u64)) -> Self {
        PaymentOutputs { outputs: vec![PaymentOutput::new(address, amount)] }
    }
}

impl From<(&Address, u64)> for PaymentOutputs {
    fn from((address, amount): (&Address, u64)) -> Self {
        PaymentOutputs { outputs: vec![PaymentOutput::new(address.clone(), amount)] }
    }
}

impl From<&[(Address, u64)]> for PaymentOutputs {
    fn from(outputs: &[(Address, u64)]) -> Self {
        let outputs = outputs.iter().map(|(address, amount)| PaymentOutput::new(address.clone(), *amount)).collect();
        PaymentOutputs { outputs }
    }
}

impl From<&[(&Address, u64)]> for PaymentOutputs {
    fn from(outputs: &[(&Address, u64)]) -> Self {
        let outputs = outputs.iter().map(|(address, amount)| PaymentOutput::new((*address).clone(), *amount)).collect();
        PaymentOutputs { outputs }
    }
}
