<?php

namespace App\Services\Meta;

use DateTimeInterface;

/**
 * Everything a campaign needs, in one place.
 *
 * Campaign creation used to be four scalars, which was enough while the only
 * thing a campaign carried was a name and an objective. Advantage campaign
 * budget puts the money here instead of on the ad set, so the two transports
 * now have a real payload to build and need to build the same one, which is
 * what AdSetSpec already does for the level below.
 */
readonly class CampaignSpec
{
    /**
     * @param  list<string>  $specialAdCategories  the advertiser's declaration
     * @param  string  $budgetLevel  'adset' or 'campaign'
     * @param  string  $budgetMode  'daily' or 'lifetime'
     * @param  int  $budgetMinorUnits  ignored unless the budget sits on the campaign
     */
    public function __construct(
        public string $adAccountId,
        public string $name,
        public string $objective,
        public array $specialAdCategories = [],
        public string $budgetLevel = 'adset',
        public string $budgetMode = 'daily',
        public int $budgetMinorUnits = 0,
        public string $bidStrategy = 'LOWEST_COST_WITHOUT_CAP',
        public ?DateTimeInterface $endsAt = null,
    ) {}

    public function holdsTheBudget(): bool
    {
        return $this->budgetLevel === 'campaign';
    }

    /**
     * Meta takes an empty list to mean no special category, so our own NONE
     * answer is translated here rather than sent.
     *
     * @return list<string>
     */
    public function declaredCategories(): array
    {
        return array_values(array_filter(
            $this->specialAdCategories,
            fn (string $category): bool => $category !== 'NONE',
        ));
    }

    /**
     * The budget fields, empty unless the campaign is the one holding it.
     *
     * bid_strategy comes with the budget rather than being optional: Meta
     * rejects a campaign-level budget without one, because with the money at
     * this level there is no ad set strategy for it to fall back to.
     *
     * A lifetime budget is a total to spend by a date, so the date is part of
     * the same statement. The publish gate refuses a lifetime budget with no
     * end date rather than letting Meta explain it after the campaign exists.
     *
     * @return array<string, mixed>
     */
    public function budgetFields(): array
    {
        if (! $this->holdsTheBudget() || $this->budgetMinorUnits <= 0) {
            return [];
        }

        $lifetime = $this->budgetMode === 'lifetime';

        return array_filter([
            $lifetime ? 'lifetime_budget' : 'daily_budget' => $this->budgetMinorUnits,
            'bid_strategy' => $this->bidStrategy,
            'stop_time' => $lifetime ? $this->endsAt?->format(DateTimeInterface::ATOM) : null,
        ], fn (mixed $value): bool => $value !== null);
    }

    /**
     * Always false, and not the Advantage campaign budget switch.
     *
     * It reads like one, and this returned 'true' for a campaign budget until a
     * validate_only run said otherwise: "You cannot use ad set budget sharing
     * with campaign budget" (subcode 4834002). Ad set budget sharing is a
     * separate feature that lets ad sets lend each other budget, and it is
     * incompatible with holding the budget on the campaign.
     *
     * What actually makes a campaign use Advantage campaign budget is simply
     * having daily_budget or lifetime_budget on the campaign. Omitting this
     * field is not an option either: without it a campaign whose budget sits on
     * the ad set fails with subcode 4834011.
     */
    public function budgetSharing(): string
    {
        return 'false';
    }
}
