#![deny(clippy::pedantic)]
#![allow(clippy::doc_markdown)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::must_use_candidate)]

use std::fmt;
use std::hash::Hash;

use bitcoin_hashes::hash160;
use config::MintClientConfig;
use fedimint_core::core::{Decoder, ModuleInstanceId, ModuleKind};
use fedimint_core::encoding::{Decodable, Encodable};
use fedimint_core::module::{CommonModuleInit, ModuleCommon, ModuleConsensusVersion};
use fedimint_core::secp256k1::PublicKey;
use fedimint_core::{Amount, extensible_associated_module_type, plugin_types_trait_impl_common};
use serde::{Deserialize, Serialize};
use tbs::{BlindedMessage, Message};
use thiserror::Error;

pub mod config;
pub mod endpoint_constants;

pub const KIND: ModuleKind = ModuleKind::from_static_str("mintv2");
pub const MODULE_CONSENSUS_VERSION: ModuleConsensusVersion = ModuleConsensusVersion::new(1, 0);

/// Compact representation of a power-of-2 amount denomination
/// Represents 2^denomination msats
#[derive(
    Debug,
    Copy,
    Clone,
    Eq,
    PartialEq,
    Hash,
    Ord,
    PartialOrd,
    Serialize,
    Deserialize,
    Encodable,
    Decodable,
)]
pub struct Denomination(pub u8);

impl Denomination {
    /// Convert to Amount (only call at boundaries)
    pub fn amount(self) -> Amount {
        Amount::from_msats(1 << self.0 as usize)
    }
}

impl fmt::Display for Denomination {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "2^{} msats", self.0)
    }
}

/// The mint module currently doesn't define any consensus items and generally
/// throws an error on encountering one. To allow old clients to still decode
/// blocks in the future, should we decide to add consensus items, this has to
/// be an enum with only a default variant.
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Encodable, Decodable)]
pub enum MintConsensusItem {
    #[encodable_default]
    Default { variant: u64, bytes: Vec<u8> },
}

impl std::fmt::Display for MintConsensusItem {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "MintConsensusItem")
    }
}

#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
pub struct MintOutputBlindSignature(pub tbs::BlindedSignature);

/// A verifiable one time use IOU from the mint.
///
/// Digital version of a "note of deposit" in a free-banking era.
///
/// Consist of a user-generated nonce and a threshold signature over it
/// generated by the federated mint.
///
/// In this form it can only be validated, not spent since for that the
/// corresponding secret spend key is required.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
pub struct Note {
    pub denomination: Denomination,
    pub nonce: PublicKey,
    pub signature: tbs::Signature,
}

impl Note {
    pub fn amount(&self) -> Amount {
        self.denomination.amount()
    }
}

#[derive(Debug)]
pub struct MintCommonInit;

impl CommonModuleInit for MintCommonInit {
    const CONSENSUS_VERSION: ModuleConsensusVersion = MODULE_CONSENSUS_VERSION;
    const KIND: ModuleKind = KIND;

    type ClientConfig = MintClientConfig;

    fn decoder() -> Decoder {
        MintModuleTypes::decoder_builder().build()
    }
}

extensible_associated_module_type!(MintInput, MintInputV0, UnknownMintInputVariantError);

impl MintInput {
    pub fn new_v0(note: Note) -> Self {
        Self::V0(MintInputV0 { note })
    }
}

#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
pub struct MintInputV0 {
    pub note: Note,
}

impl std::fmt::Display for MintInputV0 {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Mint Note {}", self.note.denomination)
    }
}

extensible_associated_module_type!(MintOutput, MintOutputV0, UnknownMintOutputVariantError);

impl MintOutput {
    pub fn new_v0(denomination: Denomination, nonce: BlindedMessage, tweak: [u8; 16]) -> Self {
        MintOutput::V0(MintOutputV0 {
            denomination,
            nonce,
            tweak,
        })
    }
}

#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
pub struct MintOutputV0 {
    pub denomination: Denomination,
    pub nonce: BlindedMessage,
    pub tweak: [u8; 16],
}

impl MintOutputV0 {
    pub fn amount(&self) -> Amount {
        self.denomination.amount()
    }
}

impl std::fmt::Display for MintOutputV0 {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Mint Note {}", self.denomination)
    }
}

/// Recovery item for stateless ecash recovery
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
pub enum RecoveryItem {
    Output {
        denomination: Denomination,
        nonce_hash: hash160::Hash,
        tweak: [u8; 16],
    },
    Input {
        nonce_hash: hash160::Hash,
    },
}

#[derive(Debug, Clone, PartialEq, Hash, Encodable, Decodable)]
pub struct MintOutputOutcome;

impl std::fmt::Display for MintOutputOutcome {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "MintOutputOutcome")
    }
}

pub struct MintModuleTypes;

pub fn verify_note(note: Note, pk: tbs::AggregatePublicKey) -> bool {
    tbs::verify(nonce_message(note.nonce), note.signature, pk)
}

pub fn nonce_message(nonce: PublicKey) -> Message {
    tbs::Message::from_bytes_sha256(&nonce.serialize())
}

plugin_types_trait_impl_common!(
    KIND,
    MintModuleTypes,
    MintClientConfig,
    MintInput,
    MintOutput,
    MintOutputOutcome,
    MintConsensusItem,
    MintInputError,
    MintOutputError
);

#[derive(Debug, Clone, Eq, PartialEq, Hash, Error, Encodable, Decodable)]
pub enum MintInputError {
    #[error("The mint input version is not supported by this federation")]
    UnknownInputVariant(#[from] UnknownMintInputVariantError),
    #[error("The note is already spent")]
    SpentCoin,
    #[error("The note has an invalid amount not issued by the mint")]
    InvalidDenomination,
    #[error("The note has an invalid signature")]
    InvalidSignature,
}

#[derive(Debug, Clone, Eq, PartialEq, Hash, Error, Encodable, Decodable)]
pub enum MintOutputError {
    #[error("The mint output version is not supported by this federation")]
    UnknownOutputVariant(#[from] UnknownMintOutputVariantError),
    #[error("The note has an invalid amount not issued by the mint")]
    InvalidDenomination,
}
