use std::{marker::Send, sync::Arc, collections::HashMap};

use async_trait::async_trait;
use thiserror::Error;

use frost::{Curve, FrostError, MultisigKeys, sign::PreprocessMachine};

use transcript::RecommendedTranscript;

mod coins;
mod wallet;

#[cfg(test)]
mod tests;

#[derive(Clone, Error, Debug)]
pub enum CoinError {
  #[error("failed to connect to coin daemon")]
  ConnectionError
}

#[derive(Clone, Error, Debug)]
pub enum NetworkError {}

#[derive(Clone, Error, Debug)]
pub enum SignError {
  #[error("coin had an error {0}")]
  CoinError(CoinError),
  #[error("network had an error {0}")]
  NetworkError(NetworkError),
  #[error("FROST had an error {0}")]
  FrostError(FrostError)
}

#[async_trait]
pub trait Network: Send {
  async fn round(&mut self, data: Vec<u8>) -> Result<HashMap<u16, Vec<u8>>, NetworkError>;
}

pub trait Output: Sized + Clone {
  type Id: AsRef<[u8]>;

  fn id(&self) -> Self::Id;
  fn amount(&self) -> u64;

  fn serialize(&self) -> Vec<u8>;
  fn deserialize<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self>;
}

#[async_trait]
pub trait Coin {
  type Curve: Curve;

  type Fee: Copy;
  type Transaction;
  type Block;

  type Output: Output;
  type SignableTransaction;
  type TransactionMachine: PreprocessMachine<Signature = Self::Transaction>;

  type Address: Send;

  const ID: &'static [u8];
  const CONFIRMATIONS: usize;
  const MAX_INPUTS: usize;
  const MAX_OUTPUTS: usize; // TODO: Decide if this includes change or not

  // Doesn't have to take self, enables some level of caching which is pleasant
  fn address(&self, key: <Self::Curve as Curve>::G) -> Self::Address;

  async fn get_height(&self) -> Result<usize, CoinError>;
  async fn get_block(&self, height: usize) -> Result<Self::Block, CoinError>;
  async fn get_outputs(
    &self,
    block: &Self::Block,
    key: <Self::Curve as Curve>::G
  ) -> Vec<Self::Output>;

  async fn prepare_send(
    &self,
    keys: Arc<MultisigKeys<Self::Curve>>,
    transcript: RecommendedTranscript,
    height: usize,
    inputs: Vec<Self::Output>,
    payments: &[(Self::Address, u64)],
    fee: Self::Fee
  ) -> Result<Self::SignableTransaction, CoinError>;

  async fn attempt_send(
    &self,
    transaction: Self::SignableTransaction,
    included: &[u16]
  ) -> Result<Self::TransactionMachine, CoinError>;

  async fn publish_transaction(
    &self,
    tx: &Self::Transaction
  ) -> Result<(Vec<u8>, Vec<<Self::Output as Output>::Id>), CoinError>;

  #[cfg(test)]
  async fn mine_block(&self, address: Self::Address);

  #[cfg(test)]
  async fn test_send(&self, key: Self::Address);
}

// Generate a static view key for a given chain in a globally consistent manner
// Doesn't consider the current group key to increase the simplicity of verifying Serai's status
// Takes an index, k, for more modern privacy protocols which use multiple view keys
pub fn view_key<C: Coin>(k: u64) -> <C::Curve as Curve>::F {
  C::Curve::hash_to_F(b"Serai DEX View Key", &[C::ID, &k.to_le_bytes()].concat())
}