use std::sync::Arc;
use std::time::UNIX_EPOCH;

use thiserror::*;

use shared::core;
use shared::core::ChainData;
use crate::db;
use crate::db::DatabaseError;
use crate::db::database;
use crate::log;

use shared::core::Hasher;
#[allow(dead_code)]
#[derive(Debug)]
pub struct Blockchain {
  id: String,
  mempool: Vec<ChainData>,
  db: database::ChainDb,
}

#[allow(dead_code)]
#[derive(Error, Debug)]
pub enum BlockchainError {
  #[error("Database operation failed")]
  Database(#[from] DatabaseError),

  #[error("invalid account creation")]
  InvalidAccountCreation,

  #[error("Transactional error")]
  Tx(#[from] shared::core::TxError),

  #[error("Validation Error")]
  Validation(#[from] ValidationError),

  #[error("Insufficient fonds on address {0}")]
  InsufficientFunds(String),

  #[error("Block Creation Error")]
  BlockCreation,
}

const BLOCKCHAIN_ID: &str = "watch-chain";

#[derive(Debug, thiserror::Error)]
pub enum ValidationError {
  #[error("Invalid Block Hash Detected at height {0}")]
  InvalidBlockHash(u64),
  #[error("Previous Block Hash doesn't match at height {0}")]
  InvalidPreviousBlockHash(u64),
  #[error("Invalid Block JSON: {0}")]
  InvalidBlockJson(#[from] serde_json::Error),
}

#[allow(dead_code)]
impl Blockchain {
  fn hash_transaction_pool(&self) -> Vec<String> {
    self
      .mempool
      .iter()
      .map(|tx| Hasher::hash_chain_data(tx))
      .collect()
  }

  pub fn award_currency(&self, address: String, amount: u64) -> Result<(), BlockchainError> {
    Ok(self.db.add_balance(&address, amount)?)
  }

  pub fn create_block(&mut self) -> Result<Arc<core::Block>, BlockchainError> {
    match self.blocks() {
      Ok(blocks) => {
        let previous_hash = if blocks.len() > 0 {
          blocks.last().unwrap().head().block_hash()
        } else {
          ""
        };
        let tx_hashes = self.hash_transaction_pool();
        let merkle_root = Hasher::calculate_merkle_root(&tx_hashes);

        let timestamp = std::time::SystemTime::now()
          .duration_since(UNIX_EPOCH)
          .unwrap()
          .as_secs();
        let nonce = 0;

        let mut new_head = core::BlockHeader {
          previous_hash: previous_hash.to_string(),
          merkle_root,
          timestamp,
          nonce,
          height: blocks.len() as u64 + 1,
          block_hash: "".to_string(),
        };

        let mut block_hash = String::new();
        while !block_hash.starts_with("0") {
          new_head.nonce += 1;
          block_hash = Hasher::calculate_block_hash(&new_head)
        }

        new_head.block_hash = block_hash;

        let new_block = Arc::new(core::Block::new(new_head, tx_hashes));
        self.add_block(new_block.clone())?;
        Ok(new_block)
      }
      Err(_) => Err(BlockchainError::BlockCreation),
    }
  }

  fn apply_transaction(&mut self, tx: &core::Tx) -> Result<(), BlockchainError> {
    tx.validate()?;
    let from = tx.from();
    let to = tx.to();
    if let Some(from_balance) = self.db.get_balance(tx.from())? {
      let to_balance = if let Some(b) = self.db.get_balance(tx.to())? {
        b
      } else {
        0
      };

      if tx.value() > from_balance {
        return Err(BlockchainError::InsufficientFunds(tx.from().to_string()))
      }

      let new_from_balance = from_balance - tx.value();
      let new_to_balance = to_balance + tx.value();

      let changes = vec![(from, new_from_balance), (to, new_to_balance)];
      Ok(self.db.set_balance_batch(changes)?)
    } else {
      Err(BlockchainError::Database(DatabaseError::AccountNotFound(from.to_string())))
    }
  }

  pub fn apply_chain_data(&mut self, data: ChainData) -> Result<(), BlockchainError> {
    match &data {
      ChainData::Transaction(tx) => {
        self.apply_transaction(tx)?;
        self.mempool.push(data);
      }
    }
    Ok(())
  }
}

impl Blockchain {
  pub fn list_blocks(&self) -> Result<Vec<String>, BlockchainError> {
    let mut ret = Vec::new();
    let blocks = self.blocks()?;
    for b in blocks.iter() {
      ret.push(b.head.block_hash().to_string())
    }
    Ok(ret)
  }

  pub fn blocks(&self) -> Result<Vec<std::sync::Arc<core::Block>>, BlockchainError> {
    Ok(self.db.get_all_blocks()?)
  }

  fn insert_block(&self, block: &core::Block) -> Result<(), BlockchainError> {
    self.db.add_block(block)?;
    Ok(())
  }

  pub fn add_block(&mut self, block: Arc<core::Block>) -> Result<(), BlockchainError> {
    match self.validate_block(&block) {
      Ok(()) => Ok(self.insert_block(&block)?),
      Err(e) => Err(BlockchainError::Validation(e)),
    }
  }

  pub fn display_block_by_key(&self, key: String) {
    if let Ok(Some(b)) = self.db.get_block_by_key(&key) {
      log(format!("{:#?}", b))
    }
  }

  pub fn display_block_by_height(&self, height: u64) {
    if let Ok(Some(b)) = self.db.get_block_by_height(height) {
      log(format!("{:#?}", b))
    }
  }

  fn validate_block(&self, block: &core::Block) -> Result<(), ValidationError> {
    let head = block.head();
    let hash = Hasher::calculate_block_hash(block.head());
    if hash != head.block_hash() {
      return Err(ValidationError::InvalidBlockHash(head.height));
    }
    if let Ok(blocks) = self.blocks() {
      if let Some(last_block) = blocks.last() {
        if head.previous_hash() != last_block.head().block_hash() {
          return Err(ValidationError::InvalidPreviousBlockHash(head.height));
        }
      }
    }
    Ok(())
  }

  fn validate_chain(&self) -> Result<(), ValidationError> {
    if let Ok(blocks) = self.blocks() {
      if let Some(mut prev_block) = blocks.first() {
        for (i, block) in blocks.iter().skip(1).enumerate() {
          let head = block.head();
          let hash = Hasher::calculate_block_hash(block.head());

          if hash != head.block_hash() {
            return Err(ValidationError::InvalidBlockHash(i as u64));
          } else if head.previous_hash() != prev_block.head().block_hash() && blocks.len() > 1 {
            return Err(ValidationError::InvalidPreviousBlockHash(i as u64));
          }
          prev_block = block;
        }
      }
    }
    Ok(())
  }

  pub async fn shutdown(&self) -> Result<(), BlockchainError> {
    self.db.dump_mempool(&self.mempool)?;
    self.db.shutdown().await?;
    Ok(())
  }

  pub fn build(path: Option<String>) -> Result<Blockchain, BlockchainError> {
    let db = db::ChainDb::new(path).or_else(|e| Err(BlockchainError::Database(e)))?;
    let mempool = db.recover_mempool()?;

    let chain = Blockchain {
      mempool,
      id: BLOCKCHAIN_ID.to_string(),
      db,
    };
    chain
      .validate_chain()
      .or_else(|e| return Err(BlockchainError::Validation(e)))?;
    Ok(chain)
  }
}
