<?php

namespace Lcmigrator\LcMigrator\SecureAccess;

use Lcmigrator\LcMigrator\Logger\Logger;

/**
 * Secure-access handoff encryption.
 *
 * - encrypt() / decrypt(): RSA-OAEP for the short login token (≤ ~214 bytes).
 * - encryptPayload() / decryptPayload(): AES-256-GCM for larger JSON metadata,
 *   with the AES key wrapped by the same RSA public key (hybrid / envelope).
 *
 * WordPress only holds the public key; the expert backend decrypts off-site.
 * Login receives the plain token via ?lc_sal= after off-site decrypt.
 */
class SecureAccessTokenCipher
{
    /** Hybrid envelope prefix — distinguishes metadata from raw RSA blobs. */
    const HYBRID_PREFIX = 'LCH1:';

    const AES_GCM_NONCE_BYTES = 12;
    const AES_GCM_TAG_BYTES = 16;
    const AES_KEY_BYTES = 32;

    /**
     * Encrypt a raw token (small payload) with RSA-OAEP.
     *
     * @return string|false Base64-encoded ciphertext, or false on failure.
     */
    public function encrypt(string $plaintext)
    {
        if ($plaintext === '') {
            return false;
        }

        $ciphertext = $this->rsaEncryptRaw($plaintext);
        if ($ciphertext === false || $ciphertext === '') {
            return false;
        }

        return base64_encode($ciphertext);
    }

    /**
     * Decrypt a base64 RSA-OAEP ciphertext back to the raw token.
     *
     * @return string|false Plain token, or false on failure.
     */
    public function decrypt(string $encodedPayload)
    {
        if ($encodedPayload === '' || strpos($encodedPayload, self::HYBRID_PREFIX) === 0) {
            return false;
        }

        $ciphertext = base64_decode($encodedPayload, true);
        if ($ciphertext === false || $ciphertext === '') {
            return false;
        }

        return $this->rsaDecryptRaw($ciphertext);
    }

    /**
     * Encrypt an arbitrary-length payload (e.g. JSON metadata) with AES-256-GCM.
     * The random AES key is wrapped with RSA-OAEP using the same key pair.
     *
     * Wire format: LCH1:<base64_rsa_wrapped_key>.<base64_nonce+tag+ciphertext>
     *
     * @return string|false Envelope string, or false on failure.
     */
    public function encryptPayload(string $plaintext)
    {
        if ($plaintext === '' || !function_exists('openssl_encrypt')) {
            return false;
        }

        $aesKey = random_bytes(self::AES_KEY_BYTES);
        $nonce = random_bytes(self::AES_GCM_NONCE_BYTES);
        $tag = '';

        $ciphertext = openssl_encrypt(
            $plaintext,
            'aes-256-gcm',
            $aesKey,
            OPENSSL_RAW_DATA,
            $nonce,
            $tag,
            '',
            self::AES_GCM_TAG_BYTES
        );

        if ($ciphertext === false || !is_string($tag) || strlen($tag) !== self::AES_GCM_TAG_BYTES) {
            Logger::getInstance()->error('SecureAccessTokenCipher: aes-256-gcm encrypt failed');
            return false;
        }

        $wrappedKey = $this->rsaEncryptRaw($aesKey);
        if ($wrappedKey === false || $wrappedKey === '') {
            return false;
        }

        $aesBlob = base64_encode($nonce . $tag . $ciphertext);

        return self::HYBRID_PREFIX . base64_encode($wrappedKey) . '.' . $aesBlob;
    }

    /**
     * Decrypt a hybrid envelope produced by encryptPayload().
     *
     * @return string|false Plain payload, or false on failure.
     */
    public function decryptPayload(string $encodedPayload)
    {
        if ($encodedPayload === '' || strpos($encodedPayload, self::HYBRID_PREFIX) !== 0) {
            return false;
        }

        $remainder = substr($encodedPayload, strlen(self::HYBRID_PREFIX));
        $dot = strpos($remainder, '.');
        if ($dot === false) {
            return false;
        }

        $wrappedKey = base64_decode(substr($remainder, 0, $dot), true);
        $aesBlob = base64_decode(substr($remainder, $dot + 1), true);
        if ($wrappedKey === false || $wrappedKey === '' || $aesBlob === false || $aesBlob === '') {
            return false;
        }

        $minBlob = self::AES_GCM_NONCE_BYTES + self::AES_GCM_TAG_BYTES;
        if (strlen($aesBlob) < $minBlob) {
            return false;
        }

        $aesKey = $this->rsaDecryptRaw($wrappedKey);
        if ($aesKey === false || $aesKey === '') {
            return false;
        }

        $nonce = substr($aesBlob, 0, self::AES_GCM_NONCE_BYTES);
        $tag = substr($aesBlob, self::AES_GCM_NONCE_BYTES, self::AES_GCM_TAG_BYTES);
        $ciphertext = substr($aesBlob, $minBlob);

        $plaintext = openssl_decrypt(
            $ciphertext,
            'aes-256-gcm',
            $aesKey,
            OPENSSL_RAW_DATA,
            $nonce,
            $tag
        );

        if ($plaintext === false || $plaintext === '') {
            return false;
        }

        return $plaintext;
    }

    /**
     * @return string|false Raw RSA ciphertext bytes.
     */
    private function rsaEncryptRaw(string $plaintext)
    {
        if ($plaintext === '' || !function_exists('openssl_public_encrypt')) {
            return false;
        }

        $publicKey = $this->loadPublicKey();
        if ($publicKey === false) {
            Logger::getInstance()->error('SecureAccessTokenCipher: public key unavailable');
            return false;
        }

        $ciphertext = '';
        $ok = openssl_public_encrypt(
            $plaintext,
            $ciphertext,
            $publicKey,
            OPENSSL_PKCS1_OAEP_PADDING
        );

        if ($publicKey !== null) {
            openssl_free_key($publicKey);
        }

        if (!$ok || $ciphertext === '') {
            Logger::getInstance()->error('SecureAccessTokenCipher: openssl_public_encrypt failed');
            return false;
        }

        return $ciphertext;
    }

    /**
     * @return string|false Raw plaintext bytes.
     */
    private function rsaDecryptRaw(string $ciphertext)
    {
        if ($ciphertext === '' || !function_exists('openssl_private_decrypt')) {
            return false;
        }

        $privateKey = $this->loadPrivateKey();
        if ($privateKey === false) {
            Logger::getInstance()->error('SecureAccessTokenCipher: private key unavailable');
            return false;
        }

        $plaintext = '';
        $ok = openssl_private_decrypt(
            $ciphertext,
            $plaintext,
            $privateKey,
            OPENSSL_PKCS1_OAEP_PADDING
        );

        if ($privateKey !== null) {
            openssl_free_key($privateKey);
        }

        if (!$ok || $plaintext === '') {
            return false;
        }

        return $plaintext;
    }

    /**
     * @return resource|false|null
     */
    private function loadPublicKey()
    {
        $pem = $this->resolvePublicKeyPem();
        if ($pem === '') {
            return false;
        }

        return openssl_pkey_get_public($pem);
    }

    /**
     * @return resource|false|null
     */
    private function loadPrivateKey()
    {
        $pem = $this->resolvePrivateKeyPem();
        if ($pem === '') {
            return false;
        }

        return openssl_pkey_get_private($pem);
    }

    private function resolvePublicKeyPem(): string
    {
        if (defined('LC_SECURE_ACCESS_PUBLIC_KEY') && LC_SECURE_ACCESS_PUBLIC_KEY !== '') {
            return (string) LC_SECURE_ACCESS_PUBLIC_KEY;
        }

        $path = defined('LC_SECURE_ACCESS_PUBLIC_KEY_PATH')
            ? LC_SECURE_ACCESS_PUBLIC_KEY_PATH
            : LC_MIGRATOR_PATH . '/src/SecureAccess/keys/secure_access.pub';

        return $this->readKeyFile($path);
    }

    private function resolvePrivateKeyPem(): string
    {
        if (defined('LC_SECURE_ACCESS_PRIVATE_KEY') && LC_SECURE_ACCESS_PRIVATE_KEY !== '') {
            return (string) LC_SECURE_ACCESS_PRIVATE_KEY;
        }

        if (defined('LC_SECURE_ACCESS_PRIVATE_KEY_PATH') && LC_SECURE_ACCESS_PRIVATE_KEY_PATH !== '') {
            return $this->readKeyFile(LC_SECURE_ACCESS_PRIVATE_KEY_PATH);
        }

        $devPath = LC_MIGRATOR_PATH . '/src/SecureAccess/keys/secure_access.pem';
        if (is_readable($devPath)) {
            return $this->readKeyFile($devPath);
        }

        return '';
    }

    private function readKeyFile(string $path): string
    {
        if ($path === '' || !is_readable($path)) {
            return '';
        }

        $contents = file_get_contents($path);
        return is_string($contents) ? $contents : '';
    }
}
