<?php
namespace boru\ai\Embedding;

use boru\ai\Config\Config;
use boru\ai\Provider\ProviderRegistry;

class EmbeddingService
{
    /** @var ProviderRegistry */
    protected $providerRegistry;

    /** @var EmbeddingStoreInterface */
    protected $embeddingStore;

    /** @var Config|null */
    protected $config;

    public function __construct(ProviderRegistry $providerRegistry = null, EmbeddingStoreInterface $embeddingStore = null, Config $config = null)
    {
        $this->providerRegistry = $providerRegistry ? $providerRegistry : new ProviderRegistry();
        $this->embeddingStore = $embeddingStore ? $embeddingStore : new NullEmbeddingStore();
        $this->config = $config;
    }

    public function embed(EmbeddingRequest $request)
    {
        $request = $this->normalizeRequest($request);
        $provider = $this->providerRegistry->get($request->provider());

        if (!$provider instanceof EmbeddingProviderInterface) {
            throw new \RuntimeException('Provider does not support embeddings: ' . $request->provider());
        }

        $policy = EmbeddingCachePolicy::fromConfig($this->config, $request->provider(), $request);
        $cacheKey = $this->cacheKey($request, $policy);

        if ($policy->canRead() && $cacheKey !== null) {
            $cached = $this->embeddingStore->findResult($cacheKey);
            if ($cached instanceof EmbeddingResult) {
                $cached->cacheHit(true);
                return $cached;
            }
        }

        $result = $provider->embed($request);
        if (!$result instanceof EmbeddingResult) {
            throw new \RuntimeException('Embedding provider must return an EmbeddingResult');
        }

        if (!$result->provider()) {
            $result->provider($request->provider());
        }
        if (!$result->model()) {
            $result->model($request->model());
        }
        $result->cacheHit(false);

        if ($policy->canWrite() && $cacheKey !== null) {
            $this->embeddingStore->saveResult($cacheKey, $request, $result, $policy->ttlSeconds());
        }

        return $result;
    }

    protected function normalizeRequest(EmbeddingRequest $request)
    {
        $normalized = $request->copy();
        $providerName = $normalized->provider();
        $model = $normalized->model();

        if ((!$providerName || trim($providerName) === '') && $model) {
            $resolved = $this->providerRegistry->resolveByModel($model);
            if ($resolved) {
                $providerName = $resolved->name();
            }
        }

        if ((!$providerName || trim($providerName) === '') && $this->config instanceof Config) {
            $providerName = $this->config->get('vector.provider', null);
            if (!$providerName) {
                $providerName = $this->config->get('defaults.provider', null);
            }
        }

        if (!$providerName || trim($providerName) === '') {
            throw new \RuntimeException('Unable to resolve embedding provider');
        }

        if (!$this->providerRegistry->has($providerName)) {
            throw new \InvalidArgumentException('Unknown provider: ' . $providerName);
        }

        if (!$model || trim($model) === '') {
            if ($this->config instanceof Config) {
                $model = $this->config->get('providers.' . $providerName . '.vector.model', null);
                if (!$model) {
                    $model = $this->config->get('vector.model', null);
                }
                if (!$model) {
                    $model = $this->config->get('defaults.model', null);
                }
            }
        }

        if (!$model || trim($model) === '') {
            throw new \RuntimeException('Unable to resolve embedding model for provider: ' . $providerName);
        }

        if (count($normalized->inputs()) === 0) {
            throw new \InvalidArgumentException('Embedding request requires at least one input');
        }

        $normalized->provider($providerName);
        $normalized->model($model);

        return $normalized;
    }

    protected function cacheKey(EmbeddingRequest $request, EmbeddingCachePolicy $policy)
    {
        $manualKey = $request->option('cache_key');
        if ($manualKey !== null && trim((string)$manualKey) !== '') {
            return (string)$manualKey;
        }

        $payload = array(
            'provider' => $request->provider(),
            'model' => $request->model(),
            'inputs' => $request->inputs(),
            'options' => $this->cacheableOptions($request->option()),
        );

        if ($policy->namespace()) {
            $payload['namespace'] = $policy->namespace();
        }

        return 'emb_' . sha1(json_encode($this->sortRecursive($payload)));
    }

    protected function cacheableOptions(array $options)
    {
        unset(
            $options['cache'],
            $options['cache_key'],
            $options['cache_read'],
            $options['cache_write'],
            $options['cache_ttl_seconds'],
            $options['cache_namespace']
        );

        return $options;
    }

    protected function sortRecursive($value)
    {
        if (!is_array($value)) {
            return $value;
        }

        if ($this->isAssoc($value)) {
            ksort($value);
        }

        foreach ($value as $key => $item) {
            $value[$key] = $this->sortRecursive($item);
        }

        return $value;
    }

    protected function isAssoc(array $value)
    {
        return array_keys($value) !== range(0, count($value) - 1);
    }
}
