<?php

namespace Modules\ProductCompat\Entities\Services;

use App\Product;
use Illuminate\Support\Facades\Http;
use Modules\ProductCompat\Entities\Enrichment\EnrichmentResult;

/**
 * Gathers the SOURCE text blocks the AI extraction pass reasons over. It has
 * two feeds:
 *
 *   1. Structured provider results already collected by EnrichmentManager —
 *      each provider's rich source_texts and raw payload titles.
 *   2. Optional 1–2 extra web searches (gated by the `ai_extra_search` setting)
 *      that REUSE the same pc_settings keys the existing search providers read
 *      (searxng_url, google_cse_key/cx, serp_key) — no duplicated config, and
 *      each respects its provider's own rate-limit flag.
 *
 * Output is a list of numbered blocks [S1..Sn], structured-first, each capped
 * at max_source_chars, the whole set capped at max_total_chars. Every block
 * also carries the candidate image URLs harvested from that origin, so the
 * extraction service can validate AI-returned image_urls against them.
 */
class SearchTextCollector
{
    /**
     * @param  array<string, EnrichmentResult>  $providerResults  key => result (may be empty for product-context-only mode)
     * @return array<int, array{id:string, origin:string, text:string, urls:array<int,string>}>
     */
    public function collect(Product $product, array $providerResults): array
    {
        $maxSources = (int) config('productcompat.ai.max_sources', 8);
        $maxChars   = (int) config('productcompat.ai.max_source_chars', 2000);
        $maxTotal   = (int) config('productcompat.ai.max_total_chars', 12000);

        // Structured first: harvest from the already-collected provider results.
        $structured = [];
        $extra      = [];

        foreach ($providerResults as $key => $result) {
            if (! $result instanceof EnrichmentResult || ! $result->ok) {
                continue;
            }
            $texts = $this->textsFromResult($result);
            $urls  = $this->urlsFromResult($result);
            if ($texts === '' && empty($urls)) {
                continue;
            }
            $structured[] = ['origin' => $key, 'text' => $texts, 'urls' => $urls];
        }

        // Optional extra searches — only when enabled and we still have room.
        if (Settings::bool('ai_extra_search', true)) {
            foreach ($this->extraSearches($product) as $block) {
                $extra[] = $block;
            }
        }

        // Structured-first ordering, then number + cap.
        $ordered = array_merge($structured, $extra);

        $blocks = [];
        $total  = 0;
        $n      = 0;
        foreach ($ordered as $b) {
            if ($n >= $maxSources || $total >= $maxTotal) {
                break;
            }
            $text = mb_substr(trim((string) ($b['text'] ?? '')), 0, $maxChars);
            $urls = array_values(array_slice(array_unique((array) ($b['urls'] ?? [])), 0, 6));
            if ($text === '' && empty($urls)) {
                continue;
            }
            // Respect the total-chars budget.
            $remaining = $maxTotal - $total;
            if ($remaining <= 0) {
                break;
            }
            if (mb_strlen($text) > $remaining) {
                $text = mb_substr($text, 0, $remaining);
            }
            $n++;
            $total += mb_strlen($text);
            $blocks[] = [
                'id'     => 'S' . $n,
                'origin' => (string) ($b['origin'] ?? 'source'),
                'text'   => $text,
                'urls'   => $urls,
            ];
        }

        return $blocks;
    }

    /**
     * Flatten a provider result's rich source_texts and raw payload title into
     * a single text blob.
     */
    protected function textsFromResult(EnrichmentResult $result): string
    {
        $parts = [];

        foreach ((array) ($result->rich['source_texts'] ?? []) as $t) {
            $t = trim((string) $t);
            if ($t !== '') {
                $parts[] = $t;
            }
        }
        if (! empty($result->rich['description'])) {
            $parts[] = trim((string) $result->rich['description']);
        }

        // Raw payload titles (Icecat/search providers stash a title / query there).
        $raw = $result->raw;
        if (is_array($raw)) {
            foreach (['title', 'query'] as $k) {
                if (! empty($raw[$k]) && is_string($raw[$k])) {
                    $parts[] = trim($raw[$k]);
                }
            }
        }

        return trim(implode("\n", array_values(array_unique(array_filter($parts)))));
    }

    /** Image URLs a provider surfaced in its rich payload. */
    protected function urlsFromResult(EnrichmentResult $result): array
    {
        $out = [];
        foreach ((array) ($result->rich['image_urls'] ?? []) as $u) {
            if (is_string($u) && preg_match('#^https?://#i', $u)) {
                $out[] = $u;
            }
        }

        return array_values(array_unique($out));
    }

    /**
     * Run 1–2 extra web-search variants reusing the existing providers' own
     * settings keys. Best-effort: any provider that isn't configured or that is
     * currently rate-limited is skipped. Returns per-origin blocks with text +
     * image URLs.
     *
     * @return array<int, array{origin:string, text:string, urls:array<int,string>}>
     */
    protected function extraSearches(Product $product): array
    {
        $brand = trim((string) optional($product->brand)->name);
        $query = trim($brand . ' ' . (string) $product->name);
        if ($query === '') {
            return [];
        }

        $timeout = (int) config('productcompat.enrichment.timeout', 15);
        $ua      = config('productcompat.enrichment.user_agent', 'ProductCompatBot/1.0');
        $blocks  = [];

        // ---- SearXNG (free, self-hosted) — reuses searxng_url. ----
        $searxng = rtrim((string) Settings::get('searxng_url', config('productcompat.enrichment.searxng.base_url')), '/');
        if ($searxng !== '') {
            // 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 {
                $resp = Http::timeout($timeout)
                    ->withHeaders(['User-Agent' => $ua, 'ngrok-skip-browser-warning' => 'true'])
                    ->get($searxng . '/search', ['q' => $query . ' specifications', 'format' => 'json']);
                if ($resp->ok()) {
                    [$text, $urls] = $this->harvestSearch((array) ($resp->json()['results'] ?? []), ['title', 'content'], ['img_src', 'thumbnail']);
                    if ($text !== '' || $urls) {
                        $blocks[] = ['origin' => 'searxng_extra', 'text' => $text, 'urls' => $urls];
                    }
                }
            } catch (\Throwable $e) {
                // best-effort — skip on any error
            }
        }

        // ---- Google CSE (reuses google_cse_key/cx; skip if rate-limited today). ----
        $gKey = Settings::get('google_cse_key', env('PRODUCTCOMPAT_GOOGLE_KEY'));
        $gCx  = Settings::get('google_cse_cx', env('PRODUCTCOMPAT_GOOGLE_CX'));
        if (! empty($gKey) && ! empty($gCx) && ! $this->googleRateLimited()) {
            try {
                $resp = Http::timeout($timeout)->get('https://www.googleapis.com/customsearch/v1', [
                    'key' => $gKey, 'cx' => $gCx, 'q' => $query . ' specifications', 'num' => 10,
                ]);
                if ($resp->status() === 429) {
                    Settings::set('google_limit_reached_at', now()->toDateTimeString());
                } elseif ($resp->ok()) {
                    [$text, $urls] = $this->harvestSearch((array) ($resp->json()['items'] ?? []), ['title', 'snippet'], ['image']);
                    if ($text !== '' || $urls) {
                        $blocks[] = ['origin' => 'google_extra', 'text' => $text, 'urls' => $urls];
                    }
                }
            } catch (\Throwable $e) {
                // best-effort — skip on any error
            }
        }

        // ---- SerpApi (reuses serp_key) — only if SearXNG + Google both yielded nothing. ----
        if (empty($blocks)) {
            $cfg    = config('productcompat.enrichment.serp_api', []);
            $serpKey = Settings::get('serp_key', $cfg['key'] ?? null);
            if (! empty($serpKey)) {
                try {
                    $resp = Http::timeout($timeout)->get($cfg['base_url'] ?? 'https://serpapi.com/search.json', [
                        'engine'  => $cfg['engine'] ?? 'google',
                        'q'       => $query . ' specifications',
                        'num'     => 10,
                        'api_key' => $serpKey,
                    ]);
                    if ($resp->ok()) {
                        [$text, $urls] = $this->harvestSearch((array) ($resp->json()['organic_results'] ?? []), ['title', 'snippet'], []);
                        if ($text !== '' || $urls) {
                            $blocks[] = ['origin' => 'serp_extra', 'text' => $text, 'urls' => $urls];
                        }
                    }
                } catch (\Throwable $e) {
                    // best-effort — skip on any error
                }
            }
        }

        return $blocks;
    }

    /** Google CSE respects a daily-quota flag the provider sets on HTTP 429. */
    protected function googleRateLimited(): bool
    {
        $at = Settings::get('google_limit_reached_at');
        if (! $at) {
            return false;
        }
        try {
            // Quota resets daily — treat a same-day limit flag as active.
            return \Illuminate\Support\Carbon::parse($at)->isToday();
        } catch (\Throwable $e) {
            return false;
        }
    }

    /**
     * Merge search-result rows into one text blob + a list of image URLs. Text
     * comes from $textKeys; image URLs from $imgKeys (http(s) only).
     *
     * @return array{0:string, 1:array<int,string>}
     */
    protected function harvestSearch(array $rows, array $textKeys, array $imgKeys): array
    {
        $texts = [];
        $urls  = [];
        foreach ($rows as $row) {
            if (! is_array($row)) {
                continue;
            }
            $line = [];
            foreach ($textKeys as $k) {
                if (! empty($row[$k]) && is_string($row[$k])) {
                    $line[] = trim($row[$k]);
                }
            }
            if ($line) {
                $texts[] = implode(' ', $line);
            }
            foreach ($imgKeys as $k) {
                $u = $row[$k] ?? null;
                if (is_string($u) && preg_match('#^https?://#i', $u)) {
                    $urls[] = $u;
                }
            }
        }

        return [
            trim(implode("\n", array_values(array_unique(array_filter($texts))))),
            array_values(array_slice(array_unique($urls), 0, 6)),
        ];
    }
}
