<?php

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

namespace Duplicator\Utils;

use DUP_PRO_Log;
use Duplicator\Utils\Exceptions\ChunkingTimeoutException;
use Exception;
use VendorDuplicator\phpseclib\Crypt\RSA;
use VendorDuplicator\phpseclib\Net\SFTP;

/**
 * SFTP class adapter
 */
class SFTPAdapter
{
    /** @var string */
    protected $server = '';
    /** @var int */
    protected $port = 22;
    /** @var string */
    protected $username = '';
    /** @var string */
    protected $password = '';
    /** @var string */
    protected $privateKey = '';
    /** @var string */
    protected $privateKeyPassword = '';
    /** @var ?IncrementalStatusMessage */
    protected $messages = null;
    /** @var float */
    private $timeLimit = -1.0;
    /** @var float */
    private $timeStart = -1.0;
    /** @var SFTP */
    private $sftp = null;

    /**
     * Class contructor
     *
     * @param string $server             hosting domain or ip address
     * @param int    $port               hosting port
     * @param string $username           hosting username
     * @param string $password           hosting password
     * @param string $privateKey         hosting private key
     * @param string $privateKeyPassword hosting private key password
     */
    public function __construct(
        $server,
        $port = 22,
        $username = '',
        $password = '',
        $privateKey = '',
        $privateKeyPassword = ''
    ) {
        if (strlen($server) == 0) {
            throw new Exception(__('Server name is required to make sftp connection', 'duplicator-pro'));
        }

        if ($port <= 0) {
            throw new Exception(__('Server port is required to make sftp connection', 'duplicator-pro'));
        }

        if (strlen($username) == 0) {
            throw new Exception(__('Username is required to make sftp connection', 'duplicator-pro'));
        }

        if (strlen($password) == 0 && strlen($privateKey) == 0) {
            throw new Exception(__(
                'You should provide either sftp user pasword or the private key to make sftp connection',
                'duplicator-pro'
            ));
        }

        if (strlen($privateKey) > 0 && strlen($privateKeyPassword) == 0) {
            throw new Exception(__(
                'You should provide private key password',
                'duplicator-pro'
            ));
        }

        $this->server             = $server;
        $this->port               = $port;
        $this->username           = $username;
        $this->password           = $password;
        $this->privateKey         = $privateKey;
        $this->privateKeyPassword = $privateKeyPassword;

        $this->sftp = new SFTP($this->server, $this->port);
    }

    /**
     * Class destructor
     *
     * @return void
     */
    public function __destruct()
    {
        $this->disconnect();
    }

    /**
     * Connect to an SFTP Server
     *
     * @return bool true on success, false on failure
     */
    public function connect()
    {
        if ($this->messages === null) {
            $this->messages = new IncrementalStatusMessage();
        }

        $this->disconnect();

        if (!empty($this->privateKey)) {
            $key = $this->getPrivateKey();
        } else {
            $key = null;
        }

        $this->messages->addMessage(sprintf(
            __('Connecting to SFTP server %1$s:%2$d', 'duplicator-pro'),
            $this->server,
            $this->port
        ));
        DUP_PRO_Log::trace("Connect to SFTP server " . $this->server . ':' . $this->port);
        $this->messages->addMessage(sprintf(__('Attempting to login to SFTP server %1$s', 'duplicator-pro'), $this->server));
        DUP_PRO_Log::trace("Attempting to login to SFTP server " . $this->server);

        if (!is_null($key)) {
            $this->messages->addMessage(__('Login to SFTP using private key', 'duplicator-pro'));
            DUP_PRO_Log::trace("Login to SFTP using private key");
            if ($this->sftp->login($this->username, $key)) {
                $this->messages->addMessage(__('Successfully connected to server using private key', 'duplicator-pro'));
                DUP_PRO_Log::infoTrace('Successfully connected to server using private key');
            } else {
                DUP_PRO_Log::trace("Error opening SFTP connection using private key");
                $this->messages->addMessage(__('Error opening SFTP connection using private key', 'duplicator-pro'));
                return false;
            }
        } else {
            DUP_PRO_Log::trace("Login to SFTP");
            if ($this->sftp->login($this->username, $this->password)) {
                $this->messages->addMessage(__('Successfully connected to server using password', 'duplicator-pro'));
                DUP_PRO_Log::infoTrace('Successfully connected to server using password');
            } else {
                DUP_PRO_Log::trace("Error opening SFTP connection using password");
                $this->messages->addMessage(__('Error opening SFTP connection using password', 'duplicator-pro'));
                return false;
            }
        }
        return true;
    }

    /**
     * Disconnect SFTP connection
     *
     * @return void
     */
    protected function disconnect()
    {
        if (!$this->sftp->isConnected()) {
            return;
        }
        DUP_PRO_Log::infoTrace("Disconnect SFTP Connection");
        $this->sftp->disconnect();
    }

    /**
     * Set an SFTP Private Key
     *
     * @return RSA return key object or false
     */
    protected function getPrivateKey()
    {
        if (strlen($this->privateKey) == 0) {
            DUP_PRO_Log::trace("Private key is null");
            throw new Exception('Private key is null');
        }

        $key = new RSA();
        if (!empty($this->privateKeyPassword)) {
            DUP_PRO_Log::trace("Get Private Key Object with Password");
            $key->setPassword($this->privateKeyPassword);
        } else {
            DUP_PRO_Log::trace("Get Private Key Object");
        }
        $key->loadKey($this->privateKey);
        DUP_PRO_Log::trace("Private Key Loaded");
        return $key;
    }

    /**
     * Checks whether a file or directory exists
     *
     * @param string $path file path
     *
     * @return bool
     */
    public function fileExists($path)
    {
        return $this->sftp->file_exists($path);
    }

    /**
     * Gets file size
     *
     * @param string $path file path
     *
     * @return mixed
     */
    public function filesize($path)
    {
        return $this->sftp->filesize($path);
    }

    /**
     * Deletes a file on the SFTP server.
     *
     * @param string $path      file path
     * @param bool   $recursive true delete recurively
     *
     * @return bool
     */
    public function delete($path, $recursive = true)
    {
        return $this->sftp->delete($path, $recursive);
    }

    /**
     * Returns a list of files in the given directory
     *
     * @param string $dir       dir to stan
     * @param bool   $recursive if true scan recurively
     *
     * @return mixed
     */
    public function filesList($dir = '.', $recursive = false)
    {
        return $this->sftp->nlist($dir, $recursive);
    }

    /**
     * Uploads a file to the SFTP server.
     *
     * @param string          $remote_file remote file name
     * @param string|resource $data        local file data
     * @param int             $offset      offset from which data transfer will continue (not used if -1)
     *
     * @return bool
     */
    public function put($remote_file, $data, $offset = -1)
    {
        return $this->sftp->put(
            $remote_file,
            $data,
            SFTP::SOURCE_LOCAL_FILE | SFTP::RESUME,
            $offset,
            $offset,
            array(
                $this,
                'uploadProgress',
            )
        );
    }

    /**
     * Create directory recursively
     *
     * @param string $storagePath storage directory path
     *
     * @return string return the directory path
     */
    public function mkDirRecursive($storagePath = '')
    {
        if (strlen($storagePath) == 0) {
            throw new Exception('Storage Folder is null.');
        }

        if (!$this->sftp->isConnected()) {
            throw new Exception('You must connect to SFTP before making a directory.');
        }

        $storageFolders = explode("/", $storagePath);
        $path           = '';
        foreach ($storageFolders as $dir) {
            $path = $path . '/' . $dir;
            if (!$this->sftp->file_exists($path)) {
                if (!$this->sftp->mkdir($path)) {
                    $errorMessage = 'Directory not created ' . $path . '. Make sure you have write permissions on your SFTP server.';
                    throw new Exception($errorMessage);
                }
            }
        }
        DUP_PRO_Log::trace("Directory $storagePath is created via SFTP");
        return $storagePath;
    }

    /**
     * Method that should be used to start the chunking count before the files get sent
     *
     * @param float $timeLimit Time in seeconds to limit the chunked upload to
     *
     * @return void
     */
    public function startChunkingTimer($timeLimit = -1)
    {
        $this->timeStart = microtime(true);
        $this->timeLimit = $timeLimit;
    }

    /**
     * Call back that can be used for phpseclib to exit the process during chunking
     *
     * @param float $sent size of the sent chunf of the file
     *
     * @return bool
     */
    public function uploadProgress($sent)
    {
        if ($this->timeLimit > -1) {
            if (microtime(true) - $this->timeStart >= $this->timeLimit) {
                throw new ChunkingTimeoutException("Time Limit Was Reached");
            }
        }
        return true;
    }

    /**
     * @return callable
     */
    public function getCallProgressCallback()
    {
        return array(
            $this,
            'uploadProgress',
        );
    }

    /**
     * Set incremental messages manager
     *
     * @param IncrementalStatusMessage $messages messages
     *
     * @return void
     */
    public function setMessages(IncrementalStatusMessage $messages)
    {
        $this->messages = $messages;
    }
}
