<?php

declare(strict_types=1);

/*
 * This file is part of the RegexParser package.
 *
 * (c) Younes ENNAJI <younes.ennaji.pro@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace RegexParser\Cache;

use Psr\Cache\CacheItemPoolInterface;
use RegexParser\Node\AlternationNode;
use RegexParser\Node\AnchorNode;
use RegexParser\Node\AssertionNode;
use RegexParser\Node\BackrefNode;
use RegexParser\Node\CalloutNode;
use RegexParser\Node\CharClassNode;
use RegexParser\Node\CharLiteralNode;
use RegexParser\Node\CharTypeNode;
use RegexParser\Node\ClassOperationNode;
use RegexParser\Node\CommentNode;
use RegexParser\Node\ConditionalNode;
use RegexParser\Node\ControlCharNode;
use RegexParser\Node\DefineNode;
use RegexParser\Node\DotNode;
use RegexParser\Node\GroupNode;
use RegexParser\Node\KeepNode;
use RegexParser\Node\LimitMatchNode;
use RegexParser\Node\LiteralNode;
use RegexParser\Node\PcreVerbNode;
use RegexParser\Node\PosixClassNode;
use RegexParser\Node\QuantifierNode;
use RegexParser\Node\RangeNode;
use RegexParser\Node\RegexNode;
use RegexParser\Node\ScriptRunNode;
use RegexParser\Node\SequenceNode;
use RegexParser\Node\SubroutineNode;
use RegexParser\Node\UnicodeNode;
use RegexParser\Node\UnicodePropNode;
use RegexParser\Node\VersionConditionNode;

/**
 * PSR-6 bridge for AST caching.
 *
 * This adapter lets you plug a PSR-6 cache pool into the RegexParser cache
 * system. The Regex service passes a compiled cache payload string
 * (generated by {@see \RegexParser\Regex::compileCachePayload()}); this
 * adapter evaluates that payload once and stores the resulting RegexNode
 * instance directly in the pool so that subsequent reads are fast and
 * type-safe.
 */
final readonly class PsrCacheAdapter implements RemovableCacheInterface
{
    public function __construct(
        private CacheItemPoolInterface $pool,
        private string $prefix = 'regex_',
        private ?\Closure $keyFactory = null
    ) {}

    public function generateKey(string $regex): string
    {
        if (null !== $this->keyFactory) {
            $custom = ($this->keyFactory)($regex);

            return $this->prefix.(\is_string($custom) ? $custom : hash('sha256', serialize($custom)));
        }

        return $this->prefix.hash('sha256', $regex);
    }

    public function write(string $key, string $content): void
    {
        $item = $this->pool->getItem($key);

        $value = $this->decodePayload($content);
        $item->set($value ?? $content);

        $this->pool->save($item);
    }

    public function load(string $key): mixed
    {
        $item = $this->pool->getItem($key);

        return $item->isHit() ? $item->get() : null;
    }

    public function getTimestamp(string $key): int
    {
        // PSR-6 does not expose timestamps; return 0 (unknown).
        return 0;
    }

    public function clear(?string $regex = null): void
    {
        if (null !== $regex) {
            $this->pool->deleteItem($this->generateKey($regex));

            return;
        }

        $this->pool->clear();
    }

    public function getStats(): array
    {
        return ['hits' => 0, 'misses' => 0];
    }

    /**
     * Decodes the compiled cache payload string into a RegexNode instance.
     *
     * The Regex service passes a small PHP script generated by
     * {@see \RegexParser\Regex::compileCachePayload()}. For PSR-6 caches we
     * execute this script once and store the resulting AST object directly
     * in the pool so that subsequent reads return a RegexNode instead of
     * the raw payload string.
     */
    private function decodePayload(string $content): ?RegexNode
    {
        $serialized = $this->extractSerializedString($content);
        if (null === $serialized) {
            return null;
        }

        $allowedClasses = [
            RegexNode::class,
            AlternationNode::class,
            AnchorNode::class,
            AssertionNode::class,
            BackrefNode::class,
            CalloutNode::class,
            CharClassNode::class,
            CharLiteralNode::class,
            CharTypeNode::class,
            ClassOperationNode::class,
            CommentNode::class,
            ConditionalNode::class,
            ControlCharNode::class,
            DefineNode::class,
            DotNode::class,
            GroupNode::class,
            KeepNode::class,
            LimitMatchNode::class,
            LiteralNode::class,
            PcreVerbNode::class,
            PosixClassNode::class,
            QuantifierNode::class,
            RangeNode::class,
            ScriptRunNode::class,
            SequenceNode::class,
            SubroutineNode::class,
            UnicodeNode::class,
            UnicodePropNode::class,
            VersionConditionNode::class,
        ];
        $value = @unserialize($serialized, ['allowed_classes' => $allowedClasses]);

        return $value instanceof RegexNode ? $value : null;
    }

    /**
     * Extracts the serialized AST string from the generated payload without executing it.
     */
    private function extractSerializedString(string $content): ?string
    {
        $code = ltrim($content);
        if (str_starts_with($code, '<?php')) {
            $code = substr($code, 5);
        }

        $offset = stripos($code, 'unserialize(');
        if (false === $offset) {
            return null;
        }

        $argumentBlock = substr($code, $offset + \strlen('unserialize('));
        $commaPos = strpos($argumentBlock, ',');
        if (false === $commaPos) {
            return null;
        }

        $argument = trim(substr($argumentBlock, 0, $commaPos));
        if ('' === $argument) {
            return null;
        }

        if (\in_array($argument[0], ["'", '"'], true) && $argument[0] === substr($argument, -1)) {
            $argument = substr($argument, 1, -1);
        }

        return stripcslashes($argument);
    }
}
