<?php

namespace App\Campaigns;

use App\Agent\Workspace;
use App\Models\Campaign;

/**
 * Decides whether a campaign may be published.
 *
 * Plain code on purpose. The agent may request publication; only this grants
 * it. Every reason is phrased so it can be shown to the user unchanged, because
 * the same list drives both the prompt and whatever the interface renders.
 */
class PublishGate
{
    public function __construct(private readonly Workspace $workspace) {}

    /**
     * @return list<string> reasons publishing is blocked; empty means allowed
     */
    public function check(Campaign $campaign): array
    {
        return array_values(array_filter([
            ...$this->basics($campaign),
            ...$this->audience($campaign),
            ...$this->ads($campaign),
            ...$this->account($campaign),
            ...$this->provenance($campaign),
        ]));
    }

    public function passes(Campaign $campaign): bool
    {
        return $this->check($campaign) === [];
    }

    /** @return list<string|null> */
    private function basics(Campaign $campaign): array
    {
        $ceiling = (float) config('platforms.guardrails.max_daily_budget');

        return [
            blank($campaign->name) ? 'campaign name not set' : null,

            blank($campaign->objective) ? 'objective not set' : null,

            match (true) {
                blank($campaign->landing_url) => 'landing page URL not set',
                ! filter_var($campaign->landing_url, FILTER_VALIDATE_URL) => 'landing page URL is not valid',
                default => null,
            },

            match (true) {
                blank($campaign->budget) => 'daily budget not set',
                (float) $campaign->budget < 5 => 'daily budget is below the 5.00 platform minimum',
                (float) $campaign->budget > $ceiling => sprintf(
                    'daily budget of %.2f is above the %.2f ceiling this product allows',
                    $campaign->budget,
                    $ceiling,
                ),
                default => null,
            },

            $campaign->bid_strategy
            && $campaign->bid_strategy !== 'LOWEST_COST_WITHOUT_CAP'
            && blank($campaign->bid_amount)
                ? 'bid strategy needs a target cost' : null,
        ];
    }

    /** @return list<string|null> */
    private function audience(Campaign $campaign): array
    {
        return [
            empty($campaign->locations['countries'] ?? []) ? 'target country not set' : null,
        ];
    }

    /** @return list<string|null> */
    private function ads(Campaign $campaign): array
    {
        $ads = $campaign->ads()->get();
        $limit = (int) config('platforms.guardrails.max_ads_per_publish');

        $problems = [
            $ads->isEmpty() ? 'no ad copy written yet' : null,
            $ads->count() > $limit
                ? sprintf('%d ads, but at most %d may be published at once', $ads->count(), $limit)
                : null,
        ];

        foreach ($ads as $index => $ad) {
            $label = $ads->count() > 1 ? 'ad '.($index + 1).': ' : '';

            $problems[] = blank($ad->headline) ? $label.'headline missing' : null;
            $problems[] = blank($ad->primary_text) ? $label.'primary text missing' : null;
            $problems[] = blank($ad->cta) ? $label.'call to action missing' : null;
            $problems[] = $this->assetReady($campaign, $ad) ? null : $label.'creative not uploaded to the platform yet';
        }

        return $problems;
    }

    /** @return list<string|null> */
    private function account(Campaign $campaign): array
    {
        return [
            blank($campaign->ad_account_id) ? 'ad account not verified' : null,
            blank($campaign->page_id) ? 'Page not selected' : null,
            $this->needsPixel($campaign) && blank($campaign->pixel_id)
                ? 'pixel not selected, and a conversion objective requires one' : null,
        ];
    }

    /**
     * A value guessed after a lookup failed must be confirmed before it can
     * spend money. This is the check that makes provenance worth recording.
     *
     * @return list<string>
     */
    private function provenance(Campaign $campaign): array
    {
        return collect($this->workspace->unconfirmed($campaign))
            ->map(fn (string $field): string => str_replace('_', ' ', $field).' was guessed and needs confirming')
            ->all();
    }

    private function needsPixel(Campaign $campaign): bool
    {
        return $campaign->objective !== null && $campaign->objective !== 'OUTCOME_TRAFFIC';
    }

    private function assetReady(Campaign $campaign, $ad): bool
    {
        return $ad->asset
            && $campaign->ad_account_id
            && $ad->asset->refFor($campaign->platform, $campaign->ad_account_id)?->reference();
    }
}
