<?php

/**
 *
 * @package   Duplicator
 * @copyright (c) 2022, Snap Creek LLC
 */

namespace Duplicator\Libs\Snap;

/**
 * Original installer files manager
 *
 * This class saves a file or folder in the original files folder and saves the original location persistent.
 * By entry we mean a file or a folder but not the files contained within it.
 * In this way it is possible, for example, to move an entire plugin to restore it later.
 */
class SnapOrigFileManager
{
    const MODE_MOVE             = 'move';
    const MODE_COPY             = 'copy';
    const ORIG_FOLDER_PREFIX    = 'original_files_';
    const PERSISTANCE_FILE_NAME = 'entries_stored.json';

    protected string $persistanceFile;
    protected string $origFilesFolder;
    /** @var array<string, array{baseName:string, source:string, stored: string, mode:string, isRelative: bool}> */
    protected $origFolderEntries = [];
    protected string $rootPath;

    /**
     * Class constructor
     *
     * @param string $root                 WordPress root path
     * @param string $origFolderParentPath orig files folder path
     * @param string $hash                 package hash
     */
    public function __construct($root, $origFolderParentPath, $hash)
    {
        $this->rootPath        = SnapIO::safePathUntrailingslashit($root, true);
        $this->origFilesFolder = $this->generateOrigFolderPath($origFolderParentPath, $hash);
        $this->persistanceFile = $this->origFilesFolder . '/' . self::PERSISTANCE_FILE_NAME;
        $this->load();
    }

    /**
     * Generate orig folder path
     *
     * @param string $origFolderParentPath orig files folder parent path
     * @param string $hash                 package hash
     *
     * @return string
     */
    protected function generateOrigFolderPath($origFolderParentPath, $hash): string
    {
        return SnapIO::safePathTrailingslashit($origFolderParentPath, true) . self::ORIG_FOLDER_PREFIX . $hash;
    }

    /**
     * Create a main folder if don't exist and load the entries
     *
     * @param boolean $reset if strue reset orig file folder
     *
     * @return void
     */
    public function init($reset = false): void
    {
        $this->createMainFolder($reset);
        $this->load();
    }

    /**
     * Create orig file folder
     *
     * @param boolean $reset if true delete current folder
     *
     * @return boolean  return true if succeded
     *
     * @throws \Exception
     */
    protected function createMainFolder($reset = false): bool
    {
        if ($reset) {
            $this->deleteMainFolder();
        }

        if (!file_exists($this->origFilesFolder)) {
            if (!SnapIO::mkdir($this->origFilesFolder, 'u+rwx')) {
                throw new \Exception('Can\'t create the original files folder ' . SnapLog::v2str($this->origFilesFolder));
            }
        }

        $htaccessFile = $this->origFilesFolder . '/.htaccess';
        if (!file_exists($htaccessFile)) {
            $content = <<<HTACCESS
Order Allow,Deny
Deny from All
HTACCESS;
            @file_put_contents($htaccessFile, $content);
        }

        if (!file_exists($this->persistanceFile)) {
            $this->save();
        }

        return true;
    }

    /**
     * @return string Main folder path
     * @throws \Exception
     */
    public function getMainFolder(): string
    {
        if (!file_exists($this->origFilesFolder)) {
            throw new \Exception('Can\'t get the original files folder ' . SnapLog::v2str($this->origFilesFolder));
        }

        return $this->origFilesFolder;
    }

    /**
     * delete origianl files folder
     *
     * @return boolean
     * @throws \Exception
     */
    public function deleteMainFolder(): bool
    {
        if (file_exists($this->origFilesFolder) && !SnapIO::rrmdir($this->origFilesFolder)) {
            throw new \Exception('Can\'t delete the original files folder ' . SnapLog::v2str($this->origFilesFolder));
        }
        $this->origFolderEntries = [];

        return true;
    }

    /**
     * add a entry on original folder.
     *
     * @param string      $identifier entry identifier
     * @param string      $path       entry path. can be a file or a folder
     * @param string      $mode       MODE_MOVE move the item in original folder
     *                                MODE_COPY copy the item in original folder
     * @param bool|string $rename     if rename is a string the item is renamed in original folder.
     *
     * @return boolean true if succeded
     */
    public function addEntry($identifier, $path, $mode = self::MODE_MOVE, $rename = false): bool
    {
        if (!file_exists($path)) {
            return false;
        }

        $baseName = empty($rename) ? basename($path) : $rename;

        if (($relativePath = SnapIO::getRelativePath($path, $this->rootPath)) === false) {
            $isRelative = false;
        } else {
            $isRelative = true;
        }
        $parentFolder = $isRelative ? dirname($relativePath) : SnapIO::removeRootPath(dirname($path));
        if (empty($parentFolder) || $parentFolder === '.') {
            $parentFolder = '';
        } else {
            $parentFolder .= '/';
        }
        $targetFolder = $this->origFilesFolder . '/' . $parentFolder;
        if (!file_exists($targetFolder)) {
            SnapIO::mkdirP($targetFolder);
        }
        $dest = $targetFolder . $baseName;

        switch ($mode) {
            case self::MODE_MOVE:
                // Don't use rename beacause new files must have the current script owner
                if (!SnapIO::rcopy($path, $dest)) {
                    throw new \Exception('Can\'t copy the original file  ' . SnapLog::v2str($path));
                }
                if (!SnapIO::rrmdir($path)) {
                    throw new \Exception('Can\'t remove the original file  ' . SnapLog::v2str($path));
                }
                break;
            case self::MODE_COPY:
                if (!SnapIO::rcopy($path, $dest)) {
                    throw new \Exception('Can\'t copy the original file  ' . SnapLog::v2str($path));
                }
                break;
            default:
                throw new \Exception('invalid mode addEntry');
        }

        $this->origFolderEntries[$identifier] = [
            'baseName'   => $baseName,
            'source'     => $isRelative ? $relativePath : $path,
            'stored'     => $parentFolder . $baseName,
            'mode'       => $mode,
            'isRelative' => $isRelative,
        ];

        $this->save();
        return true;
    }

    /**
     * Get entry info from identifier
     *
     * @param string $identifier orig file identifier
     *
     * @return false|array{baseName:string, source:string, stored: string, mode:string, isRelative: bool} false if entry don't exists
     */
    public function getEntry($identifier)
    {
        if (isset($this->origFolderEntries[$identifier])) {
            return $this->origFolderEntries[$identifier];
        } else {
            return false;
        }
    }

    /**
     * Get entry stored path in original folder
     *
     * @param string $identifier orig file identifier
     *
     * @return boolean|string false if entry don't exists
     */
    public function getEntryStoredPath($identifier)
    {
        if (isset($this->origFolderEntries[$identifier])) {
            return $this->origFilesFolder . '/' . $this->origFolderEntries[$identifier]['stored'];
        } else {
            return false;
        }
    }

    /**
     * Return true if identifier org file is relative path
     *
     * @param string $identifier orig file identifier
     *
     * @return boolean
     */
    public function isRelative($identifier)
    {
        if (isset($this->origFolderEntries[$identifier])) {
            return $this->origFolderEntries[$identifier]['isRelative'];
        } else {
            return false;
        }
    }

    /**
     * Get entry target restore path
     *
     * @param string      $identifier          orig file identifier
     * @param null|string $defaultIfIsAbsolute if isn't null return the value if path is absolute
     *
     * @return false|string false if entry don't exists
     */
    public function getEntryTargetPath($identifier, $defaultIfIsAbsolute = null)
    {
        if (isset($this->origFolderEntries[$identifier])) {
            if ($this->origFolderEntries[$identifier]['isRelative']) {
                return $this->rootPath . '/' . $this->origFolderEntries[$identifier]['source'];
            } else {
                if (is_null($defaultIfIsAbsolute)) {
                    return $this->origFolderEntries[$identifier]['source'];
                } else {
                    return $defaultIfIsAbsolute;
                }
            }
        } else {
            return false;
        }
    }

    /**
     * this function restore current entry in original position.
     * If mode is copy it simply delete the entry else move the entry in original position
     *
     * @param string      $identifier          identified of current entrye
     * @param boolean     $save                update saved entries
     * @param null|string $defaultIfIsAbsolute if isn't null return the value if path is absolute
     *
     * @return boolean true if succeded
     */
    public function restoreEntry($identifier, $save = true, $defaultIfIsAbsolute = null): bool
    {
        if (!isset($this->origFolderEntries[$identifier])) {
            return false;
        }

        $stored = $this->getEntryStoredPath($identifier);
        if (($original = $this->getEntryTargetPath($identifier, $defaultIfIsAbsolute)) === false) {
            return false;
        }

        switch ($this->origFolderEntries[$identifier]['mode']) {
            case self::MODE_MOVE:
                if (!SnapIO::rename($stored, $original)) {
                    throw new \Exception('Can\'t move the original file  ' . SnapLog::v2str($stored));
                }
                break;
            case self::MODE_COPY:
                if (!SnapIO::rrmdir($stored)) {
                    throw new \Exception('Can\'t delete entry ' . SnapLog::v2str($stored));
                }
                break;
            default:
                throw new \Exception('invalid mode addEntry');
        }

        unset($this->origFolderEntries[$identifier]);
        if ($save) {
            $this->save();
        }
        return true;
    }

    /**
     * Put all entries on original position and empty original folder
     *
     * @param string[] $exclude identifiers list t exclude
     *
     * @return boolean
     */
    public function restoreAll($exclude = []): bool
    {
        foreach (array_keys($this->origFolderEntries) as $ident) {
            if (in_array($ident, $exclude)) {
                continue;
            }
            $this->restoreEntry($ident, false);
        }
        $this->save();
        return true;
    }

    /**
     * Save notices from json file
     *
     * @return bool
     */
    public function save(): bool
    {
        if (!file_put_contents($this->persistanceFile, SnapJson::jsonEncodePPrint($this->origFolderEntries))) {
            throw new \Exception('Can\'t write persistence file');
        }
        return true;
    }

    /**
     * Load notice from json file
     *
     * @return bool
     */
    private function load(): bool
    {
        $json = SnapIO::safeFileGetContents($this->persistanceFile);
        if ($json !== false) {
            $this->origFolderEntries = json_decode($json, true) ?: [];
        } else {
            $this->origFolderEntries = [];
        }
        return true;
    }
}
