<?php

/**
 * Copyright 2024-2025 Simply.IN Sp. z o.o.
 *
 * NOTICE OF LICENSE
 *
 * Licensed under the EUPL-1.2 or later.
 * You may not use this work except in compliance with the Licence.
 *
 * Copy of the Licence is available at:
 * https://joinup.ec.europa.eu/software/page/eupl
 * It is bundled with this package in the file LICENSE.txt
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the Licence is distributed on an as is basis,
 * without warranties or conditions of any kind, either express or implied.
 * Check the Licence for the specific language governing permissions
 * and limitations under the License.
 *
 * @author   Simply.IN Sp. z o.o.
 * @copyright 2024-2025 Simply.IN Sp. z o.o.
 * @license   https://joinup.ec.europa.eu/software/page/eupl
 */

declare(strict_types=1);

namespace SimplyIn\Webhook;

use Configuration;
use PrestaShopLogger;
use SimplyIn\Auth\SecretCipher;

if (!defined('_PS_VERSION_')) {
    exit;
}

/**
 * Persistence for the inbound-webhook HMAC signing secret (`paymentWebhookSigningSecret` from the
 * plugin-sync response). Stored encrypted at rest via {@see SecretCipher}.
 *
 * Rotation model (mirrors the Woo/Magento receivers): a present, non-empty value is saved; a present
 * but empty value clears the secret (rotate-off); an absent value (null) leaves the current secret
 * untouched.
 */
final class WebhookSecretStore
{
    public const CONFIG_KEY = 'SIMPLYIN_WEBHOOK_HMAC_SECRET';

    public function __construct(
        private readonly SecretCipher $cipher,
    ) {
    }

    /**
     * Decrypted signing secret, or '' when none is synchronized yet.
     */
    public function get(): string
    {
        $stored = Configuration::get(self::CONFIG_KEY);
        if ($stored === false || $stored === '') {
            return '';
        }

        return $this->cipher->decrypt((string) $stored);
    }

    public function isSynchronized(): bool
    {
        return $this->get() !== '';
    }

    /**
     * Whether a secret value is stored in configuration at all — regardless of whether it can be
     * decrypted. Lets the caller distinguish "no secret synced yet" (IP-allowlist migration window)
     * from "a secret is configured but undecryptable" (must fail closed, never downgrade).
     */
    public function hasStoredValue(): bool
    {
        $stored = Configuration::get(self::CONFIG_KEY);

        return $stored !== false && $stored !== '';
    }

    /**
     * Apply the rotation model to a value received from plugin-sync.
     *
     * @param string|null $secret null = no change; '' = clear; non-empty = store (encrypted)
     */
    public function persist(?string $secret): void
    {
        if ($secret === null) {
            return;
        }
        if ($secret === '') {
            Configuration::deleteByName(self::CONFIG_KEY);

            return;
        }

        $encrypted = $this->cipher->encrypt($secret);
        if ($encrypted === '') {
            // Encryption failed: keep the existing secret (safer than clearing) but log, since we
            // silently continue with a possibly stale secret and would reject the rotated one.
            if (class_exists('PrestaShopLogger')) {
                PrestaShopLogger::addLog('SimplyIn webhook: HMAC secret encryption failed; secret not updated', 2, null, 'SimplyIn', null, true);
            }

            return;
        }

        Configuration::updateValue(self::CONFIG_KEY, $encrypted);
    }
}
