<?php

  // Namespace
  namespace BMI\Plugin\Staging;

  // Use
  use BMI\Plugin\Backup_Migration_Plugin as BMP;
  use BMI\Plugin\Checker\BMI_Checker as Checker;
  use BMI\Plugin\Database\BMI_Search_Replace_Engine as BMISearchReplace;

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

  // Require controller
  require_once BMI_INCLUDES . '/staging/controller.php';

  /**
   * Subclass of main staging controller (local handler)
   */
  class BMI_StagingLocal extends BMI_Staging {

    protected $data = [];
    protected $step = 0;
    private $filesList = null;
    private $dirsList = null;
    private $rootLength = 0;
    private $excludedDirectories = [];

    public function __construct($name, $initialize = false) {

      parent::__construct(...func_get_args());
      if ($initialize) $this->initialization();

    }

    public 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->prepareFilesAndDatabase();
      else if ($this->step == 2) $this->duplicateDatabase();
      else if ($this->step == 3) $this->searchReplace();
      else if ($this->step == 4) $this->databaseFinishedCopyFiles();
      else if ($this->step == 5) $this->setupWpConfigAndLoginScript();
      else if ($this->step == 6) $this->performFinish();
      else if (isset($this->step) && is_numeric($this->step)) $this->sendSuccess();
      else $this->returnError($translatedMainError, $englishMainError);

    }

    private function getAllLiveTables() {

      global $wpdb;
      $tables = [];

      $allTables = $wpdb->get_results('SHOW TABLES');

      foreach ($allTables as $table) {
        foreach ($table as $name) $tables[] = $name;
      }

      return $tables;

    }

    private function checkIfPrefixCanBeUsed($tables, $prefix) {

      $sizeOfPrefix = strlen($prefix);

      for ($i = 0; $i < sizeof($tables); ++$i) {
        $name = $tables[$i];

        if (substr($name, 0, $sizeOfPrefix) == $prefix) return false;
        if (strpos($name, $prefix)) return false;
      }

      return $prefix;

    }

    private function generateUniquePrefix() {
      return 'bmstg' . substr(time(), -4) . '_';
    }

    private function generateDatabasePrefix() {

      $tables = $this->getAllLiveTables();
      $prefix = $this->generateUniquePrefix();

      $i = 0;

      while ($this->checkIfPrefixCanBeUsed($tables, $prefix) === false && $i <= 3) {
        sleep(1);
        $prefix = $this->generateUniquePrefix();

        $i++;
      }

      if ($i >= 3) return 'error';
      return $prefix;

    }

    private function createDatabasePrefixAndInitDir() {

      $path = trailingslashit(ABSPATH) . $this->name;

      $this->log(__('Preparing constants and staging details', 'backup-backup'), 'STEP');

      // Directory creation
      $this->log(__('Creating root directory of new site', 'backup-backup'));
      if (file_exists($path) && is_dir($path)) {
        $translated = __('Seems like desired directory of staging site already exist, try with different staging site name.', 'backup-backup');
        $english = 'Seems like desired directory of staging site already exist, try with different staging site name.';
        $this->returnError($translated, $english);
      }

      @mkdir($path, 0755);
      touch($path . DIRECTORY_SEPARATOR . '.bmi_staging');

      $this->log(__('Path of new website:', 'backup-backup') . ' ' . $path, 'SUCCESS');

      // Generation of database prefix
      $this->log(__('Generating database prefix', 'backup-backup'));
      $dbPrefix = $this->generateDatabasePrefix();
      if ($dbPrefix == 'error') {
        $translated = __('There was an error during database prefix generation, maybe all generated were already used, try again.', 'backup-backup');
        $english = 'There was an error during database prefix generation, maybe all generated were already used, try again.';
        $this->returnError($translated, $english);
      }
      $this->log(__('Prefix of new site will be:', 'backup-backup') . ' ' . $dbPrefix, 'SUCCESS');

      $this->config[$this->name]['name'] = $this->name;
      $this->config[$this->name]['prefix'] = $dbPrefix;

      // Password for first log in
      $this->log(__('Generating first log in password', 'backup-backup'));
      $password = $this->getRandomPassword();
      $this->log(__('Password created and saved.', 'backup-backup'), 'SUCCESS');

      // Set initial configuration
      $this->log(__('Initializing configuration of the website', 'backup-backup'), 'STEP');
      $this->initialConfiguration($dbPrefix, $password);
      $this->log(__('Configuration created', 'backup-backup'), 'SUCCESS');

    }

    private function initialConfiguration($dbPrefix, $password) {

      global $table_prefix;

      $ip = $this->getIpAddress();

      // Basic for all
      $this->siteConfig['name'] = $this->name; // Name of that staging site
      $this->siteConfig['url'] = home_url($this->name); // URL to the website
      $this->siteConfig['root_source'] = trailingslashit(ABSPATH); // ABSPATH of source website
      $this->siteConfig['root_staging'] = untrailingslashit(ABSPATH) . DIRECTORY_SEPARATOR . $this->name; // ABSPATH of staging site
      $this->siteConfig['db_prefix'] = $dbPrefix; // Database prefix of that site
      $this->siteConfig['creation_date'] = time(); // Creation date and time of that staging site
      $this->siteConfig['password'] = $password; // Password for password-less login
      $this->siteConfig['creator_ip'] = $ip; // IP of user who created it
      $this->siteConfig['login_ip'] = $ip; // IP for autologin script (limit password-less login to only that IP)
      $this->siteConfig['login_user_id'] = get_current_user_id(); // User ID for password less authentication
      $this->siteConfig['source_home_url'] = home_url(); // Homepage URL of source website
      $this->siteConfig['source_site_url'] = site_url(); // Website (admin) URL of source website
      $this->siteConfig['source_db_prefix'] = $table_prefix; // Database prefix of source website

      // TasteWP
      $this->siteConfig['communication_secret'] = 'local'; // Local if it's not TasteWP website // Secret code to get authless access to website
      $this->siteConfig['expiration_time'] = 'never'; // Never if it's not TasteWP website // Expiration time of the website

    }

    private function getExcludedFilesAndDirectories($abspath = false) {

      $excludedDirectories = [];
      
      if ($abspath) {
        $excludedDirectories = $this->getAllStagingSiteDirectories();
      }
      
      $excludedDirectories[] = '.';
      $excludedDirectories[] = '..';
      $excludedDirectories[] = 'wp-config.php';
      $excludedDirectories[] = '.DS_Store';
      $excludedDirectories[] = '.quarantine';
      $excludedDirectories[] = '.git';
      $excludedDirectories[] = '.tmb';
      $excludedDirectories[] = 'node_modules';
      $excludedDirectories[] = 'debug.log';

      return $excludedDirectories;

    }

    private function getAbspathFiles($path) {

      $excludedDirectories = $this->getExcludedFilesAndDirectories(true);
      $topFiles = array_diff(scandir($path), $excludedDirectories);

      return $topFiles;

    }

    private function processFiles($prefixPath, &$files) {

      $dirs = [];

      foreach ($files as $index => $file) {
        $path = $prefixPath . $file;
        if (strpos($path, BMI_BACKUPS_ROOT) !== false) continue;
        if (is_link($path)) continue;
        if (is_dir($path)) {
          if (!(is_readable($path) && is_writable($path))) continue;
          if (file_exists(trailingslashit($path) . '.bmi_staging')) continue;
          $this->siteConfig['total_size'] += 4096;
          $this->siteConfig['total_directories'] += 1;
          fwrite($this->dirsList, $file . "\n");
          $dirs[] = $path;
        } else if (is_file($path)) {
          if (!is_readable($path)) continue;
          $this->siteConfig['total_size'] += filesize($path);
          $this->siteConfig['total_files'] += 1;
          fwrite($this->filesList, $file . "\n");
        }
      }

      return $dirs;

    }

    private function processDirectoryRecursively($dir) {

      if (!(is_readable($dir) && is_writable($dir))) return;

      $files = array_diff(scandir($dir), $this->excludedDirectories);

      foreach ($files as $key => $value) {
        $path = $dir . DIRECTORY_SEPARATOR . $value;
        if (strpos($path, BMI_BACKUPS_ROOT) !== false) continue;
        if (is_link($path)) continue;
        if (is_dir($path)) {
          $this->siteConfig['total_size'] += 4096;
          $this->siteConfig['total_directories'] += 1;
          fwrite($this->dirsList, substr($path, $this->rootLength) . "\n");
          if (!file_exists(trailingslashit($path) . '.bmi_staging')) {
            $this->processDirectoryRecursively($path);
          }
        } else if (is_file($path)) {
          if (!is_readable($path)) continue;
          $this->siteConfig['total_size'] += filesize($path);
          $this->siteConfig['total_files'] += 1;
          fwrite($this->filesList, substr($path, $this->rootLength) . "\n");
        }
      }

    }

    private function processFilesRecursively($leftDirs) {

      $this->rootLength = strlen(trailingslashit(ABSPATH));
      $this->excludedDirectories = $this->getExcludedFilesAndDirectories();

      for ($i = 0; $i < sizeof($leftDirs); ++$i) {
        $this->processDirectoryRecursively($leftDirs[$i]);
      }

    }

    private function getTablesForDuplication() {

      global $wpdb, $table_prefix;

      $currentPrefixes = $this->getAllStagingSitePrefixes();
      $relatedTables = [];


      $tables = $wpdb->get_results($wpdb->prepare("SELECT (DATA_LENGTH + INDEX_LENGTH) as `size`, TABLE_NAME AS `name` FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s;", array(DB_NAME)));
      foreach ($tables as $tableObject) {

        $name = $tableObject->name;
        $size = $tableObject->size;

        if (substr($name, 0, strlen($table_prefix)) != $table_prefix) {
          $this->log('Ommiting this table: ' . $name, 'verbose');
          continue;
        } else {
          $this->log('Adding this table: ' . $name, 'verbose');
        }

        $tableOfStagingSite = false;
        for ($i = 0; $i < sizeof($currentPrefixes); ++$i) {
          $subPrefix = $currentPrefixes[$i];
          if ($table_prefix != $subPrefix && substr($name, 0, strlen($subPrefix)) == $subPrefix) {
            $tableOfStagingSite = true;
            break;
          }
        }
        if ($tableOfStagingSite) {
          $this->log('Excluding this table as part of staging site: ' . $name, 'verbose');
          continue;
        }

        $this->siteConfig['total_db_size'] += intval($size);
        $relatedTables[$name] = $this->siteConfig['db_prefix'] . substr($name, strlen($table_prefix));

      }

      return $relatedTables;

    }

    private function updateUserRolesInOptions() {

      global $wpdb;

      // Update option name
      $newOptionTable = $this->siteConfig['db_prefix'] . 'options';
      $newOptionName = $this->siteConfig['db_prefix'] . 'user_roles';
      $oldOptionName = $this->siteConfig['source_db_prefix'] . 'user_roles';

      // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
      $wpdb->query($wpdb->prepare("UPDATE " . BMP::escapeSQLIDentifier($newOptionTable) . " SET `option_name` = %s WHERE `option_name` = %s;", $newOptionName, $oldOptionName));

      if ($wpdb->last_error !== '') {
        $translated = __('There was an error during update of user roles:', 'backup-backup') . ' ' . $wpdb->last_error;
        $english = 'There was an error during update of user roles:' . ' ' . $wpdb->last_error;
        return $this->returnError($translated, $english);
      }
      
      // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
      $wpdb->query("DELETE FROM " . BMP::escapeSQLIDentifier($newOptionTable) . " WHERE `option_name` = 'BMI::STORAGE::LOCAL::PATH';");

      if ($wpdb->last_error !== '') {
        $translated = __('There was an error during BMI config hard removal:', 'backup-backup') . ' ' . $wpdb->last_error;
        $english = 'There was an error during BMI config hard removal:' . ' ' . $wpdb->last_error;
        return $this->returnError($translated, $english);
      }

    }
    
    private function duplicateTableAlternative($source, $destination) {
      
      global $wpdb;
      
      // Remove failed table if created
      // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
      $wpdb->query("DROP TABLE IF EXISTS " . BMP::escapeSQLIDentifier($destination) . ";");

      if ($wpdb->last_error !== '') {
        $translated = __('There was an error during previous destination table removal:', 'backup-backup') . ' ' . $wpdb->last_error;
        $english = 'There was an error during previous destination table removal:' . ' ' . $wpdb->last_error;
        return $this->returnError($translated, $english); 
      }
      
      // Create new table
      // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
      $wpdb->query("CREATE TABLE " . BMP::escapeSQLIDentifier($destination) . " LIKE " . BMP::escapeSQLIDentifier($source) . ";");
      if ($wpdb->last_error !== '') {
        $translated = __('There was an error during database table creation:', 'backup-backup') . ' ' . $wpdb->last_error;
        $english = 'There was an error during database table creation:' . ' ' . $wpdb->last_error;
        return $this->returnError($translated, $english); 
      }

      // Duplicate data
      // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
      $wpdb->query("INSERT INTO " . BMP::escapeSQLIDentifier($destination) . " SELECT * from " . BMP::escapeSQLIDentifier($source) . ";");
      if ($wpdb->last_error !== '') {
        $translated = __('There was an error during database table data duplication:', 'backup-backup') . ' ' . $wpdb->last_error;
        $english = 'There was an error during database table data duplication:' . ' ' . $wpdb->last_error;
        return $this->returnError($translated, $english);
      }

    }

    private function duplicateTable($source, $destination) {

      global $wpdb;

      // Create new table
      // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
      $sourceCreate = $wpdb->get_row("SHOW CREATE TABLE " . BMP::escapeSQLIDentifier($source) . ";");
      $isTable = isset($sourceCreate->{'Create Table'});
      if ($isTable) {
        $sourceCreate->{'Create Table'} = str_ireplace('CREATE TABLE ' . BMP::escapeSQLIDentifier($source), 'CREATE TABLE IF NOT EXISTS ' . BMP::escapeSQLIDentifier($destination), $sourceCreate->{'Create Table'});
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- SQL generated from escaped SHOW CREATE TABLE output
        $wpdb->query($sourceCreate->{'Create Table'});
        //Insert data 
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
        $wpdb->query("INSERT INTO " . BMP::escapeSQLIDentifier($destination) . " SELECT * from " . BMP::escapeSQLIDentifier($source) . ";");
      } else {
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Identifier is safely escaped via escapeSQLIDentifier()
        $wpdb->query("CREATE TABLE " . BMP::escapeSQLIDentifier($destination) . " AS SELECT * FROM " . BMP::escapeSQLIDentifier($source) . ";");
      }



      if ($wpdb->last_error !== '') {
        $this->duplicateTableAlternative($source, $destination);
      }
      
      if ($wpdb->last_error !== '') {
        $translated = __('There was an error during database table creation:', 'backup-backup') . ' ' . $wpdb->last_error;
        $english = 'There was an error during database table creation:' . ' ' . $wpdb->last_error;
        return $this->returnError($translated, $english); 
      }

    }

    private function parseDomain($domain, $removeWWW = true) {

      if (substr($domain, 0, 8) == 'https://') $domain = substr($domain, 8);
      if (substr($domain, 0, 7) == 'http://') $domain = substr($domain, 7);
      if ($removeWWW === true) {
        if (substr($domain, 0, 4) == 'www.') $domain = substr($domain, 4);
      }
      $domain = untrailingslashit($domain);

      return $domain;

    }

    private function performSearchReplace($table, $from, $to, $start = 0, $end = 0, $limitColumns = false) {

      $replaceEngine = new BMISearchReplace([$table], $start, $end, true);

      $stats = $replaceEngine->perform($from, $to, $limitColumns);

      $this->siteConfig['totalTables'] += $stats['tables'];
      $this->siteConfig['totalRows'] += $stats['rows'];
      $this->siteConfig['totalChanges'] += $stats['change'];
      $this->siteConfig['totalUpdates'] += $stats['updates'];

      $this->siteConfig['currentTableTotalUpdates'] += $stats['updates'];
      $this->siteConfig['currentSearchReplacePage'] = $stats['currentPage'];
      $this->siteConfig['totalSearchReplacePages'] = $stats['totalPages'];

      $replaceEngine = null;

    }
    
    public function requestDelete() {
      return $this->abort(true);
    }

    public function reconstructConfigurations() {

      global $table_prefix;

      // Load any surviving configuration entries so we preserve them
      $existingConfig = [];
      if (file_exists($this->configPath)) {
        $raw = file_get_contents($this->configPath);
        $raw = trim(substr($raw, 8));
        if (is_serialized($raw)) {
          $parsed = maybe_unserialize($raw);
          if (is_array($parsed)) $existingConfig = $parsed;
        }
      }
      $this->config = $existingConfig;

      $abspath      = trailingslashit(ABSPATH);
      $reconstructed = [];
      $skipped       = [];
      $errors        = [];

      $entries = @scandir($abspath);
      if ($entries === false) {
        return ['status' => 'error', 'message' => 'Cannot read ABSPATH directory.'];
      }

      foreach ($entries as $entry) {

        if ($entry === '.' || $entry === '..') continue;

        $candidatePath = $abspath . $entry;
        if (!is_dir($candidatePath)) continue;

        $markerFile = $candidatePath . DIRECTORY_SEPARATOR . '.bmi_staging';
        if (!file_exists($markerFile)) continue;

        $stagingName = $entry;
        $stagingRoot = untrailingslashit($candidatePath);

        // Skip sites that already have an intact config file
        if (isset($this->config[$stagingName]) && isset($this->config[$stagingName]['config'])) {
          $existingConfigFile = BMI_STAGING . DIRECTORY_SEPARATOR . sanitize_text_field($this->config[$stagingName]['config']) . '.php';
          if (file_exists($existingConfigFile)) {
            $skipped[] = $stagingName;
            continue;
          }
        }

        $dbPrefix    = null;
        $wpConfigPath = $stagingRoot . DIRECTORY_SEPARATOR . 'wp-config.php';

        if (file_exists($wpConfigPath) && is_readable($wpConfigPath)) {
          $wpConfigContent = file_get_contents($wpConfigPath);
          if (preg_match('/\$table_prefix\s*=\s*[\'"]([^\'"]+)[\'"]\s*;/', $wpConfigContent, $matches)) {
            $candidate = $matches[1];
            if ($candidate !== $table_prefix) {
              $dbPrefix = $candidate;
            }
          }
        }

        if ($dbPrefix === null) {
          $errors[] = $stagingName;
          continue;
        }

        // Build site config
        $configId   = uniqid();
        $siteConfig = [
          'name'                 => $stagingName,
          'url'                  => home_url($stagingName),
          'root_source'          => trailingslashit(ABSPATH),
          'root_staging'         => $stagingRoot,
          'db_prefix'            => $dbPrefix,
          'creation_date'        => filemtime($markerFile),
          'password'             => $this->getRandomPassword(),
          'creator_ip'           => $this->getIpAddress(),
          'login_ip'             => $this->getIpAddress(),
          'login_user_id'        => get_current_user_id(),
          'source_home_url'      => home_url(),
          'source_site_url'      => site_url(),
          'source_db_prefix'     => $table_prefix,
          'communication_secret' => 'local',
          'expiration_time'      => 'never',
          'step'                 => 7,
          'batch'                => 1,
          'total_size'           => 'N/A',
          'total_files'          => 'N/A',
          'total_directories'    => 'N/A',
          'total_db_size'        => 'N/A',
          'amountOfTables'       => 'N/A',
          'sumOfTotalSize'       => 'N/A',
        ];

        $siteConfigPath = BMI_STAGING . DIRECTORY_SEPARATOR . $configId . '.php';
        file_put_contents($siteConfigPath, '<?php //' . serialize($siteConfig));

        // Register in global config
        $this->config[$stagingName] = [
          'config' => $configId,
          'name'   => $stagingName,
          'prefix' => $dbPrefix,
        ];

        $reconstructed[] = $stagingName;

      }

      $this->saveConfig();

      return [
        'status'        => (count($errors) > 0 && count($reconstructed) === 0) ? 'error' : 'success',
        'reconstructed' => $reconstructed,
        'skipped'       => $skipped,
        'errors'        => $errors,
      ];

    }

    // Step: 0 (initialization of the staging site, create entry)
    private function initialization() {

      $this->log('Step 0 - Initialization of the process', 'VERBOSE');

      $this->log(__('Name of subsite:', 'backup-backup') . ' ' . $this->name);
      $this->log(__('Expected URL of subsite:', 'backup-backup') . ' ' . home_url($this->name));

      $this->printInitialLogs();

      $this->createDatabasePrefixAndInitDir();
      if ($this->wasError) return;

      // Set progress to do something
      $this->progress(4);

      // Set next step to be 1
      $this->setContinuation(1);

    }

    // Step: 1 (preparation of file list and database recipes)
    private function prepareFilesAndDatabase() {

      // Create list of all files and directories
      if (!isset($this->siteConfig['batch']) || $this->siteConfig['batch'] == 1) {
        $this->log(__('Scanning all files on your website, it may take a while...', 'backup-backup'), 'STEP');

        $pathDirsListFile = BMI_TMP . DIRECTORY_SEPARATOR . '.staging_directories';
        $pathFilesListFile = BMI_TMP . DIRECTORY_SEPARATOR . '.staging_files';

        $this->siteConfig['total_size'] = 0;
        $this->siteConfig['total_files'] = 0;
        $this->siteConfig['total_directories'] = 0;

        if (file_exists($pathDirsListFile)) @unlink($pathDirsListFile);
        if (file_exists($pathFilesListFile)) @unlink($pathFilesListFile);

        $this->filesList = fopen($pathFilesListFile, 'a');
        $this->dirsList = fopen($pathDirsListFile, 'a');

        $topLevelFiles = $this->getAbspathFiles(ABSPATH);
        $leftDirs = $this->processFiles(trailingslashit(ABSPATH), $topLevelFiles);

        $this->processFilesRecursively($leftDirs);

        fclose($this->filesList);
        fclose($this->dirsList);

        $this->log(__('All files scanned and prepared for duplication...', 'backup-backup'), 'SUCCESS');
        $this->progress(10);

        $this->setContinuation(1, 2);
      }

      // Display details about scan and check database size
      if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 2) {
        $this->log(__('Looking for database tables that should be duplicated and their sizes...', 'backup-backup'), 'STEP');

        $this->siteConfig['total_db_size'] = 0;
        $this->siteConfig['tables'] = $this->getTablesForDuplication();
        $this->siteConfig['amountOfTables'] = sizeof($this->siteConfig['tables']);
        $this->siteConfig['sumOfTotalSize'] = intval($this->siteConfig['total_db_size']) + intval($this->siteConfig['total_size']);

        $this->log(__('Amount of files to duplicate:', 'backup-backup') . ' ' . $this->siteConfig['total_files']);
        $this->log(__('Amount of directories to duplicate:', 'backup-backup') . ' ' . $this->siteConfig['total_directories']);
        $this->log(__('Amount of tables to duplicate:', 'backup-backup') . ' ' . $this->siteConfig['amountOfTables']);
        $this->log(__('Size of database to duplicate:', 'backup-backup') . ' ' . BMP::humanSize(intval($this->siteConfig['total_db_size'])));
        $this->log(__('Size of files to duplicate:', 'backup-backup') . ' ' . BMP::humanSize(intval($this->siteConfig['total_size'])));
        $this->log(__('Total duplication size:', 'backup-backup') . ' ' . BMP::humanSize($this->siteConfig['sumOfTotalSize']));
        $this->log(__('Tables prepared for duplication...', 'backup-backup'), 'SUCCESS');

        $this->log(__('Checking if there is enough space for the staging site...', 'backup-backup'), 'STEP');
        $this->log(__('Space checking may take a while', 'backup-backup'));

        $this->progress(15);

        $this->setContinuation(1, 3);
      }

      // Check if there is enough space for duplication
      if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 3) {
        require_once BMI_INCLUDES . '/check/checker.php';
        $checker = new Checker($this->logger);

        $bytes = intval(intval($this->siteConfig['sumOfTotalSize']) * 1.1);
        $this->log(str_replace('%s2', BMP::humanSize($bytes), str_replace('%s1', $bytes, __('Checking in total: %s1 bytes (%s2)', 'backup-backup'))));

        if (!$checker->check_free_space($bytes, true)) {

            $translated = __('There is not enough space on your server in order to create staging site.', 'backup-backup');
            $english = 'There is not enough space on your server in order to create staging site.';
            return $this->returnError($translated, $english);

        } else {

          $this->log(__("Confirmed, there is more than enough space, checked: ", 'backup-backup') . ($bytes) . __(" bytes", 'backup-backup'), 'SUCCESS');

        }

        $this->progress(20);

        // Set next batch to start step 2
        $this->log('Setting new step for next request to 2 @ batch 1', 'VERBOSE');
        $this->setContinuation(2);

      }

    }

    // Step: 2 (duplicate database)
    private function duplicateDatabase() {

      if (!isset($this->siteConfig['finishedTables'])) {
        $this->siteConfig['finishedTables'] = [];
      }

      if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 1) {
        $this->log(__('Duplicating database tables...', 'backup-backup'), 'STEP');
      }

      $startTime = time();
      foreach ($this->siteConfig['tables'] as $source_table => $destination_table) {

        $this->duplicateTable($source_table, $destination_table);
        $this->log(str_replace('%s1', $source_table, str_replace('%s2', $destination_table, __('Table %s1 cloned as %s2', 'backup-backup'))));

        $this->siteConfig['finishedTables'][] = $destination_table;
        unset($this->siteConfig['tables'][$source_table]);

        $processPercentage = intval((sizeof($this->siteConfig['finishedTables']) / $this->siteConfig['amountOfTables']) * 20);
        $this->progress(20 + $processPercentage);

        if ((time() - $startTime) >= 4) {
          $this->setContinuation(2, (intval($this->siteConfig['batch']) + 1));
          break;
        }

      }

      if (sizeof($this->siteConfig['tables']) == 0) {
        unset($this->siteConfig['tables']);
      }

      if (!isset($this->siteConfig['tables'])) {
        $this->log(__('All tables were successfully duplicated', 'backup-backup'), 'SUCCESS');
        $this->setContinuation(3);
      }

    }

    // Step: 3 (search & replace of new tables)
    private function searchReplace() {

      require_once BMI_INCLUDES . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'search-replace.php';

      // Domain
      $sourceURL = $this->parseDomain($this->siteConfig['source_home_url']);
      $destinationURL = $this->parseDomain($this->siteConfig['url']);

      // Paths
      $sourceABSPATH = untrailingslashit($this->siteConfig['root_source']);
      $destinationABSPATH = untrailingslashit($this->siteConfig['root_staging']);

      // Items to be replaced
      $replaces = [
        ['from' => $sourceABSPATH, 'to' => $destinationABSPATH],
        ['from' => $sourceURL, 'to' => $destinationURL],
        ['from' => $this->siteConfig['source_db_prefix'], 'to' => $this->siteConfig['db_prefix']]
      ];

      $variants = [
        __("Batch for path adjustment (%s/%s) updated: %s fields.", 'backup-backup'),
        __("Batch for domain adjustments (%s/%s) updated: %s fields.", 'backup-backup'),
        __("Batch for prefix key adjustments (%s/%s) updated: %s fields.", 'backup-backup')
      ];

      $variantsEmpty = [
        __("Path replacements are not required for table: %s", 'backup-backup'),
        __("Domain replacements are not required for table: %s", 'backup-backup'),
        __("Prefix key replacements are not required for table: %s", 'backup-backup')
      ];

      // Table for replacement
      if (isset($this->siteConfig['finishedTables']) && sizeof($this->siteConfig['finishedTables']) > 0) {

        // Get first table from duplicated tables
        $table = $this->siteConfig['finishedTables'][0];

      } else {

        // All tables processed, search replace finished
        unset($this->siteConfig['finishedTables']);
        $this->setContinuation(4);

      }

      if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 1) {
        $this->log(__('Performing database search replace on duplicated tables...', 'backup-backup'), 'STEP');

        $pagesize = '?';
        if (defined('BMI_MAX_SEARCH_REPLACE_PAGE')) $pagesize = BMI_MAX_SEARCH_REPLACE_PAGE;
        $this->log(__('Page size for that process: ', 'backup-backup') . $pagesize, 'INFO');

        $this->siteConfig['totalTables'] = 0;
        $this->siteConfig['totalRows'] = 0;
        $this->siteConfig['totalChanges'] = 0;
        $this->siteConfig['totalUpdates'] = 0;

        $this->siteConfig['currentSearchReplaceItem'] = 0;
        $this->siteConfig['currentTableTotalUpdates'] = 0;
        $this->siteConfig['currentSearchReplacePage'] = 0;
        $this->siteConfig['totalSearchReplacePages'] = 0;

        $this->setContinuation(3, 2);
      }

      if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 2) {

        // Give details of current process
        if ($this->siteConfig['currentSearchReplaceItem'] == 0 && $this->siteConfig['currentSearchReplacePage'] == 0) {
          $this->log(sprintf(__('Performing search replace for table: %s', 'backup-backup'), $table), 'STEP');
        }

        // Control time of the process
        $startTime = time();
        $tableCompleted = false;
        $maxExecutionTime = 5;

        for ($i = $this->siteConfig['currentSearchReplaceItem']; $i < sizeof($replaces); ++$i) {

          $this->siteConfig['currentSearchReplaceItem'] = $i;
          if ((time() - $startTime) >= $maxExecutionTime) break;
          $tableCompleted = false;

          $from = $replaces[$i]['from'];
          $to = $replaces[$i]['to'];

          while ($tableCompleted === false && (time() - $startTime) < $maxExecutionTime) {

            // Progress
            $start = $this->siteConfig['currentSearchReplacePage'];
            $end = $this->siteConfig['totalSearchReplacePages'];

            // Path replace for current table
            if ($i == 2) {
              if (strpos($table, 'usermeta') !== false) {
                $this->performSearchReplace($table, $from, $to, $start, $end, ['meta_key']);
              }
            } else {
              $this->performSearchReplace($table, $from, $to, $start, $end);
            }

            // Handle batch logs
            if ($this->siteConfig['currentTableTotalUpdates'] == 0 && $this->siteConfig['totalSearchReplacePages'] == 0) {
              $this->log(sprintf($variantsEmpty[$i], $table), 'INFO');
            } else {
              $st = $this->siteConfig['currentSearchReplacePage'];
              $en = $this->siteConfig['totalSearchReplacePages'];
              $this->log(sprintf($variants[$i], $st, $en, $this->siteConfig['currentTableTotalUpdates']));
              $this->siteConfig['currentTableTotalUpdates'] = 0;
            }

            // Done of that table
            if ($this->siteConfig['currentSearchReplacePage'] >= $this->siteConfig['totalSearchReplacePages']) {
              if ($i >= sizeof($replaces) - 1) {
                unset($this->siteConfig['finishedTables'][0]);
                $this->siteConfig['finishedTables'] = array_values($this->siteConfig['finishedTables']);
                $this->log(sprintf(__('Search replace for table %s finished', 'backup-backup'), $table), 'SUCCESS');
                $this->siteConfig['currentSearchReplaceItem'] = 0;

                $processPercentage = intval((($this->siteConfig['amountOfTables'] - sizeof($this->siteConfig['finishedTables'])) / $this->siteConfig['amountOfTables']) * 20);
                $this->progress(40 + $processPercentage);
              }

              $this->siteConfig['currentTableTotalUpdates'] = 0;
              $this->siteConfig['currentSearchReplacePage'] = 0;
              $this->siteConfig['totalSearchReplacePages'] = 0;
              $tableCompleted = true;
            }

          }

          if (!$tableCompleted) break;

        }

        if ($tableCompleted && sizeof($this->siteConfig['finishedTables']) == 0) $this->setContinuation(3, 3); // If path replacement finished
        else $this->setContinuation(3, 2); // If path raplecement didnt finish, continue next batch (time exceed 5 seconds)

      }

      if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 3) $this->setContinuation(4);

    }

    // Step: 4 (summary of S&R and file duplication)
    private function databaseFinishedCopyFiles() {

      $totalBatchExecution = 5; // 5 seconds
      $milestoneUpdate = 500; // per 500 files/directories
      $batchLimit = $milestoneUpdate + 1; // Limit of files/directories per batch

      if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 1) {

        // Remove unused variables
        unset($this->siteConfig['currentSearchReplaceItem']);
        unset($this->siteConfig['currentTableTotalUpdates']);
        unset($this->siteConfig['currentSearchReplacePage']);
        unset($this->siteConfig['totalSearchReplacePages']);

        // Display summary of search replace
        $this->log(__('Displaying summary of search replace process', 'backup-backup'), 'STEP');
        $this->log(sprintf(__('Search replace processed %s tables in total', 'backup-backup'), $this->siteConfig['totalTables']));
        $this->log(sprintf(__('In total it processed %s rows', 'backup-backup'), $this->siteConfig['totalRows']));
        $this->log(sprintf(__('After all it changed only %s columns', 'backup-backup'), $this->siteConfig['totalChanges']));
        $this->log(sprintf(__('Which results in %s changes in total of all cells', 'backup-backup'), $this->siteConfig['totalUpdates']));

        // Remove unused variables
        unset($this->siteConfig['totalTables']);
        unset($this->siteConfig['totalRows']);
        unset($this->siteConfig['totalChanges']);
        unset($this->siteConfig['totalUpdates']);

        // Update user roles in options table
        $this->updateUserRolesInOptions();

        $this->log(__('Search replace of all tables finished successfully', 'backup-backup'), 'SUCCESS');

        // Go to new process without new batch request
        $this->siteConfig['batch'] = 2;

      }

      if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 2) {

        if (!isset($this->siteConfig['dirSeek']) || $this->siteConfig['dirSeek'] == 0) {
          $this->log(__('Duplication of directories to staging site', 'backup-backup'), 'STEP');
          $this->siteConfig['dirSeek'] = 0;
        }

        $pathDirsListFile = BMI_TMP . DIRECTORY_SEPARATOR . '.staging_directories';

        $file = new \SplFileObject($pathDirsListFile);
        $file->seek($file->getSize());

        $startTime = time();
        $currentSeekedElements = 0;
        $totalLines = $file->key() + 1;
        $stagingRoot = trailingslashit($this->siteConfig['root_staging']);
        $file->seek($this->siteConfig['dirSeek']);

        while (!$file->eof()) {

          if ((time() - $startTime) > $totalBatchExecution || $currentSeekedElements > $batchLimit) break;

          $file->seek($this->siteConfig['dirSeek']);
          $path = $stagingRoot . trim($file->current());

          if (!(file_exists($path) && is_dir($path))) @mkdir($path);
          if ($this->siteConfig['dirSeek'] % $milestoneUpdate === 0 && $this->siteConfig['dirSeek'] != 0) {
            $processPercentageTotal = number_format(($this->siteConfig['dirSeek'] / $this->siteConfig['total_directories']) * 100, 2);
            $this->log(sprintf(
              __('Directory creation milestone: %s/%s (%s)', 'backup-backup'),
              $this->siteConfig['dirSeek'],
              $this->siteConfig['total_directories'],
              $processPercentageTotal . '%'
            ));
            $processPercentage = intval(($this->siteConfig['dirSeek'] / $this->siteConfig['total_directories']) * 5);
            $this->progress(60 + $processPercentage);
          }

          $this->siteConfig['dirSeek']++;
          $currentSeekedElements++;

        }

        if ($this->siteConfig['dirSeek'] >= $this->siteConfig['total_directories']) {
          unlink($pathDirsListFile);
          unset($this->siteConfig['dirSeek']);
          $this->log(sprintf(
            __('Directory creation milestone: %s/%s (%s)', 'backup-backup'),
            $this->siteConfig['total_directories'],
            $this->siteConfig['total_directories'],
            '100%'
          ));
          $this->progress(65);
          $this->setContinuation(4, 3);
        } else {
          $this->setContinuation(4, 2);
        }

      }

      if (isset($this->siteConfig['batch']) && $this->siteConfig['batch'] == 3) {

        if (!isset($this->siteConfig['fileSeek']) || $this->siteConfig['fileSeek'] == 0) {
          $this->log(__('Duplication of files to staging site', 'backup-backup'), 'STEP');
          $this->log(__('Duplication of files make take a while', 'backup-backup'));
          $this->log(__('Disclaimer: ZIP Archives are not getting cloned on staging sites', 'backup-backup'), 'WARN');
          $this->siteConfig['fileSeek'] = 0;
        }

        $pathFilesListFile = BMI_TMP . DIRECTORY_SEPARATOR . '.staging_files';

        $file = new \SplFileObject($pathFilesListFile);
        $file->seek($file->getSize());

        $startTime = time();
        $currentSeekedElements = 0;
        $totalLines = $file->key() + 1;
        $sourceRoot = trailingslashit($this->siteConfig['root_source']);
        $stagingRoot = trailingslashit($this->siteConfig['root_staging']);
        $file->seek($this->siteConfig['fileSeek']);
        $disallowedExtensions = ['zip', 'tar', 'gz', 'tmp', 'rar', '7z'];

        while (!$file->eof()) {

          if ((time() - $startTime) > $totalBatchExecution || $currentSeekedElements > $batchLimit) break;

          $file->seek($this->siteConfig['fileSeek']);
          $path = trim($file->current());

          if (file_exists($sourceRoot . $path) && !file_exists($stagingRoot . $path)) {
            $arrayWithExtension = explode('.', $path);
            $ext = strtolower(array_pop($arrayWithExtension));
            if (!in_array($ext, $disallowedExtensions) && strpos($path, 'backup-migration-config.php') === false) {
              @copy($sourceRoot . $path, $stagingRoot . $path);
            }
          }

          if ($this->siteConfig['fileSeek'] % $milestoneUpdate === 0 && $this->siteConfig['fileSeek'] != 0) {
            $processPercentageTotal = number_format(($this->siteConfig['fileSeek'] / $this->siteConfig['total_files']) * 100, 2);
            $this->log(sprintf(
              __('File duplication milestone: %s/%s (%s)', 'backup-backup'),
              $this->siteConfig['fileSeek'],
              $this->siteConfig['total_files'],
              $processPercentageTotal . '%'
            ));
            $processPercentage = intval(($this->siteConfig['fileSeek'] / $this->siteConfig['total_files']) * 25);
            $this->progress(65 + $processPercentage);
          }

          $this->siteConfig['fileSeek']++;
          $currentSeekedElements++;

        }

        if ($this->siteConfig['fileSeek'] >= $this->siteConfig['total_files']) {
          unlink($pathFilesListFile);
          unset($this->siteConfig['fileSeek']);
          $this->log(sprintf(
            __('File duplication milestone: %s/%s (%s)', 'backup-backup'),
            $this->siteConfig['total_files'],
            $this->siteConfig['total_files'],
            '100%'
          ));
          $this->progress(90);
          $this->setContinuation(5);
        } else {
          $this->setContinuation(4, 3);
        }

      }

    }

    // Step: 5 (setup passwordless login script and paste adjusted wp-config)
    private function setupWpConfigAndLoginScript() {

      $this->copyOverPasswordLessScript();

      $this->log(__('Inserting wp-config.php file for staging site', 'backup-backup'), 'STEP');

      $sourceWPConfig = trailingslashit($this->siteConfig['root_source']) . 'wp-config.php';
      $destinationWPConfig = trailingslashit($this->siteConfig['root_staging']) . 'wp-config.php';

      $previousPrefix = $this->siteConfig['source_db_prefix'];
      $destinationPrefix = $this->siteConfig['db_prefix'];

      $previousRoot = untrailingslashit($this->siteConfig['root_source']);
      $destinationRoot = untrailingslashit($this->siteConfig['root_staging']);

      $sourceURL = $this->parseDomain($this->siteConfig['source_home_url']);
      $destinationURL = $this->parseDomain($this->siteConfig['url']);

      $wpconfig = file_get_contents($sourceWPConfig);

      // Table Prefix
      $this->log(__('Replacing table prefix in wp-config.php', 'backup-backup'));
      if (strpos($wpconfig, '"' . $previousPrefix . '";') !== false) {
        $wpconfig = str_replace('"' . $previousPrefix . '";', '"' . $destinationPrefix . '";', $wpconfig);
      } elseif (strpos($wpconfig, "'" . $previousPrefix . "';") !== false) {
        $wpconfig = str_replace("'" . $previousPrefix . "';", "'" . $destinationPrefix . "';", $wpconfig);
      }

      // Paths e.g. for wp_debug_log
      $this->log(__('Adjusting paths in wp-config.php', 'backup-backup'));
      if (strpos($wpconfig, '"' . $previousRoot . '";') !== false) {
        $wpconfig = str_replace('"' . $previousRoot . '";', '"' . $destinationRoot . '";', $wpconfig);
      } elseif (strpos($wpconfig, "'" . $previousRoot . "';") !== false) {
        $wpconfig = str_replace("'" . $previousRoot . "';", "'" . $destinationRoot . "';", $wpconfig);
      }
      
      // Paths e.g. for wp_home & wp_siteurl
      $this->log(__('Adjusting domains in wp-config.php', 'backup-backup'));
      $wpconfig = explode("\n", $wpconfig);
      for ($i = 0; $i < sizeof($wpconfig); ++$i) {
        
        $line = $wpconfig[$i];
        
        if (strpos($line, 'WP_SITEURL') !== false || strpos($line, 'WP_HOME') !== false) {
          $wpconfig[$i] = str_replace($sourceURL, $destinationURL, $line);
        }
        
      }
      
      $wpconfig = implode("\n", $wpconfig);

      file_put_contents($destinationWPConfig, $wpconfig);
      $this->log(__('File adjustments for wp-config.php finished successfully', 'backup-backup'), 'SUCCESS');
      
      $this->setContinuation(6);

    }

    // Step: 6 (cleanup and misc)
    private function performFinish() {
      
      $this->cleanup();
      $this->setContinuation(7);
      
    }

    public function __destruct() {

      parent::__destruct();

    }

  }
