<?php

namespace Modules\ProductCompat\Entities\Enrichment\Providers;

use App\Product;
use Illuminate\Support\Facades\Http;
use Modules\ProductCompat\Entities\Enrichment\EnrichmentProvider;
use Modules\ProductCompat\Entities\Enrichment\EnrichmentResult;
use Modules\ProductCompat\Entities\Services\CustomFieldParser;
use Modules\ProductCompat\Entities\Services\Settings;

/**
 * Self-hosted SearXNG metasearch — FREE and with no daily cap (you run the
 * instance). Queries its JSON API and mines part-number-like tokens from result
 * titles/snippets. Works for the part-numberless products. Set the instance URL
 * in module Settings; the instance must allow `format=json` in its settings.yml.
 */
class SearxngProvider implements EnrichmentProvider
{
    protected CustomFieldParser $parser;

    public function __construct(?CustomFieldParser $parser = null)
    {
        $this->parser = $parser ?? new CustomFieldParser();
    }

    public function key(): string
    {
        return 'searxng';
    }

    public function isAvailable(): bool
    {
        return ! empty(Settings::get('searxng_url', config('productcompat.enrichment.searxng.base_url')));
    }

    public function fetch(Product $product): EnrichmentResult
    {
        $base = rtrim((string) Settings::get('searxng_url', config('productcompat.enrichment.searxng.base_url')), '/');
        $timeout = (int) config('productcompat.enrichment.timeout', 15);
        $ua = config('productcompat.enrichment.user_agent', 'ProductCompatBot/1.0');
        $conf = (int) (config('productcompat.enrichment.confidence.searxng') ?? 50);

        $brand = trim((string) optional($product->brand)->name);
        $query = trim($brand . ' ' . $product->name . ' part number');
        if ($query === 'part number') {
            return EnrichmentResult::empty(['reason' => 'no brand/name to search']);
        }

        // Politeness delay so a self-hosted / ngrok-tunnelled instance isn't hit
        // back-to-back (configurable; 0 = off).
        $delayMs = (int) Settings::get('searxng_delay_ms', 0);
        if ($delayMs > 0) {
            usleep($delayMs * 1000);
        }

        try {
            // ngrok-skip-browser-warning lets a free ngrok tunnel serve the JSON
            // API without its browser interstitial (harmless for direct hosts).
            $resp = Http::timeout($timeout)
                ->withHeaders(['User-Agent' => $ua, 'ngrok-skip-browser-warning' => 'true'])
                ->get($base . '/search', ['q' => $query, 'format' => 'json']);
        } catch (\Throwable $e) {
            return EnrichmentResult::fail($e->getMessage());
        }

        if ($resp->status() === 429) {
            return EnrichmentResult::fail('searxng_limit: HTTP 429 (instance rate-limited)');
        }
        if ($resp->failed()) {
            return EnrichmentResult::fail('SearXNG HTTP ' . $resp->status() . ' (is format=json enabled?)', $resp->body());
        }

        $json = $resp->json() ?? [];
        $texts = [];
        foreach ((array) ($json['results'] ?? []) as $r) {
            $texts[] = ($r['title'] ?? '') . ' ' . ($r['content'] ?? '');
        }

        // Keep only part numbers corroborated by >=2 results, top 5.
        $parts = array_map(function ($p) use ($conf) {
            return ['value' => $p['value'], 'type' => $p['type'], 'confidence' => min(95, $conf + max(0, $p['hits'] - 1) * 12)];
        }, $this->parser->extractCorroborated($texts, 1, 5));

        return new EnrichmentResult(true, ['part_numbers' => $parts], [
            'query'   => $query,
            'results' => count((array) ($json['results'] ?? [])),
            'matched' => count($parts),
        ]);
    }
}
