<?php

namespace PortalPeople\netsuite\models;

use Yii;

/**
 * Decodes inbound payloads from the NetSuite PP_Order_Change SuiteScript.
 *
 * Supports all payload formats produced by prepareOutboundPayload():
 *   - json               : raw JSON body (no envelope)
 *   - json+gzip          : envelope, data = base64(gzip(json))
 *   - json+aes-cbc       : envelope, data = base64(AES-CBC(json))
 *   - json+gzip+aes-cbc  : envelope, data = base64(AES-CBC(base64(gzip(json))))
 *
 * The decryption key must match the API Secret configured in NetSuite and is
 * read from Yii::$app->params['netsuite']['decryption_key'].
 */
class NetsuiteDecoder extends \yii\base\BaseObject
{
    /**
     * Decode a raw inbound HTTP body from NetSuite.
     *
     * @param string $rawBody Raw request body.
     * @return array|null Decoded payload data, or null on failure.
     */
    public function decode(string $rawBody): ?array
    {
        if (empty($rawBody)) {
            Yii::error('NetsuiteDecoder: empty request body', __CLASS__);
            return null;
        }

        $envelope = json_decode($rawBody, true);

        // No envelope (plain JSON format) or unrecognised structure
        if (!is_array($envelope) || empty($envelope['format']) || $envelope['format'] === 'json') {
            $result = json_decode($rawBody, true);
            if ($result === null) {
                Yii::error('NetsuiteDecoder: failed to JSON-decode plain body', __CLASS__);
            }
            return $result;
        }

        $data = $envelope['data'] ?? null;
        if ($data === null) {
            Yii::error('NetsuiteDecoder: envelope missing data field', __CLASS__);
            return null;
        }

        $formatParts = explode('+', (string) $envelope['format']);
        $isGzip      = in_array('gzip', $formatParts, true);
        $isAes       = in_array('aes-cbc', $formatParts, true);

        // Step 1 – decrypt first (ciphertext is always base64-encoded by NS)
        if ($isAes) {
            $data = $this->decryptAesCbc($data, $envelope['iv'] ?? null);
            if ($data === null) {
                return null;
            }
        }

        // Step 2 – decompress (compressed data is always base64(gzip) from NS)
        if ($isGzip) {
            $data = $this->decompressGzip($data);
            if ($data === null) {
                return null;
            }
        }

        $result = json_decode($data, true);
        if ($result === null) {
            Yii::error('NetsuiteDecoder: failed to JSON-decode transformed payload', __CLASS__);
        }
        return $result;
    }

    /**
     * Decrypt an AES-CBC ciphertext produced by NetSuite N/crypto.
     *
     * NetSuite outputs the ciphertext and IV both as BASE_64 strings.
     * When IV is null (fallback path in SuiteScript) a zero-byte IV is used.
     *
     * Key length determines algorithm: 16 → AES-128, 24 → AES-192, 32 → AES-256.
     *
     * @param string      $ciphertextBase64 Base64-encoded ciphertext.
     * @param string|null $ivBase64         Base64-encoded IV, or null.
     * @return string|null Decrypted plaintext, or null on failure.
     */
    protected function decryptAesCbc(string $ciphertextBase64, ?string $ivBase64): ?string
    {
        $key = $this->resolveDecryptionKey();
        if (empty($key)) {
            Yii::error('NetsuiteDecoder: netsuite.decryption_key is not configured', __CLASS__);
            return null;
        }

        $keyLen = strlen($key);
        if ($keyLen <= 16) {
            $algo     = 'AES-128-CBC';
            $keyBytes = str_pad($key, 16, "\0");
        } elseif ($keyLen <= 24) {
            $algo     = 'AES-192-CBC';
            $keyBytes = str_pad($key, 24, "\0");
        } else {
            $algo     = 'AES-256-CBC';
            $keyBytes = substr($key, 0, 32);
        }

        $iv         = $ivBase64 ? base64_decode($ivBase64) : str_repeat("\0", 16);
        $ciphertext = base64_decode($ciphertextBase64);

        $result = openssl_decrypt($ciphertext, $algo, $keyBytes, OPENSSL_RAW_DATA, $iv);

        if ($result === false) {
            Yii::error('NetsuiteDecoder: AES decryption failed — verify decryption_key and IV match', __CLASS__);
            return null;
        }

        return $result;
    }

    /**
     * Resolve decryption key from config and transparently decrypt it when
     * stored in the standard 'encrypted::' format.
     *
     * @return string
     */
    protected function resolveDecryptionKey(): string
    {
        $key = (string) (Yii::$app->params['netsuite']['decryption_key'] ?? '');
        if ($key === '' || strpos($key, 'encrypted::') !== 0) {
            return $key;
        }

        try {
            $ciphertext = base64_decode(substr($key, 11));
            $encryptionKey = Yii::$app->params['settings']['seckey'] ?? 'default-encryption-key';
            $decrypted = Yii::$app->security->decryptByPassword($ciphertext, $encryptionKey);
            return is_string($decrypted) ? $decrypted : '';
        } catch (\Throwable $e) {
            Yii::error('NetsuiteDecoder: failed to decrypt netsuite.decryption_key: ' . $e->getMessage(), __CLASS__);
            return '';
        }
    }

    /**
     * Decompress gzip data produced by NetSuite N/compress.
     *
     * NetSuite's file.File.getContents() returns base64-encoded binary for
     * compressed files, so we base64_decode before gzdecode. A fallback
     * without base64_decode is attempted if the first pass fails.
     *
     * @param string $base64Data Base64-encoded gzip binary (or raw gzip as fallback).
     * @return string|null Decompressed string, or null on failure.
     */
    protected function decompressGzip(string $base64Data): ?string
    {
        $raw    = base64_decode($base64Data);
        $result = @gzdecode($raw);

        if ($result === false) {
            // Fallback: attempt without base64 decode in case binary was not encoded
            $result = @gzdecode($base64Data);
        }

        if ($result === false) {
            Yii::error('NetsuiteDecoder: gzip decompression failed', __CLASS__);
            return null;
        }

        return $result;
    }
}
