<?php

  // Namespace
  namespace BMI\Plugin\Staging;

  // Use
  use BMI\Plugin\Backup_Migration_Plugin as BMP;

  // Exit on direct access
  if (!defined('ABSPATH')) exit;

  // Require controller
  require_once BMI_INCLUDES . '/staging/controller.php';
  
  /**
   * Subclass of main staging controller (tastewp/remote handler)
   */
  class BMI_Staging_TasteWP extends BMI_Staging {
    
    /**
     * Initialization of TasteWP Staging Site Handler
     */
    function __construct($name, $initialize = false, $backupName = false, $delete = false) {
      
      parent::__construct(...func_get_args());
      
      $this->backupName = $backupName;
      
      if ($delete) $this->abort(true);
      else {
        if ($initialize) $this->initialization();
        else $this->continueProcess();
      }
      
    }
    
    private function humanToBytes($num) {
      
      $num_split = str_split(strval(strtolower($num)));
      $num_split_temp = $num_split;
      $num_end = end($num_split);
      array_pop($num_split);
      $num_join = implode('', $num_split);
      if ('m' === $num_end) {
        $num = doubleval($num_join) * 1048576;
      } elseif ('k' === $num_end) {
        $num = doubleval($num_join) * 1024;
      } elseif ('g' === $num_end) {
        $num = doubleval($num_join) * 1073741824;
      }

      return $num;
      
    }
    
    private function getTotalAvailableMemory() {
      
      $wpMax = $this->humanToBytes(WP_MAX_MEMORY_LIMIT);
      $memory = $this->humanToBytes(ini_get('memory_limit'));
      $used = memory_get_usage(false);
      
      $lowest = $wpMax;
      if ($memory < $lowest) $lowest = $memory;
      
      $lowest1 = $wpMax - (1024 * 5) - $used;
      $lowest2 = $memory - (1024 * 5) - $used;
      
      if ($lowest2 > 0) $lowest = $lowest2;
      else if ($lowest1 > 0) $lowest = $lowest1;
      else $lowest = $lowest - $used;
      
      $lowest = intval(abs($lowest));
      
      $this->log('WP MAX MEMORY LIMIT: ' . $wpMax, 'VERBOSE');
      $this->log('MEMORY LIMIT PHP INI: ' . $memory, 'VERBOSE');
      $this->log('USED MEMORY: ' . $used, 'VERBOSE');
      $this->log('AVAILABLE MEMORY: ' . $lowest, 'VERBOSE');
      $this->log('Safe limit of memory divided by 2: ' . ($lowest / 2), 'VERBOSE');
      
      $this->log('Safe limit of memory divided by 2 in Megabytes: ' . number_format((($lowest / 2) / 1024 / 1024), 2), 'VERBOSE');
      
      return abs($lowest / 2);
      
    }
    
    private function initialization() {
      
      global $table_prefix;
      $ip = $this->getIpAddress();
      
      $this->printInitialLogs();
      $this->siteConfig['backupName'] = $this->backupName;
      $this->siteConfig['backupPath'] = BMP::fixSlashes(BMI_BACKUPS . DIRECTORY_SEPARATOR . $this->backupName);
        
      $this->siteConfig['creation_date'] = time();
      $this->siteConfig['creator_ip'] = $ip;
      $this->siteConfig['db_prefix'] = $table_prefix;
      
      $this->siteConfig['total_files'] = '---';
      $this->siteConfig['expiration_time'] = time() + 60*60;
      
      $this->log(__('Staging site remote creation process initialized', 'backup-backup'), 'success');
      $this->setContinuation(1);
      
    }
    
    private function sendUploadPing(&$batchData, $last) {
      
      $url = $this->tastewpURL . '/api/bmi/staging/upload';
      $headers = [
        'Content-Type' => 'application/octet-stream',
        'tastewp-backupname' => $this->siteConfig['backupName'],
        'tastewp-localname' => $this->name,
        'tastewp-is-last' => $last,
        'tastewp-secret' => $this->siteConfig['communication_secret'],
        'tastewp-uploadid' => $this->siteConfig['uploadId'],
        'tastewp-entry' => $this->siteConfig['entry']
      ];
      
      $response = $this->httpRequest($url, $batchData, 'PUT', $headers);
      return $response;
      
    }
    
    private function continueProcess() {
      
      if (isset($this->siteConfig['step'])) {
        $this->step = intval($this->siteConfig['step']);
      } else {
        $this->step = null;
      }

      if (!is_numeric($this->step)) {

        // End with error
        $this->log(__('Step code was not provided for the request, prevents continuation of the process...'), 'ERROR');
        $this->log('Step code was not provided for the request, prevents continuation of the process...', 'VERBOSE');
        $this->log('#201', 'END-CODE');
        return ['status' => 'error'];

      }
      
      // Default error
      $translatedMainError = __('Something unexpected happened, we need to abort the process.', 'backup-backup');
      $englishMainError = 'Something unexpected happened, we need to abort the process (#207).';
      
      // Step controller
      if ($this->step == 1) $this->initializeHandshakeWithTasteWP();
      if ($this->step == 2) $this->processWithUpload();
      if ($this->step == 3) $this->validateAndPrepareRecipe();
      else if (isset($this->step) && is_numeric($this->step)) $this->sendSuccess(true);
      else $this->returnError($translatedMainError, $englishMainError);
      
    }
    
    private function initializeHandshakeWithTasteWP() {
      
      // TODO: Check if the backup is stored locally, otherwise download from cloud
      // if () {}
      
      if (!isset($this->siteConfig['batch']) || $this->siteConfig['batch'] == 1) {
        
        if (!isset($this->siteConfig['backupPath']) || !file_exists($this->siteConfig['backupPath']) || is_dir($this->siteConfig['backupPath'])) {
          $translatedFileExistError = __('Backup path was not set or backup file does not exist.', 'backup-backup');
          $englishFileExistError = 'Backup path was not set or backup file does not exist (#208).';
          $this->returnError($translatedFileExistError, $englishFileExistError);
        }
        
        $this->log(__('Getting MD5 of selected backup file...', 'backup-backup'), 'step');
        $this->log('Backup path: ' . $this->siteConfig['backupPath'], 'verbose');
        $md5 = md5_file($this->siteConfig['backupPath']);
        $this->log('Calculated MD5 is: ' . $md5, 'verbose');
        $this->log(__('File MD5 checksum saved for future.', 'backup-backup'), 'success');
        
        $this->log(__('Making handshake with TasteWP and asking for blessing.', 'backup-backup'), 'step');
        $this->log('Sending initial request to TasteWP', 'verbose');
        
        $url = $this->tastewpURL . '/api/bmi/staging/init/upload';
        $data = [ 'name' => $this->name, 'backupName' => $this->siteConfig['backupName'], 'md5' => $md5 ];
        $response = $this->httpRequest($url, $data);
        
        if (!isset($response['status']) || $response['status'] != 'success') {
          if (isset($response['message'])) {
            $this->returnError($response['message'], $response['message']);
          } else {
            $translatedUnknownResponseError = __('Reponse from TasteWP does not have any status code, please try again.', 'backup-backup');
            $englishUnknownResponseError = 'Reponse from TasteWP does not have any status code, please try again (#210). ' . print_r($response, true);
            $this->returnError($translatedUnknownResponseError, $englishUnknownResponseError);
          }
        }
        $this->progress(5);
        
        // Error handlers validate the response
        $translatedKeyExistError = __('One or more keys does not exist in response, please try again.', 'backup-backup');
        $englishKeyExistError = 'Missing required keys to continue the process (#209), the key is: ';
        if (!isset($response['data'])) $this->returnError($translatedKeyExistError, $englishKeyExistError . 'data');
        if (!isset($response['data']['secret'])) $this->returnError($translatedKeyExistError, $englishKeyExistError . 'secret');
        if (!isset($response['data']['activation'])) $this->returnError($translatedKeyExistError, $englishKeyExistError . 'activation');
        if (!isset($response['data']['uploadId'])) $this->returnError($translatedKeyExistError, $englishKeyExistError . 'uploadId');
        if (!isset($response['data']['entry'])) $this->returnError($translatedKeyExistError, $englishKeyExistError . 'entry');
        if (!isset($response['data']['maxUploadSize'])) $this->returnError($translatedKeyExistError, $englishKeyExistError . 'maxUploadSize');
        
        $this->siteConfig['name'] = $this->name;
        $this->siteConfig['sumOfTotalSize'] = filesize($this->siteConfig['backupPath']);
        $this->siteConfig['display_name'] = $this->siteConfig['backupName'];
        $this->siteConfig['url'] = $this->tastewpURL . '/stg/' . $response['data']['activation'];
        
        $this->siteConfig['communication_secret'] = $response['data']['secret'];
        $this->siteConfig['activation'] = $response['data']['activation'];
        $this->siteConfig['uploadId'] = $response['data']['uploadId'];
        $this->siteConfig['entry'] = $response['data']['entry'];
        $this->siteConfig['maxUploadSize'] = $response['data']['maxUploadSize'];
        $availableMemory = $this->getTotalAvailableMemory();
        
        if ($availableMemory < intval($this->siteConfig['maxUploadSize'])) {
          $this->siteConfig['maxUploadSize'] = $availableMemory;
        }
        
        $this->log(__('Blessing received, upload details saved...', 'backup-backup'), 'success');
        $this->progress(8);
        $this->setContinuation(2);
        
      }
      
    }
    
    private function processWithUpload() {
      
      $translatedUploadError1 = __('One or more variables are not defined, cannot calculate batching.', 'backup-backup');
      $translatedUploadError2 = __('There was some error during the upload process, please try again.', 'backup-backup');
      $englishUploadError1 = 'One or more variables are not defined, cannot calculate batching (#213).';
      $englishUploadError2 = 'There was some error during the upload process, please try again (#214).';
      
      $sizeOfFile = intval($this->siteConfig['sumOfTotalSize']);
      $maxUploadLimitBytes = intval($this->siteConfig['maxUploadSize']); // By Default it was: 32 MBs, but it may be different for each user      
      
      if (!isset($this->siteConfig['batch']) || $this->siteConfig['batch'] == 1) {
        
        $this->log(__('Calculating amount out total upload batches', 'backup-backup'), 'step');
        
        if (!is_numeric($maxUploadLimitBytes) || is_nan($maxUploadLimitBytes) || !is_numeric($sizeOfFile) || is_nan($sizeOfFile)) {
          $this->returnError($translatedUploadError1, $englishUploadError1);
        }
        
        $this->log(__('Max upload size in megabytes:', 'backup-backup') . ' ' . number_format($maxUploadLimitBytes / 1024 / 1024, 2) . ' MB');
        $this->log(__('Size of backup file in megabytes:', 'backup-backup') . ' ' . number_format($sizeOfFile / 1024 / 1024, 2) . ' MB');
        $this->log(__('Expected total amount of batches:', 'backup-backup') . ' ' . ceil($sizeOfFile / $maxUploadLimitBytes));
        
        $this->log('Size of backup file in bytes: ' . $sizeOfFile . ' B', 'verbose');
        $this->log('Size of backup file in megabytes: ' . number_format($sizeOfFile / 1024 / 1024, 2) . ' MB', 'verbose');
        $this->log('Max upload size in bytes: ' . $maxUploadLimitBytes . ' B', 'verbose');
        $this->log('Max upload size in megabytes: ' . number_format($maxUploadLimitBytes / 1024 / 1024, 2) . ' MB', 'verbose');
        
        $this->log('Total amount of batched should be ceil(' . $sizeOfFile . ' / ' . $maxUploadLimitBytes . '): ' . ceil($sizeOfFile / $maxUploadLimitBytes), 'verbose');
        
        $this->siteConfig['totalUploadBatches'] = ceil($sizeOfFile / $maxUploadLimitBytes);
        $this->siteConfig['currentUploadBatch'] = 0;
         
        $this->log(__('Backup upload to TasteWP', 'backup-backup'), 'step');
        $this->progress(10);
        $this->setContinuation(2, 2);
        
      }
      
      if (!isset($this->siteConfig['batch']) || $this->siteConfig['batch'] == 2) {
        
        $totalSize = $sizeOfFile;
        $maxBatchSize = $maxUploadLimitBytes;
        $totalBatches = $this->siteConfig['totalUploadBatches'];
        $currentBatch = $this->siteConfig['currentUploadBatch'];
        
        if (intval($totalBatches) <= (intval($currentBatch) + 1)) $isLast = 'true';
        else $isLast = 'false';
        
        $start = $maxBatchSize * $currentBatch;
        $end = $start + $maxBatchSize;
        if ($end > $totalSize) $end = $totalSize;
        $percent = number_format(($end / $totalSize) * 100, 2);
        $this->progress(10 + intval(($end / $totalSize) * 80));
        
        $backupFileHandler = fopen($this->siteConfig['backupPath'], 'r');
        $batchData = stream_get_contents($backupFileHandler, $maxBatchSize, $start);
        fclose($backupFileHandler);
        
        $response = $this->sendUploadPing($batchData, $isLast);
        if (!isset($response['status']) || $response['status'] != 'success') {
          if (isset($response['message'])) $this->returnError($response['message'], $response['message']);
          else $this->returnError($translatedUploadError2, $englishUploadError2);
        }
        
        $this->log(sprintf(__('Upload milestone: %s/%s (%s) [Batch: %s/%s]', 'backup-backup'), $end, $totalSize, $percent . '%', ($currentBatch + 1), $totalBatches), 'info');
        $this->log('Chunk range: ' . $start . ' - ' . $end, 'verbose');
        
        if ($isLast === 'true') {
          $this->progress(90);
          $this->setContinuation(3);
        } else {
          $this->siteConfig['currentUploadBatch']++;
          $this->setContinuation(2, 2);
        }
        
      }
      
    }
    
    public function validateAndPrepareRecipe() {
      
      $translatedUploadError2 = __('There was some error during the upload process, please try again.', 'backup-backup');
      $englishUploadError2 = 'There was some error during the upload process, please try again (#316).';
      
      if (!isset($this->siteConfig['batch']) || $this->siteConfig['batch'] == 1) {
        
        $this->log(__('Upload process finished successfully.', 'backup-backup'), 'success');
        $this->log(__('Preparing uploaded backup file for activation.', 'backup-backup'), 'step');
        $this->log(__('This process may take a while.', 'backup-backup'), 'warn');
        $this->log('At this moment TasteWP validates and prepares one-time use recipe.', 'verbose');
        
        $url = $this->tastewpURL . '/api/bmi/staging/prepare';
        $data = [
          'secret' => $this->siteConfig['communication_secret'],
          'upload' => $this->siteConfig['uploadId'],
          'entry' => $this->siteConfig['entry']
        ];
        
        $response = $this->httpRequest($url, $data);
        
        if (!isset($response['status']) || $response['status'] != 'success') {
          if (isset($response['message'])) $this->returnError($response['message'], $response['message']);
          else $this->returnError($translatedUploadError2, $englishUploadError2);
        }
        
        $this->progress(100);
        $this->siteConfig['total_files'] = intval($response['fileAmount']);
        $this->siteConfig['total_size'] = intval($response['recipeSize']);
        $this->siteConfig['db_prefix'] = htmlspecialchars($response['tablePrefix']);
        
        sleep(2);
        
        $this->log(__('Your staging site is prepared for activation.', 'backup-backup'), 'success');
        $this->log('It means everything is fine with the backup file and user can initialize the site now with activation URL.', 'verbose');
        
      }
      
      $this->setContinuation(4);
      
    }
    
    public function __destruct() {

      parent::__destruct();

    }
    
  }