<?php

namespace App\Services\Scraping;

use App\Contracts\ListingScraper;
use App\DTOs\ScrapedListingResult;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
use InvalidArgumentException;
use RuntimeException;

class PuppeteerListingScraper implements ListingScraper
{
    public function scrape(string $url): ScrapedListingResult
    {
        $url = trim($url);

        if ($url === '' || filter_var($url, FILTER_VALIDATE_URL) === false) {
            throw new InvalidArgumentException('A valid listing URL is required for Puppeteer scraping.');
        }

        if (! config('scraping.puppeteer.enabled')) {
            Log::info('Puppeteer listing scrape skipped because scraping is disabled.', ['url' => $url]);

            return new ScrapedListingResult(
                url: $url,
                products: [],
                error: 'Puppeteer scraping is disabled.',
                source: 'puppeteer',
            );
        }

        $script = $this->scriptPath();

        if (! is_file($script)) {
            throw new RuntimeException("Puppeteer listing script not found at [{$script}]. Run npm install in the project root.");
        }

        $command = [
            (string) config('scraping.puppeteer.node_binary'),
            $script,
            $url,
        ];

        Log::info('Puppeteer listing scrape starting', [
            'url' => $url,
            'script' => $script,
        ]);

        $result = Process::timeout((int) config('scraping.puppeteer.timeout_seconds'))
            ->path(base_path())
            ->env($this->scrapingEnvironment())
            ->run($command);

        if ($result->failed()) {
            $output = trim($result->output());

            if ($output !== '') {
                $payload = $this->tryDecodePayload($output, $url);

                if (filled($payload['error'] ?? null)) {
                    return ScrapedListingResult::fromPayload($url, $payload, source: 'puppeteer');
                }
            }

            $message = trim($result->errorOutput()) !== ''
                ? trim($result->errorOutput())
                : trim($result->output());

            Log::warning('Puppeteer listing scrape process failed', [
                'url' => $url,
                'exit_code' => $result->exitCode(),
                'output' => $result->output(),
                'error_output' => $result->errorOutput(),
            ]);

            return new ScrapedListingResult(
                url: $url,
                products: [],
                error: $message !== '' ? $message : 'Puppeteer scrape failed.',
                source: 'puppeteer',
            );
        }

        $payload = $this->decodePayload($result->output(), $url);

        $scraped = ScrapedListingResult::fromPayload($url, $payload, source: 'puppeteer');

        Log::info('Puppeteer listing scrape finished', [
            'url' => $scraped->url,
            'product_count' => count($scraped->products),
            'browser_engine' => $payload['browser_engine'] ?? null,
            'engines_tried' => $payload['engines_tried'] ?? [],
            'error' => $scraped->error,
        ]);

        return $scraped;
    }

    private function scriptPath(): string
    {
        $configured = (string) config('scraping.puppeteer.listing_script');

        return Str::startsWith($configured, DIRECTORY_SEPARATOR)
            ? $configured
            : base_path($configured);
    }

    /**
     * @return array<string, string>
     */
    private function scrapingEnvironment(): array
    {
        $environment = [];

        foreach ([
            'SCRAPING_BROWSER_CHANNEL' => config('scraping.puppeteer.browser_channel'),
            'SCRAPING_BROWSER_ENGINE' => config('scraping.puppeteer.browser_engine'),
            'SCRAPING_BROWSER_ENGINES' => config('scraping.puppeteer.browser_engines'),
            'SCRAPING_HEADLESS' => config('scraping.puppeteer.headless'),
            'SCRAPING_PROXY' => config('scraping.puppeteer.proxy'),
            'SCRAPING_PROXY_USERNAME' => config('scraping.puppeteer.proxy_username'),
            'SCRAPING_PROXY_PASSWORD' => config('scraping.puppeteer.proxy_password'),
            'SCRAPING_COOKIES_FILE' => $this->cookiesFilePath(),
        ] as $key => $value) {
            if (filled($value)) {
                $environment[$key] = (string) $value;
            }
        }

        return $environment;
    }

    private function cookiesFilePath(): ?string
    {
        $configured = config('scraping.puppeteer.cookies_file');

        if (! filled($configured)) {
            return null;
        }

        $path = Str::startsWith((string) $configured, DIRECTORY_SEPARATOR)
            ? (string) $configured
            : base_path((string) $configured);

        return is_file($path) ? $path : null;
    }

    /**
     * @return array<string, mixed>
     */
    private function decodePayload(string $output, string $url): array
    {
        return $this->tryDecodePayload($output, $url);
    }

    /**
     * @return array<string, mixed>
     */
    private function tryDecodePayload(string $output, string $url): array
    {
        $output = trim($output);

        if ($output === '') {
            return [
                'url' => $url,
                'products' => [],
                'error' => 'Puppeteer returned an empty response.',
            ];
        }

        try {
            $decoded = json_decode($output, true, flags: JSON_THROW_ON_ERROR);
        } catch (\JsonException $exception) {
            Log::warning('Puppeteer listing scrape returned invalid JSON', [
                'url' => $url,
                'output' => Str::limit($output, 2000),
                'error' => $exception->getMessage(),
            ]);

            return [
                'url' => $url,
                'products' => [],
                'error' => 'Puppeteer returned invalid JSON.',
            ];
        }

        return is_array($decoded) ? $decoded : [
            'url' => $url,
            'products' => [],
            'error' => 'Puppeteer returned a non-object JSON payload.',
        ];
    }
}
