<?php
/**
 * ===========================================
 * FLOWBOT DCI - SEARCH ENGINE AGGREGATOR
 * ===========================================
 * Combines multiple search engines for comprehensive results
 *
 * Features:
 * - Search across multiple engines simultaneously
 * - Deduplicate results from different sources
 * - Fallback to available engines if one fails
 * - Rate limiting per engine
 * - Result merging and ranking
 */

declare(strict_types=1);

namespace FlowbotDCI\Services\SearchEngine;

class SearchEngineAggregator
{
    const VERSION = '2.0';

    /** @var SearchEngineInterface[] */
    private array $engines = [];

    private array $errors = [];
    private array $stats = [];

    /**
     * Register a search engine
     */
    public function registerEngine(SearchEngineInterface $engine): self
    {
        $this->engines[$engine->getName()] = $engine;
        return $this;
    }

    /**
     * Register multiple engines from config
     * v2.0 - All free engines, no API keys required
     */
    public function registerFromConfig(array $config): self
    {
        // Yahoo (FREE - no API key, web scraping)
        if ($config['yahoo']['enabled'] ?? true) {
            $this->registerEngine(new YahooAdapter($config['yahoo'] ?? []));
        }

        // Bing Scraper (FREE - no API key, web scraping)
        if ($config['bing']['enabled'] ?? true) {
            $this->registerEngine(new BingScraperAdapter($config['bing'] ?? []));
        }

        // DuckDuckGo (FREE - no API key, web scraping)
        if ($config['duckduckgo']['enabled'] ?? true) {
            $this->registerEngine(new DuckDuckGoAdapter($config['duckduckgo'] ?? []));
        }

        // SearXNG (FREE - public metasearch instances)
        if ($config['searxng']['enabled'] ?? false) {
            $this->registerEngine(new SearXNGAdapter($config['searxng'] ?? []));
        }

        // Legacy support: Brave Search (requires API key)
        if (($config['brave']['enabled'] ?? false) && !empty($config['brave']['api_key'])) {
            $this->registerEngine(new BraveAdapter($config['brave']));
        }

        // Legacy support: Google API (requires API key)
        if (($config['google']['enabled'] ?? false) && !empty($config['google']['api_key'])) {
            $this->registerEngine(new GoogleAdapter($config['google']));
        }

        return $this;
    }

    /**
     * Get list of available engines
     */
    public function getAvailableEngines(): array
    {
        $available = [];
        foreach ($this->engines as $name => $engine) {
            if ($engine->isAvailable()) {
                $available[] = $name;
            }
        }
        return $available;
    }

    /**
     * Search using all available engines
     *
     * @param string $query Search query
     * @param int $maxPerEngine Max results per engine
     * @param array|null $useEngines Specific engines to use (null = all available)
     * @return array Deduplicated and merged results
     */
    public function searchAll(string $query, int $maxPerEngine = 10, ?array $useEngines = null): array
    {
        $this->errors = [];
        $this->stats = [];
        $allResults = [];

        $enginesToUse = $useEngines ?? array_keys($this->engines);

        foreach ($enginesToUse as $engineName) {
            if (!isset($this->engines[$engineName])) {
                $this->errors[$engineName] = "Engine not registered: {$engineName}";
                continue;
            }

            $engine = $this->engines[$engineName];

            if (!$engine->isAvailable()) {
                $this->errors[$engineName] = "Engine not available (not configured)";
                continue;
            }

            $startTime = microtime(true);

            try {
                $results = $engine->search($query, $maxPerEngine);

                $this->stats[$engineName] = [
                    'results' => count($results),
                    'time' => round((microtime(true) - $startTime) * 1000, 2),
                    'success' => true,
                ];

                // Add to all results
                foreach ($results as $result) {
                    $allResults[] = $result;
                }

            } catch (\Exception $e) {
                $this->errors[$engineName] = $e->getMessage();
                $this->stats[$engineName] = [
                    'results' => 0,
                    'time' => round((microtime(true) - $startTime) * 1000, 2),
                    'success' => false,
                    'error' => $e->getMessage(),
                ];
            }

            // Check for engine-specific errors
            $lastError = $engine->getLastError();
            if ($lastError) {
                $this->errors[$engineName] = $lastError;
            }
        }

        // Deduplicate by URL
        return $this->deduplicateResults($allResults);
    }

    /**
     * Search with fallback (try engines in order until one succeeds)
     *
     * @param string $query Search query
     * @param int $maxResults Max results to return
     * @param array|null $priority Engine priority order
     * @return array Results from first successful engine
     */
    public function searchWithFallback(string $query, int $maxResults = 10, ?array $priority = null): array
    {
        $this->errors = [];
        $this->stats = [];

        $engineOrder = $priority ?? array_keys($this->engines);

        foreach ($engineOrder as $engineName) {
            if (!isset($this->engines[$engineName])) {
                continue;
            }

            $engine = $this->engines[$engineName];

            if (!$engine->isAvailable()) {
                $this->errors[$engineName] = "Not available";
                continue;
            }

            $startTime = microtime(true);

            try {
                $results = $engine->search($query, $maxResults);

                if (!empty($results)) {
                    $this->stats[$engineName] = [
                        'results' => count($results),
                        'time' => round((microtime(true) - $startTime) * 1000, 2),
                        'success' => true,
                        'used' => true,
                    ];

                    return $results;
                }

                $this->errors[$engineName] = $engine->getLastError() ?? "No results";

            } catch (\Exception $e) {
                $this->errors[$engineName] = $e->getMessage();
            }
        }

        return [];
    }

    /**
     * Search specific engine
     *
     * @param string $engineName Engine name
     * @param string $query Search query
     * @param int $maxResults Max results
     * @return array Results
     */
    public function searchEngine(string $engineName, string $query, int $maxResults = 10): array
    {
        if (!isset($this->engines[$engineName])) {
            $this->errors[$engineName] = "Engine not registered";
            return [];
        }

        $engine = $this->engines[$engineName];

        if (!$engine->isAvailable()) {
            $this->errors[$engineName] = "Engine not available";
            return [];
        }

        $results = $engine->search($query, $maxResults);

        if ($engine->getLastError()) {
            $this->errors[$engineName] = $engine->getLastError();
        }

        return $results;
    }

    /**
     * Deduplicate results by URL
     */
    private function deduplicateResults(array $results): array
    {
        $seen = [];
        $deduplicated = [];

        foreach ($results as $result) {
            $url = $result['url'] ?? '';
            if (empty($url)) {
                continue;
            }

            // Normalize URL for comparison
            $normalizedUrl = strtolower(rtrim($url, '/'));

            if (!isset($seen[$normalizedUrl])) {
                $seen[$normalizedUrl] = true;
                $deduplicated[] = $result;
            }
        }

        return $deduplicated;
    }

    /**
     * Get errors from last search
     */
    public function getErrors(): array
    {
        return $this->errors;
    }

    /**
     * Get stats from last search
     */
    public function getStats(): array
    {
        return $this->stats;
    }

    /**
     * Get total results count from last search
     */
    public function getTotalResults(): int
    {
        $total = 0;
        foreach ($this->stats as $stat) {
            $total += $stat['results'] ?? 0;
        }
        return $total;
    }

    /**
     * Check if any engine is available
     */
    public function hasAvailableEngines(): bool
    {
        foreach ($this->engines as $engine) {
            if ($engine->isAvailable()) {
                return true;
            }
        }
        return false;
    }
}
