<?php

  // Namespace
  namespace BMI\Plugin\Zipper;

  // Use
  use BMI\Plugin\Backup_Migration_Plugin as BMP;
  use BMI\Plugin\BMI_Logger as Logger;
  use BMI\Plugin\Progress\BMI_ZipProgress as Progress;
  use BMI\Plugin\BMI_Zip_Explorer as ZipExplorer;
  use BMI\Plugin\Scanner\BMI_BackupsScanner as Backups;

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

  /**
   * BMI_Zipper
   */
  class BMI_Zipper {
    public function makeZIP($files, $output, $name, &$zip_progress, $cron = false) {

      // Verbose
      Logger::log(__("Creating backup ", 'backup-backup'));
      Logger::log(__("Found ", 'backup-backup') . sizeof($files) . __(" files to backup.", 'backup-backup'));

      // Require Universal Zip Library
      require_once BMI_INCLUDES . '/zipper/src/zip.php';

      // Start microtime for ZIP Process
      $start = microtime(true);

      // Logs
      $zip_progress->log(__("Preparing map of files...", 'backup-backup'), 'step');

      // Try to catch error
      try {

        // Create new ZIP
        $zip = new Zip();
        $zip->zip_start($output, $files, $name, $zip_progress, $start);

        // Logs
        $zip_progress->log(__("Files prepared.", 'backup-backup'), 'success');
        $zip_progress->log(__("Starting compression process...", 'backup-backup'), 'info');

        // Close ZIP and Save
        $lala = $zip->zip_end(2, $cron);
        if (!$lala) {
          $zip_progress->log(__("Something went wrong – removing backup files...", 'backup-backup'), 'error');

          return false;
        }

        return $lala;
      } catch (\Throwable $e) {

        // Error print
        $zip_progress->log(__("Reverting backup, removing file...", 'backup-backup'), 'step');
        $zip_progress->log(__("There was an error during backup...", 'backup-backup'), 'error');
        $zip_progress->log($e->getMessage(), 'error');

        return false;
      } catch (\Exception $e) {

        // Error print
        $zip_progress->log(__("Reverting backup, removing file...", 'backup-backup'), 'step');
        $zip_progress->log(__("There was an error during backup...", 'backup-backup'), 'error');
        $zip_progress->log($e->getMessage(), 'error');

        return false;
      }

      return true;
    }

    public function getZipFileContent($zipname, $filename) {
      if (class_exists('ZipArchive') || class_exists('\ZipArchive')) {
        $zip = new \ZipArchive();

        if ($zip->open($zipname) === true) {
          if ($content = $zip->getFromName($filename)) {
            return json_decode($content);
          } else {
            return false;
          }
        } else {
          return false;
        }
      } else {
        if (!class_exists('PclZip')) {
          if (!defined('PCLZIP_TEMPORARY_DIR')) {
            $bmi_tmp_dir = BMI_TMP;
            if (!file_exists($bmi_tmp_dir)) {
              @mkdir($bmi_tmp_dir, 0775, true);
            }
            define('PCLZIP_TEMPORARY_DIR', $bmi_tmp_dir . DIRECTORY_SEPARATOR . 'bmi-');
          }
          if (defined('BMI_PRO_PCLZIP') && file_exists(BMI_PRO_PCLZIP)) {
            require_once BMI_PRO_PCLZIP;
          } else {
            require_once trailingslashit(ABSPATH) . 'wp-admin/includes/class-pclzip.php';
          }
        }
        $lib = new \PclZip($zipname);
        $content = $lib->extract(PCLZIP_OPT_BY_NAME, $filename, PCLZIP_OPT_EXTRACT_AS_STRING);
        if (isset($content[0]) && isset($content[0]['content'])) {
          return json_decode($content[0]['content']);
        } else {
          return false;
        }
      }
    }

    public function getZipContentList($zippath, $savepath) {

      if (class_exists('ZipArchive') || class_exists('\ZipArchive')) {

        $zip = new \ZipArchive();
        $zip->open($zippath);

        if (!isset($zip->numFiles) || $zip->numFiles == 0 || $zip->numFiles === false) {
          $zip->close();
          return false;
        }

        $tmpf = fopen($savepath, 'a+');
        $totalAmount = $zip->numFiles;

        for ($i = 0; $i < $zip->numFiles; ++$i) {

          $stat = $zip->statIndex($i);
          fwrite($tmpf, $stat['name'] . "\n");
          unset($stat);

        }

        fclose($tmpf);
        $zip->close();

        return $totalAmount;

      } else {

        if (!class_exists('PclZip')) {
          if (!defined('PCLZIP_TEMPORARY_DIR')) {
            $bmi_tmp_dir = BMI_TMP;
            if (!file_exists($bmi_tmp_dir)) {
              @mkdir($bmi_tmp_dir, 0775, true);
            }
            define('PCLZIP_TEMPORARY_DIR', $bmi_tmp_dir . DIRECTORY_SEPARATOR . 'bmi-');
          }
          if (defined('BMI_PRO_PCLZIP') && file_exists(BMI_PRO_PCLZIP)) {
            require_once BMI_PRO_PCLZIP;
          } else {
            require_once trailingslashit(ABSPATH) . 'wp-admin/includes/class-pclzip.php';
          }
        }

        $zip = new \PclZip($zippath);
        $list = $zip->listContent();
        if ($list == 0) {
          return false;
        }

        $tmpf = fopen($savepath, 'a+');

        $totalAmount = sizeof($list);
        for ($i = 0; $i < $totalAmount; ++$i) {

          fwrite($tmpf, $list[$i]['filename'] . "\n");

        }

        fclose($tmpf);

        return $totalAmount;

      }

    }

    public function getPartsToRestore($zippath, $savepath) {
      if (!defined('BMI_PRO_INC') || !file_exists(BMI_PRO_INC . '/zip-explorer.php')) {
          return false;
      }
  
      require_once BMI_PRO_INC . '/zip-explorer.php';

      $restorePartsFile = BMI_TMP . DIRECTORY_SEPARATOR . 'restore_parts.json';
      if (!file_exists($restorePartsFile)) {
          return false;
      }
  
      $restorePartsContent = json_decode(file_get_contents($restorePartsFile), true);
      if (!$restorePartsContent || !isset($restorePartsContent['backupName'])) {
          return false;
      }
  
      if ($restorePartsContent['backupName'] !== basename($zippath)) {
          Logger::log('Ignoring restore parts file, because it does not match the backup name.');
          @unlink($restorePartsFile);
          return false;
      }
  
      $zipExplorer = new ZipExplorer($zippath);
      $amount = 0;
      if (isset($restorePartsContent['dirs']) && is_array($restorePartsContent['dirs'])) {
          foreach ($restorePartsContent['dirs'] as $dir) {
              $amount += $zipExplorer->scanDir($dir, $savepath);
          }

      }
  
      if (isset($restorePartsContent['files']) && is_array($restorePartsContent['files'])) {
          $amount += sizeof($restorePartsContent['files']);
          $files = implode("\n", $restorePartsContent['files']);
          file_put_contents($savepath, $files, FILE_APPEND);
      }

      file_put_contents($savepath, "\nbmi_backup_manifest.json", FILE_APPEND);
      
  
      return $amount;
    }

    public function getZipFileContentPlain($zipname, $filename) {
      if (class_exists('ZipArchive')) {
        $zip = new \ZipArchive();

        if ($zip->open($zipname) === true) {
          if ($content = $zip->getFromName($filename)) {
            return $content;
          } else {
            return false;
          }
        } else {
          return false;
        }
      } else {
        if (!class_exists('PclZip')) {
          if (!defined('PCLZIP_TEMPORARY_DIR')) {
            $bmi_tmp_dir = BMI_TMP;
            if (!file_exists($bmi_tmp_dir)) {
              @mkdir($bmi_tmp_dir, 0775, true);
            }
            define('PCLZIP_TEMPORARY_DIR', $bmi_tmp_dir . DIRECTORY_SEPARATOR . 'bmi-');
          }
          if (defined('BMI_PRO_PCLZIP') && file_exists(BMI_PRO_PCLZIP)) {
            require_once BMI_PRO_PCLZIP;
          } else {
            require_once trailingslashit(ABSPATH) . 'wp-admin/includes/class-pclzip.php';
          }
        }
        $lib = new \PclZip($zipname);

        $content = $lib->extract(PCLZIP_OPT_BY_NAME, $filename, PCLZIP_OPT_EXTRACT_AS_STRING);
        if (sizeof($content) > 0) {
          return $content[0]['content'];
        } else {
          return false;
        }
      }
    }

    public function lock_zip($zippath, $unlock = false) {

      // Require Universal Zip Library
      require_once BMI_INCLUDES . '/zipper/src/zip.php';
      require_once BMI_INCLUDES . DIRECTORY_SEPARATOR . 'scanner' . DIRECTORY_SEPARATOR . 'backups.php';

      try {

        // Path to lock file
        $filename = '.lock';

        // Load lib
        if (!class_exists('PclZip')) {
          if (!defined('PCLZIP_TEMPORARY_DIR')) {
            $bmi_tmp_dir = BMI_TMP;
            if (!file_exists($bmi_tmp_dir)) {
              @mkdir($bmi_tmp_dir, 0775, true);
            }
            define('PCLZIP_TEMPORARY_DIR', $bmi_tmp_dir . DIRECTORY_SEPARATOR . 'bmi-');
          }
          if (defined('BMI_PRO_PCLZIP') && file_exists(BMI_PRO_PCLZIP)) {
            require_once BMI_PRO_PCLZIP;
          } else {
            require_once trailingslashit(ABSPATH) . 'wp-admin/includes/class-pclzip.php';
          }
        }
        $lib = new \PclZip($zippath);
        $backups = Backups::getInstance();
        $backups->deleteBackupManifest(basename($zippath));

        // Unlocking case
        if ($unlock) {
          if ($this->is_locked_zip($zippath)) {
            $lib->delete(PCLZIP_OPT_BY_NAME, $filename);
          } else {
            return true;
          }
        } else {
          if (!$this->is_locked_zip($zippath)) {

            // Locking case
            $content = json_encode(['locked' => 'true']);
            $lib->add([[PCLZIP_ATT_FILE_NAME => $filename, PCLZIP_ATT_FILE_CONTENT => $content]]);
          }
        }

        return true;
      } catch (\Exception $e) {
        Logger::error($e);

        return false;
      } catch (\Throwable $e) {
        Logger::error($e);

        return false;
      }
    }

    public function is_locked_zip($zippath) {
      $lock = $this->getZipFileContent($zippath, '.lock');
      if ($lock) {
        if (isset($lock->locked) && $lock->locked == 'true') {
          return true;
        } else {
          return false;
        }
      } else {
        return false;
      }
    }

    /**
     * Checks if a ZIP archive is protected with a password/encrypted.
     *
     * @param string $filepath Absolute path to the ZIP file.
     * @return bool True if protected/encrypted, false otherwise.
     */
    public static function isZipProtected($filepath) {
      if (!file_exists($filepath) || !is_readable($filepath)) {
        return false;
      }

      if (class_exists('ZipArchive')) {
        $zip = new \ZipArchive();
        if ($zip->open($filepath) === true) {
          $isProtected = false;
          
          for ($i = 0; $i < $zip->numFiles; $i++) {
            $stat = $zip->statIndex($i);
            // 'encryption_method' was introduced in PHP 7.2 / libzip 1.2.0
            if (isset($stat['encryption_method']) && $stat['encryption_method'] !== \ZipArchive::EM_NONE) {
                $isProtected = true;
                break;
            }
          }
          $zip->close();
          
          if ($isProtected) {
            return true;
          }
        }
      }
      return false;
    }

    /**
     * Validates a password for a given ZIP file path, safely ignoring empty files.
     *
     * @param string $zipPath The absolute or relative path to the ZIP file.
     * @param string $password The password to test.
     * @return bool True if the password is correct, false otherwise.
     * @throws \Exception If the file cannot be opened or lacks testable files.
     */
    public static function validateZipPassword(string $zipPath, string $password): bool {
      $zip = new \ZipArchive();

      $res = $zip->open($zipPath);
      if ($res !== true) {
        throw new \Exception("Failed to open ZIP file. Error Code: " . $res);
      }

      $zip->setPassword($password);

      if ($zip->locateName('.backup_name') !== false) {
        $stat = $zip->statName('.backup_name');
        if ($stat && isset($stat['encryption_method']) && $stat['encryption_method'] !== \ZipArchive::EM_NONE) {
          $content = $zip->getFromName('.backup_name');
          $zip->close();
          if ($content !== false && trim($content) === basename($zipPath)) {
            return true;
          }
          return false;
        }
      }

      $minFileSize = PHP_INT_MAX;
      $minFileIndex = -1;

      for ($i = 0; $i < $zip->numFiles; $i++) {
        $stat = $zip->statIndex($i);
        
        if ($stat === false || $stat['size'] === 0) {
          continue;
        }

         $isEncrypted = isset($stat['encryption_method']) && $stat['encryption_method'] !== \ZipArchive::EM_NONE;

        if ($isEncrypted && $stat['size'] > 0) {
          if ($stat['size'] < $minFileSize) {
            $minFileSize = $stat['size'];
            $minFileIndex = $i;
            if ($minFileSize < 5 * 1024 * 1024) {
              break;
            }
          }
        }
      }

      if ($minFileIndex !== -1) {
        $content = $zip->getFromIndex($minFileIndex);
        $zip->close();
        if ($content === false || strlen($content) === 0) {
          return false;
        }
        return true;
      }

      $zip->close();
      throw new \Exception("ZIP archive contains no non-empty encrypted files. Cannot validate password.");
    }
  
    public function isFileExists($zippath, $filename) {
      if (class_exists('ZipArchive')) {
        $zip = new \ZipArchive();

        if ($zip->open($zippath) === true) {
          if ($zip->locateName($filename) !== false) {
            return true;
          } else {
            return false;
          }
        } else {
          return false;
        }
      } else {
        if (!class_exists('PclZip')) {
          if (!defined('PCLZIP_TEMPORARY_DIR')) {
            $bmi_tmp_dir = BMI_TMP;
            if (!file_exists($bmi_tmp_dir)) {
              @mkdir($bmi_tmp_dir, 0775, true);
            }
            define('PCLZIP_TEMPORARY_DIR', $bmi_tmp_dir . DIRECTORY_SEPARATOR . 'bmi-');
          }
          if (defined('BMI_PRO_PCLZIP') && file_exists(BMI_PRO_PCLZIP)) {
            require_once BMI_PRO_PCLZIP;
          } else {
            require_once trailingslashit(ABSPATH) . 'wp-admin/includes/class-pclzip.php';
          }
        }
        $lib = new \PclZip($zippath);

        $content = $lib->extract(PCLZIP_OPT_BY_NAME, $filename, PCLZIP_OPT_EXTRACT_AS_STRING);
        if (sizeof($content) > 0) {
          return true;
        } else {
          return false;
        }
      }
    }
  }
