use std::collections::{BTreeMap, BTreeSet};
use std::error::Error;
use std::ops::Deref;
use amplify::confinement::{self, Confined};
use bp::seals::txout::blind::SingleBlindSeal;
use bp::Txid;
use commit_verify::mpc;
use rgb::{
    validation, Anchor, AnchoredBundle, BundleId, ContractId, ExposedSeal, GraphSeal, OpId,
    Operation, Opout, SchemaId, SecretSeal, SubSchema, Transition, TransitionBundle,
};
use strict_encoding::TypeName;
use crate::accessors::{BundleExt, MergeRevealError, RevealError};
use crate::containers::{
    Bindle, BuilderSeal, Cert, Consignment, ContentId, Contract, Terminal, Transfer,
};
use crate::interface::{
    ContractIface, Iface, IfaceId, IfaceImpl, IfacePair, TransitionBuilder, TypedState,
};
use crate::persistence::hoard::ConsumeError;
use crate::persistence::stash::StashInconsistency;
use crate::persistence::{Stash, StashError};
use crate::resolvers::ResolveHeight;
use crate::Outpoint;
#[derive(Debug, Display, Error, From)]
#[display(doc_comments)]
pub enum ConsignerError<E1: Error, E2: Error> {
    TooManyTerminals,
    TooManyBundles,
    ConcealedPublicState(Opout),
    #[display(inner)]
    #[from]
    Reveal(RevealError),
    #[display(inner)]
    #[from]
    #[from(InventoryInconsistency)]
    InventoryError(InventoryError<E1>),
    #[display(inner)]
    #[from]
    #[from(StashInconsistency)]
    StashError(StashError<E2>),
}
#[derive(Debug, Display, Error, From)]
#[display(inner)]
pub enum InventoryError<E: Error> {
    Connectivity(E),
    #[from]
    Consume(ConsumeError),
    #[from]
    #[from(confinement::Error)]
    DataError(DataError),
    #[from]
    #[from(mpc::LeafNotKnown)]
    #[from(mpc::InvalidProof)]
    #[from(RevealError)]
    #[from(StashInconsistency)]
    InternalInconsistency(InventoryInconsistency),
}
impl<E1: Error, E2: Error> From<StashError<E1>> for InventoryError<E2>
where E2: From<E1>
{
    fn from(err: StashError<E1>) -> Self {
        match err {
            StashError::Connectivity(err) => Self::Connectivity(err.into()),
            StashError::InternalInconsistency(e) => {
                Self::InternalInconsistency(InventoryInconsistency::Stash(e))
            }
        }
    }
}
#[derive(Debug, Display, Error, From)]
#[display(inner)]
pub enum InventoryDataError<E: Error> {
    Connectivity(E),
    #[from]
    #[from(validation::Status)]
    #[from(confinement::Error)]
    #[from(IfaceImplError)]
    #[from(RevealError)]
    #[from(MergeRevealError)]
    DataError(DataError),
}
impl<E: Error> From<InventoryDataError<E>> for InventoryError<E> {
    fn from(err: InventoryDataError<E>) -> Self {
        match err {
            InventoryDataError::Connectivity(e) => InventoryError::Connectivity(e),
            InventoryDataError::DataError(e) => InventoryError::DataError(e),
        }
    }
}
#[derive(Debug, Display, Error, From)]
#[display(inner)]
pub enum DataError {
    NotValidated,
    #[from]
    Invalid(validation::Status),
    UnresolvedTransactions,
    TerminalsUnmined,
    #[display(inner)]
    #[from]
    Reveal(RevealError),
    #[from]
    #[display(inner)]
    Merge(MergeRevealError),
    OutpointUnknown(Outpoint, ContractId),
    #[from]
    Confinement(confinement::Error),
    #[from]
    IfaceImpl(IfaceImplError),
    NoIfaceImpl(SchemaId, IfaceId),
    #[from]
    HeightResolver(Box<dyn Error>),
    Concealed,
}
#[derive(Clone, Debug, Display, Error, From)]
#[display(doc_comments)]
pub enum IfaceImplError {
    UnknownSchema(SchemaId),
    UnknownIface(IfaceId),
}
#[derive(Debug, Display, Error, From)]
#[display(doc_comments)]
pub enum InventoryInconsistency {
    StateAbsent(ContractId),
    DisclosureAbsent(Txid),
    BundleAbsent(OpId),
    NoBundleAnchor(BundleId),
    #[from(mpc::LeafNotKnown)]
    #[from(mpc::InvalidProof)]
    UnrelatedAnchor,
    #[from]
    BundleReveal(RevealError),
    OutsizedBundle,
    #[from]
    #[display(inner)]
    Stash(StashInconsistency),
}
#[allow(clippy::result_large_err)]
pub trait Inventory: Deref<Target = Self::Stash> {
    type Stash: Stash;
    type Error: Error;
    fn stash(&self) -> &Self::Stash;
    fn import_sigs<I>(
        &mut self,
        content_id: ContentId,
        sigs: I,
    ) -> Result<(), InventoryDataError<Self::Error>>
    where
        I: IntoIterator<Item = Cert>,
        I::IntoIter: ExactSizeIterator<Item = Cert>;
    fn import_schema(
        &mut self,
        schema: impl Into<Bindle<SubSchema>>,
    ) -> Result<validation::Status, InventoryDataError<Self::Error>>;
    fn import_iface(
        &mut self,
        iface: impl Into<Bindle<Iface>>,
    ) -> Result<validation::Status, InventoryDataError<Self::Error>>;
    fn import_iface_impl(
        &mut self,
        iimpl: impl Into<Bindle<IfaceImpl>>,
    ) -> Result<validation::Status, InventoryDataError<Self::Error>>;
    fn import_contract<R: ResolveHeight>(
        &mut self,
        contract: Contract,
        resolver: &mut R,
    ) -> Result<validation::Status, InventoryError<Self::Error>>
    where
        R::Error: 'static;
    fn accept_transfer<R: ResolveHeight>(
        &mut self,
        transfer: Transfer,
        resolver: &mut R,
        force: bool,
    ) -> Result<validation::Status, InventoryError<Self::Error>>
    where
        R::Error: 'static;
    fn consume_anchor(
        &mut self,
        anchor: Anchor<mpc::MerkleBlock>,
    ) -> Result<(), InventoryError<Self::Error>>;
    fn consume_bundle(
        &mut self,
        contract_id: ContractId,
        bundle: TransitionBundle,
        witness_txid: Txid,
    ) -> Result<(), InventoryError<Self::Error>>;
    unsafe fn import_contract_force<R: ResolveHeight>(
        &mut self,
        contract: Contract,
        resolver: &mut R,
    ) -> Result<validation::Status, InventoryError<Self::Error>>
    where
        R::Error: 'static;
    fn contracts_with_iface(
        &mut self,
        iface: impl Into<TypeName>,
    ) -> Result<Vec<ContractIface>, InventoryError<Self::Error>>
    where
        Self::Error: From<<Self::Stash as Stash>::Error>,
        InventoryError<Self::Error>: From<<Self::Stash as Stash>::Error>,
    {
        let iface = iface.into();
        let iface_id = self.iface_by_name(&iface)?.iface_id();
        self.contract_ids_by_iface(&iface)?
            .into_iter()
            .map(|id| self.contract_iface(id, iface_id))
            .collect()
    }
    fn contract_iface_named(
        &mut self,
        contract_id: ContractId,
        iface: impl Into<TypeName>,
    ) -> Result<ContractIface, InventoryError<Self::Error>>
    where
        Self::Error: From<<Self::Stash as Stash>::Error>,
        InventoryError<Self::Error>: From<<Self::Stash as Stash>::Error>,
    {
        let iface = iface.into();
        let iface_id = self.iface_by_name(&iface)?.iface_id();
        self.contract_iface(contract_id, iface_id)
    }
    fn contract_iface(
        &mut self,
        contract_id: ContractId,
        iface_id: IfaceId,
    ) -> Result<ContractIface, InventoryError<Self::Error>>;
    fn anchored_bundle(&self, opid: OpId) -> Result<AnchoredBundle, InventoryError<Self::Error>>;
    fn transition_builder(
        &mut self,
        contract_id: ContractId,
        iface: impl Into<TypeName>,
        transition_name: Option<impl Into<TypeName>>,
    ) -> Result<TransitionBuilder, InventoryError<Self::Error>>
    where
        Self::Error: From<<Self::Stash as Stash>::Error>,
    {
        let schema_ifaces = self.contract_schema(contract_id)?;
        let iface = self.iface_by_name(&iface.into())?;
        let schema = &schema_ifaces.schema;
        let iimpl = schema_ifaces
            .iimpls
            .get(&iface.iface_id())
            .ok_or(DataError::NoIfaceImpl(schema.schema_id(), iface.iface_id()))?;
        let builder = if let Some(transition_name) = transition_name {
            TransitionBuilder::named_transition(
                iface.clone(),
                schema.clone(),
                iimpl.clone(),
                transition_name.into(),
            )
        } else {
            TransitionBuilder::default_transition(iface.clone(), schema.clone(), iimpl.clone())
        }
        .expect("internal inconsistency");
        Ok(builder)
    }
    fn blank_builder(
        &mut self,
        contract_id: ContractId,
        iface: impl Into<TypeName>,
    ) -> Result<TransitionBuilder, InventoryError<Self::Error>>
    where
        Self::Error: From<<Self::Stash as Stash>::Error>,
    {
        let schema_ifaces = self.contract_schema(contract_id)?;
        let iface = self.iface_by_name(&iface.into())?;
        let schema = &schema_ifaces.schema;
        let iimpl = schema_ifaces
            .iimpls
            .get(&iface.iface_id())
            .ok_or(DataError::NoIfaceImpl(schema.schema_id(), iface.iface_id()))?;
        let builder =
            TransitionBuilder::blank_transition(iface.clone(), schema.clone(), iimpl.clone())
                .expect("internal inconsistency");
        Ok(builder)
    }
    fn transition(&self, opid: OpId) -> Result<&Transition, InventoryError<Self::Error>>;
    fn contracts_by_outpoints(
        &mut self,
        outpoints: impl IntoIterator<Item = impl Into<Outpoint>>,
    ) -> Result<BTreeSet<ContractId>, InventoryError<Self::Error>>;
    fn public_opouts(
        &mut self,
        contract_id: ContractId,
    ) -> Result<BTreeSet<Opout>, InventoryError<Self::Error>>;
    fn opouts_by_outpoints(
        &mut self,
        contract_id: ContractId,
        outpoints: impl IntoIterator<Item = impl Into<Outpoint>>,
    ) -> Result<BTreeSet<Opout>, InventoryError<Self::Error>>;
    fn opouts_by_terminals(
        &mut self,
        terminals: impl IntoIterator<Item = SecretSeal>,
    ) -> Result<BTreeSet<Opout>, InventoryError<Self::Error>>;
    fn state_for_outpoints(
        &mut self,
        contract_id: ContractId,
        outpoints: impl IntoIterator<Item = impl Into<Outpoint>>,
    ) -> Result<BTreeMap<Opout, TypedState>, InventoryError<Self::Error>>;
    fn store_seal_secret(&mut self, seal: GraphSeal) -> Result<(), InventoryError<Self::Error>>;
    fn seal_secrets(&mut self) -> Result<BTreeSet<GraphSeal>, InventoryError<Self::Error>>;
    #[allow(clippy::type_complexity)]
    fn export_contract(
        &mut self,
        contract_id: ContractId,
    ) -> Result<
        Bindle<Contract>,
        ConsignerError<Self::Error, <<Self as Deref>::Target as Stash>::Error>,
    > {
        let mut consignment =
            self.consign::<GraphSeal, false>(contract_id, [] as [GraphSeal; 0])?;
        consignment.transfer = false;
        Ok(consignment.into())
        }
    #[allow(clippy::type_complexity)]
    fn transfer(
        &mut self,
        contract_id: ContractId,
        seals: impl IntoIterator<Item = impl Into<BuilderSeal<SingleBlindSeal>>>,
    ) -> Result<
        Bindle<Transfer>,
        ConsignerError<Self::Error, <<Self as Deref>::Target as Stash>::Error>,
    > {
        let mut consignment = self.consign(contract_id, seals)?;
        consignment.transfer = true;
        Ok(consignment.into())
        }
    fn consign<Seal: ExposedSeal, const TYPE: bool>(
        &mut self,
        contract_id: ContractId,
        seals: impl IntoIterator<Item = impl Into<BuilderSeal<Seal>>>,
    ) -> Result<
        Consignment<TYPE>,
        ConsignerError<Self::Error, <<Self as Deref>::Target as Stash>::Error>,
    > {
        let mut opouts = self.public_opouts(contract_id)?;
        let (outpoint_seals, terminal_seals) = seals
            .into_iter()
            .map(|seal| match seal.into() {
                BuilderSeal::Revealed(seal) => (seal.outpoint(), seal.conceal()),
                BuilderSeal::Concealed(seal) => (None, seal),
            })
            .unzip::<_, _, Vec<_>, Vec<_>>();
        opouts.extend(self.opouts_by_outpoints(contract_id, outpoint_seals.into_iter().flatten())?);
        opouts.extend(self.opouts_by_terminals(terminal_seals.iter().copied())?);
        let mut anchored_bundles = BTreeMap::<OpId, AnchoredBundle>::new();
        let mut transitions = BTreeMap::<OpId, Transition>::new();
        let mut terminals = BTreeMap::<BundleId, Terminal>::new();
        for opout in opouts {
            if opout.op == contract_id {
                continue; }
            let transition = self.transition(opout.op)?;
            transitions.insert(opout.op, transition.clone());
            let anchored_bundle = self.anchored_bundle(opout.op)?;
            let bundle_id = anchored_bundle.bundle.bundle_id();
            for (type_id, typed_assignments) in transition.assignments.iter() {
                for index in 0..typed_assignments.len_u16() {
                    let seal = typed_assignments.to_confidential_seals()[index as usize];
                    if terminal_seals.contains(&seal) {
                        terminals.insert(bundle_id, Terminal::new(seal.into()));
                    } else if opout.no == index && opout.ty == *type_id {
                        if let Some(seal) = typed_assignments
                            .revealed_seal_at(index)
                            .expect("index exists")
                        {
                            terminals.insert(bundle_id, Terminal::new(seal.into()));
                        } else {
                            return Err(ConsignerError::ConcealedPublicState(opout));
                        }
                    }
                }
            }
            anchored_bundles.insert(opout.op, anchored_bundle.clone());
        }
        let mut ids = vec![];
        for transition in transitions.values() {
            ids.extend(transition.inputs().iter().map(|input| input.prev_out.op));
        }
        while let Some(id) = ids.pop() {
            if id == contract_id {
                continue; }
            let transition = self.transition(id)?;
            ids.extend(transition.inputs().iter().map(|input| input.prev_out.op));
            transitions.insert(id, transition.clone());
            anchored_bundles
                .entry(id)
                .or_insert(self.anchored_bundle(id)?.clone())
                .bundle
                .reveal_transition(transition)?;
        }
        let genesis = self.genesis(contract_id)?;
        let schema_ifaces = self.schema(genesis.schema_id)?;
        let mut consignment = Consignment::new(schema_ifaces.schema.clone(), genesis.clone());
        for (iface_id, iimpl) in &schema_ifaces.iimpls {
            let iface = self.iface_by_id(*iface_id)?;
            consignment
                .ifaces
                .insert(*iface_id, IfacePair::with(iface.clone(), iimpl.clone()))
                .expect("same collection size");
        }
        consignment.bundles = Confined::try_from_iter(anchored_bundles.into_values())
            .map_err(|_| ConsignerError::TooManyBundles)?;
        consignment.terminals =
            Confined::try_from(terminals).map_err(|_| ConsignerError::TooManyTerminals)?;
        Ok(consignment)
    }
}