<?php

namespace Laravel\Ai\Gateway\Gemini;

use DateInterval;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Collection;
use Laravel\Ai\Contracts\Gateway\StoreGateway;
use Laravel\Ai\Contracts\Providers\StoreProvider;
use Laravel\Ai\Gateway\Concerns\CreatesClient;
use Laravel\Ai\Gateway\Concerns\HandlesFailoverErrors;
use Laravel\Ai\Providers\Provider;
use Laravel\Ai\Responses\Data\StoreFileCounts;
use Laravel\Ai\Store;

class GeminiStoreGateway implements StoreGateway
{
    use CreatesClient;
    use HandlesFailoverErrors;

    /**
     * Get a vector store by its ID.
     */
    public function getStore(StoreProvider $provider, string $storeId): Store
    {
        $storeId = $this->normalizeStoreId($storeId);

        $response = $this->withErrorHandling(
            $provider->name(),
            fn () => $this->client($provider)->get($this->baseUrl($provider)."/{$storeId}")->throw(),
        );

        return new Store(
            provider: $provider,
            id: $response->json('name'),
            name: $response->json('displayName'),
            fileCounts: new StoreFileCounts(
                completed: $response->json('activeDocumentsCount', 0),
                pending: $response->json('pendingDocumentsCount', 0),
                failed: $response->json('failedDocumentsCount', 0),
            ),
            ready: true,
        );
    }

    /**
     * Create a new vector store.
     */
    public function createStore(
        StoreProvider $provider,
        string $name,
        ?string $description = null,
        ?Collection $fileIds = null,
        ?DateInterval $expiresWhenIdleFor = null,
    ): Store {
        $fileIds ??= new Collection;

        $response = $this->withErrorHandling($provider->name(), fn () => $this->client($provider)->post($this->baseUrl($provider).'/fileSearchStores', [
            'displayName' => $name,
        ])->throw());

        $store = $this->getStore($provider, $response->json('name'));

        if ($fileIds->isNotEmpty()) {
            foreach ($fileIds as $fileId) {
                $this->addFile($provider, $store->id, $fileId);
            }
        }

        return $store;
    }

    /**
     * Add a file to a vector store.
     */
    public function addFile(StoreProvider $provider, string $storeId, string $fileId, array $metadata = []): string
    {
        $storeId = $this->normalizeStoreId($storeId);
        $fileId = $this->normalizeFileId($fileId);

        $response = $this->withErrorHandling($provider->name(), fn () => $this->client($provider)->post($this->baseUrl($provider)."/{$storeId}:importFile", array_filter([
            'fileName' => $fileId,
            'customMetadata' => $metadata === [] ? null : $this->formatMetadata($metadata),
        ]))->throw());

        return basename((string) $response->json('name'));
    }

    /**
     * Format metadata for Gemini's custom_metadata format.
     */
    protected function formatMetadata(array $metadata): array
    {
        return (new Collection($metadata))->map(fn ($value, $key): array => match (true) {
            is_numeric($value) => ['key' => $key, 'numericValue' => $value],
            is_array($value) => ['key' => $key, 'stringListValue' => ['values' => $value]],
            default => ['key' => $key, 'stringValue' => (string) $value],
        })->values()->all();
    }

    /**
     * Remove a file from a vector store.
     */
    public function removeFile(StoreProvider $provider, string $storeId, string $documentId): bool
    {
        $storeId = $this->normalizeStoreId($storeId);
        $documentId = $this->normalizeDocumentId($storeId, $documentId);

        $this->withErrorHandling($provider->name(), fn () => $this->client($provider)->delete($this->baseUrl($provider)."/{$documentId}", [
            'force' => true,
        ])->throw());

        return true;
    }

    /**
     * Delete a vector store by its ID.
     */
    public function deleteStore(StoreProvider $provider, string $storeId): bool
    {
        $storeId = $this->normalizeStoreId($storeId);

        $this->withErrorHandling(
            $provider->name(),
            fn () => $this->client($provider)->delete($this->baseUrl($provider)."/{$storeId}")->throw(),
        );

        return true;
    }

    protected function client(Provider $provider): PendingRequest
    {
        return $this->createClient(
            $this->baseUrl($provider),
            array_filter(['x-goog-api-key' => $provider->providerCredentials()['key']]),
            $provider->additionalConfiguration()['headers'] ?? [],
            timeout: null,
            throw: false,
        );
    }

    /**
     * Get the base URL for the Gemini API.
     */
    protected function baseUrl(Provider $provider): string
    {
        return rtrim($provider->additionalConfiguration()['url'] ?? 'https://generativelanguage.googleapis.com/v1beta', '/');
    }

    /**
     * Normalize the store ID to include the resource prefix.
     */
    protected function normalizeStoreId(string $storeId): string
    {
        return str_starts_with($storeId, 'fileSearchStores/')
            ? $storeId
            : "fileSearchStores/{$storeId}";
    }

    /**
     * Normalize the file ID to include the resource prefix.
     */
    protected function normalizeFileId(string $fileId): string
    {
        return str_starts_with($fileId, 'files/')
            ? $fileId
            : "files/{$fileId}";
    }

    /**
     * Normalize the document ID to include the full resource path.
     */
    protected function normalizeDocumentId(string $storeId, string $documentId): string
    {
        // Already a full document path...
        if (str_starts_with($documentId, 'fileSearchStores/')) {
            return $documentId;
        }

        $documentId = match (true) {
            str_starts_with($documentId, 'documents/') => substr($documentId, 10),
            str_starts_with($documentId, 'files/') => substr($documentId, 6),
            default => $documentId,
        };

        return "{$storeId}/documents/{$documentId}";
    }
}
