<?php

declare(strict_types=1);

namespace App\Framework\LiveComponents\Caching;

/**
 * VaryBy Value Object
 *
 * Defines which context factors should vary the cache key.
 * Allows fine-grained control over cache key generation.
 *
 * Examples:
 * - VaryBy::userId() - Cache per user
 * - VaryBy::locale() - Cache per language
 * - VaryBy::userAndLocale() - Cache per user and language
 * - VaryBy::custom(['device_type']) - Cache by custom factors
 *
 * Framework Pattern: Immutable readonly Value Object
 */
final readonly class VaryBy
{
    /**
     * @param bool $userId - Vary by user ID
     * @param bool $locale - Vary by locale
     * @param bool $roles - Vary by user roles
     * @param array<string> $featureFlags - Specific feature flags to vary by
     * @param array<string> $custom - Custom context keys to vary by
     */
    public function __construct(
        public bool $userId = false,
        public bool $locale = false,
        public bool $roles = false,
        public array $featureFlags = [],
        public array $custom = []
    ) {
    }

    /**
     * Create VaryBy that varies by nothing (global cache)
     */
    public static function none(): self
    {
        return new self();
    }

    /**
     * Create VaryBy that varies by user ID
     */
    public static function userId(): self
    {
        return new self(userId: true);
    }

    /**
     * Create VaryBy that varies by locale
     */
    public static function locale(): self
    {
        return new self(locale: true);
    }

    /**
     * Create VaryBy that varies by user roles
     */
    public static function roles(): self
    {
        return new self(roles: true);
    }

    /**
     * Create VaryBy that varies by user ID and locale
     */
    public static function userAndLocale(): self
    {
        return new self(
            userId: true,
            locale: true
        );
    }

    /**
     * Create VaryBy that varies by all standard factors
     */
    public static function all(): self
    {
        return new self(
            userId: true,
            locale: true,
            roles: true
        );
    }

    /**
     * Create VaryBy for specific feature flags
     */
    public static function featureFlags(array $flags): self
    {
        return new self(featureFlags: $flags);
    }

    /**
     * Create VaryBy for custom context keys
     */
    public static function custom(array $keys): self
    {
        return new self(custom: $keys);
    }

    /**
     * Add user ID to varyBy
     */
    public function withUserId(): self
    {
        return new self(
            userId: true,
            locale: $this->locale,
            roles: $this->roles,
            featureFlags: $this->featureFlags,
            custom: $this->custom
        );
    }

    /**
     * Add locale to varyBy
     */
    public function withLocale(): self
    {
        return new self(
            userId: $this->userId,
            locale: true,
            roles: $this->roles,
            featureFlags: $this->featureFlags,
            custom: $this->custom
        );
    }

    /**
     * Add roles to varyBy
     */
    public function withRoles(): self
    {
        return new self(
            userId: $this->userId,
            locale: $this->locale,
            roles: true,
            featureFlags: $this->featureFlags,
            custom: $this->custom
        );
    }

    /**
     * Add feature flags to varyBy
     */
    public function withFeatureFlags(array $flags): self
    {
        return new self(
            userId: $this->userId,
            locale: $this->locale,
            roles: $this->roles,
            featureFlags: [...$this->featureFlags, ...$flags],
            custom: $this->custom
        );
    }

    /**
     * Add custom keys to varyBy
     */
    public function withCustom(array $keys): self
    {
        return new self(
            userId: $this->userId,
            locale: $this->locale,
            roles: $this->roles,
            featureFlags: $this->featureFlags,
            custom: [...$this->custom, ...$keys]
        );
    }

    /**
     * Check if varyBy is empty (global cache)
     */
    public function isEmpty(): bool
    {
        return ! $this->userId
            && ! $this->locale
            && ! $this->roles
            && empty($this->featureFlags)
            && empty($this->custom);
    }

    /**
     * Apply varyBy to context and generate cache key suffix
     *
     * Only includes context factors that are specified in varyBy.
     */
    public function apply(CacheContext $context): string
    {
        if ($this->isEmpty()) {
            return 'global';
        }

        $parts = [];

        if ($this->userId && $context->userId !== null) {
            $parts[] = "u:{$context->userId}";
        }

        if ($this->locale) {
            $parts[] = "l:{$context->locale}";
        }

        if ($this->roles && ! empty($context->roles)) {
            $roles = $context->roles;
            sort($roles);
            $parts[] = 'r:' . implode(',', $roles);
        }

        if (! empty($this->featureFlags)) {
            $enabledFlags = [];
            foreach ($this->featureFlags as $flag) {
                if ($context->hasFeature($flag)) {
                    $enabledFlags[] = $flag;
                }
            }
            if (! empty($enabledFlags)) {
                sort($enabledFlags);
                $parts[] = 'f:' . implode(',', $enabledFlags);
            }
        }

        if (! empty($this->custom)) {
            foreach ($this->custom as $key) {
                $value = $context->getCustom($key);
                if ($value !== null) {
                    $parts[] = "{$key}:" . $this->serializeValue($value);
                }
            }
        }

        return empty($parts) ? 'default' : implode(':', $parts);
    }

    /**
     * Serialize value for cache key
     */
    private function serializeValue(mixed $value): string
    {
        if (is_bool($value)) {
            return $value ? '1' : '0';
        }

        if (is_scalar($value)) {
            return (string) $value;
        }

        if (is_array($value)) {
            return md5(json_encode($value));
        }

        return md5(serialize($value));
    }

    /**
     * Convert to array for debugging
     */
    public function toArray(): array
    {
        return [
            'user_id' => $this->userId,
            'locale' => $this->locale,
            'roles' => $this->roles,
            'feature_flags' => $this->featureFlags,
            'custom' => $this->custom,
        ];
    }
}
