<?php

namespace App\Services\AI;

use App\Contracts\AsyncCompetitorAdProvider;
use App\Contracts\CompetitorAdProvider;
use App\DTOs\CompetitorAd;
use App\Exceptions\ProviderNotConfiguredException;
use App\Jobs\ScrapeMetaAdLibraryJob;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
use RuntimeException;

/**
 * Opens Meta's public Ad Library in a dedicated Puppeteer process and maps
 * at most 20 ads into the creative wizard listing.
 *
 * Isolated from catalog listing scraping (config/scraping.php and
 * scripts/scraping/*). Toggle via config('creative.competitor_ad_provider').
 */
class PuppeteerMetaAdLibraryProvider implements CompetitorAdProvider, AsyncCompetitorAdProvider
{
    public function search(string $query, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
    {
        return $this->runBlocking($this->keywordLibraryUrl($query, $country, $mediaType));
    }

    /**
     * Puppeteer Ad Library search is keyword-only. Page-ID lookup is not scraped.
     *
     * @param  array<int, string>  $pageIds
     */
    public function searchByPage(array $pageIds, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
    {
        return collect();
    }

    public function beginKeywordSearch(string $query, ?string $country = null, ?string $mediaType = null): string
    {
        return $this->startScrape($this->keywordLibraryUrl($query, $country, $mediaType));
    }

    public function collectRun(string $runId): ?Collection
    {
        if (preg_match('/^[A-Za-z0-9]+$/', $runId) !== 1) {
            throw new RuntimeException('Invalid Ad Library run id.');
        }

        $path = $this->outputPath($runId);

        if (! File::exists($path)) {
            return null;
        }

        $payload = json_decode(File::get($path), true);

        if (! is_array($payload)) {
            return null;
        }

        $status = strtoupper((string) ($payload['status'] ?? ''));

        if ($status === '' || $status === 'RUNNING') {
            return null;
        }

        if ($status !== 'SUCCEEDED') {
            throw new RuntimeException('Puppeteer Meta Ad Library search failed.');
        }

        return $this->adsFromPayload($payload);
    }

    private function runBlocking(string $url): Collection
    {
        $this->assertConfigured();

        $result = Process::timeout((int) config('creative.puppeteer_ad_library.timeout_seconds', 180))
            ->path(base_path())
            ->env($this->environment())
            ->run($this->command($url));

        $payload = json_decode($result->output(), true);

        if (! is_array($payload)) {
            throw new RuntimeException('Puppeteer Meta Ad Library search failed.');
        }

        return $this->adsFromPayload($payload);
    }

    /**
     * @param  array<string, mixed>  $payload
     * @return Collection<int, CompetitorAd>
     */
    private function adsFromPayload(array $payload): Collection
    {
        $status = strtoupper((string) ($payload['status'] ?? ''));

        if ($status !== 'SUCCEEDED') {
            throw new RuntimeException('Puppeteer Meta Ad Library search failed.');
        }

        $ads = $payload['ads'] ?? [];

        if (! is_array($ads)) {
            return collect();
        }

        return collect($ads)
            ->filter(fn (mixed $ad): bool => is_array($ad))
            ->reject(fn (array $ad): bool => $this->looksLikeIncompleteAd($ad))
            ->take(20)
            ->values()
            ->map(fn (array $ad): CompetitorAd => $this->mapAd($ad));
    }

    /**
     * Catalog/DPA GraphQL snapshots often store "{{product.brand}}" with no
     * media. After the listing DOM is merged in, those ads should have real
     * copy or a video poster. Drop leftovers that still have neither.
     *
     * @param  array<string, mixed>  $ad
     */
    private function looksLikeIncompleteAd(array $ad): bool
    {
        $text = trim((string) ($ad['body'] ?? '').(string) ($ad['title'] ?? ''));
        $hasMedia = filled(data_get($ad, 'images.0.resizedUrl'))
            || filled(data_get($ad, 'images.0.originalUrl'))
            || filled(data_get($ad, 'videos.0.previewImageUrl'))
            || filled(data_get($ad, 'videos.0.videoHdUrl'));

        return ($text === '' || str_contains($text, '{{')) && ! $hasMedia;
    }

    public function keywordLibraryUrl(string $query, ?string $country = null, ?string $mediaType = null): string
    {
        return $this->libraryUrl([
            'active_status' => 'active',
            'ad_type' => 'all',
            'country' => $this->resolvedCountry($country),
            'is_targeted_country' => 'false',
            'media_type' => $this->resolvedMediaType($mediaType),
            'q' => $query,
            'search_type' => 'keyword_unordered',
            'sort_data' => [
                'mode' => 'total_impressions',
                'direction' => 'desc',
            ],
        ]);
    }

    /**
     * @param  array<string, mixed>  $query
     */
    private function libraryUrl(array $query): string
    {
        return 'https://www.facebook.com/ads/library/?'.http_build_query($query);
    }

    private function startScrape(string $url): string
    {
        $this->assertConfigured();

        $runId = (string) Str::ulid();
        $outPath = $this->outputPath($runId);

        File::ensureDirectoryExists(dirname($outPath));
        File::put($outPath, json_encode([
            'status' => 'RUNNING',
            'ads' => [],
            'error' => null,
        ], JSON_THROW_ON_ERROR));

        ScrapeMetaAdLibraryJob::dispatch($url, $outPath);

        return $runId;
    }

    /**
     * Run the Node scrape in the queue worker, the same way catalog listing
     * scraping uses Process::run() — no extra Windows console.
     */
    public function executeQueuedScrape(string $url, string $outPath): void
    {
        $this->assertConfigured();

        $timeout = (int) config('creative.puppeteer_ad_library.timeout_seconds', 180);

        Process::timeout($timeout)
            ->path(base_path())
            ->env($this->environment())
            ->run($this->command($url, $outPath));

        if (! File::exists($outPath)) {
            File::put($outPath, json_encode([
                'status' => 'FAILED',
                'ads' => [],
                'error' => 'Puppeteer Meta Ad Library search failed.',
            ], JSON_THROW_ON_ERROR));

            return;
        }

        $payload = json_decode(File::get($outPath), true);
        $status = strtoupper((string) (is_array($payload) ? ($payload['status'] ?? '') : ''));

        if ($status === '' || $status === 'RUNNING') {
            File::put($outPath, json_encode([
                'status' => 'FAILED',
                'ads' => [],
                'error' => 'Puppeteer Meta Ad Library search did not return results.',
            ], JSON_THROW_ON_ERROR));
        }
    }

    /**
     * @return list<string>
     */
    private function command(string $url, ?string $outPath = null): array
    {
        $command = [
            (string) config('creative.puppeteer_ad_library.node_binary', 'node'),
            $this->scriptPath(),
            '--url',
            $url,
            '--max',
            (string) config('creative.puppeteer_ad_library.max_ads', 20),
        ];

        if ($outPath !== null) {
            $command[] = '--out';
            $command[] = $outPath;
        }

        return $command;
    }

    /**
     * @param  array<string, mixed>  $ad
     */
    private function mapAd(array $ad): CompetitorAd
    {
        $body = trim((string) ($ad['body'] ?? ''));
        $title = trim((string) ($ad['title'] ?? ''));
        $caption = trim((string) ($ad['linkDomain'] ?? $ad['caption'] ?? ''));
        $startDate = trim((string) ($ad['startDate'] ?? ''));

        $ad['ad_creative_bodies'] = $body !== '' ? [$body] : [];
        $ad['ad_creative_link_titles'] = $title !== '' ? [$title] : [];
        $ad['ad_creative_link_captions'] = $caption !== '' ? [$caption] : [];
        $ad['ad_snapshot_url'] = $ad['adLibraryUrl'] ?? null;
        $ad['ad_delivery_start_time'] = $startDate !== '' ? $startDate : null;

        $thumbnailUrl = data_get($ad, 'images.0.resizedUrl')
            ?? data_get($ad, 'images.0.originalUrl')
            ?? data_get($ad, 'videos.0.previewImageUrl');

        return new CompetitorAd(
            externalAdId: (string) ($ad['id'] ?? ''),
            platform: 'facebook',
            advertiserName: (string) ($ad['pageName'] ?? 'Unknown advertiser'),
            thumbnailUrl: is_string($thumbnailUrl) ? $thumbnailUrl : null,
            snapshot: $ad,
        );
    }

    private function resolvedCountry(?string $country): string
    {
        $normalized = strtoupper(trim((string) $country));

        return preg_match('/^[A-Z]{2}$/', $normalized) === 1 ? $normalized : 'US';
    }

    private function resolvedMediaType(?string $mediaType): string
    {
        $value = strtoupper(trim((string) $mediaType));

        return match ($value) {
            'VIDEO' => 'video',
            'IMAGE' => 'image',
            default => 'all',
        };
    }

    private function outputPath(string $runId): string
    {
        return storage_path('app/private/ad-library/'.$runId.'.json');
    }

    private function scriptPath(): string
    {
        $configured = (string) config('creative.puppeteer_ad_library.script', 'scripts/ad-library/scrape-ad-library.js');

        return Str::startsWith($configured, DIRECTORY_SEPARATOR) || preg_match('/^[A-Za-z]:\\\\/', $configured) === 1
            ? $configured
            : base_path($configured);
    }

    /**
     * @return array<string, string>
     */
    private function environment(): array
    {
        $environment = [];
        $headless = config('creative.puppeteer_ad_library.headless');

        if (filled($headless)) {
            $environment['AD_LIBRARY_PUPPETEER_HEADLESS'] = (string) $headless;
        }

        return $environment;
    }

    private function assertConfigured(): void
    {
        if (! config('creative.puppeteer_ad_library.enabled', true)) {
            throw new ProviderNotConfiguredException(
                'Puppeteer Ad Library scraping is disabled. Set AD_LIBRARY_PUPPETEER_ENABLED=true.'
            );
        }

        $script = $this->scriptPath();

        if (! is_file($script)) {
            throw new ProviderNotConfiguredException(
                "Puppeteer Ad Library script not found at [{$script}]."
            );
        }
    }
}
