<?php

declare(strict_types=1);

namespace App\Services\Vision;

use Illuminate\Http\UploadedFile;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use RuntimeException;

final class VectorGatewayClient
{
    public function isConfigured(): bool
    {
        return (bool) config('vision.vector_gateway.enabled', true)
            && $this->baseUrl() !== ''
            && $this->apiKey() !== '';
    }

    public function upsertByUrl(string $imageUrl, int|string $id, array $metadata = []): array
    {
        $response = $this->postJson(
            $this->url((string) config('vision.vector_gateway.upsert_endpoint', '/vectors/upsert')),
            [
                'url' => $imageUrl,
                'id' => (string) $id,
                'metadata' => $metadata,
            ]
        );

        if ($response->failed()) {
            throw new RuntimeException($this->failureMessage('Vector upsert', $response));
        }

        $json = $response->json();

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

    public function upsertByFileContents(string $contents, string $filename, int|string $id, array $metadata = []): array
    {
        $response = $this->request()
            ->attach('file', $contents, $filename)
            ->post(
                $this->url((string) config('vision.vector_gateway.upsert_file_endpoint', '/vectors/upsert/file')),
                [
                    'id' => (string) $id,
                    'metadata_json' => $metadata === [] ? null : json_encode($metadata, JSON_THROW_ON_ERROR),
                ]
            );

        if ($response->failed()) {
            throw new RuntimeException($this->failureMessage('Vector upsert', $response));
        }

        $json = $response->json();

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

    /**
     * @return list<array{id: int|string, score: float, metadata: array<string, mixed>}>
     */
    public function searchByUrl(string $imageUrl, int $limit = 5): array
    {
        $response = $this->postJson(
            $this->url((string) config('vision.vector_gateway.search_endpoint', '/vectors/search')),
            [
                'url' => $imageUrl,
                'limit' => max(1, $limit),
            ]
        );

        if ($response->failed()) {
            throw new RuntimeException($this->failureMessage('Vector search', $response));
        }

        return $this->extractMatches($response->json());
    }

    /**
     * @return list<array{id: int|string, score: float, metadata: array<string, mixed>}>
     */
    public function searchByFileContents(string $contents, string $filename, int $limit = 5): array
    {
        $response = $this->request()
            ->attach('file', $contents, $filename)
            ->post(
                $this->url((string) config('vision.vector_gateway.search_file_endpoint', '/vectors/search/file')),
                [
                    'limit' => max(1, $limit),
                ]
            );

        if ($response->failed()) {
            throw new RuntimeException($this->failureMessage('Vector search', $response));
        }

        return $this->extractMatches($response->json());
    }

    /**
     * @return list<array{id: int|string, score: float, metadata: array<string, mixed>}>
     */
    public function searchByUploadedFile(UploadedFile $file, int $limit = 5): array
    {
        $realPath = $file->getRealPath();
        if (! is_string($realPath) || $realPath === '') {
            throw new RuntimeException('Uploaded file has no readable temporary path for vector search.');
        }

        $contents = file_get_contents($realPath);
        if (! is_string($contents) || $contents === '') {
            throw new RuntimeException('Unable to read uploaded image bytes for vector search.');
        }

        return $this->searchByFileContents($contents, $file->getClientOriginalName() ?: 'search-image', $limit);
    }

    public function deleteByIds(array $ids): array
    {
        $response = $this->postJson(
            $this->url((string) config('vision.vector_gateway.delete_endpoint', '/vectors/delete')),
            [
                'ids' => array_values(array_map(static fn (int|string $id): string => (string) $id, $ids)),
            ]
        );

        if ($response->failed()) {
            throw new RuntimeException($this->failureMessage('Vector delete', $response));
        }

        $json = $response->json();

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

    private function request(): PendingRequest
    {
        if (! $this->isConfigured()) {
            throw new RuntimeException('Vision vector gateway is not configured. Set VISION_VECTOR_GATEWAY_URL and VISION_VECTOR_GATEWAY_API_KEY.');
        }

        return Http::acceptJson()
            ->withHeaders([
                'X-API-Key' => $this->apiKey(),
            ])
            ->connectTimeout(max(1, (int) config('vision.vector_gateway.connect_timeout_seconds', 5)))
            ->timeout(max(1, (int) config('vision.vector_gateway.timeout_seconds', 20)))
            ->retry(
                max(0, (int) config('vision.vector_gateway.retries', 1)),
                max(0, (int) config('vision.vector_gateway.retry_delay_ms', 250)),
                throw: false,
            );
    }

    /**
     * @param array<string, mixed> $payload
     */
    private function postJson(string $url, array $payload): Response
    {
        $response = $this->request()->post($url, $payload);

        if (! $response instanceof Response) {
            throw new RuntimeException('Vector gateway request did not return an HTTP response.');
        }

        return $response;
    }

    private function baseUrl(): string
    {
        return rtrim((string) config('vision.vector_gateway.base_url', ''), '/');
    }

    private function apiKey(): string
    {
        return trim((string) config('vision.vector_gateway.api_key', ''));
    }

    private function url(string $path): string
    {
        return $this->baseUrl() . '/' . ltrim($path, '/');
    }

    private function failureMessage(string $operation, Response $response): string
    {
        $body = trim($response->body());

        if ($body === '') {
            return $operation . ' failed with HTTP ' . $response->status() . '.';
        }

        return $operation . ' failed with HTTP ' . $response->status() . ': ' . $body;
    }

    /**
     * @param mixed $json
     * @return list<array{id: int|string, score: float, metadata: array<string, mixed>}>
     */
    private function extractMatches(mixed $json): array
    {
        $candidates = [];

        if (is_array($json)) {
            $candidates = $this->extractCandidateRows($json);
        }

        $results = [];
        foreach ($candidates as $candidate) {
            if (! is_array($candidate)) {
                continue;
            }

            $id = $candidate['id']
                ?? $candidate['point_id']
                ?? $candidate['payload']['id']
                ?? $candidate['metadata']['id']
                ?? null;

            if (! is_int($id) && ! is_string($id)) {
                continue;
            }

            $score = $candidate['score']
                ?? $candidate['similarity']
                ?? $candidate['distance']
                ?? 0.0;

            $metadata = $candidate['metadata'] ?? $candidate['payload'] ?? [];
            if (! is_array($metadata)) {
                $metadata = [];
            }

            $results[] = [
                'id' => $id,
                'score' => (float) $score,
                'metadata' => $metadata,
            ];
        }

        return $results;
    }

    /**
     * @param array<mixed> $json
     * @return array<int, mixed>
     */
    private function extractCandidateRows(array $json): array
    {
        $keys = ['results', 'matches', 'points', 'data'];

        foreach ($keys as $key) {
            if (! isset($json[$key]) || ! is_array($json[$key])) {
                continue;
            }

            $value = $json[$key];
            if (array_is_list($value)) {
                return $value;
            }

            foreach (['results', 'matches', 'points', 'items'] as $nestedKey) {
                if (isset($value[$nestedKey]) && is_array($value[$nestedKey]) && array_is_list($value[$nestedKey])) {
                    return $value[$nestedKey];
                }
            }
        }

        return array_is_list($json) ? $json : [];
    }
}
