<?php

namespace App\Services\AI;

use App\Contracts\CompetitorAdProvider;
use App\DTOs\CompetitorAd;
use App\Exceptions\ProviderNotConfiguredException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;

/**
 * Searches Meta's public Ad Library (Graph API `/ads_archive`) for ads matching a
 * company/keyword query, for use as a competitor-ad reference in the creative wizard.
 */
class MetaAdLibraryProvider implements CompetitorAdProvider
{
    public function search(string $query, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
    {
        return $this->fetch([
            'search_terms' => $query,
            'ad_type' => 'ALL',
            'ad_reached_countries' => json_encode([$this->resolvedCountry($country)]),
            'languages' => json_encode(['en']),
            'search_type' => 'KEYWORD_EXACT_PHRASE',
            'media_type' => $this->resolvedMediaType($mediaType),
            'ad_active_status' => 'ACTIVE',
            'limit' => 50,
        ], $after);
    }

    /**
     * Preferred entry point when the advertiser's Page ID is already known
     * (e.g. resolved from a brand_pages lookup table). Returns only ads from
     * that exact Page — no keyword false positives, no impersonators.
     *
     * @param  array<int, string>  $pageIds  Up to 10 Facebook Page IDs.
     */
    public function searchByPage(array $pageIds, ?string $country = null, ?string $after = null, ?string $mediaType = null): Collection
    {
        $pageIds = $this->normalizedPageIds($pageIds);

        if ($pageIds === []) {
            return collect();
        }

        return $this->fetch([
            'search_page_ids' => json_encode($pageIds),
            'ad_type' => 'ALL',
            'ad_reached_countries' => json_encode([$this->resolvedCountry($country)]),
            'media_type' => $this->resolvedMediaType($mediaType),
            'ad_active_status' => 'ACTIVE',
            'limit' => 50,
        ], $after);
    }

    /**
     * @param  array<string, mixed>  $parameters
     */
    private function fetch(array $parameters, ?string $after): Collection
    {
        $after = $this->resolvedAfter($after);

        $parameters['fields'] = implode(',', [
            'id',
            'page_id',
            'page_name',
            'ad_creative_bodies',
            'ad_creative_link_titles',
            'ad_creative_link_captions',
            'ad_snapshot_url',
            'publisher_platforms',
            'ad_delivery_start_time',
            'br_total_reach',
            'estimated_audience_size',
            'eu_total_reach',
            'impressions',
            'total_reach_by_location',
        ]);

        if ($after !== null) {
            $parameters['after'] = $after;
        }

        $response = Http::baseUrl(config('services.meta.ad_library_url', 'https://graph.facebook.com/v26.0/ads_archive'))
            ->withToken($this->accessToken())
            ->acceptJson()
            ->get('', $parameters)
            ->throw();

        return collect($response->json('data', []))
            ->sortByDesc(fn (array $ad): int => $this->adStartTimestamp($ad))
            ->values()
            ->map(function (array $ad): CompetitorAd {
                $ad['ad_snapshot_url'] = $this->normalizedSnapshotUrl($ad);
                $pageId = $ad['page_id'] ?? null;

                return new CompetitorAd(
                    externalAdId: (string) ($ad['id'] ?? ''),
                    platform: $ad['publisher_platforms'][0] ?? 'facebook',
                    advertiserName: $ad['page_name'] ?? 'Unknown advertiser',
                    thumbnailUrl: $pageId
                        ? "https://graph.facebook.com/v26.0/{$pageId}/picture?type=large"
                        : null,
                    snapshot: $ad,
                );
            });
    }

    /**
     * @param  array<int, mixed>  $pageIds
     * @return list<string>
     */
    private function normalizedPageIds(array $pageIds): array
    {
        $normalized = [];

        foreach ($pageIds as $pageId) {
            $value = trim((string) $pageId);

            if (preg_match('/^\d+$/', $value) === 1) {
                $normalized[] = $value;
            }
        }

        return array_values(array_unique(array_slice($normalized, 0, 10)));
    }

    private function resolvedCountry(?string $country): string
    {
        foreach ([$country, request()->input('country'), request()->query('country')] as $candidate) {
            $normalized = $this->normalizeCountry($candidate);

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

        return $this->configuredCountry() ?? 'US';
    }

    private function configuredCountry(): ?string
    {
        $configured = config('services.meta.ad_library_countries');

        if (is_string($configured)) {
            $trimmed = trim($configured);

            if (str_starts_with($trimmed, '[')) {
                $decoded = json_decode($trimmed, true);

                return is_array($decoded) ? $this->normalizeCountry($decoded[0] ?? null) : null;
            }
        }

        return $this->normalizeCountry($configured);
    }

    private function normalizeCountry(mixed $value): ?string
    {
        if (is_array($value)) {
            $value = $value[0] ?? null;
        }

        $value = strtoupper(trim((string) $value));

        if (preg_match('/^[A-Z]{2}$/', $value) !== 1) {
            return null;
        }

        return $value;
    }

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

        return $value === 'VIDEO' ? 'VIDEO' : 'IMAGE';
    }

    private function resolvedAfter(?string $after): ?string
    {
        $value = trim((string) ($after ?? request()->input('after') ?? request()->query('after') ?? ''));

        return $value !== '' ? $value : null;
    }

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

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

        $timestamp = strtotime($startedAt);

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

    /**
     * A link to the ad that carries no credential.
     *
     * Meta returns `ad_snapshot_url` ending in a bare `&access_token`, expecting
     * the caller to fill in its own. This did: it appended
     * `services.meta.access_token`, the token every Meta write in the product
     * uses, and the result went into `CompetitorAdReference.snapshot` - a plain
     * `array` column - and out again as `<a href>` in the creative wizard. So the
     * token was written to the database in the clear, rendered into the page
     * source of anyone who opened the picker, and kept in their browser history
     * for as long as they kept it. That is not a snapshot URL, it is a
     * credential leak with a preview attached.
     *
     * The public Ad Library permalink shows the same ad and needs no token at
     * all, which is what `ApifyMetaAdLibraryProvider` and
     * `PuppeteerMetaAdLibraryProvider` already return in this field. Meta's own
     * archive id is `id`, so the link is built rather than borrowed.
     *
     * Anything arriving with a token already attached is stripped rather than
     * trusted, because the whole point is that this field never holds one.
     *
     * @param  array<string, mixed>  $ad
     */
    private function normalizedSnapshotUrl(array $ad): ?string
    {
        $archiveId = trim((string) ($ad['id'] ?? ''));

        if (preg_match('/^\d+$/', $archiveId) === 1) {
            return 'https://www.facebook.com/ads/library/?id='.$archiveId;
        }

        // No usable id, so fall back to whatever Meta gave us - with any
        // credential removed, including the bare `&access_token` this used to
        // complete.
        $snapshotUrl = self::withoutAccessToken(trim((string) ($ad['ad_snapshot_url'] ?? '')));

        if ($snapshotUrl === '' || preg_match('#^https?://#i', $snapshotUrl) !== 1) {
            return null;
        }

        return $snapshotUrl;
    }

    /**
     * Remove an access token from a URL, whether or not it has a value.
     *
     * Public and static because the same string may already be sitting in
     * `CompetitorAdReference.snapshot` from before this was fixed, and that row
     * is read back and rendered; see the model's accessor.
     */
    public static function withoutAccessToken(string $url): string
    {
        if (! str_contains($url, 'access_token')) {
            return $url;
        }

        $stripped = preg_replace('/([?&])access_token(=[^&#]*)?(?=&|#|$)/i', '$1', $url) ?? $url;

        // Tidy the separator the removal leaves behind: a trailing ? or &, or a
        // ?& where the token was first of several.
        $stripped = preg_replace('/[?&]+(#|$)/', '$1', $stripped) ?? $stripped;

        return preg_replace('/\?&+/', '?', $stripped) ?? $stripped;
    }

    private function accessToken(): string
    {
        $token = config('services.meta.access_token');

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

        return $token;
    }
}
