<?php

declare(strict_types=1);

namespace App\Framework\Cache;

use App\Framework\Cache\Contracts\Scannable;

/**
 * Tag index system for cache tag management
 *
 * Manages the mapping between cache tags and their associated keys.
 * Uses a separate namespace in the cache to store tag-to-key mappings.
 */
final class TagIndex
{
    private const string TAG_INDEX_PREFIX = '__tag_index:';
    private const string KEY_TAG_PREFIX = '__key_tags:';

    public function __construct(
        private readonly CacheDriver $driver
    ) {
    }

    /**
     * Add a key to one or more tags
     */
    public function tagKey(CacheKey $key, CacheTag ...$tags): bool
    {
        if (empty($tags)) {
            return true;
        }

        $keyString = (string)$key;
        $success = true;

        // Add key to each tag's index
        foreach ($tags as $tag) {
            $tagIndexKey = CacheKey::fromString(self::TAG_INDEX_PREFIX . $tag->toString());

            // Get existing keys for this tag
            $existingKeys = $this->getKeysForTag($tag);
            if (! in_array($keyString, $existingKeys, true)) {
                $existingKeys[] = $keyString;

                // Store updated key list for tag
                $tagItem = CacheItem::forSet($tagIndexKey, json_encode($existingKeys));
                $success &= $this->driver->set($tagItem);
            }
        }

        // Store tags for this key (for cleanup purposes)
        $keyTagsKey = CacheKey::fromString(self::KEY_TAG_PREFIX . $keyString);
        $tagValues = array_map(fn ($tag) => $tag->toString(), $tags);
        $keyTagsItem = CacheItem::forSet($keyTagsKey, json_encode($tagValues));
        $success &= $this->driver->set($keyTagsItem);

        return (bool)$success;
    }

    /**
     * Remove a key from one or more tags
     */
    public function untagKey(CacheKey $key, CacheTag ...$tags): bool
    {
        if (empty($tags)) {
            return $this->removeKeyFromAllTags($key);
        }

        $keyString = (string)$key;
        $success = true;

        // Remove key from each specified tag
        foreach ($tags as $tag) {
            $tagIndexKey = CacheKey::fromString(self::TAG_INDEX_PREFIX . $tag->toString());

            $existingKeys = $this->getKeysForTag($tag);
            $filteredKeys = array_filter($existingKeys, fn ($k) => $k !== $keyString);

            if (count($filteredKeys) !== count($existingKeys)) {
                if (empty($filteredKeys)) {
                    // No keys left for this tag, remove the tag index
                    $success &= $this->driver->forget($tagIndexKey);
                } else {
                    // Update the tag index
                    $tagItem = CacheItem::forSet($tagIndexKey, json_encode(array_values($filteredKeys)));
                    $success &= $this->driver->set($tagItem);
                }
            }
        }

        // Update the key's tag list
        $this->updateKeyTags($key, $tags, false);

        return $success;
    }

    /**
     * Get all keys associated with a tag
     */
    public function getKeysForTag(CacheTag $tag): array
    {
        $tagIndexKey = CacheKey::fromString(self::TAG_INDEX_PREFIX . $tag->toString());
        $result = $this->driver->get($tagIndexKey);

        $item = $result->getItem($tagIndexKey);
        if (! $item->isHit) {
            return [];
        }

        $decoded = json_decode($item->value, true);

        return is_array($decoded) ? $decoded : [];
    }

    /**
     * Get all tags associated with a key
     */
    public function getTagsForKey(CacheKey $key): array
    {
        $keyTagsKey = CacheKey::fromString(self::KEY_TAG_PREFIX . (string)$key);
        $result = $this->driver->get($keyTagsKey);

        $item = $result->getItem($keyTagsKey);
        if (! $item->isHit) {
            return [];
        }

        $decoded = json_decode($item->value, true);
        if (! is_array($decoded)) {
            return [];
        }

        return array_map(fn ($tagValue) => CacheTag::fromString($tagValue), $decoded);
    }

    /**
     * Remove a key from all its tags (used when key is deleted)
     */
    public function removeKeyFromAllTags(CacheKey $key): bool
    {
        $tags = $this->getTagsForKey($key);
        if (empty($tags)) {
            return true;
        }

        $success = $this->untagKey($key, ...$tags);

        // Remove the key's tag list
        $keyTagsKey = CacheKey::fromString(self::KEY_TAG_PREFIX . (string)$key);
        $success &= $this->driver->forget($keyTagsKey);

        return (bool)$success;
    }

    /**
     * Get all available tags
     */
    public function getAllTags(): array
    {
        if (! ($this->driver instanceof Scannable)) {
            return [];
        }

        try {
            $tagKeys = $this->driver->scanPrefix(self::TAG_INDEX_PREFIX, 1000);

            return array_map(function ($tagKey) {
                $tagValue = substr($tagKey, strlen(self::TAG_INDEX_PREFIX));

                return CacheTag::fromString($tagValue);
            }, $tagKeys);

        } catch (\Throwable $e) {
            return [];
        }
    }

    /**
     * Clear all tags and their indexes
     */
    public function clearAllTags(): bool
    {
        if (! ($this->driver instanceof Scannable)) {
            return false;
        }

        try {
            // Clear tag indexes
            $tagIndexKeys = $this->driver->scanPrefix(self::TAG_INDEX_PREFIX, 1000);
            $tagCacheKeys = array_map(fn ($key) => CacheKey::fromString($key), $tagIndexKeys);

            // Clear key-tag mappings
            $keyTagKeys = $this->driver->scanPrefix(self::KEY_TAG_PREFIX, 1000);
            $keyTagCacheKeys = array_map(fn ($key) => CacheKey::fromString($key), $keyTagKeys);

            $allKeys = array_merge($tagCacheKeys, $keyTagCacheKeys);

            if (empty($allKeys)) {
                return true;
            }

            return $this->driver->forget(...$allKeys);

        } catch (\Throwable $e) {
            return false;
        }
    }

    /**
     * Get statistics about the tag system
     */
    public function getStats(): array
    {
        $stats = [
            'total_tags' => 0,
            'total_tagged_keys' => 0,
            'scannable_driver' => $this->driver instanceof Scannable,
        ];

        if ($this->driver instanceof Scannable) {
            try {
                $tagKeys = $this->driver->scanPrefix(self::TAG_INDEX_PREFIX, 1000);
                $stats['total_tags'] = count($tagKeys);

                $keyTagKeys = $this->driver->scanPrefix(self::KEY_TAG_PREFIX, 1000);
                $stats['total_tagged_keys'] = count($keyTagKeys);

            } catch (\Throwable $e) {
                $stats['error'] = $e->getMessage();
            }
        }

        return $stats;
    }

    /**
     * Update the tag list for a key
     */
    private function updateKeyTags(CacheKey $key, array $tags, bool $add): void
    {
        $existingTags = $this->getTagsForKey($key);
        $existingTagValues = array_map(fn ($tag) => $tag->value, $existingTags);
        $newTagValues = array_map(fn ($tag) => $tag->value, $tags);

        if ($add) {
            $updatedTagValues = array_unique(array_merge($existingTagValues, $newTagValues));
        } else {
            $updatedTagValues = array_diff($existingTagValues, $newTagValues);
        }

        $keyTagsKey = CacheKey::fromString(self::KEY_TAG_PREFIX . (string)$key);

        if (empty($updatedTagValues)) {
            $this->driver->forget($keyTagsKey);
        } else {
            $keyTagsItem = CacheItem::forSet($keyTagsKey, json_encode(array_values($updatedTagValues)));
            $this->driver->set($keyTagsItem);
        }
    }
}
