<?php

declare(strict_types=1);

namespace App\Framework\MachineLearning\ModelManagement;

use App\Framework\MachineLearning\ModelManagement\ValueObjects\ModelMetadata;
use App\Framework\MachineLearning\ModelManagement\ValueObjects\ModelType;
use App\Framework\MachineLearning\ModelManagement\Exceptions\ModelAlreadyExistsException;
use App\Framework\MachineLearning\ModelManagement\Exceptions\ModelNotFoundException;
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\CacheItem;
use App\Framework\Core\ValueObjects\Version;
use App\Framework\Core\ValueObjects\Duration;

/**
 * Cache Model Registry - Cache-based ML Model Storage
 *
 * Stores model metadata in cache with the following structure:
 * - Individual models: ml:models:{modelName}:{version}
 * - Model list by name: ml:models:{modelName}:versions
 * - All model names: ml:models:names
 * - Models by type: ml:models:type:{type}
 * - Models by environment: ml:models:env:{environment}
 *
 * Cache TTL: 7 days for model metadata (semi-permanent)
 */
final readonly class CacheModelRegistry implements ModelRegistry
{
    private const CACHE_PREFIX = 'ml:models';
    private const DEFAULT_TTL_DAYS = 7;

    public function __construct(
        private Cache $cache,
        private int $ttlDays = self::DEFAULT_TTL_DAYS
    ) {}

    public function register(ModelMetadata $metadata): void
    {
        $modelKey = $this->getModelKey($metadata->modelName, $metadata->version);

        // Check if model already exists
        $hasResults = $this->cache->has($modelKey);
        if (!empty($hasResults[$modelKey->toString()] ?? false)) {
            throw ModelAlreadyExistsException::forModel($metadata->getModelId());
        }

        // Store model metadata
        $this->cache->set(
            CacheItem::forSet(
                $modelKey,
                $metadata->toArray(),
                Duration::fromDays($this->ttlDays)
            )
        );

        // Add to versions list
        $this->addToVersionsList($metadata->modelName, $metadata->version);

        // Add to model names list
        $this->addToModelNamesList($metadata->modelName);

        // Add to type index
        $this->addToTypeIndex($metadata->modelType, $metadata);

        // Add to environment index (if deployed)
        if ($metadata->isDeployed()) {
            $this->addToEnvironmentIndex($metadata->environment, $metadata);
        }
    }

    public function get(string $modelName, Version $version): ?ModelMetadata
    {
        $modelKey = $this->getModelKey($modelName, $version);
        $data = $this->getCacheValue($modelKey);

        if ($data === null) {
            return null;
        }

        return ModelMetadata::fromArray($data);
    }

    public function getLatest(string $modelName): ?ModelMetadata
    {
        $versions = $this->getVersionsList($modelName);

        if (empty($versions)) {
            return null;
        }

        // Versions are already sorted (newest first)
        $latestVersion = $versions[0];

        return $this->get($modelName, $latestVersion);
    }

    public function getAll(string $modelName): array
    {
        $versions = $this->getVersionsList($modelName);
        $models = [];

        foreach ($versions as $version) {
            $model = $this->get($modelName, $version);
            if ($model !== null) {
                $models[] = $model;
            }
        }

        return $models;
    }

    public function getByType(ModelType $type): array
    {
        $typeKey = CacheKey::fromString(self::CACHE_PREFIX . ":type:{$type->value}");
        $modelIds = $this->getCacheValue($typeKey) ?? [];

        $models = [];

        foreach ($modelIds as $modelId) {
            // Parse model ID: "name@version"
            [$modelName, $versionString] = explode('@', $modelId, 2);
            $version = Version::fromString($versionString);

            $model = $this->get($modelName, $version);
            if ($model !== null) {
                $models[] = $model;
            }
        }

        return $models;
    }

    public function getByEnvironment(string $environment): array
    {
        $envKey = CacheKey::fromString(self::CACHE_PREFIX . ":env:{$environment}");
        $modelIds = $this->getCacheValue($envKey) ?? [];

        $models = [];

        foreach ($modelIds as $modelId) {
            [$modelName, $versionString] = explode('@', $modelId, 2);
            $version = Version::fromString($versionString);

            $model = $this->get($modelName, $version);
            if ($model !== null) {
                $models[] = $model;
            }
        }

        return $models;
    }

    public function getProductionModels(): array
    {
        return $this->getByEnvironment('production');
    }

    public function update(ModelMetadata $metadata): void
    {
        $modelKey = $this->getModelKey($metadata->modelName, $metadata->version);

        // Check if model exists
        if (!$this->cache->has($modelKey)) {
            throw new ModelNotFoundException(
                "Model '{$metadata->getModelId()}' not found in registry"
            );
        }

        // Update model metadata
        $this->cache->set(
            CacheItem::forSet(
                $modelKey,
                $metadata->toArray(),
                Duration::fromDays($this->ttlDays)
            )
        );

        // Update environment index if deployment changed
        if ($metadata->isDeployed()) {
            $this->addToEnvironmentIndex($metadata->environment, $metadata);
        }
    }

    public function delete(string $modelName, Version $version): bool
    {
        $modelKey = $this->getModelKey($modelName, $version);

        if (!$this->cache->has($modelKey)) {
            return false;
        }

        // Get metadata before deletion for cleanup
        $metadata = $this->get($modelName, $version);

        // Delete model
        $this->cache->forget($modelKey);

        // Remove from versions list
        $this->removeFromVersionsList($modelName, $version);

        // Remove from type index
        if ($metadata !== null) {
            $this->removeFromTypeIndex($metadata->modelType, $metadata);

            // Remove from environment index
            if ($metadata->isDeployed()) {
                $this->removeFromEnvironmentIndex($metadata->environment, $metadata);
            }
        }

        // If no more versions, remove from model names list
        if ($this->getVersionCount($modelName) === 0) {
            $this->removeFromModelNamesList($modelName);
        }

        return true;
    }

    public function exists(string $modelName, Version $version): bool
    {
        $modelKey = $this->getModelKey($modelName, $version);
        $results = $this->cache->has($modelKey);
        return !empty($results[$modelKey->toString()] ?? false);
    }

    public function getAllModelNames(): array
    {
        $namesKey = CacheKey::fromString(self::CACHE_PREFIX . ':names');
        return $this->getCacheValue($namesKey) ?? [];
    }

    public function getVersionCount(string $modelName): int
    {
        return count($this->getVersionsList($modelName));
    }

    public function getTotalCount(): int
    {
        $count = 0;

        foreach ($this->getAllModelNames() as $modelName) {
            $count += $this->getVersionCount($modelName);
        }

        return $count;
    }

    public function clear(): void
    {
        // Get all model names
        $modelNames = $this->getAllModelNames();

        // Delete all models
        foreach ($modelNames as $modelName) {
            $versions = $this->getVersionsList($modelName);

            foreach ($versions as $version) {
                $this->delete($modelName, $version);
            }
        }

        // Clear indexes
        $this->cache->forget(CacheKey::fromString(self::CACHE_PREFIX . ':names'));
    }

    /**
     * Get cache key for model
     */
    private function getModelKey(string $modelName, Version $version): CacheKey
    {
        return CacheKey::fromString(
            self::CACHE_PREFIX . ":{$modelName}:{$version->toString()}"
        );
    }

    /**
     * Helper to extract value from CacheResult
     */
    private function getCacheValue(CacheKey $key): mixed
    {
        $result = $this->cache->get($key);
        return $result->isHit && $result->value !== null ? $result->value : null;
    }

    /**
     * Get versions list for model
     *
     * @return array<Version> Sorted by version (newest first)
     */
    private function getVersionsList(string $modelName): array
    {
        $versionsKey = CacheKey::fromString(self::CACHE_PREFIX . ":{$modelName}:versions");
        $versionStrings = $this->getCacheValue($versionsKey) ?? [];

        $versions = array_map(
            fn(string $v) => Version::fromString($v),
            $versionStrings
        );

        // Sort by version (newest first)
        usort($versions, fn(Version $a, Version $b) => $b->isNewerThan($a) ? 1 : -1);

        return $versions;
    }

    /**
     * Add version to versions list
     */
    private function addToVersionsList(string $modelName, Version $version): void
    {
        $versionsKey = CacheKey::fromString(self::CACHE_PREFIX . ":{$modelName}:versions");
        $versions = $this->getCacheValue($versionsKey) ?? [];

        $versionString = $version->toString();

        if (!in_array($versionString, $versions, true)) {
            $versions[] = $versionString;

            $this->cache->set(
                CacheItem::forSet(
                    $versionsKey,
                    $versions,
                    Duration::fromDays($this->ttlDays)
                )
            );
        }
    }

    /**
     * Remove version from versions list
     */
    private function removeFromVersionsList(string $modelName, Version $version): void
    {
        $versionsKey = CacheKey::fromString(self::CACHE_PREFIX . ":{$modelName}:versions");
        $versions = $this->getCacheValue($versionsKey) ?? [];

        $versionString = $version->toString();
        $versions = array_filter($versions, fn($v) => $v !== $versionString);

        if (empty($versions)) {
            $this->cache->forget($versionsKey);
        } else {
            $this->cache->set(
                CacheItem::forSet(
                    $versionsKey,
                    array_values($versions),
                    Duration::fromDays($this->ttlDays)
                )
            );
        }
    }

    /**
     * Add model name to global names list
     */
    private function addToModelNamesList(string $modelName): void
    {
        $namesKey = CacheKey::fromString(self::CACHE_PREFIX . ':names');
        $names = $this->getCacheValue($namesKey) ?? [];

        if (!in_array($modelName, $names, true)) {
            $names[] = $modelName;

            $this->cache->set(
                CacheItem::forSet(
                    $namesKey,
                    $names,
                    Duration::fromDays($this->ttlDays)
                )
            );
        }
    }

    /**
     * Remove model name from global names list
     */
    private function removeFromModelNamesList(string $modelName): void
    {
        $namesKey = CacheKey::fromString(self::CACHE_PREFIX . ':names');
        $names = $this->getCacheValue($namesKey) ?? [];

        $names = array_filter($names, fn($n) => $n !== $modelName);

        if (empty($names)) {
            $this->cache->forget($namesKey);
        } else {
            $this->cache->set(
                CacheItem::forSet(
                    $namesKey,
                    array_values($names),
                    Duration::fromDays($this->ttlDays)
                )
            );
        }
    }

    /**
     * Add model to type index
     */
    private function addToTypeIndex(ModelType $type, ModelMetadata $metadata): void
    {
        $typeKey = CacheKey::fromString(self::CACHE_PREFIX . ":type:{$type->value}");
        $modelIds = $this->getCacheValue($typeKey) ?? [];

        $modelId = $metadata->getModelId();

        if (!in_array($modelId, $modelIds, true)) {
            $modelIds[] = $modelId;

            $this->cache->set(
                CacheItem::forSet(
                    $typeKey,
                    $modelIds,
                    Duration::fromDays($this->ttlDays)
                )
            );
        }
    }

    /**
     * Remove model from type index
     */
    private function removeFromTypeIndex(ModelType $type, ModelMetadata $metadata): void
    {
        $typeKey = CacheKey::fromString(self::CACHE_PREFIX . ":type:{$type->value}");
        $modelIds = $this->getCacheValue($typeKey) ?? [];

        $modelId = $metadata->getModelId();
        $modelIds = array_filter($modelIds, fn($id) => $id !== $modelId);

        if (empty($modelIds)) {
            $this->cache->forget($typeKey);
        } else {
            $this->cache->set(
                CacheItem::forSet(
                    $typeKey,
                    array_values($modelIds),
                    Duration::fromDays($this->ttlDays)
                )
            );
        }
    }

    /**
     * Add model to environment index
     */
    private function addToEnvironmentIndex(string $environment, ModelMetadata $metadata): void
    {
        $envKey = CacheKey::fromString(self::CACHE_PREFIX . ":env:{$environment}");
        $modelIds = $this->getCacheValue($envKey) ?? [];

        $modelId = $metadata->getModelId();

        if (!in_array($modelId, $modelIds, true)) {
            $modelIds[] = $modelId;

            $this->cache->set(
                CacheItem::forSet(
                    $envKey,
                    $modelIds,
                    Duration::fromDays($this->ttlDays)
                )
            );
        }
    }

    /**
     * Remove model from environment index
     */
    private function removeFromEnvironmentIndex(string $environment, ModelMetadata $metadata): void
    {
        $envKey = CacheKey::fromString(self::CACHE_PREFIX . ":env:{$environment}");
        $modelIds = $this->getCacheValue($envKey) ?? [];

        $modelId = $metadata->getModelId();
        $modelIds = array_filter($modelIds, fn($id) => $id !== $modelId);

        if (empty($modelIds)) {
            $this->cache->forget($envKey);
        } else {
            $this->cache->set(
                CacheItem::forSet(
                    $envKey,
                    array_values($modelIds),
                    Duration::fromDays($this->ttlDays)
                )
            );
        }
    }
}
