<?php
declare(strict_types=1);

namespace Instinctor\Core;

if (!defined('ABSPATH')) exit; // No direct access.

if ( ! defined( 'ABSPATH' ) && ( ! defined( 'INSTINCTOR_ENV' ) || INSTINCTOR_ENV !== 'standalone' ) ) exit;

/**
 * CartSession — cache-safe cart/checkout state storage.
 *
 * WP context: cookie-keyed transient. Cookie is set lazily on first write
 * (typically POST add-to-cart), so unauthenticated GET pageviews stay
 * cacheable until the visitor actually has cart state.
 *
 * Standalone: $_SESSION (unchanged from prior behavior).
 *
 * Replaces direct $_SESSION['instinctor_*'] access throughout the cart
 * and checkout flow, which forced session_start() on every front-end
 * pageview and broke nginx/Varnish caching.
 *
 * @since 4.2
 */
final class CartSession
{
    private const COOKIE_NAME = 'instinctor_cart_id';
    private const TRANSIENT_PREFIX = 'instinctor_cart_';
    private const TTL = 7 * DAY_IN_SECONDS;

    private static ?array $cache = null;
    private static ?string $cartId = null;

    public static function get(string $key, $default = null)
    {
        if (self::isStandalone()) {
            self::ensurePhpSession();
            // $_SESSION values come from session storage, which is server-
            // controlled, but WP.org review wants every superglobal access
            // sanitized regardless. Scalars get sanitize_text_field; structured
            // values (cart arrays etc.) pass through as-is — their fields are
            // sanitized at the points where they enter the session in set().
            $raw = $_SESSION['instinctor_' . $key] ?? $default; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized on the return line below
            return is_string($raw) ? sanitize_text_field($raw) : $raw;
        }

        $data = self::loadFromTransient();
        return $data[$key] ?? $default;
    }

    public static function set(string $key, $value): void
    {
        if (self::isStandalone()) {
            self::ensurePhpSession();
            $_SESSION['instinctor_' . $key] = $value;
            return;
        }

        $data = self::loadFromTransient();
        $data[$key] = $value;
        self::saveToTransient($data);
    }

    public static function unset(string $key): void
    {
        if (self::isStandalone()) {
            self::ensurePhpSession();
            unset($_SESSION['instinctor_' . $key]);
            return;
        }

        $data = self::loadFromTransient();
        unset($data[$key]);
        self::saveToTransient($data);
    }

    public static function has(string $key): bool
    {
        if (self::isStandalone()) {
            self::ensurePhpSession();
            return isset($_SESSION['instinctor_' . $key]);
        }

        $data = self::loadFromTransient(false);
        return $data !== null && array_key_exists($key, $data);
    }

    private static function isStandalone(): bool
    {
        return Plugin::isStandalone();
    }

    private static function ensurePhpSession(): void
    {
        if (session_status() === PHP_SESSION_NONE && !headers_sent()) {
            @session_start();
        }
    }

    /**
     * @param bool $createIfMissing  If true and no cookie exists, generate
     *                               a new cart ID and (lazy) set the cookie
     *                               on the next write. False is the read path.
     * @return array|null            Cached cart state, or null/[] if none.
     */
    private static function loadFromTransient(bool $createIfMissing = true): ?array
    {
        if (self::$cache !== null) return self::$cache;

        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized below
        $cookieId = isset($_COOKIE[self::COOKIE_NAME]) ? sanitize_key( wp_unslash( $_COOKIE[self::COOKIE_NAME] ) ) : '';
        if ($cookieId !== '' && preg_match('/^[a-f0-9]{32}$/', $cookieId)) {
            self::$cartId = $cookieId;
            $data = get_transient(self::TRANSIENT_PREFIX . $cookieId);
            self::$cache = is_array($data) ? $data : [];
            return self::$cache;
        }

        if (!$createIfMissing) return null;

        // No cookie yet — return empty state. The cookie is created lazily
        // when something is actually written, not on read.
        self::$cache = [];
        return self::$cache;
    }

    private static function saveToTransient(array $data): void
    {
        if (self::$cartId === null) {
            self::$cartId = bin2hex(random_bytes(16));
            self::sendCookie(self::$cartId);
        }
        self::$cache = $data;
        set_transient(self::TRANSIENT_PREFIX . self::$cartId, $data, self::TTL);
    }

    private static function sendCookie(string $id): void
    {
        if (headers_sent()) return;

        $secure = is_ssl();
        setcookie(self::COOKIE_NAME, $id, [
            'expires'  => time() + self::TTL,
            'path'     => COOKIEPATH ? COOKIEPATH : '/',
            'domain'   => COOKIE_DOMAIN ? COOKIE_DOMAIN : '',
            'secure'   => $secure,
            'httponly' => true,
            'samesite' => 'Lax',
        ]);
        $_COOKIE[self::COOKIE_NAME] = $id;
    }

    /**
     * Reset in-memory cache. For tests / multi-request long-running processes.
     */
    public static function reset(): void
    {
        self::$cache = null;
        self::$cartId = null;
    }
}
