<?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\Session;

use Cart;
use Cookie;
use Db;
use Throwable;
use Validate;

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

final class PrestashopSessionAdapter implements SimplyInSessionInterface
{
    private const CART_COLUMN = 'checkout_session_data';
    private const CART_NAMESPACE = 'simplyin';
    private const COOKIE_PREFIX = 'simplyin_';

    /** @var array<string, mixed> */
    private array $transientFlags = [];

    /** @var null|array<string, mixed> */
    private ?array $cartStateCache = null;

    public function __construct(
        private readonly Cookie $cookie,
        private readonly ?Cart $cart,
    ) {
        $this->purgeLegacyCookieKeys();
    }

    private function purgeLegacyCookieKeys(): void
    {
        $changed = false;
        foreach (SessionKey::cases() as $case) {
            if ($case->backend() === SessionBackend::COOKIE) {
                continue;
            }
            $cookieKey = $this->cookieKey($case->value);
            if (isset($this->cookie->$cookieKey)) {
                unset($this->cookie->$cookieKey);
                $changed = true;
            }
        }
        if (!$changed) {
            return;
        }

        try {
            $this->cookie->write();
        } catch (Throwable $e) {
            // Purging legacy keys is best-effort and must not break the request, but a swallowed
            // failure here is invisible — surface it.
            if (class_exists('PrestaShopLogger')) {
                \PrestaShopLogger::addLog('SimplyIn session: legacy cookie purge write failed: ' . $e->getMessage(), 2, null, 'SimplyIn', null, true);
            }
        }
    }

    public function get(string $key, mixed $default = null): mixed
    {
        $backend = $this->resolveBackend($key);

        $value = match ($backend) {
            SessionBackend::COOKIE => $this->readFromCookie($key),
            SessionBackend::CART_CHECKOUT_SESSION_DATA => $this->readFromCart($key),
            SessionBackend::PHP_SESSION => $this->readFromSession($key),
        };

        if ($value !== null) {
            return $value;
        }

        if (array_key_exists($key, $this->transientFlags)) {
            return $this->transientFlags[$key];
        }

        return $default;
    }

    public function set(string $key, mixed $value): void
    {
        $backend = $this->resolveBackend($key);

        match ($backend) {
            SessionBackend::COOKIE => $this->writeToCookie($key, $value),
            SessionBackend::CART_CHECKOUT_SESSION_DATA => $this->writeToCart($key, $value),
            SessionBackend::PHP_SESSION => $this->writeToSession($key, $value),
        };
    }

    public function unset(array|string $key): void
    {
        if (is_array($key)) {
            foreach ($key as $single) {
                $this->unset($single);
            }

            return;
        }

        $backend = $this->resolveBackend($key);
        match ($backend) {
            SessionBackend::COOKIE => $this->removeFromCookie($key),
            SessionBackend::CART_CHECKOUT_SESSION_DATA => $this->removeFromCart($key),
            SessionBackend::PHP_SESSION => $this->removeFromSession($key),
        };
        unset($this->transientFlags[$key]);
    }

    public function clear(): void
    {
        foreach (SessionKey::cases() as $case) {
            $this->unset($case->value);
        }
        $this->transientFlags = [];

        $this->ensureSessionStarted();
        unset($_SESSION['simplyin']);

        if ($this->cart instanceof Cart && Validate::isLoadedObject($this->cart)) {

            $this->persistCartState([]);
        }
    }

    public function clearCheckout(): void
    {
        $preserved = [
            SessionKey::AUTH_TOKEN->value,
            SessionKey::EMAIL->value,
        ];

        foreach (SessionKey::cases() as $case) {
            if (!in_array($case->value, $preserved, true)) {
                $this->unset($case->value);
            }
        }
        $this->transientFlags = [];
    }

    private function resolveBackend(string $key): SessionBackend
    {
        $enum = SessionKey::tryFrom($key);

        return $enum?->backend() ?? SessionBackend::COOKIE;
    }

    private function cookieKey(string $key): string
    {
        return self::COOKIE_PREFIX . $key;
    }

    private function readFromCookie(string $key): mixed
    {
        $cookieKey = $this->cookieKey($key);
        if (!isset($this->cookie->$cookieKey) || $this->cookie->$cookieKey === '') {
            return null;
        }

        return $this->cookie->$cookieKey;
    }

    private function writeToCookie(string $key, mixed $value): void
    {
        $cookieKey = $this->cookieKey($key);
        $this->cookie->$cookieKey = is_scalar($value) || $value === null
            ? $value
            : json_encode($value);
        $this->cookie->write();
    }

    private function removeFromCookie(string $key): void
    {
        $cookieKey = $this->cookieKey($key);
        if (isset($this->cookie->$cookieKey)) {
            unset($this->cookie->$cookieKey);
            $this->cookie->write();
        }
    }

    private function ensureSessionStarted(): void
    {
        static $failureLogged = false;

        if (session_status() !== PHP_SESSION_NONE || headers_sent()) {
            return;
        }

        // The warning stays suppressed (a raw E_WARNING would corrupt JSON/ajax output in dev
        // mode), but the result is checked: callers write to $_SESSION assuming it works, so a
        // failed start silently loses the whole checkout state. Log it once per request.
        $started = @session_start();

        if (!$started && !$failureLogged) {
            $failureLogged = true;
            if (class_exists('PrestaShopLogger')) {
                \PrestaShopLogger::addLog('SimplyIn session: session_start() failed; session-backed checkout state will not persist', 3, null, 'SimplyIn', null, true);
            }
        }
    }

    private function readFromSession(string $key): mixed
    {
        $this->ensureSessionStarted();

        return $_SESSION['simplyin'][$key] ?? null;
    }

    private function writeToSession(string $key, mixed $value): void
    {
        $this->ensureSessionStarted();
        if (!isset($_SESSION['simplyin']) || !is_array($_SESSION['simplyin'])) {
            $_SESSION['simplyin'] = [];
        }
        $_SESSION['simplyin'][$key] = $value;
    }

    private function removeFromSession(string $key): void
    {
        $this->ensureSessionStarted();
        unset($_SESSION['simplyin'][$key]);
        if (empty($_SESSION['simplyin'])) {
            unset($_SESSION['simplyin']);
        }
    }

    private function readFromCart(string $key): mixed
    {
        $state = $this->loadCartState();

        return $state[$key] ?? null;
    }

    private function writeToCart(string $key, mixed $value): void
    {
        if (!$this->cart instanceof Cart || !Validate::isLoadedObject($this->cart)) {
            $this->transientFlags[$key] = $value;

            return;
        }
        $state = $this->loadCartState();
        $state[$key] = $value;
        $this->persistCartState($state);
    }

    private function removeFromCart(string $key): void
    {
        if (!$this->cart instanceof Cart || !Validate::isLoadedObject($this->cart)) {
            unset($this->transientFlags[$key]);

            return;
        }
        $state = $this->loadCartState();
        if (!array_key_exists($key, $state)) {
            return;
        }
        unset($state[$key]);
        $this->persistCartState($state);
    }

    /**
     * @return array<string, mixed>
     */
    private function loadCartState(): array
    {
        if ($this->cartStateCache !== null) {
            return $this->cartStateCache;
        }
        if (!$this->cart instanceof Cart || !Validate::isLoadedObject($this->cart)) {
            $this->cartStateCache = [];

            return [];
        }

        $raw = Db::getInstance()->getValue(
            'SELECT `' . self::CART_COLUMN . '` FROM `' . _DB_PREFIX_ . 'cart` WHERE `id_cart` = ' . (int) $this->cart->id,
        );
        if (!is_string($raw) || $raw === '') {
            $this->cartStateCache = [];

            return [];
        }
        $decoded = json_decode($raw, true);
        $this->cartStateCache = (is_array($decoded) && isset($decoded[self::CART_NAMESPACE]) && is_array($decoded[self::CART_NAMESPACE]))
            ? $decoded[self::CART_NAMESPACE]
            : [];

        return $this->cartStateCache;
    }

    /**
     * @param array<string, mixed> $state
     */
    private function persistCartState(array $state): void
    {
        if (!$this->cart instanceof Cart || !Validate::isLoadedObject($this->cart)) {
            return;
        }

        $raw = Db::getInstance()->getValue(
            'SELECT `' . self::CART_COLUMN . '` FROM `' . _DB_PREFIX_ . 'cart` WHERE `id_cart` = ' . (int) $this->cart->id,
        );
        $decoded = is_string($raw) && $raw !== '' ? json_decode($raw, true) : [];
        $full = is_array($decoded) ? $decoded : [];

        if ($state === []) {
            unset($full[self::CART_NAMESPACE]);
        } else {
            $full[self::CART_NAMESPACE] = $state;
        }

        $encoded = $full === [] ? '' : json_encode($full, JSON_UNESCAPED_SLASHES);
        Db::getInstance()->execute(
            'UPDATE `' . _DB_PREFIX_ . 'cart` SET `' . self::CART_COLUMN . '` = "' . pSQL($encoded) . '" WHERE `id_cart` = ' . (int) $this->cart->id,
        );
        $this->cartStateCache = $state;
    }
}
