<?php

/* 
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

namespace Model\Transactions;

//use Model\Messages\Gradido\Transaction;
//use Model\Messages\Gradido\TransactionBody;
use Cake\ORM\TableRegistry;
use Cake\Datasource\ConnectionManager;

class Transaction extends TransactionBase {
  
    private $mProtoTransaction = null;
    private $mTransactionBody = null;
    
    
  
    public function __construct($base64Data) 
    {
        //$transactionBin = base64_decode($base64Data, true);
        //if($transactionBin == false) {
      //sodium_base64_VARIANT_URLSAFE_NO_PADDING
      if(is_a($base64Data, '\Proto\Gradido\GradidoTransaction')) {
        $this->mProtoTransaction = $base64Data;
        $this->mTransactionBody = new TransactionBody($this->mProtoTransaction->getBodyBytes());
        return;
      }
      
      try {
        $transactionBin = sodium_base642bin($base64Data, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING);
      } catch(\SodiumException $e) {
        //$this->addError('Transaction', $e->getMessage());// . ' ' . $base64Data);
        //return;
         $transactionBin = base64_decode($base64Data, true);
         if($transactionBin == false) {
           $this->addError('Transaction', $e->getMessage());// . ' ' . $base64Data);
           $this->addError('base64', $base64Data);
           return;
         }
      }
        //*/}
        
        if($transactionBin == false) {
          //$this->addError('base64 decode failed');
          $this->addError('Transaction', 'base64 decode error: ' . $base64Data);
        } else {
          //var_dump($transactionBin);
          $this->mProtoTransaction = new \Proto\Gradido\GradidoTransaction();
          try {
            $this->mProtoTransaction->mergeFromString($transactionBin);
            //var_dump($this->mProtoTransaction);
            // cannot catch Exception with cakePHP, I don't know why
          } catch(\Google\Protobuf\Internal\GPBDecodeException $e) {
            //var_dump($e);
            $this->addError('Transaction', $e->getMessage());
            return;
          }//*/
          
          //echo 'serialize to json: <br>';
          //echo $this->mProtoTransaction->serializeToJsonString();
          //echo "body bytes: <br>";
          //var_dump($this->mProtoTransaction->getBodyBytes());
          //echo "<br>end body bytes<br>";
          $this->mTransactionBody = new TransactionBody($this->mProtoTransaction->getBodyBytes());
        }
    }
    
    static public function build(\Proto\Gradido\TransactionBody $transactionBody, $senderKeyPair) 
    {
        $protoTransaction = new \Proto\Gradido\GradidoTransaction();
        
        $recevied = new \Proto\Gradido\TimestampSeconds();
        $recevied->setSeconds(time());
        $protoTransaction->setReceived($recevied);
        
        $bodyBytes = $transactionBody->serializeToString();
        
        $sigMap = SignatureMap::build($bodyBytes, [$senderKeyPair]);
        $protoTransaction->setSigMap($sigMap->getProto());
        
        $protoTransaction->setBodyBytes($bodyBytes);
        
        return $protoTransaction;
        
    }
    
    public function getTransactionBody() {
      return $this->mTransactionBody;
    }
    
    public function getFirstPublic() 
    {
      if(!$this->mProtoTransaction || !$this->mProtoTransaction->getSigMap()) {
          return '';
      }
      $sigPairs = $this->mProtoTransaction->getSigMap()->getSigPair();
      return $sigPairs[0]->getPubKey();
    }
    
    public function getFirstSigningUser()
    {
        return $this->getStateUserFromPublickey($this->getFirstPublic());
    }
    
    public function getId() {
      return $this->mProtoTransaction->getId();
    }
    
    public function validate() {
        $sigMap = $this->mProtoTransaction->getSigMap();
        if(!$sigMap) {
          $this->addError('Transaction', 'signature map is zero');
          //var_dump($this->mProtoTransaction);
          return false;
        }
        //var_dump($sigMap);
        //die();
        $sigPairs = $sigMap->getSigPair();
        $bodyBytes = $this->mProtoTransaction->getBodyBytes();
        
        
        if(!$sigPairs || count($sigPairs) < 1) {
          $this->addError('Transaction::validate', 'no signature found');
          return false;
        }
        
        // check signature(s)
        foreach($sigPairs as $sigPair) {
          //echo 'sig Pair: '; var_dump($sigPair); echo "<br>";
          $pubkey = $sigPair->getPubKey();
          $signature = $sigPair->getEd25519();
          //echo "verify bodybytes: <br>" . bin2hex($bodyBytes) . '<br>';
          if (!\Sodium\crypto_sign_verify_detached($signature, $bodyBytes, $pubkey)) {
              $this->addError('Transaction::validate', 'signature for key ' . bin2hex($pubkey) . ' isn\'t valid ' );
              return false;
          } 
        }
        
        if(!$this->mTransactionBody->validate($sigPairs)) {
          $this->addErrors($this->mTransactionBody->getErrors());
          return false;
        }
        
        return true;
    }
    
    public function save()
    {
      $connection = ConnectionManager::get('default');
      $connection->begin();
      //id transaction_id signature     pubkey 
      
       if (!$this->mTransactionBody->save($this->getFirstPublic(), $this->mProtoTransaction->getSigMap())) {
          $this->addErrors($this->mTransactionBody->getErrors());
          $connection->rollback();
          // correct auto-increment value to prevent gaps
          $transactionsTable = $this->getTable('transactions');
          $transactions = $transactionsTable->find()->select(['id'])->contain(false);
          $count = $transactions->count();
          $connection = ConnectionManager::get('default');
          $connection->execute("ALTER TABLE `transactions` auto_increment = $count;");
          
          return false;
      }
      
      // save transaction signatures
      $transactionsSignaturesTable = TableRegistry::getTableLocator()->get('transaction_signatures');
      $transactionId = $this->mTransactionBody->getTransactionID();
      //signature     pubkey
      
      $sigPairs = $this->mProtoTransaction->getSigMap()->getSigPair();
      //echo "sigPairs: "; var_dump($sigPairs);
      $signatureEntitys = [];
      foreach($sigPairs as $sigPair) {
          $signatureEntity = $transactionsSignaturesTable->newEntity();
          $signatureEntity->transaction_id = $transactionId;
          $signatureEntity->signature = $sigPair->getEd25519();
          $signatureEntity->pubkey = $sigPair->getPubKey();
          array_push($signatureEntitys, $signatureEntity);
      }
      //debug($signatureEntitys);
      if(!$transactionsSignaturesTable->saveMany($signatureEntitys)) {
        foreach($signatureEntitys as $entity) {
          $errors = $entity->getErrors();
          if(!$errors && count($errors) > 0) {
            $pubkeyHex = bin2hex($entity->pubkey);
            $this->addError('Transaction::save', 'error saving signature for pubkey: ' . $pubkeyHex . ', with errors: ' . json_encode($errors) );
          }
        }
        $connection->rollback();
        return false;
      }
      
      $connection->commit();
      
      $specificTransaction = $this->mTransactionBody->getSpecificTransaction();
      
      $specificTransaction->sendNotificationEmail($this->mTransactionBody->getMemo());
      $this->addWarnings($specificTransaction->getWarnings());
      return true;
    }

    static public function fromTable($id) 
    {   
        $transactionsTable = TableRegistry::getTableLocator()->get('transactions');
        $transactionEntry = $transactionsTable
                ->find('all')
                ->where(['id' => $id])
                ->contain([
                    'TransactionCreations', 
                    'TransactionSendCoins', 
                    'TransactionSignatures'])
                ->first();
        //var_dump($transactionEntry->toArray());
        $protoTransaction = new \Proto\Gradido\Transaction();
        
        
        
        $protoTransaction->setId($transactionEntry->id);

        
        $recevied = new \Proto\Gradido\TimestampSeconds();
        $recevied->setSeconds($transactionEntry->received->getTimestamp());
        $protoTransaction->setReceived($recevied);
        
        
        $sigMap = SignatureMap::fromEntity($transactionEntry->transaction_signatures);
        $protoTransaction->setSigMap($sigMap->getProto());
        
        //echo "sig map: check<br>";
        $protoTransaction->setTxHash(stream_get_contents($transactionEntry->tx_hash));
        
        $body = TransactionBody::fromEntity($transactionEntry->memo, $transactionEntry);
        if(is_array($body)) {
          return ['state' => 'error', 'msg' => 'error creating body transaction', 'details' => $body];
        }
        
        // validate signatures
        $sigPairs = $sigMap->getProto()->getSigPair();
        
        if(!$sigPairs || count($sigPairs) < 1) {
          return ['state' => 'error', 'msg' => 'error no signatures found'];
        }
        
        //echo "verify bodybytes: <br>" . bin2hex($bodyBytes) . '<br>';
        $created = new \Proto\Gradido\TimestampSeconds();
        $created->setSeconds($recevied->getSeconds());
        $body->setCreated($created);
        $bodyBytes = $body->serializeToString();
        $createTrys = 0;
        $createRight = false;
        // check signature(s) and 
        // try to get created field of TransactionBody right, because it wasn't saved
        foreach($sigPairs as $sigPair) {
          //echo 'sig Pair: '; var_dump($sigPair); echo "<br>";
          $pubkey = $sigPair->getPubKey();
          $signature = $sigPair->getEd25519();
          if(!$createRight) {
            while($createTrys < 500) {
              if(\Sodium\crypto_sign_verify_detached($signature, $bodyBytes, $pubkey)) {
                $createRight = true;
                break;
              } else {
                $createTrys++;
                $created->setSeconds($created->getSeconds() - 1);
                //$body->setCreated($created);
                $bodyBytes = $body->serializeToString();
              }
            }
          }
            
          if (!\Sodium\crypto_sign_verify_detached($signature, $bodyBytes, $pubkey)) {
              return ['state' => 'error', 'msg' => 'signature for key ' . bin2hex($pubkey) . ' isn\'t valid '];
          } 
        }
        
        $protoTransaction->setBodyBytes($bodyBytes);
        
        
        
        return $protoTransaction;
    }    

}