<?php

namespace App\Campaigns\Targeting;

use App\Models\Brief;
use App\Models\Campaign;
use App\Models\Location;
use App\Models\PlatformLocation;
use App\Models\TargetingSet;
use Illuminate\Support\Facades\DB;
use RuntimeException;

class TargetingSelections
{
    /** @return list<string> */
    public function countries(Campaign $campaign): array
    {
        $set = $campaign->targetingSet()->first();

        if ($set === null) {
            return array_values((array) ($campaign->locations['countries'] ?? []));
        }

        if ($set->mode === 'clear') {
            return [];
        }

        if ($set->mode === 'inherit') {
            return $this->briefCountries($campaign->brief);
        }

        return $set->locations()
            ->where('kind', 'country')
            ->where('operation', 'include')
            ->orderBy('id')
            ->pluck('country_code')
            ->filter()
            ->values()
            ->all();
    }

    /** @param list<string> $codes */
    public function saveCampaignCountries(Campaign $campaign, array $codes, ?int $expectedRevision = null): TargetingSet
    {
        if ($campaign->isPublished()) {
            throw new RuntimeException('Location targeting cannot be changed on a published campaign. Create a new campaign for a different audience.');
        }

        return DB::transaction(function () use ($campaign, $codes, $expectedRevision): TargetingSet {
            $set = TargetingSet::query()->where('campaign_id', $campaign->getKey())->lockForUpdate()->first();

            if ($expectedRevision !== null && $expectedRevision !== ($set?->revision ?? 0)) {
                throw new RuntimeException('Targeting changed since it was opened. Review the latest selection and try again.');
            }

            $set ??= TargetingSet::query()->create([
                'campaign_id' => $campaign->getKey(),
                'platform' => $campaign->platform,
                'mode' => 'override',
                'revision' => 0,
                'schema_version' => 1,
            ]);

            $this->replaceCountries($set, $codes);
            $set->update(['mode' => 'override', 'revision' => $set->revision + 1]);
            $campaign->update(['locations' => array_replace((array) $campaign->locations, ['countries' => $codes])]);
            $campaign->unsetRelation('targetingSet');

            return $set;
        });
    }

    public function addCampaignLocation(Campaign $campaign, PlatformLocation $mapping, ?int $expectedRevision = null): TargetingSet
    {
        if ($campaign->isPublished()) {
            throw new RuntimeException('Location targeting cannot be changed on a published campaign. Create a new campaign for a different audience.');
        }

        $location = $mapping->location;
        $countries = $this->countries($campaign);

        if ($mapping->platform !== $campaign->platform
            || ! in_array($location->type, ['city', 'region'], true)
            || blank($mapping->external_id)
            || $mapping->status !== 'verified'
            || $mapping->verified_at === null
            || $mapping->verified_at->lt(now()->subDays(30))
            || ! in_array($location->country_code, $countries, true)) {
            throw new RuntimeException('Choose a verified city or region within this campaign’s selected countries.');
        }

        return DB::transaction(function () use ($campaign, $mapping, $location, $countries, $expectedRevision): TargetingSet {
            $set = TargetingSet::query()->where('campaign_id', $campaign->getKey())->lockForUpdate()->first();

            if ($expectedRevision !== null && $expectedRevision !== ($set?->revision ?? 0)) {
                throw new RuntimeException('Targeting changed since it was opened. Review the latest selection and try again.');
            }

            $set ??= TargetingSet::query()->create([
                'campaign_id' => $campaign->getKey(),
                'platform' => $campaign->platform,
                'mode' => 'override',
                'revision' => 0,
                'schema_version' => 1,
            ]);

            if ($set->wasRecentlyCreated || $set->mode !== 'override') {
                $this->replaceCountries($set, $countries);
            }

            $set->locations()->updateOrCreate(
                ['location_id' => $location->getKey(), 'operation' => 'include'],
                [
                    'platform_location_id' => $mapping->getKey(),
                    'kind' => $location->type,
                    'display_label' => $mapping->provider_name,
                    'country_code' => $location->country_code,
                ],
            );
            $set->update(['mode' => 'override', 'revision' => $set->revision + 1]);
            $campaign->unsetRelation('targetingSet');

            return $set;
        });
    }

    /** @return list<array{external_id: string, label: string, country_code: string|null}> */
    public function selectedCampaignLocations(Campaign $campaign, string $countryCode): array
    {
        return $campaign->targetingSet()->first()?->locations()
            ->where('kind', '!=', 'country')
            ->where('operation', 'include')
            ->where('country_code', $countryCode)
            ->with('platformLocation:id,external_id')
            ->orderBy('id')
            ->get()
            ->filter(fn ($selection): bool => $selection->platformLocation !== null)
            ->map(fn ($selection): array => [
                'external_id' => $selection->platformLocation->external_id,
                'label' => $selection->display_label,
                'country_code' => $selection->country_code,
            ])
            ->values()
            ->all() ?? [];
    }

    public function removeCampaignLocation(Campaign $campaign, string $countryCode, string $externalId, ?int $expectedRevision = null): TargetingSet
    {
        if ($campaign->isPublished()) {
            throw new RuntimeException('Location targeting cannot be changed on a published campaign. Create a new campaign for a different audience.');
        }

        return DB::transaction(function () use ($campaign, $countryCode, $externalId, $expectedRevision): TargetingSet {
            $set = TargetingSet::query()->where('campaign_id', $campaign->getKey())->lockForUpdate()->first()
                ?? throw new RuntimeException('This campaign has no selected city or region to remove.');

            if ($expectedRevision !== null && $expectedRevision !== $set->revision) {
                throw new RuntimeException('Targeting changed since it was opened. Review the latest selection and try again.');
            }

            $selection = $set->locations()
                ->where('kind', '!=', 'country')
                ->where('operation', 'include')
                ->where('country_code', $countryCode)
                ->whereHas('platformLocation', fn ($query) => $query->where('platform', $campaign->platform)->where('external_id', $externalId))
                ->first() ?? throw new RuntimeException('This city or region is not selected on the campaign.');

            $selection->delete();
            $set->update(['revision' => $set->revision + 1]);
            $campaign->unsetRelation('targetingSet');

            return $set;
        });
    }

    /** @return array{countries?: list<string>, cities?: list<array{key: string}>, regions?: list<array{key: string}>} */
    public function metaGeoLocations(Campaign $campaign): array
    {
        if ($campaign->platform !== 'meta') {
            throw new RuntimeException('Meta locations require a Meta campaign.');
        }

        $countries = $this->countries($campaign);
        $legacy = (array) ($campaign->locations ?? []);

        if (filled($legacy['cities'] ?? null) || filled($legacy['regions'] ?? null)) {
            throw new RuntimeException('Legacy city or region targeting must be reselected from Meta before publishing.');
        }

        if (filled($legacy['meta_geo_locations'] ?? null)) {
            throw new RuntimeException('Imported Meta location criteria must be reselected before publishing a new draft.');
        }

        $geo = [];
        $narrowedCountries = [];
        $set = $campaign->targetingSet()->first();

        if ($set !== null && $set->platform !== 'meta') {
            throw new RuntimeException('The targeting set does not belong to this Meta campaign.');
        }

        foreach ($set?->locations()->where('kind', '!=', 'country')->with('platformLocation.location')->get() ?? [] as $selection) {
            $mapping = $selection->platformLocation;

            if ($selection->operation !== 'include'
                || ! in_array($selection->kind, ['city', 'region'], true)
                || $selection->radius !== null
                || $mapping === null
                || $mapping->platform !== 'meta'
                || blank($mapping->external_id)
                || $mapping->status !== 'verified'
                || $mapping->verified_at === null
                || $mapping->verified_at->lt(now()->subDays(30))
                || $mapping->location_id !== $selection->location_id
                || $mapping->location->type !== $selection->kind
                || $mapping->location->country_code !== $selection->country_code
                || ! in_array($selection->country_code, $countries, true)) {
                throw new RuntimeException('A selected Meta city or region is invalid or out of date. Search and select it again before publishing.');
            }

            $geo[$selection->kind === 'city' ? 'cities' : 'regions'][] = ['key' => $mapping->external_id];
            $narrowedCountries[] = $selection->country_code;
        }

        $wholeCountries = array_values(array_diff($countries, $narrowedCountries));

        if ($wholeCountries !== []) {
            $geo = ['countries' => $wholeCountries, ...$geo];
        }

        if ($geo === []) {
            throw new RuntimeException('Choose at least one country or verified Meta city or region before publishing.');
        }

        return $geo;
    }

    /** @return array{countries: list<string>, geo_targets: list<string>} */
    public function googleGeoTargets(Campaign $campaign): array
    {
        if ($campaign->platform !== 'google') {
            throw new RuntimeException('Google locations require a Google campaign.');
        }

        $countries = $this->countries($campaign);
        $legacy = (array) ($campaign->locations ?? []);

        if (filled($legacy['cities'] ?? null) || filled($legacy['regions'] ?? null)) {
            throw new RuntimeException('Legacy city or region targeting must be reselected from Google before publishing.');
        }

        if (filled($legacy['google_location_criteria'] ?? null)) {
            throw new RuntimeException('Imported Google location criteria must be reselected before publishing a new draft.');
        }

        $targets = [];
        $narrowedCountries = [];
        $set = $campaign->targetingSet()->first();

        if ($set !== null && $set->platform !== 'google') {
            throw new RuntimeException('The targeting set does not belong to this Google campaign.');
        }

        foreach ($set?->locations()->where('kind', '!=', 'country')->with('platformLocation.location')->get() ?? [] as $selection) {
            $mapping = $selection->platformLocation;

            if ($selection->operation !== 'include'
                || ! in_array($selection->kind, ['city', 'region'], true)
                || $selection->radius !== null
                || $mapping === null
                || $mapping->platform !== 'google'
                || ! preg_match('/^geoTargetConstants\/[0-9]+$/D', $mapping->external_id)
                || $mapping->status !== 'verified'
                || $mapping->verified_at === null
                || $mapping->verified_at->lt(now()->subDays(30))
                || $mapping->location_id !== $selection->location_id
                || $mapping->location->type !== $selection->kind
                || $mapping->location->country_code !== $selection->country_code
                || ! in_array($selection->country_code, $countries, true)) {
                throw new RuntimeException('A selected Google city or region is invalid or out of date. Search and select it again before publishing.');
            }

            $targets[] = $mapping->external_id;
            $narrowedCountries[] = $selection->country_code;
        }

        return [
            'countries' => array_values(array_diff($countries, $narrowedCountries)),
            'geo_targets' => array_values(array_unique($targets)),
        ];
    }

    /** @return list<string> */
    public function locationProblems(Campaign $campaign): array
    {
        if ($campaign->isPublished()) {
            return [];
        }

        if ($campaign->platform === 'google') {
            try {
                $this->googleGeoTargets($campaign);

                return [];
            } catch (RuntimeException $exception) {
                return [$exception->getMessage()];
            }
        }

        if ($campaign->platform !== 'meta') {
            return $campaign->targetingSet()->first()?->locations()->where('kind', '!=', 'country')->exists()
                ? ['City or region targeting is not available for '.$campaign->platform.' yet.']
                : [];
        }

        try {
            $this->metaGeoLocations($campaign);

            return [];
        } catch (RuntimeException $exception) {
            return [$exception->getMessage()];
        }
    }

    /** @param list<string> $codes */
    public function rememberBriefCountries(Brief $brief, array $codes): void
    {
        if ($codes === [] || filled($brief->locations['countries'] ?? null)) {
            return;
        }

        DB::transaction(function () use ($brief, $codes): void {
            $set = TargetingSet::query()->firstOrCreate(
                ['brief_id' => $brief->getKey()],
                ['mode' => 'override', 'revision' => 1, 'schema_version' => 1],
            );
            $this->replaceCountries($set, $codes);
            $brief->update(['locations' => array_replace((array) $brief->locations, ['countries' => $codes])]);
        });
    }

    public function backfillBrief(Brief $brief): void
    {
        $codes = array_values((array) ($brief->locations['countries'] ?? []));

        if ($codes === [] || $brief->targetingSet()->exists()) {
            return;
        }

        DB::transaction(function () use ($brief, $codes): void {
            $set = TargetingSet::query()->create([
                'brief_id' => $brief->getKey(),
                'mode' => 'override',
                'revision' => 1,
                'schema_version' => 1,
            ]);
            $this->replaceCountries($set, $codes);
        });
    }

    public function backfillCampaign(Campaign $campaign): void
    {
        if ($campaign->targetingSet()->exists()) {
            return;
        }

        $rawLocations = $campaign->getRawOriginal('locations');
        $stored = is_string($rawLocations) ? json_decode($rawLocations, true, 512, JSON_THROW_ON_ERROR) : $rawLocations;
        $codes = array_values((array) ($stored['countries'] ?? []));

        if ($codes === [] && ($campaign->brief === null || blank($campaign->brief->locations['countries'] ?? null))) {
            return;
        }

        DB::transaction(function () use ($campaign, $codes): void {
            $set = TargetingSet::query()->create([
                'campaign_id' => $campaign->getKey(),
                'platform' => $campaign->platform,
                'mode' => $codes === [] ? 'inherit' : 'override',
                'revision' => 1,
                'schema_version' => 1,
            ]);

            if ($codes !== []) {
                $this->replaceCountries($set, $codes);
            }
        });
    }

    /** @return list<string> */
    private function briefCountries(?Brief $brief): array
    {
        if ($brief === null) {
            return [];
        }

        $set = $brief->targetingSet;

        if ($set === null) {
            return array_values((array) ($brief->locations['countries'] ?? []));
        }

        return $set->locations()
            ->where('kind', 'country')
            ->where('operation', 'include')
            ->orderBy('id')
            ->pluck('country_code')
            ->filter()
            ->values()
            ->all();
    }

    /** @param list<string> $codes */
    private function replaceCountries(TargetingSet $set, array $codes): void
    {
        $locationIds = [];

        foreach ($codes as $code) {
            $name = config('countries.'.$code);
            if (! is_string($name)) {
                throw new RuntimeException('Unrecognised country code: '.$code.'.');
            }

            $location = Location::query()->firstOrCreate(
                ['iso_code' => $code],
                ['type' => 'country', 'name' => $name, 'country_code' => $code, 'source' => 'iso_3166_1', 'source_key' => $code],
            );
            $locationIds[] = $location->getKey();

            $set->locations()->updateOrCreate(
                ['location_id' => $location->getKey(), 'operation' => 'include'],
                ['kind' => 'country', 'country_code' => $code, 'display_label' => $location->name],
            );
        }

        $set->locations()->where('kind', 'country')->where('operation', 'include')
            ->whereNotIn('location_id', $locationIds)->delete();
    }
}
