<?php

namespace App\Services\AI;

use App\Contracts\AsyncCompetitorAdProvider;
use App\Contracts\CompetitorAdProvider;
use App\DTOs\CompetitorAd;
use App\Exceptions\ProviderNotConfiguredException;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Sleep;
use RuntimeException;

/**
 * Searches Meta's public Ad Library via the Apify `sukhdipp/meta-ad-library-scraper`
 * actor, for use as a competitor-ad reference in the creative wizard.
 *
 * Unlike the official Graph API `ads_archive` endpoint, this covers ordinary
 * commercial ads (not just political/EU-reaching ones), needs no app review,
 * and supports matching by advertiser domain to avoid same-name false positives.
 */
class ApifyMetaAdLibraryProvider implements CompetitorAdProvider, AsyncCompetitorAdProvider
{
    private const ACTOR_ID = 'sukhdipp~meta-ad-library-scraper';

    /**
     * Keyword search. Prefer searchByPage()/searchByBrandDomain() when you
     * can resolve the brand's Page ID or domain — keyword search alone can
     * match unrelated advertisers (dynamic template ads, resellers, etc.).
     *
     * @param  array<int, string>|null  $linkDomains  Restrict results to these landing domains.
     */
    public function search(
        string $query,
        ?string $country = null,
        ?string $after = null,
        ?string $mediaType = null,
        ?array $linkDomains = null,
        string $sortBy = 'longest_running',
    ): Collection {
        $runId = $this->beginKeywordSearch($query, $country, $mediaType, $linkDomains);

        return $this->waitForRun($runId, $sortBy);
    }

    /**
     * Preferred entry point: search by the brand's own website domain to avoid
     * same-name impostor pages (resellers, dropshippers, unrelated local businesses).
     */
    public function searchByBrandDomain(
        string $brandName,
        string $domain,
        ?string $country = null,
        ?string $mediaType = null,
        string $sortBy = 'longest_running',
    ): Collection {
        $runId = $this->startRun([
            'searchTerms' => [$brandName],
            'advertiserDomains' => [$domain],
            'countries' => [$this->resolvedCountry($country)],
            'activeStatus' => 'active',
            'adType' => 'all',
            'mediaType' => $this->resolvedMediaType($mediaType),
            'maxAds' => 20,
        ]);

        return $this->waitForRun($runId, $sortBy);
    }

    /**
     * Search by known Facebook Page ID(s) — most precise option when you already
     * have the page resolved (e.g. from your brand_pages lookup table).
     *
     * @param  array<int, string>  $pageIds
     */
    public function searchByPage(
        array $pageIds,
        ?string $country = null,
        ?string $after = null,
        ?string $mediaType = null,
        string $sortBy = 'longest_running',
    ): Collection {
        $runId = $this->beginPageSearch($pageIds, $country, $mediaType);

        return $this->waitForRun($runId, $sortBy);
    }

    /**
     * @param  array<int, string>|null  $linkDomains
     */
    public function beginKeywordSearch(
        string $query,
        ?string $country = null,
        ?string $mediaType = null,
        ?array $linkDomains = null,
    ): string {
        $input = [
            'searchTerms' => [$query],
            'countries' => [$this->resolvedCountry($country)],
            'activeStatus' => 'active',
            'adType' => 'all',
            'mediaType' => $this->resolvedMediaType($mediaType),
            'maxAds' => 20,
            // Requires the query to actually appear in the ad copy, not just
            // somewhere in Meta's index (e.g. a dynamic product-catalog field).
            'bodyContains' => [$query],
        ];

        if ($linkDomains !== null && $linkDomains !== []) {
            $input['linkDomains'] = array_values($linkDomains);
        }

        return $this->startRun($input);
    }

    /**
     * @param  array<int, string>  $pageIds
     */
    public function beginPageSearch(array $pageIds, ?string $country = null, ?string $mediaType = null): string
    {
        $pageIds = array_values(array_filter(
            array_map(fn ($id): string => trim((string) $id), $pageIds),
            fn (string $id): bool => preg_match('/^\d+$/', $id) === 1,
        ));

        if ($pageIds === []) {
            throw new RuntimeException('A numeric Facebook Page ID is required.');
        }

        return $this->startRun([
            'pageIds' => array_slice($pageIds, 0, 10),
            'countries' => [$this->resolvedCountry($country)],
            'activeStatus' => 'active',
            'adType' => 'all',
            'mediaType' => $this->resolvedMediaType($mediaType),
            'maxAds' => 20,
        ]);
    }

    /**
     * @return Collection<int, CompetitorAd>|null  null while the Apify run is still in progress
     */
    public function collectRun(string $runId, string $sortBy = 'longest_running'): ?Collection
    {
        $run = $this->apify()
            ->get('/actor-runs/'.$runId)
            ->throw()
            ->json('data', []);

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

        if (in_array($status, ['READY', 'RUNNING', 'TIMING-OUT', 'ABORTING'], true)) {
            return null;
        }

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

        $datasetId = trim((string) ($run['defaultDatasetId'] ?? ''));

        if ($datasetId === '') {
            return collect();
        }

        $items = $this->apify()
            ->get('/datasets/'.$datasetId.'/items')
            ->throw()
            ->json();

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

        $ads = collect($items)
            ->filter(fn (mixed $ad): bool => is_array($ad))
            ->reject(fn (array $ad): bool => $this->looksLikeTemplateGarbage($ad))
            ->values();

        return $this->sortedAds($ads, $sortBy)
            ->map(fn (array $ad): CompetitorAd => $this->mapAd($ad));
    }

    /**
     * @param  Collection<int, array<string, mixed>>  $ads
     * @return Collection<int, array<string, mixed>>
     */
    private function sortedAds(Collection $ads, string $sortBy): Collection
    {
        return match ($sortBy) {
            'latest' => $ads
                ->sortByDesc(fn (array $ad): int => $this->startTimestamp($ad))
                ->values(),
            'oldest' => $ads
                ->sortBy(fn (array $ad): int => $this->startTimestamp($ad) ?: PHP_INT_MAX)
                ->values(),
            'scaling' => $ads
                ->sortByDesc(fn (array $ad): int => (int) ($ad['variantCount'] ?? 0))
                ->values(),
            // "longest_running" — best available public proxy for a winning ad,
            // since real impression/spend numbers aren't published for ordinary
            // commercial ads outside the EU/UK.
            default => $ads
                ->sortByDesc(fn (array $ad): int => (int) ($ad['totalActiveDays'] ?? 0))
                ->values(),
        };
    }

    /**
     * @param  array<string, mixed>  $ad
     */
    private function startTimestamp(array $ad): int
    {
        $startDate = trim((string) ($ad['startDate'] ?? ''));

        if ($startDate === '') {
            return 0;
        }

        $timestamp = strtotime($startDate);

        return $timestamp === false ? 0 : $timestamp;
    }

    /**
     * Drops ads that are unrendered dynamic-template/catalog placeholders
     * (e.g. "{{product.brand}}") — these slip through keyword search because
     * Meta appears to match against catalog metadata, not just visible copy.
     *
     * @param  array<string, mixed>  $ad
     */
    private function looksLikeTemplateGarbage(array $ad): bool
    {
        $text = trim((string) ($ad['body'] ?? '').(string) ($ad['title'] ?? ''));

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

    /**
     * @param  array<string, mixed>  $input
     */
    private function startRun(array $input): string
    {
        $run = $this->apify()
            ->post('/acts/'.self::ACTOR_ID.'/runs', $input)
            ->throw()
            ->json('data', []);

        $runId = trim((string) ($run['id'] ?? ''));

        if ($runId === '') {
            throw new RuntimeException('Apify did not return a run id.');
        }

        return $runId;
    }

    private function waitForRun(string $runId, string $sortBy = 'longest_running'): Collection
    {
        for ($attempt = 0; $attempt < 180; $attempt++) {
            $results = $this->collectRun($runId, $sortBy);

            if ($results !== null) {
                return $results;
            }

            Sleep::usleep(250_000);
        }

        throw new RuntimeException('Apify Meta Ad Library search timed out.');
    }

    private function apify(): PendingRequest
    {
        return Http::baseUrl('https://api.apify.com/v2')
            ->withQueryParameters(['token' => $this->apifyToken()])
            ->acceptJson()
            ->asJson()
            ->connectTimeout(5)
            ->timeout(10);
    }

    /**
     * @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] : array_values((array) ($ad['creativeBodies'] ?? []));
        $ad['ad_creative_link_titles'] = $title !== '' ? [$title] : array_values((array) ($ad['creativeTitles'] ?? []));
        $ad['ad_creative_link_captions'] = $caption !== '' ? [$caption] : [];
        $ad['ad_snapshot_url'] = $ad['adLibraryUrl'] ?? null;
        $ad['ad_delivery_start_time'] = $startDate !== '' ? $startDate : null;

        $platform = $ad['platforms'][0] ?? 'facebook';
        $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: is_string($platform) ? strtolower($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 apifyToken(): string
    {
        $token = config('services.apify.token');

        if (blank($token)) {
            throw new ProviderNotConfiguredException(
                'Apify token is not configured. Set APIFY_TOKEN in .env.'
            );
        }

        return $token;
    }
}
