use serde::{Deserialize, Serialize};
use std::collections::HashMap;

use bitcoin::util::{bip32, sighash, psbt};
use bitcoin::Network;

use crate::{tx, GetInfo, HexBytes};

#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub struct HDPathInfo {
	pub master_fingerprint: bip32::Fingerprint,
	pub path: bip32::DerivationPath,
}

pub fn sighashtype_to_string(sht: psbt::PsbtSighashType) -> &'static str {
	if let Ok(t) = sht.ecdsa_hash_ty() {
		match t {
			sighash::EcdsaSighashType::All => "ALL",
			sighash::EcdsaSighashType::None => "NONE",
			sighash::EcdsaSighashType::Single => "SINGLE",
			sighash::EcdsaSighashType::AllPlusAnyoneCanPay => "ALL|ANYONECANPAY",
			sighash::EcdsaSighashType::NonePlusAnyoneCanPay => "NONE|ANYONECANPAY",
			sighash::EcdsaSighashType::SinglePlusAnyoneCanPay => "SINGLE|ANYONECANPAY",
		}
	} else if let Ok(t) = sht.schnorr_hash_ty() {
		match t {
            sighash::SchnorrSighashType::Default => "SIGHASH_DEFAULT",
            sighash::SchnorrSighashType::All => "SIGHASH_ALL",
            sighash::SchnorrSighashType::None => "SIGHASH_NONE",
            sighash::SchnorrSighashType::Single => "SIGHASH_SINGLE",
            sighash::SchnorrSighashType::AllPlusAnyoneCanPay => "SIGHASH_ALL|SIGHASH_ANYONECANPAY",
            sighash::SchnorrSighashType::NonePlusAnyoneCanPay => "SIGHASH_NONE|SIGHASH_ANYONECANPAY",
            sighash::SchnorrSighashType::SinglePlusAnyoneCanPay => "SIGHASH_SINGLE|SIGHASH_ANYONECANPAY",
		}
	} else {
		unreachable!();
	}
}

pub fn sighashtype_values() -> &'static [&'static str] {
	&["ALL", "NONE", "SINGLE", "ALL|ANYONECANPAY", "NONE|ANYONECANPAY", "SINGLE|ANYONECANPAY"]
}

pub fn ecdsa_sighashtype_from_string(sht: &str) -> Result<psbt::PsbtSighashType, &'static str> {
	lazy_static! {
		static ref ERR: &'static str = Box::leak(format!(
			"invalid ecdsa SIGHASH type value -- possible values: {:?}", &sighashtype_values(),
		).into_boxed_str());
	}

	use bitcoin::EcdsaSighashType::*;
	let ecdsa_sighash = match sht {
		"ALL" => All,
		"NONE" => None,
		"SINGLE" => Single,
		"ALL|ANYONECANPAY" => AllPlusAnyoneCanPay,
		"NONE|ANYONECANPAY" => NonePlusAnyoneCanPay,
		"SINGLE|ANYONECANPAY" => SinglePlusAnyoneCanPay,
		_ => return Err(&ERR),
	};
	Ok(psbt::PsbtSighashType::from(ecdsa_sighash))
}

#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub struct PsbtInputInfo {
	#[serde(skip_serializing_if = "Option::is_none")]
	pub non_witness_utxo: Option<tx::TransactionInfo>,
	#[serde(skip_serializing_if = "Option::is_none")]
	pub witness_utxo: Option<tx::OutputInfo>,
	#[serde(skip_serializing_if = "HashMap::is_empty")]
	pub partial_sigs: HashMap<HexBytes, HexBytes>,
	#[serde(skip_serializing_if = "Option::is_none")]
	pub sighash_type: Option<String>,
	#[serde(skip_serializing_if = "Option::is_none")]
	pub redeem_script: Option<tx::OutputScriptInfo>,
	#[serde(skip_serializing_if = "Option::is_none")]
	pub witness_script: Option<tx::OutputScriptInfo>,
	#[serde(skip_serializing_if = "HashMap::is_empty")]
	pub hd_keypaths: HashMap<HexBytes, HDPathInfo>,
	#[serde(skip_serializing_if = "Option::is_none")]
	pub final_script_sig: Option<tx::InputScriptInfo>,
	#[serde(skip_serializing_if = "Option::is_none")]
	pub final_script_witness: Option<Vec<HexBytes>>,
}

impl GetInfo<PsbtInputInfo> for psbt::Input {
	fn get_info(&self, network: Network) -> PsbtInputInfo {
		PsbtInputInfo {
			non_witness_utxo: self.non_witness_utxo.as_ref().map(|u| u.get_info(network)),
			witness_utxo: self.witness_utxo.as_ref().map(|u| u.get_info(network)),
			partial_sigs: {
				let mut partial_sigs = HashMap::new();
				for (key, value) in self.partial_sigs.iter() {
					partial_sigs.insert(key.to_bytes().into(), value.clone().to_vec().into());
				}
				partial_sigs
			},
			sighash_type: self.sighash_type.map(|s| sighashtype_to_string(s).to_owned()),
			redeem_script: self.redeem_script.as_ref()
				.map(|s| tx::OutputScript(s).get_info(network)),
			witness_script: self.witness_script.as_ref()
				.map(|s| tx::OutputScript(s).get_info(network)),
			hd_keypaths: {
				let mut hd_keypaths = HashMap::new();
				for (key, value) in self.bip32_derivation.iter() {
					hd_keypaths.insert(key.serialize().to_vec().into(),
						HDPathInfo {
							master_fingerprint: value.0[..].into(),
							path: value.1.clone(),
						},
					);
				}
				hd_keypaths
			},
			final_script_sig: self.final_script_sig.as_ref()
				.map(|s| tx::InputScript(s).get_info(network)),
			final_script_witness: self.final_script_witness.as_ref()
				.map(|w| w.iter().map(|p| p.clone().into()).collect()),
		}
	}
}

#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub struct PsbtOutputInfo {
	#[serde(skip_serializing_if = "Option::is_none")]
	pub redeem_script: Option<tx::OutputScriptInfo>,
	#[serde(skip_serializing_if = "Option::is_none")]
	pub witness_script: Option<tx::OutputScriptInfo>,
	#[serde(skip_serializing_if = "HashMap::is_empty")]
	pub hd_keypaths: HashMap<HexBytes, HDPathInfo>,
}

impl GetInfo<PsbtOutputInfo> for psbt::Output {
	fn get_info(&self, network: Network) -> PsbtOutputInfo {
		PsbtOutputInfo {
			redeem_script: self.redeem_script.as_ref()
				.map(|s| tx::OutputScript(s).get_info(network)),
			witness_script: self.witness_script.as_ref()
				.map(|s| tx::OutputScript(s).get_info(network)),
			hd_keypaths: {
				let mut hd_keypaths = HashMap::new();
				for (key, value) in self.bip32_derivation.iter() {
					hd_keypaths.insert(key.serialize().to_vec().into(),
						HDPathInfo {
							master_fingerprint: value.0[..].into(),
							path: value.1.clone(),
						},
					);
				}
				hd_keypaths
			},
		}
	}
}

#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub struct PsbtInfo {
	pub unsigned_tx: tx::TransactionInfo,
	pub inputs: Vec<PsbtInputInfo>,
	pub outputs: Vec<PsbtOutputInfo>,
}

impl GetInfo<PsbtInfo> for psbt::PartiallySignedTransaction {
	fn get_info(&self, network: Network) -> PsbtInfo {
		PsbtInfo {
			unsigned_tx: self.unsigned_tx.get_info(network),
			inputs: self.inputs.iter().map(|i| i.get_info(network)).collect(),
			outputs: self.outputs.iter().map(|o| o.get_info(network)).collect(),
		}
	}
}
