//! Module for wallet structs and types generated by the sync engine from block chain data or to track the wallet's
//! sync status.
//! The structs will be (or be transposed into) the fundamental wallet components for the wallet interfacing with this
//! sync engine.

use std::{
    collections::{BTreeMap, BTreeSet},
    convert::Infallible,
    fmt::Debug,
    ops::Range,
    sync::{
        Arc,
        atomic::{self, AtomicU8},
    },
};

use incrementalmerkletree::Position;
use orchard::tree::MerkleHashOrchard;
use shardtree::{ShardTree, store::memory::MemoryShardStore};
use tokio::sync::mpsc;
use zcash_address::unified::ParseError;
use zcash_client_backend::proto::compact_formats::CompactBlock;
use zcash_keys::{address::UnifiedAddress, encoding::encode_payment_address};
use zcash_primitives::{block::BlockHash, transaction::TxId};
use zcash_protocol::{
    PoolType, ShieldedProtocol,
    consensus::{self, BlockHeight},
    memo::Memo,
    value::Zatoshis,
};
use zcash_transparent::{address::Script, bundle::OutPoint};

use zingo_status::confirmation_status::ConfirmationStatus;

use crate::{
    client::FetchRequest,
    error::{ServerError, SyncModeError},
    keys::{self, KeyId, transparent::TransparentAddressId},
    scan::compact_blocks::calculate_block_tree_bounds,
    sync::{MAX_REORG_ALLOWANCE, ScanPriority, ScanRange},
    witness,
};

pub mod traits;

#[cfg(feature = "wallet_essentials")]
pub mod serialization;

/// Block height and txid of relevant transactions that have yet to be scanned. These may be added due to transparent
/// output/spend discovery or for targetted rescan.
///
/// `narrow_scan_area` is used to narrow the surrounding area scanned around the target from a shard to 100 blocks.
/// For example, this is useful when targetting transparent outputs as scanning the whole shard will not affect the
/// spendability of the scan target but will significantly reduce memory usage and/or storage as well as prioritise
/// creating spendable notes.
///
/// Scan targets with block heights below sapling activation height are not supported.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ScanTarget {
    /// Block height.
    pub block_height: BlockHeight,
    /// Txid.
    pub txid: TxId,
    /// Narrow surrounding scan area of target.
    pub narrow_scan_area: bool,
}

/// Initial sync state.
///
/// All fields will be reset when a new sync session starts.
#[derive(Debug, Clone)]
pub struct InitialSyncState {
    /// One block above the fully scanned wallet height at start of sync session.
    ///
    /// If chain height is not larger than fully scanned height when sync is called, this value will be set to chain
    /// height instead.
    pub(crate) sync_start_height: BlockHeight,
    /// The tree sizes of the fully scanned height and chain tip at start of sync session.
    pub(crate) wallet_tree_bounds: TreeBounds,
    /// Total number of blocks scanned in previous sync sessions.
    pub(crate) previously_scanned_blocks: u32,
    /// Total number of sapling outputs scanned in previous sync sessions.
    pub(crate) previously_scanned_sapling_outputs: u32,
    /// Total number of orchard outputs scanned in previous sync sessions.
    pub(crate) previously_scanned_orchard_outputs: u32,
}

impl InitialSyncState {
    /// Create new `InitialSyncState`
    #[must_use]
    pub fn new() -> Self {
        InitialSyncState {
            sync_start_height: 0.into(),
            wallet_tree_bounds: TreeBounds {
                sapling_initial_tree_size: 0,
                sapling_final_tree_size: 0,
                orchard_initial_tree_size: 0,
                orchard_final_tree_size: 0,
            },
            previously_scanned_blocks: 0,
            previously_scanned_sapling_outputs: 0,
            previously_scanned_orchard_outputs: 0,
        }
    }
}

impl Default for InitialSyncState {
    fn default() -> Self {
        Self::new()
    }
}

/// Encapsulates the current state of sync
#[derive(Debug, Clone)]
pub struct SyncState {
    /// A vec of block ranges with scan priorities from wallet birthday to chain tip.
    /// In block height order with no overlaps or gaps.
    pub(crate) scan_ranges: Vec<ScanRange>,
    /// The block ranges that contain all sapling outputs of complete sapling shards.
    ///
    /// There is an edge case where a range may include two (or more) shards. However, this only occurs when the lower
    /// shards are already scanned so will cause no issues when punching in the higher scan priorites.
    pub(crate) sapling_shard_ranges: Vec<Range<BlockHeight>>,
    /// The block ranges that contain all orchard outputs of complete orchard shards.
    ///
    /// There is an edge case where a range may include two (or more) shards. However, this only occurs when the lower
    /// shards are already scanned so will cause no issues when punching in the higher scan priorites.
    pub(crate) orchard_shard_ranges: Vec<Range<BlockHeight>>,
    /// Scan targets for relevant transactions to the wallet.
    pub(crate) scan_targets: BTreeSet<ScanTarget>,
    /// Initial sync state.
    pub(crate) initial_sync_state: InitialSyncState,
}

impl SyncState {
    /// Create new `SyncState`
    #[must_use]
    pub fn new() -> Self {
        SyncState {
            scan_ranges: Vec::new(),
            sapling_shard_ranges: Vec::new(),
            orchard_shard_ranges: Vec::new(),
            scan_targets: BTreeSet::new(),
            initial_sync_state: InitialSyncState::new(),
        }
    }

    /// Scan ranges
    #[must_use]
    pub fn scan_ranges(&self) -> &[ScanRange] {
        &self.scan_ranges
    }

    /// Sapling shard ranges
    #[must_use]
    pub fn sapling_shard_ranges(&self) -> &[Range<BlockHeight>] {
        &self.sapling_shard_ranges
    }

    /// Orchard shard ranges
    #[must_use]
    pub fn orchard_shard_ranges(&self) -> &[Range<BlockHeight>] {
        &self.orchard_shard_ranges
    }

    /// Returns true if all scan ranges are scanned.
    pub(crate) fn scan_complete(&self) -> bool {
        self.scan_ranges
            .iter()
            .all(|scan_range| scan_range.priority() == ScanPriority::Scanned)
    }

    /// Returns the block height at which all blocks equal to and below this height are scanned.
    /// Returns `None` if `self.scan_ranges` is empty.
    #[must_use]
    pub fn fully_scanned_height(&self) -> Option<BlockHeight> {
        if let Some(scan_range) = self
            .scan_ranges
            .iter()
            .find(|scan_range| scan_range.priority() != ScanPriority::Scanned)
        {
            Some(scan_range.block_range().start - 1)
        } else {
            self.scan_ranges
                .last()
                .map(|range| range.block_range().end - 1)
        }
    }

    /// Returns the highest block height that has been scanned.
    /// If no scan ranges have been scanned, returns the block below the wallet birthday.
    /// Returns `None` if `self.scan_ranges` is empty.
    #[must_use]
    pub fn highest_scanned_height(&self) -> Option<BlockHeight> {
        if let Some(last_scanned_range) = self
            .scan_ranges
            .iter()
            .filter(|scan_range| {
                scan_range.priority() == ScanPriority::Scanned
                    || scan_range.priority() == ScanPriority::ScannedWithoutMapping
                    || scan_range.priority() == ScanPriority::RefetchingNullifiers
            })
            .next_back()
        {
            Some(last_scanned_range.block_range().end - 1)
        } else {
            self.wallet_birthday().map(|start| start - 1)
        }
    }

    /// Returns the wallet birthday or `None` if `self.scan_ranges` is empty.
    ///
    #[must_use]
    pub fn wallet_birthday(&self) -> Option<BlockHeight> {
        self.scan_ranges
            .first()
            .map(|range| range.block_range().start)
    }

    /// Returns the last known chain height to the wallet or `None` if `self.scan_ranges` is empty.
    #[must_use]
    pub fn last_known_chain_height(&self) -> Option<BlockHeight> {
        self.scan_ranges
            .last()
            .map(|range| range.block_range().end - 1)
    }
}

impl Default for SyncState {
    fn default() -> Self {
        Self::new()
    }
}

/// Sync modes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncMode {
    /// Sync is not running.
    NotRunning,
    /// Sync is held in a paused state and the wallet guard is dropped.
    Paused,
    /// Sync is running.
    Running,
    /// Sync is shutting down.
    Shutdown,
}

impl SyncMode {
    /// Constructor from u8.
    ///
    /// Returns `None` if `mode` is not a valid enum variant.
    pub fn from_u8(mode: u8) -> Result<Self, SyncModeError> {
        match mode {
            0 => Ok(Self::NotRunning),
            1 => Ok(Self::Paused),
            2 => Ok(Self::Running),
            3 => Ok(Self::Shutdown),
            _ => Err(SyncModeError::InvalidSyncMode(mode)),
        }
    }

    /// Creates [`crate::wallet::SyncMode`] from an atomic u8.
    ///
    /// # Panic
    ///
    /// Panics if `atomic_sync_mode` corresponds to an invalid enum variant.
    /// It is the consumers responsibility to ensure the library restricts the user API to only set valid values via
    /// [`crate::wallet::SyncMode`].
    pub fn from_atomic_u8(atomic_sync_mode: Arc<AtomicU8>) -> Result<SyncMode, SyncModeError> {
        SyncMode::from_u8(atomic_sync_mode.load(atomic::Ordering::Acquire))
    }
}

/// Initial and final tree sizes.
#[derive(Debug, Clone, Copy)]
#[allow(missing_docs)]
pub struct TreeBounds {
    pub sapling_initial_tree_size: u32,
    pub sapling_final_tree_size: u32,
    pub orchard_initial_tree_size: u32,
    pub orchard_final_tree_size: u32,
}

/// Output ID for a given pool type.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub struct OutputId {
    /// ID of associated transaction.
    txid: TxId,
    /// Index of output within the transactions bundle of the given pool type.
    output_index: u16,
}

impl OutputId {
    /// Creates new `OutputId` from parts.
    #[must_use]
    pub fn new(txid: TxId, output_index: u16) -> Self {
        OutputId { txid, output_index }
    }

    /// Transaction ID of output's associated transaction.
    #[must_use]
    pub fn txid(&self) -> TxId {
        self.txid
    }

    /// Index of output within the transactions bundle of the given pool type.
    #[must_use]
    pub fn output_index(&self) -> u16 {
        self.output_index
    }
}

impl std::fmt::Display for OutputId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{{
                txid: {}
                output index: {}
            }}",
            self.txid, self.output_index
        )
    }
}

impl From<&OutPoint> for OutputId {
    fn from(value: &OutPoint) -> Self {
        OutputId::new(*value.txid(), value.n() as u16)
    }
}

impl From<OutputId> for OutPoint {
    fn from(value: OutputId) -> Self {
        OutPoint::new(value.txid.into(), u32::from(value.output_index))
    }
}

/// Binary tree map of nullifiers from transaction spends or actions
#[derive(Debug)]
pub struct NullifierMap {
    /// Sapling nullifer map
    pub sapling: BTreeMap<sapling_crypto::Nullifier, ScanTarget>,
    /// Orchard nullifer map
    pub orchard: BTreeMap<orchard::note::Nullifier, ScanTarget>,
}

impl NullifierMap {
    /// Construct new nullifier map.
    #[must_use]
    pub fn new() -> Self {
        Self {
            sapling: BTreeMap::new(),
            orchard: BTreeMap::new(),
        }
    }

    /// Clear nullifier map.
    pub fn clear(&mut self) {
        self.sapling.clear();
        self.orchard.clear();
    }
}

impl Default for NullifierMap {
    fn default() -> Self {
        Self::new()
    }
}

/// Wallet block data
#[derive(Debug, Clone)]
pub struct WalletBlock {
    pub(crate) block_height: BlockHeight,
    pub(crate) block_hash: BlockHash,
    pub(crate) prev_hash: BlockHash,
    pub(crate) time: u32,
    pub(crate) txids: Vec<TxId>,
    pub(crate) tree_bounds: TreeBounds,
}

impl WalletBlock {
    pub(crate) async fn from_compact_block(
        consensus_parameters: &impl consensus::Parameters,
        fetch_request_sender: mpsc::UnboundedSender<FetchRequest>,
        block: &CompactBlock,
    ) -> Result<Self, ServerError> {
        let tree_bounds =
            calculate_block_tree_bounds(consensus_parameters, fetch_request_sender, block).await?;

        Ok(Self {
            block_height: block.height(),
            block_hash: block.hash(),
            prev_hash: block.prev_hash(),
            time: block.time,
            txids: block
                .vtx
                .iter()
                .map(zcash_client_backend::proto::compact_formats::CompactTx::txid)
                .collect(),
            tree_bounds,
        })
    }

    /// Block height.
    #[must_use]
    pub fn block_height(&self) -> BlockHeight {
        self.block_height
    }

    /// Block hash.
    #[must_use]
    pub fn block_hash(&self) -> BlockHash {
        self.block_hash
    }

    /// Previous block hash.
    #[must_use]
    pub fn prev_hash(&self) -> BlockHash {
        self.prev_hash
    }

    /// Time block was mined.
    #[must_use]
    pub fn time(&self) -> u32 {
        self.time
    }

    /// Transaction IDs of transactions in the block.
    #[must_use]
    pub fn txids(&self) -> &[TxId] {
        &self.txids
    }

    /// Tree size bounds
    #[must_use]
    pub fn tree_bounds(&self) -> TreeBounds {
        self.tree_bounds
    }
}

/// Wallet transaction
pub struct WalletTransaction {
    pub(crate) txid: TxId,
    pub(crate) status: ConfirmationStatus,
    pub(crate) transaction: zcash_primitives::transaction::Transaction,
    pub(crate) datetime: u32,
    pub(crate) transparent_coins: Vec<TransparentCoin>,
    pub(crate) sapling_notes: Vec<SaplingNote>,
    pub(crate) orchard_notes: Vec<OrchardNote>,
    pub(crate) outgoing_sapling_notes: Vec<OutgoingSaplingNote>,
    pub(crate) outgoing_orchard_notes: Vec<OutgoingOrchardNote>,
}

impl WalletTransaction {
    /// Transaction ID
    #[must_use]
    pub fn txid(&self) -> TxId {
        self.txid
    }

    /// Confirmation status
    #[must_use]
    pub fn status(&self) -> ConfirmationStatus {
        self.status
    }

    /// [`zcash_primitives::transaction::Transaction`]
    #[must_use]
    pub fn transaction(&self) -> &zcash_primitives::transaction::Transaction {
        &self.transaction
    }

    /// Datetime. In form of seconds since unix epoch.
    #[must_use]
    pub fn datetime(&self) -> u32 {
        self.datetime
    }

    /// Transparent coins
    #[must_use]
    pub fn transparent_coins(&self) -> &[TransparentCoin] {
        &self.transparent_coins
    }

    /// Transparent coins mutable
    pub fn transparent_coins_mut(&mut self) -> Vec<&mut TransparentCoin> {
        self.transparent_coins.iter_mut().collect()
    }

    /// Sapling notes
    #[must_use]
    pub fn sapling_notes(&self) -> &[SaplingNote] {
        &self.sapling_notes
    }

    /// Sapling notes mutable
    pub fn sapling_notes_mut(&mut self) -> Vec<&mut SaplingNote> {
        self.sapling_notes.iter_mut().collect()
    }

    /// Orchard notes
    #[must_use]
    pub fn orchard_notes(&self) -> &[OrchardNote] {
        &self.orchard_notes
    }

    /// Orchard notes mutable
    pub fn orchard_notes_mut(&mut self) -> Vec<&mut OrchardNote> {
        self.orchard_notes.iter_mut().collect()
    }

    /// Outgoing sapling notes
    #[must_use]
    pub fn outgoing_sapling_notes(&self) -> &[OutgoingSaplingNote] {
        &self.outgoing_sapling_notes
    }

    /// Outgoing orchard notes
    #[must_use]
    pub fn outgoing_orchard_notes(&self) -> &[OutgoingOrchardNote] {
        &self.outgoing_orchard_notes
    }

    /// Returns nullifers from sapling bundle.
    /// Returns empty vec if bundle is `None`.
    pub fn sapling_nullifiers(&self) -> Vec<&sapling_crypto::Nullifier> {
        self.transaction
            .sapling_bundle()
            .map_or_else(Vec::new, |bundle| {
                bundle
                    .shielded_spends()
                    .iter()
                    .map(sapling_crypto::bundle::SpendDescription::nullifier)
                    .collect::<Vec<_>>()
            })
    }

    /// Returns nullifers from orchard bundle.
    /// Returns empty vec if bundle is `None`.
    pub fn orchard_nullifiers(&self) -> Vec<&orchard::note::Nullifier> {
        self.transaction
            .orchard_bundle()
            .map_or_else(Vec::new, |bundle| {
                bundle
                    .actions()
                    .iter()
                    .map(orchard::Action::nullifier)
                    .collect::<Vec<_>>()
            })
    }

    /// Returns outpoints from transparent bundle.
    /// Returns empty vec if bundle is `None`.
    pub fn outpoints(&self) -> Vec<&OutPoint> {
        self.transaction
            .transparent_bundle()
            .map_or_else(Vec::new, |bundle| {
                bundle
                    .vin
                    .iter()
                    .map(zcash_transparent::bundle::TxIn::prevout)
                    .collect::<Vec<_>>()
            })
    }

    /// Updates transaction status if `status` is a valid update for the current transaction status.
    /// For example, if `status` is `Mempool` but the current transaction status is `Confirmed`, the status will remain
    /// unchanged.
    /// `datetime` refers to the time in which the status was updated, or the time the block was mined when updating
    /// to `Confirmed` status.
    pub fn update_status(&mut self, status: ConfirmationStatus, datetime: u32) {
        match status {
            ConfirmationStatus::Transmitted(_)
                if matches!(self.status(), ConfirmationStatus::Calculated(_)) =>
            {
                self.status = status;
                self.datetime = datetime;
            }
            ConfirmationStatus::Mempool(_)
                if matches!(
                    self.status(),
                    ConfirmationStatus::Calculated(_) | ConfirmationStatus::Transmitted(_)
                ) =>
            {
                self.status = status;
                self.datetime = datetime;
            }
            ConfirmationStatus::Confirmed(_)
                if matches!(
                    self.status(),
                    ConfirmationStatus::Calculated(_)
                        | ConfirmationStatus::Transmitted(_)
                        | ConfirmationStatus::Mempool(_)
                ) =>
            {
                self.status = status;
                self.datetime = datetime;
            }

            ConfirmationStatus::Failed(_)
                if !matches!(self.status(), ConfirmationStatus::Failed(_)) =>
            {
                self.status = status;
                self.datetime = datetime;
            }
            _ => (),
        }
    }
}

#[cfg(feature = "test-features")]
impl WalletTransaction {
    /// Creates a minimal `WalletTransaction` for testing purposes.
    ///
    /// Constructs a valid v5 transaction with empty bundles and the given `txid` and `status`.
    pub fn new_for_test(txid: TxId, status: ConfirmationStatus) -> Self {
        use zcash_primitives::transaction::{TransactionData, TxVersion};
        use zcash_protocol::consensus::BranchId;

        let transaction = TransactionData::from_parts(
            TxVersion::V5,
            BranchId::Nu5,
            0,
            BlockHeight::from_u32(0),
            None,
            None,
            None,
            None,
        )
        .freeze()
        .expect("empty v5 transaction should always be valid");

        Self {
            txid,
            status,
            transaction,
            datetime: 0,
            transparent_coins: Vec::new(),
            sapling_notes: Vec::new(),
            orchard_notes: Vec::new(),
            outgoing_sapling_notes: Vec::new(),
            outgoing_orchard_notes: Vec::new(),
        }
    }
}

#[cfg(feature = "wallet_essentials")]
impl WalletTransaction {
    /// Returns the total value sent to receivers, excluding value sent to the wallet's own addresses.
    #[must_use]
    pub fn total_value_sent(&self) -> u64 {
        let transparent_value_sent = self
            .transaction
            .transparent_bundle()
            .map_or(0, |bundle| {
                bundle
                    .vout
                    .iter()
                    .map(|output| output.value().into_u64())
                    .sum()
            })
            .saturating_sub(self.total_output_value::<TransparentCoin>());

        let sapling_value_sent =
            self.total_external_outgoing_note_value::<OutgoingSaplingNote, SaplingNote>();
        let orchard_value_sent =
            self.total_external_outgoing_note_value::<OutgoingOrchardNote, OrchardNote>();

        transparent_value_sent + sapling_value_sent + orchard_value_sent
    }

    /// Returns total sum of all output values.
    #[must_use]
    pub fn total_value_received(&self) -> u64 {
        self.total_output_value::<TransparentCoin>()
            + self.total_output_value::<SaplingNote>()
            + self.total_output_value::<OrchardNote>()
    }

    /// Returns total sum of output values for a given pool.
    #[must_use]
    pub fn total_output_value<Op: OutputInterface>(&self) -> u64 {
        Op::transaction_outputs(self)
            .iter()
            .map(OutputInterface::value)
            .sum()
    }

    /// Returns total sum of outgoing note values for a given shielded pool.
    #[must_use]
    pub fn total_outgoing_note_value<Op: OutgoingNoteInterface>(&self) -> u64 {
        Op::transaction_outgoing_notes(self)
            .iter()
            .map(OutgoingNoteInterface::value)
            .sum()
    }

    /// Returns total sum of outgoing note values for outputs that are not wallet-owned.
    #[must_use]
    pub fn total_external_outgoing_note_value<Outgoing, Incoming>(&self) -> u64
    where
        Outgoing: OutgoingNoteInterface,
        Incoming: OutputInterface,
    {
        Outgoing::transaction_outgoing_notes(self)
            .iter()
            .filter(|outgoing_note| {
                !Incoming::transaction_outputs(self)
                    .iter()
                    .any(|wallet_note| wallet_note.output_id() == outgoing_note.output_id())
            })
            .map(OutgoingNoteInterface::value)
            .sum()
    }
}

impl std::fmt::Debug for WalletTransaction {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        f.debug_struct("WalletTransaction")
            .field("txid", &self.txid)
            .field("confirmation_status", &self.status)
            .field("datetime", &self.datetime)
            .field("transparent_coins", &self.transparent_coins)
            .field("sapling_notes", &self.sapling_notes)
            .field("orchard_notes", &self.orchard_notes)
            .field("outgoing_sapling_notes", &self.outgoing_sapling_notes)
            .field("outgoing_orchard_notes", &self.outgoing_orchard_notes)
            .finish()
    }
}

/// Provides a common API for all key identifiers.
pub trait KeyIdInterface {
    /// Account ID.
    fn account_id(&self) -> zip32::AccountId;
}

/// Provides a common API for all output types.
pub trait OutputInterface: Sized {
    /// Identifier for key used to decrypt output.
    type KeyId: KeyIdInterface;
    /// Transaction input type associated with spend detection of output.
    type Input: Clone + Debug + PartialEq + Eq + PartialOrd + Ord;

    /// Output's associated pool type.
    const POOL_TYPE: PoolType;

    /// Output ID.
    fn output_id(&self) -> OutputId;

    /// Identifier for key used to decrypt output.
    fn key_id(&self) -> Self::KeyId;

    /// Transaction ID of transaction this output was spent.
    /// If `None`, output is not spent.
    fn spending_transaction(&self) -> Option<TxId>;

    /// Sets spending transaction.
    fn set_spending_transaction(&mut self, spending_transaction: Option<TxId>);

    /// Note value..
    // TODO: change to Zatoshis checked type
    fn value(&self) -> u64;

    /// Returns the type used to link with transaction inputs for spend detection.
    /// Returns `None` in the case the nullifier is not available for shielded outputs.
    ///
    /// Nullifier for shielded outputs.
    /// Outpoint for transparent outputs.
    fn spend_link(&self) -> Option<Self::Input>;

    /// Inputs within `transaction` used to detect an output's spend status.
    ///
    /// Nullifiers for shielded outputs.
    /// Out points for transparent outputs.
    fn transaction_inputs(transaction: &WalletTransaction) -> Vec<&Self::Input>;

    /// Outputs within `transaction`.
    fn transaction_outputs(transaction: &WalletTransaction) -> &[Self];
}

/// Provides a common API for all shielded output types.
pub trait NoteInterface: OutputInterface {
    /// Decrypted note type.
    type ZcashNote;
    /// Nullifier type.
    type Nullifier: Copy + Clone + PartialEq + Eq + PartialOrd + Ord;

    /// Note's associated shielded protocol.
    const SHIELDED_PROTOCOL: ShieldedProtocol;

    /// Decrypted note with recipient and value
    fn note(&self) -> &Self::ZcashNote;

    /// Derived nullifier
    fn nullifier(&self) -> Option<Self::Nullifier>;

    /// Commitment tree leaf position
    fn position(&self) -> Option<Position>;

    /// Memo
    fn memo(&self) -> &Memo;

    /// List of block ranges where the nullifiers must be re-fetched to guarantee the note has not been spent.
    /// These scan ranges were marked `ScannedWithoutMapping` or `RefetchingNullifiers` priority before this note was
    /// scanned, meaning the nullifiers were discarded due to memory constraints and will be re-fetched later in the
    /// sync process.
    fn refetch_nullifier_ranges(&self) -> &[Range<BlockHeight>];
}

///  Transparent coin (output) with metadata relevant to the wallet.
#[derive(Debug, Clone)]
pub struct TransparentCoin {
    /// Output ID.
    pub(crate) output_id: OutputId,
    /// Identifier for key used to derive address.
    pub(crate) key_id: TransparentAddressId,
    /// Encoded transparent address.
    pub(crate) address: String,
    /// Script.
    pub(crate) script: Script,
    /// Coin value.
    pub(crate) value: Zatoshis,
    /// Transaction ID of transaction this output was spent.
    /// If `None`, output is not spent.
    pub(crate) spending_transaction: Option<TxId>,
}

impl TransparentCoin {
    /// Address received to.
    #[must_use]
    pub fn address(&self) -> &str {
        &self.address
    }

    /// Script.
    #[must_use]
    pub fn script(&self) -> &Script {
        &self.script
    }
}

impl OutputInterface for TransparentCoin {
    type KeyId = TransparentAddressId;
    type Input = OutPoint;

    const POOL_TYPE: PoolType = PoolType::Transparent;

    fn output_id(&self) -> OutputId {
        self.output_id
    }

    fn key_id(&self) -> Self::KeyId {
        self.key_id
    }

    fn spending_transaction(&self) -> Option<TxId> {
        self.spending_transaction
    }

    fn set_spending_transaction(&mut self, spending_transaction: Option<TxId>) {
        self.spending_transaction = spending_transaction;
    }

    fn value(&self) -> u64 {
        self.value.into_u64()
    }

    fn spend_link(&self) -> Option<Self::Input> {
        Some(self.output_id.into())
    }

    fn transaction_inputs(transaction: &WalletTransaction) -> Vec<&Self::Input> {
        transaction.outpoints()
    }

    fn transaction_outputs(transaction: &WalletTransaction) -> &[Self] {
        &transaction.transparent_coins
    }
}

/// Wallet note, shielded output with metadata relevant to the wallet.
#[derive(Debug, Clone)]
pub struct WalletNote<N, Nf: Copy> {
    /// Output ID.
    pub(crate) output_id: OutputId,
    /// Identifier for key used to decrypt output.
    pub(crate) key_id: KeyId,
    /// Decrypted note with recipient and value.
    pub(crate) note: N,
    /// Derived nullifier.
    pub(crate) nullifier: Option<Nf>, //TODO: syncing without nullifier deriving key
    /// Commitment tree leaf position.
    pub(crate) position: Option<Position>,
    /// Memo.
    pub(crate) memo: Memo,
    /// Transaction ID of transaction this output was spent.
    /// If `None`, output is not spent.
    pub(crate) spending_transaction: Option<TxId>,
    /// List of block ranges where the nullifiers must be re-fetched to guarantee the note has not been spent.
    /// These scan ranges were marked `ScannedWithoutMapping` or `RefetchingNullifiers` priority before this note was
    /// scanned, meaning the nullifiers were discarded due to memory constraints and will be re-fetched later in the
    /// sync process.
    pub(crate) refetch_nullifier_ranges: Vec<Range<BlockHeight>>,
}

/// Sapling note.
pub type SaplingNote = WalletNote<sapling_crypto::Note, sapling_crypto::Nullifier>;

impl OutputInterface for SaplingNote {
    type KeyId = KeyId;
    type Input = sapling_crypto::Nullifier;

    const POOL_TYPE: PoolType = PoolType::Shielded(ShieldedProtocol::Sapling);

    fn output_id(&self) -> OutputId {
        self.output_id
    }

    fn key_id(&self) -> KeyId {
        self.key_id
    }

    fn spending_transaction(&self) -> Option<TxId> {
        self.spending_transaction
    }

    fn set_spending_transaction(&mut self, spending_transaction: Option<TxId>) {
        self.spending_transaction = spending_transaction;
    }

    fn value(&self) -> u64 {
        self.note.value().inner()
    }

    fn spend_link(&self) -> Option<Self::Input> {
        self.nullifier
    }

    fn transaction_inputs(transaction: &WalletTransaction) -> Vec<&Self::Input> {
        transaction.sapling_nullifiers()
    }

    fn transaction_outputs(transaction: &WalletTransaction) -> &[Self] {
        &transaction.sapling_notes
    }
}

impl NoteInterface for SaplingNote {
    type ZcashNote = sapling_crypto::Note;
    type Nullifier = Self::Input;

    const SHIELDED_PROTOCOL: ShieldedProtocol = ShieldedProtocol::Sapling;

    fn note(&self) -> &Self::ZcashNote {
        &self.note
    }

    fn nullifier(&self) -> Option<Self::Nullifier> {
        self.nullifier
    }

    fn position(&self) -> Option<Position> {
        self.position
    }

    fn memo(&self) -> &Memo {
        &self.memo
    }

    fn refetch_nullifier_ranges(&self) -> &[Range<BlockHeight>] {
        &self.refetch_nullifier_ranges
    }
}

/// Orchard note.
pub type OrchardNote = WalletNote<orchard::Note, orchard::note::Nullifier>;

impl OutputInterface for OrchardNote {
    type KeyId = KeyId;
    type Input = orchard::note::Nullifier;

    const POOL_TYPE: PoolType = PoolType::Shielded(ShieldedProtocol::Orchard);

    fn output_id(&self) -> OutputId {
        self.output_id
    }

    fn key_id(&self) -> KeyId {
        self.key_id
    }

    fn spending_transaction(&self) -> Option<TxId> {
        self.spending_transaction
    }

    fn set_spending_transaction(&mut self, spending_transaction: Option<TxId>) {
        self.spending_transaction = spending_transaction;
    }

    fn value(&self) -> u64 {
        self.note.value().inner()
    }

    fn spend_link(&self) -> Option<Self::Input> {
        self.nullifier
    }

    fn transaction_inputs(transaction: &WalletTransaction) -> Vec<&Self::Input> {
        transaction.orchard_nullifiers()
    }

    fn transaction_outputs(transaction: &WalletTransaction) -> &[Self] {
        &transaction.orchard_notes
    }
}

impl NoteInterface for OrchardNote {
    type ZcashNote = orchard::Note;
    type Nullifier = Self::Input;

    const SHIELDED_PROTOCOL: ShieldedProtocol = ShieldedProtocol::Orchard;

    fn note(&self) -> &Self::ZcashNote {
        &self.note
    }

    fn nullifier(&self) -> Option<Self::Nullifier> {
        self.spend_link()
    }

    fn position(&self) -> Option<Position> {
        self.position
    }

    fn memo(&self) -> &Memo {
        &self.memo
    }

    fn refetch_nullifier_ranges(&self) -> &[Range<BlockHeight>] {
        &self.refetch_nullifier_ranges
    }
}

/// Provides a common API for all outgoing note types.
pub trait OutgoingNoteInterface: Sized {
    /// Decrypted note type.
    type ZcashNote;
    /// Address type.
    type Address: Clone + Copy + Debug + PartialEq + Eq;
    /// Encoding error
    type Error: Debug + std::error::Error;

    /// Note's associated shielded protocol.
    const SHIELDED_PROTOCOL: ShieldedProtocol;

    /// Output ID.
    fn output_id(&self) -> OutputId;

    /// Identifier for key used to decrypt outgoing note.
    fn key_id(&self) -> KeyId;

    /// Note value.
    fn value(&self) -> u64;

    /// Decrypted note with recipient and value.
    fn note(&self) -> &Self::ZcashNote;

    /// Memo.
    fn memo(&self) -> &Memo;

    /// Recipient address.
    fn recipient(&self) -> Self::Address;

    /// Recipient unified address as given by recipient and recorded in an encoded memo (all original receivers).
    fn recipient_full_unified_address(&self) -> Option<&UnifiedAddress>;

    /// Encoded recipient address recorded in note on chain (single receiver).
    fn encoded_recipient<P>(&self, parameters: &P) -> Result<String, Self::Error>
    where
        P: consensus::Parameters + consensus::NetworkConstants;

    /// Encoded recipient unified address as given by recipient and recorded in an encoded memo (all original receivers).
    fn encoded_recipient_full_unified_address<P>(&self, consensus_parameters: &P) -> Option<String>
    where
        P: consensus::Parameters + consensus::NetworkConstants;

    /// Outgoing notes within `transaction`.
    fn transaction_outgoing_notes(transaction: &WalletTransaction) -> &[Self];
}

/// Note sent from this capability to a recipient.
#[derive(Debug, Clone, PartialEq)]
pub struct OutgoingNote<N> {
    /// Output ID.
    pub(crate) output_id: OutputId,
    /// Identifier for key used to decrypt output.
    pub(crate) key_id: KeyId,
    /// Decrypted note with recipient and value.
    pub(crate) note: N,
    /// Memo.
    pub(crate) memo: Memo,
    /// Recipient's full unified address from encoded memo.
    pub(crate) recipient_full_unified_address: Option<UnifiedAddress>,
}

/// Outgoing sapling note.
pub type OutgoingSaplingNote = OutgoingNote<sapling_crypto::Note>;

impl OutgoingNoteInterface for OutgoingSaplingNote {
    type ZcashNote = sapling_crypto::Note;
    type Address = sapling_crypto::PaymentAddress;
    type Error = Infallible;

    const SHIELDED_PROTOCOL: ShieldedProtocol = ShieldedProtocol::Sapling;

    fn output_id(&self) -> OutputId {
        self.output_id
    }

    fn key_id(&self) -> KeyId {
        self.key_id
    }

    fn value(&self) -> u64 {
        self.note.value().inner()
    }

    fn note(&self) -> &Self::ZcashNote {
        &self.note
    }

    fn memo(&self) -> &Memo {
        &self.memo
    }

    fn recipient(&self) -> Self::Address {
        self.note.recipient()
    }

    fn recipient_full_unified_address(&self) -> Option<&UnifiedAddress> {
        self.recipient_full_unified_address.as_ref()
    }

    fn encoded_recipient<P>(&self, consensus_parameters: &P) -> Result<String, Self::Error>
    where
        P: consensus::Parameters + consensus::NetworkConstants,
    {
        Ok(encode_payment_address(
            consensus_parameters.hrp_sapling_payment_address(),
            &self.note().recipient(),
        ))
    }

    fn encoded_recipient_full_unified_address<P>(&self, consensus_parameters: &P) -> Option<String>
    where
        P: consensus::Parameters + consensus::NetworkConstants,
    {
        self.recipient_full_unified_address
            .as_ref()
            .map(|unified_address| unified_address.encode(consensus_parameters))
    }

    fn transaction_outgoing_notes(transaction: &WalletTransaction) -> &[Self] {
        &transaction.outgoing_sapling_notes
    }
}

/// Outgoing orchard note.
pub type OutgoingOrchardNote = OutgoingNote<orchard::Note>;

impl OutgoingNoteInterface for OutgoingOrchardNote {
    type ZcashNote = orchard::Note;
    type Address = orchard::Address;
    type Error = ParseError;

    const SHIELDED_PROTOCOL: ShieldedProtocol = ShieldedProtocol::Orchard;

    fn output_id(&self) -> OutputId {
        self.output_id
    }

    fn key_id(&self) -> KeyId {
        self.key_id
    }

    fn value(&self) -> u64 {
        self.note.value().inner()
    }

    fn note(&self) -> &Self::ZcashNote {
        &self.note
    }

    fn memo(&self) -> &Memo {
        &self.memo
    }

    fn recipient(&self) -> Self::Address {
        self.note.recipient()
    }

    fn recipient_full_unified_address(&self) -> Option<&UnifiedAddress> {
        self.recipient_full_unified_address.as_ref()
    }

    fn encoded_recipient<P>(&self, parameters: &P) -> Result<String, Self::Error>
    where
        P: consensus::Parameters + consensus::NetworkConstants,
    {
        keys::encode_orchard_receiver(parameters, &self.note().recipient())
    }

    fn encoded_recipient_full_unified_address<P>(&self, consensus_parameters: &P) -> Option<String>
    where
        P: consensus::Parameters + consensus::NetworkConstants,
    {
        self.recipient_full_unified_address
            .as_ref()
            .map(|unified_address| unified_address.encode(consensus_parameters))
    }

    fn transaction_outgoing_notes(transaction: &WalletTransaction) -> &[Self] {
        &transaction.outgoing_orchard_notes
    }
}

// TODO: allow consumer to define shard store. memory shard store has infallible error type but other may not so error
// handling will need to replace expects
/// Type alias for sapling memory shard store
pub type SaplingShardStore = MemoryShardStore<sapling_crypto::Node, BlockHeight>;

/// Type alias for orchard memory shard store
pub type OrchardShardStore = MemoryShardStore<MerkleHashOrchard, BlockHeight>;

/// Shard tree wallet data struct
#[derive(Debug)]
pub struct ShardTrees {
    /// Sapling shard tree
    pub sapling: ShardTree<
        SaplingShardStore,
        { sapling_crypto::NOTE_COMMITMENT_TREE_DEPTH },
        { witness::SHARD_HEIGHT },
    >,
    /// Orchard shard tree
    pub orchard: ShardTree<
        OrchardShardStore,
        { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
        { witness::SHARD_HEIGHT },
    >,
}

impl ShardTrees {
    /// Create new `ShardTrees`
    #[must_use]
    pub fn new() -> Self {
        let mut sapling = ShardTree::new(MemoryShardStore::empty(), MAX_REORG_ALLOWANCE as usize);
        let mut orchard = ShardTree::new(MemoryShardStore::empty(), MAX_REORG_ALLOWANCE as usize);

        sapling
            .checkpoint(BlockHeight::from_u32(0))
            .expect("should never fail");
        orchard
            .checkpoint(BlockHeight::from_u32(0))
            .expect("should never fail");

        Self { sapling, orchard }
    }
}

impl Default for ShardTrees {
    fn default() -> Self {
        Self::new()
    }
}
