<?php

declare(strict_types=1);

namespace ComerciaGlobalPayments\AddonPayments\SDK\Request;

use ComerciaGlobalPayments\AddonPayments\SDK\Encryption\Cypher\CypherInterface;
use ComerciaGlobalPayments\AddonPayments\SDK\Encryption\Cypher\WithInitializationVectorInterface;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * Class AbstractEncryptedRequest.
 *
 * Base class for requests with encrypted payload.
 *
 * @SuppressWarnings(PHPMD.LongVariableNames)
 */
abstract class AbstractEncryptedRequest extends AbstractRequest implements EncryptedPayloadInterface
{
    /**
     * @var string
     * @Assert\NotNull()
     */
    protected $merchantId;

    /**
     * @var object
     */
    protected $encryptedPayload;

    public static function excludedProperties(): array
    {
        return array_merge(parent::excludedProperties(), ['encryptedPayload']);
    }

    public function encrypt(CypherInterface $cypher, string $key): EncryptedPayloadInterface
    {
        $rawValues = $this->jsonSerialize();
        $rawValues = $this->encodeArray($rawValues);
        $rawValues = trim($rawValues);
        // Encode values using UTF-8 charset.
        $rawValues = iconv(mb_detect_encoding($rawValues, mb_detect_order(), true), 'UTF-8', $rawValues);
        $integrityCheck = hash('sha256', $rawValues);
        if ($cypher instanceof WithInitializationVectorInterface) {
            /** @var \ComerciaGlobalPayments\AddonPayments\SDK\Encryption\Cypher\IvGeneratorInterface $cypher */
            $initializationVector = $cypher->generateIv();
            $encryptedText = $cypher->encrypt($rawValues, $key, $initializationVector);
            $this->encryptedPayload = [
                'encrypted' => $encryptedText,
                'integrityCheck' => $integrityCheck,
                'merchantId' => $this->merchantId,
            ];

            $encodedVector = base64_encode($initializationVector);
            $this
                ->addHeader('encryptionMode', $cypher->getCanonicalName())
                ->addHeader('iv', $encodedVector);

            return $this;
        }

        /* @var \ComerciaGlobalPayments\AddonPayments\SDK\Encryption\Cypher\WithoutInitializationVectorInterface $cypher */
        $encryptedText = $cypher->encrypt($rawValues, $key);
        $this->encryptedPayload = [
            'encrypted' => $encryptedText,
            'integrityCheck' => $integrityCheck,
            'merchantId' => $this->merchantId,
        ];
        $this->addHeader('encryptionMode', $cypher->getCanonicalName());

        return $this;
    }

    public function getPayload()
    {
        return $this->encryptedPayload;
    }

    public function getMerchantId(): string
    {
        return $this->merchantId;
    }

    public function setMerchantId(string $merchantId): self
    {
        $this->merchantId = $merchantId;

        return $this;
    }

    /**
     * Encodes array data as param1=value1&param2=value2.1,value2.2 etc.
     */
    protected function encodeArray(array $value): string
    {
        $pairGlue = '=';
        $pairSeparator = '&';
        $innerGlue = ':';
        $innerSeparator = ';';
        $multipleValueSeparator = ',';
        // Clean array before encoding.
        $value = array_filter($value, function($v, $k) {
            return ('amount' === $k && 0.0 === $v) || !empty($v);
        }, ARRAY_FILTER_USE_BOTH);

        return implode($pairSeparator, array_map(
            static function ($key, $value) use ($pairGlue, $innerGlue, $innerSeparator, $multipleValueSeparator) {
                if (\is_array($value) && \is_int(key($value))) {
                    $value = implode($multipleValueSeparator, $value);

                    return "${key}${pairGlue}${value}";
                }

                if (\is_array($value)) {
                    $return = [];
                    foreach ($value as $innerKey => $innerValue) {
                        if (\is_array($innerValue)) {
                            $innerValue = implode($multipleValueSeparator, $innerValue);
                        }
                        $return[] = "${innerKey}${innerGlue}${innerValue}";
                    }

                    $value = implode($innerSeparator, $return);
                }

                return "${key}${pairGlue}${value}";
            },
            array_keys($value), array_values($value)));
    }
}
