<?php

namespace Sibs\Stargate\Webhooks;

use Sibs\Stargate\Config\SibsConfig;
use Sibs\Stargate\Constants\Logs;
use Sibs\Stargate\Exceptions\SibsException;
use Sibs\Stargate\Traits\SuppressLogTrait;

defined('ABSPATH') || exit;

class SibsWebhookDecryptor
{
    use SuppressLogTrait;

    public const AES_256_GCM = 'AES-256-GCM';

    public const DEBUG = 'debug';
    public const INFO = 'info';
    public const WARNING = 'warning';
    public const ERROR = 'error';

    /**
     * @var string
     */
    private $bodyBase64;

    /**
     * @var string
     */
    private $ivBase64;

    /**
     * @var string
     */
    private $authTagBase64;

    /**
     * @var ?string
     */
    private $secretBase64;

    /**
     * Constructor.
     *
     * @param ?string $secret
     */
    public function __construct(string $body, string $iv, string $authTag, $secret = null)
    {
        $this->bodyBase64 = $body;
        $this->ivBase64 = $iv;
        $this->authTagBase64 = $authTag;
        $this->secretBase64 = $secret;

        $this->setSuppressLog(false);
    }

    /**
     * Execute Decryption.
     *
     * @throws SibsException
     */
    public function execute(): SibsWebhookData
    {
        $this->selfLog(self::INFO, 'Begin to try to decrypt webhook', [
            'source' => Logs::WEBHOOK,
        ]);
        $this->selfLog(self::INFO, 'PHP_VERSION_ID: '.PHP_VERSION_ID, [
            'source' => Logs::WEBHOOK,
        ]);

        $webhookSecretInBase64 = $this->secretBase64 ?? SibsConfig::getWebhookSecret();
        $this->selfLog(self::INFO, 'Webhook secrets in base 64: '.$webhookSecretInBase64, [
            'source' => Logs::WEBHOOK,
        ]);

        $this->selfLog(self::INFO, 'IV (base 64): '.$this->ivBase64, [
            'source' => Logs::WEBHOOK,
        ]);
        $iv = $this->convert_encode($this->ivBase64);
        $this->selfLog(self::INFO, 'IV converted to utf-8', [
            'source' => Logs::WEBHOOK,
        ]);

        $this->selfLog(self::INFO, 'AUTH (base 64): '.$this->authTagBase64, [
            'source' => Logs::WEBHOOK,
        ]);
        $authTag = $this->convert_encode($this->authTagBase64);
        $this->selfLog(self::INFO, 'AUTH converted to utf-8', [
            'source' => Logs::WEBHOOK,
        ]);

        $this->selfLog(self::INFO, 'BODY (base 64): '.$this->bodyBase64, [
            'source' => Logs::WEBHOOK,
        ]);
        $body = $this->convert_encode($this->bodyBase64);
        $this->selfLog(self::INFO, 'BODY converted to utf-8', [
            'source' => Logs::WEBHOOK,
        ]);

        return $this->decrypt($body, $iv, $webhookSecretInBase64, $authTag);
    }

    /**
     * Convert a string from one encode to another.
     *
     * @param mixed $string
     * @param mixed $to
     * @param mixed $from
     *
     * @return array|false|string
     */
    public function convert_encode($string, $to = 'UTF-8', $from = 'BASE64')
    {
        if (ctype_xdigit($string)) {
            return hex2bin($string);
        }

        return mb_convert_encoding($string, $to, $from);
    }

    /**
     * Verify if has Sodium Extension.
     */
    public function hasSodiumExtension(): bool
    {
        return extension_loaded('sodium');
    }

    /**
     * Verify if can use Sodium Extension.
     *
     * @return bool
     */
    public function canUseSodiumExtension()
    {
        return is_callable('sodium_crypto_aead_aes256gcm_is_available')
            && sodium_crypto_aead_aes256gcm_is_available();
    }

    /**
     * Verify if Sodium can use crypto aead_aes256gcm.
     */
    public function canSodiumUseAeadAes256Gcm(): bool
    {
        $this->selfLog(self::DEBUG, 'Sodium Can Use AES-256-GCM Decrypt', [
            'source' => Logs::WEBHOOK,
        ]);

        return $this->validateAndLog(PHP_VERSION_ID >= 70000, '(PHP VERSION >= 7.0)', [
            'source' => Logs::WEBHOOK,
        ])
            && $this->validateAndLog($this->hasSodiumExtension(), 'Has Sodium Extension', [
                'source' => Logs::WEBHOOK,
            ])
            && $this->validateAndLog($this->canUseSodiumExtension(), 'Can Use Sodium Extension', [
                'source' => Logs::WEBHOOK,
            ]);
    }

    /**
     * Verify if has LibSodium Extension.
     */
    public function hasLibSodiumExtension(): bool
    {
        return extension_loaded('libsodium');
    }

    /**
     * Verify if can Use LibSodium Extension.
     */
    public function canUseLibSodiumExtension(): bool
    {
        return is_callable('\Sodium\crypto_aead_aes256gcm_is_available')
            && \Sodium\crypto_aead_aes256gcm_is_available();
    }

    /**
     * Verify if Sodium can use crypto aead_aes256gcm.
     */
    public function canLibSodiumUseAeadAes256Gcm(): bool
    {
        $this->selfLog(self::DEBUG, 'LibSodium Can Use AES-256-GCM Decrypt', [
            'source' => Logs::WEBHOOK,
        ]);

        return $this->validateAndLog(PHP_VERSION_ID >= 50300, '(PHP VERSION >= 5.3)', [
            'source' => Logs::WEBHOOK,
        ])
            && $this->validateAndLog($this->hasLibSodiumExtension(), 'Has LibSodium Extension', [
                'source' => Logs::WEBHOOK,
            ])
            && $this->validateAndLog($this->canUseLibSodiumExtension(), 'Can Use LibSodium Extension', [
                'source' => Logs::WEBHOOK,
            ]);
    }

    /**
     * Verify if has Sodium Compact.
     */
    public function hasSodiumCompact(): bool
    {
        return class_exists('\ParagonIE_Sodium_Compat');
    }

    /**
     * Verify if can use Sodium Compact.
     */
    public function canUseSodiumCompact(): bool
    {
        return is_callable('\ParagonIE_Sodium_Compat::crypto_aead_aes256gcm_is_available')
            && \ParagonIE_Sodium_Compat::crypto_aead_aes256gcm_is_available();
    }

    /**
     * Verify if Sodium Compact can use crypto aead_aes256gcm.
     */
    public function canSodiumCompactUseAeadAes256Gcm(): bool
    {
        $this->selfLog(self::DEBUG, 'Sodium Compact Can Use AES-256-GCM Decrypt', [
            'source' => Logs::WEBHOOK,
        ]);

        return $this->validateAndLog(PHP_VERSION_ID >= 50300, '(PHP VERSION >= 5.3)', [
            'source' => Logs::WEBHOOK,
        ])
            && $this->validateAndLog($this->hasSodiumCompact(), 'Has Sodium Compact', [
                'source' => Logs::WEBHOOK,
            ])
            && $this->validateAndLog($this->canUseSodiumCompact(), 'Can Use Sodium Compact', [
                'source' => Logs::WEBHOOK,
            ]);
    }

    /**
     * Verify if has Open SSL Extension.
     */
    public function hasOpenSSLExtension(): bool
    {
        return extension_loaded('openssl');
    }

    /**
     * Verify if can use Open SSL Extension.
     */
    public function canUseOpenSSLExtension(): bool
    {
        return is_callable('openssl_encrypt')
            && is_callable('openssl_decrypt');
    }

    /**
     * Verify if Sodium Compact can use crypto aead_aes256gcm.
     */
    public function canOpenSSLUseAeadAes256Gcm(): bool
    {
        $this->selfLog(self::DEBUG, 'OpenSSL Can Use AES-256-GCM Decrypt', [
            'source' => Logs::WEBHOOK,
        ]);

        return $this->validateAndLog(PHP_VERSION_ID >= 50300, '(PHP VERSION >= 5.3)', [
            'source' => Logs::WEBHOOK,
        ])
            && $this->validateAndLog($this->hasOpenSSLExtension(), 'Has OpenSSL Extension', [
                'source' => Logs::WEBHOOK,
            ])
            && $this->validateAndLog($this->canUseOpenSSLExtension(), 'Can Use OpenSSL Extension', [
                'source' => Logs::WEBHOOK,
            ]);
    }

    /**
     * Verify if has Sodium or Open SSL.
     */
    public function hasSodiumOrOpenSSL(): bool
    {
        return
            $this->hasSodiumExtension()
            || $this->hasLibSodiumExtension()
            || $this->hasSodiumCompact()
            || $this->hasOpenSSLExtension();
    }

    /**
     * Verify if can Use AES-256-GCM.
     */
    public function canUseAes256Gcm(): bool
    {
        return
            $this->canSodiumUseAeadAes256Gcm()
            || $this->canLibSodiumUseAeadAes256Gcm()
            || $this->canSodiumCompactUseAeadAes256Gcm()
            || $this->canOpenSSLUseAeadAes256Gcm();
    }

    /**
     * Use Decrypt function with AES-256-GCM Algorithm.
     *
     * @throws SibsException
     */
    private function decrypt(string $body, string $iv, string $webhookSecretInBase64, string $authTag): SibsWebhookData
    {
        $encryptedBody = $body.$authTag;

        $this->selfLog(self::INFO, 'Webhook Secret (base 64): '.$webhookSecretInBase64, [
            'source' => Logs::WEBHOOK,
        ]);

        if (empty($webhookSecretInBase64)) {
            throw new SibsException('Webhook Secret is empty', [
                'source' => Logs::WEBHOOK,
            ]);
        }

        $webhookSecret = $this->convert_encode($webhookSecretInBase64);
        $this->selfLog(self::INFO, 'Converted Webhook Secret to utf-8', [
            'source' => Logs::WEBHOOK,
        ]);

        try {
            /*
                To verify if it decoded correctly, we first verify:
                - if the $result is false, since sodium returns false when cannot decrypt
                - and if json_decode($result) === null, because if, for some reason,
                sodium thinks that decrypted correctly, $result must be a valid json

                !($result === false && json_decode($result) === null)
                                the same as
                $result !== false || json_decode($result) !== null
            */

            // Try to decrypt with sodium
            $result = $this->sodiumDecrypt($encryptedBody, $iv, $webhookSecret);

            if (false !== $result && null !== json_decode($result)) {
                return new SibsWebhookData($result);
            }

            // Try to decrypt with libsodium
            $result = $this->libSodiumDecrypt($encryptedBody, $iv, $webhookSecret);

            if (false !== $result && null !== json_decode($result)) {
                return new SibsWebhookData($result);
            }

            // Try to decrypt with sodium compact
            $result = $this->sodiumCompactDecrypt($encryptedBody, $iv, $webhookSecret);

            if (false !== $result && null !== json_decode($result)) {
                return new SibsWebhookData($result);
            }

            // Try to decrypt with open ssl
            $result = $this->openSSLDecrypt($body, $iv, $authTag, $webhookSecret);

            if (false !== $result && null !== json_decode($result)) {
                return new SibsWebhookData($result);
            }
        } catch (\SodiumException $e) {
            $this->selfLog(self::ERROR, 'SodiumException: '.$e->getMessage(), [
                'source' => Logs::WEBHOOK,
            ]);
        } catch (SibsException $e) {
            $this->selfLog(self::ERROR, 'SibsException: '.$e->getMessage(), [
                'source' => Logs::WEBHOOK,
            ]);
        }

        throw new SibsException('There was some problem while decrypting the webhook message.', [
            'source' => Logs::WEBHOOK,
        ]);
    }

    /**
     * Decrypt using sodium.
     *
     * @return false|string
     *
     * @throws SibsException
     * @throws \SodiumException
     */
    private function sodiumDecrypt(string $encryptedBody, string $iv, string $webhookSecret)
    {
        if ($this->canSodiumUseAeadAes256Gcm()) {
            $result = sodium_crypto_aead_aes256gcm_decrypt($encryptedBody, '', $iv, $webhookSecret);
            $this->selfLog(self::INFO, 'Decrypt sodium method. Result', [
                'source' => Logs::WEBHOOK,
                'result' => json_decode($result, true),
            ]);

            if (false === $result) {
                throw new SibsException(
                    'The encrypted text was bad formatted with sodium.',
                    [
                        'source' => Logs::WEBHOOK,
                    ]
                );
            }

            return $result;
        }

        return false;
    }

    /**
     * Decrypt using LibSodium.
     *
     * @return false|string
     *
     * @throws SibsException
     * @throws \SodiumException
     */
    private function libSodiumDecrypt(string $encryptedBody, string $iv, string $webhookSecret)
    {
        if ($this->canLibSodiumUseAeadAes256Gcm()) {
            $result = \Sodium\crypto_aead_aes256gcm_decrypt($encryptedBody, '', $iv, $webhookSecret);
            $this->selfLog(self::INFO, 'Decrypt libsodium method. Result', [
                'source' => Logs::WEBHOOK,
                'result' => json_decode($result, true),
            ]);

            if (false === $result) {
                throw new SibsException(
                    'The encrypted text was bad formatted with libsodium.',
                    [
                        'source' => Logs::WEBHOOK,
                    ]
                );
            }

            return $result;
        }

        return false;
    }

    /**
     * Decrypt using Sodium Compact.
     *
     * @return false|string
     *
     * @throws SibsException
     * @throws \SodiumException
     */
    private function sodiumCompactDecrypt(string $encryptedBody, string $iv, string $webhookSecret)
    {
        if ($this->canSodiumCompactUseAeadAes256Gcm()) {
            $result = \ParagonIE_Sodium_Compat::crypto_aead_aes256gcm_decrypt($encryptedBody, '', $iv, $webhookSecret);
            $this->selfLog(self::INFO, 'Decrypt ParagonIE_Sodium_Compat method. Result', [
                'source' => Logs::WEBHOOK,
                'result' => json_decode($result, true),
            ]);

            if (false === $result) {
                throw new SibsException(
                    'The encrypted text was bad formatted with ParagonIE_Sodium_Compat.',
                    [
                        'source' => Logs::WEBHOOK,
                    ]
                );
            }

            return $result;
        }

        return false;
    }

    /**
     * Decrypt using Open SSL.
     *
     * @return false|string
     *
     * @throws SibsException
     */
    private function openSSLDecrypt(string $body, string $iv, string $authTag, string $webhookSecret)
    {
        if ($this->canOpenSSLUseAeadAes256Gcm()) {
            $result = openssl_decrypt($body, self::AES_256_GCM, $webhookSecret, OPENSSL_RAW_DATA, $iv, $authTag);
            $this->selfLog(self::INFO, 'Decrypt Open SSL method. Result', [
                'source' => Logs::WEBHOOK,
                'result' => json_decode($result, true),
            ]);

            if (false === $result) {
                throw new SibsException(
                    'The encrypted text was bad formatted with Open SSL.',
                    [
                        'source' => Logs::WEBHOOK,
                    ]
                );
            }
        }

        return false;
    }
}
