<?php

namespace Ignite\Core\Files\Encryption;

use Ignite\Core\Files\File;
use Illuminate\Contracts\Filesystem\FileNotFoundException;

/**
 * This uses the gpg cli application. It would be nice to use PHP's library for this:
 * https://www.php.net/manual/en/book.gnupg.php. Something we could look into in the future.
 */
class PGPFileEncrypter extends BaseFileEncrypter
{
    /**
     * {@inheritdoc}
     */
    public function encrypt(File $file, ?File $saveTo = null): File
    {
        $this->assertExists($file);
        $localFile = $this->localizeFile($file);
        $localSaveTo = $this->localizeFile($saveTo ?? $this->makeTemporaryFile());
        if ($saveTo && $file->is($saveTo)) {
            // Overwriting the original file. First save encrypted version to a temporary place.
            $localSaveTo = $this->makeTemporaryFile();
        }

        // --pinentry-mode loopback in order to prevent the GUI popup asking for the pin. This was recommended
        //   somewhere when running this in a non-interactive way. Not sure if it's necessary or if there's a better
        //   setting. More here: https://www.gnupg.org/documentation/manuals/gpgme/Pinentry-Mode.html
        // --batch This turns off interactive mode, I guess. See: https://www.gnupg.org/documentation/manpage.html
        // --yes 'Assume "yes" on most questions.'
        // --quiet 'Try to be as quiet as possible.' Experimentally turning this off when debug is on right now.
        $command = "cat {$localFile->path()} | gpg --pinentry-mode loopback --batch --yes --encrypt ".
            (config('app.debug') ? '' : '--quiet ').
            "--recipient=".$this->getConfig('encryption.recipient')." > {$localSaveTo->path()}";

        passthru($command);

        if ($saveTo && ! $localSaveTo->is($saveTo)) {
            // We needed to work with a temporary file. Let's copy the result from the temporary file to the requested
            // save-to file.

            if ($file->is($saveTo)) {
                // If asked to overwrite the original file, we'll delete the file and copy the contents from the temporary
                // encrypted file. Otherwise, if save-to exists, we'll throw an exception instead of overwriting.
                $saveTo->delete();
            }

            $localSaveTo->copyToFile($saveTo);
        }

        return $saveTo ?? $localSaveTo;
    }

    /**
     * {@inheritdoc}
     */
    public function decrypt(File $file, ?File $saveTo = null): File
    {
        $this->assertExists($file);
        $localFile = $this->localizeFile($file);
        $localSaveTo = $this->localizeFile($saveTo ?? $this->makeTemporaryFile());
        if ($saveTo && $file->is($saveTo)) {
            // Overwriting the original file. First save decrypted version to a temporary place.
            $localSaveTo = $this->makeTemporaryFile();
        }

        // --pinentry-mode loopback in order to prevent the GUI popup asking for the pin. This was recommended
        //   somewhere when running this in a non-interactive way. Not sure if it's necessary or if there's a better
        //   setting. More here: https://www.gnupg.org/documentation/manuals/gpgme/Pinentry-Mode.html
        // --batch This turns off interactive mode, I guess. See: https://www.gnupg.org/documentation/manpage.html
        // --yes 'Assume "yes" on most questions.'
        // --quiet 'Try to be as quiet as possible.' Experimentally turning this off when debug is on right now.
        //
        // We're redirecting error output to null. I tried to capture this via
        // https://www.php.net/manual/en/book.outcontrol.php, but no luck. I think it'd be ideal to be able to capture
        // that and either log or throw an error when we get some error output.Experimentally turning this off when
        // debug is on right now.
        $command = "gpg --passphrase ".$this->getConfig('decryption.passphrase')." --pinentry-mode loopback --batch ".
            (config('app.debug') ? '' : '--quiet ').
            "--yes --decrypt {$localFile->path()} > {$localSaveTo->path()} ".(config('app.debug') ? '' : '2>/dev/null');

        passthru($command);

        if ($saveTo && ! $localSaveTo->is($saveTo)) {
            // We needed to work with a temporary file. Let's copy the result from the temporary file to the
            // requested save-to file.

            if ($file->is($saveTo)) {
                // If asked to overwrite the same file, we'll delete the file and copy the contents from the temporary
                // decrypted file. Otherwise, if save-to exists, we'll throw an exception instead of overwriting.
                $saveTo->delete();
            }

            $localSaveTo->copyToFile($saveTo);
        }

        return $saveTo ?? $localSaveTo;
    }

    /**
     * Copy the file to a local disk if not there already.
     * We want one on the local disk so that the gpg binary can get at it.
     *
     * @param File $file
     * @return void
     * @throws FileNotFoundException
     */
    protected function localizeFile(File $file): File
    {
        if ($this->isOnLocalDisk($file)) {
            return $file;
        }

        $file->copyToFile($temporaryLocalFile = $this->makeTemporaryFile());

        return $temporaryLocalFile;
    }
}
