<?php

namespace App\Campaigns\Platforms;

use App\Models\Campaign;

/**
 * What one advertising platform will and will not accept.
 *
 * Every platform disagrees about objectives, bid strategies, placements and
 * minimum spend, and getting a pairing wrong is not a validation error you can
 * reason about from the message: Meta answers an objective/event mismatch with
 * "conversion event unavailable", subcode 2446814.
 *
 * Declaring the rules per platform means the campaign builder never has to know
 * which platform it is building for, and a new platform becomes one class rather
 * than a search for every place a rule was assumed.
 */
interface PlatformRules
{
    /** The key used in config and on the campaign row, for example "meta". */
    public function key(): string;

    /** Human name, for messages shown to the user. */
    public function name(): string;

    /** @return list<string> objectives this platform accepts */
    public function objectives(): array;

    /**
     * This platform's name for an outcome the brief holds in plain words.
     *
     * The brief says "sales" because that is what survives across platforms;
     * Meta calls it OUTCOME_SALES and Google calls it SALES. Null when the
     * platform has no equivalent, which is how a suggestion drawn from the
     * landing page knows to stay quiet rather than offer something that would
     * be rejected.
     *
     * Translating here rather than at the call site: the suggestion was built
     * as 'OUTCOME_'.strtoupper($outcome), which is Meta's spelling hardcoded
     * into shared code and never matched anything on any other platform.
     */
    public function objectiveFor(string $outcome): ?string;

    /**
     * This platform's word for how the buyer wants to bid.
     *
     * The companion to objectiveFor(), and there for the same reason. One
     * intent is spelled LOWEST_COST_WITHOUT_CAP on Meta, MAXIMIZE_CLICKS on
     * Google and AUTOMATED_BID on LinkedIn, so a buyer who said "bid
     * automatically" once was asked to say it again in two more vocabularies.
     *
     * The intents are AUTOMATIC, TARGET_COST and BID_CAP: spend the budget as
     * well as you can, hold the cost per result near a number, or never pay
     * more than a number. Every platform in this product sells all three under
     * some name.
     *
     * The objective is passed because Google's answer depends on it: maximising
     * clicks and maximising conversions are the same intent aimed at different
     * results. Null when this platform cannot serve the intent, which must be
     * asked about rather than approximated.
     */
    public function bidStrategyFor(string $intent, ?string $objective = null): ?string;

    /**
     * How to put an objective in front of a person.
     *
     * A button reading OUTCOME_LEADS asks the reader to know the platform's
     * vocabulary, which is the thing offering choices exists to remove.
     *
     * @return array{label: string, hint: string}
     */
    public function describeObjective(string $objective): array;

    /**
     * The kinds of campaign this platform sells, if it sells more than one.
     *
     * Empty where the platform has no such concept, which is how the flow knows
     * not to ask. Meta is empty: an ad set carries its placements and that is
     * the whole of it. Google sells Search, Display and Performance Max as
     * different products, with different payloads and different assets, so the
     * answer decides what the rest of the questions even are.
     *
     * @return list<string>
     */
    public function campaignTypes(): array;

    /**
     * How to put a campaign type in front of a person.
     *
     * @return array{label: string, hint: string}
     */
    public function describeCampaignType(string $type): array;

    /**
     * Which type suits this campaign, and why.
     *
     * Separate from describeCampaignType() because the recommendation depends
     * on the campaign and the description does not. It used to be a fixed
     * `suggested` flag on Search, which is not a recommendation, it is a
     * default wearing one's clothes: an awareness campaign was being pointed at
     * the one network that sells intent rather than reach.
     *
     * The reason travels with it, so the option can say why rather than just
     * carrying a badge. Null when the platform has no types, or when nothing
     * about the campaign favours one yet.
     *
     * @return array{type: string, because: string}|null
     */
    public function suggestedCampaignType(?Campaign $campaign = null): ?array;

    /** @return list<string> bid strategies this platform accepts */
    public function bidStrategies(): array;

    /**
     * The bid strategies as buttons: value, label and the line that explains it.
     *
     * The words are the platform's own, because the difference between two
     * strategies is the whole decision and Meta's "LOWEST_COST_WITHOUT_CAP" and
     * Google's "MAXIMIZE_CONVERSIONS" do not describe themselves. Offered from
     * here rather than written into the tool, which used to show Meta's three
     * to every platform.
     *
     * @return list<array{value: string, label: string, hint: string, sends: string}>
     */
    public function bidStrategyOptions(): array;

    /**
     * Whether this strategy is meaningless without an amount to aim at.
     *
     * A target CPA needs the target. A "spend it all" strategy does not, and
     * asking for one produces a number the platform ignores. The publish gate
     * asked this as `!== 'LOWEST_COST_WITHOUT_CAP'`, which is Meta's answer
     * applied to everyone.
     */
    public function bidStrategyNeedsTarget(string $strategy): bool;

    /** @return list<string> placements this platform accepts */
    public function placements(): array;

    /** @return list<string> calls to action this platform accepts */
    public function callsToAction(): array;

    /**
     * Restricted categories this platform makes the advertiser declare.
     *
     * Empty when the platform has no such concept, which is how the publish
     * gate knows not to ask. On the contract rather than on the concrete
     * classes because the gate calls it for whatever platform it is handed.
     *
     * @return list<string>
     */
    public function specialAdCategories(): array;

    /**
     * How clicks are tagged, in the macros this platform actually fills in.
     *
     * One string served every platform, holding Meta's: utm_source=facebook,
     * utm_medium=paid_social and the {{campaign.name}} and {{ad.name}} macros
     * Meta substitutes at click time. A Google campaign was tagged with all of
     * it, so every click would have arrived claiming to come from facebook and
     * paid_social, with the braces passed through into the URL as text because
     * Google has never heard of them. Broken attribution on a live campaign,
     * and nothing would have said so.
     *
     * The "no tracking" option is not here: it means the same everywhere and
     * the tool adds it.
     *
     * @return list<array{value: string, label: string, hint: string, sends: string, badge?: string|null}>
     */
    public function trackingOptions(): array;

    /**
     * The account every campaign here runs on, when there is only one.
     *
     * Meta returns null: the system user token reaches many ad accounts and
     * which one to spend from is a real question. Google returns the configured
     * customer id, because it is one common internal account and there is
     * nothing to choose.
     *
     * A platform answering this has no ad account step: the id is set when the
     * campaign is created. Google used to have the step anyway, pointed at
     * list_ad_accounts, which serves Meta and refuses everything else, so the
     * flow asked "Which Google Ads account should this campaign run on?" with
     * nothing on screen and no answer the buyer could give.
     */
    public function defaultAdAccountId(): ?string;

    /** Smallest daily budget the platform will take, in whole currency units. */
    public function minimumDailyBudget(): float;

    /**
     * The ways this platform will actually take a budget.
     *
     * Was four hardcoded options written in Meta's vocabulary, offered on every
     * platform, so a Google campaign was asked to choose between an ad set
     * budget and "Advantage campaign budget" - Meta's name for a Meta feature,
     * on a platform with neither ad sets nor that feature. Three of the four
     * could not be published.
     *
     * A platform with one way of doing it returns one entry, and the question
     * is not asked at all: a choice between one thing is not a choice.
     *
     * Each entry is a choice row: value as "level:mode", plus label, hint and
     * the sentence clicking it sends.
     *
     * @return list<array{value: string, label: string, hint: string, sends: string}>
     */
    public function budgetModes(): array;

    /**
     * Longest each piece of ad copy may be.
     *
     * Over the limit the platform does not refuse the ad, it truncates it and
     * runs it, so nobody finds out until someone reads a live ad and sees a
     * headline ending mid-word.
     *
     * @return array<string, int> field name to character limit
     */
    public function copyLimits(): array;

    /**
     * Placements this objective can actually use.
     *
     * Not every placement works for every objective, and naming an impossible
     * pair is refused at the ad set, after the campaign already exists.
     *
     * @return list<string>
     */
    public function placementsFor(string $objective): array;

    /** What the platform optimises toward for an objective. */
    public function optimizationGoal(string $objective): ?string;

    /** The conversion event an objective counts, if any. */
    public function conversionEvent(string $objective): ?string;

    /** Whether this objective cannot run without a conversion pixel. */
    public function requiresPixel(string $objective): bool;

    /**
     * Whether an ad here needs an image or video uploaded to the platform.
     *
     * Every Meta ad is built around a creative, so an ad without one is
     * unpublishable. A Google responsive search ad is text: headlines,
     * descriptions and a URL, with no asset anywhere. The gate demanded an
     * uploaded creative for every ad on every platform, which is a block a
     * Search campaign can never clear.
     *
     * Takes the campaign because on Google it is not a property of the platform
     * at all: a Search ad is text and a Display ad is pictures, so the same
     * platform answers both ways depending on what is being built.
     */
    public function requiresCreativeAsset(?Campaign $campaign = null): bool;

    /**
     * Whether ads here run from a page the advertiser has to choose.
     *
     * Meta ads are published as a Facebook Page and cannot exist without one.
     * Google search ads have no equivalent, so demanding one is a block nobody
     * can clear: the publish gate asked every platform for a Page, and Google
     * campaigns reported "Page not selected" forever.
     */
    public function requiresPage(): bool;

    /**
     * Anything else this platform requires that the shared checks cannot know.
     *
     * Returned as reasons phrased for the user, empty when nothing is wrong.
     *
     * @return list<string>
     */
    public function additionalProblems(Campaign $campaign): array;

    /**
     * The steps this platform needs answering, in order.
     *
     * Only what is genuinely this platform's own. Everything a buyer would give
     * the same answer to whatever the platform - the page being advertised, the
     * money, the dates, the creatives - is asked once against the brief and
     * never appears here, which is what stops a second platform re-asking the
     * whole flow.
     *
     * The shape matches LaunchFlow's own steps: key, label, the tool that
     * settles it, whether calling that tool with no arguments puts options on
     * screen, and the sentence to ask when it does not.
     *
     * @return list<array{key: string, label: string, tool: string, offers: bool, ask?: string}>
     */
    public function steps(): array;

    /**
     * Whether this platform has what it needs to reach its API.
     *
     * A publisher class existing is not the same as the platform working. The
     * system prompt read config('platforms.publishers') and told the model
     * "Meta, Google Ads, and LinkedIn can be published to" on an environment
     * with no LinkedIn credentials at all, so the agent offered a platform that
     * would have failed at its first call with an auth error.
     *
     * Asked of the platform because only the platform knows: Meta needs a
     * system user token, Google a refresh token and a customer id, LinkedIn an
     * access token. It was a match statement inside StartCampaign, which is one
     * copy of that knowledge too many and the wrong place for it.
     *
     * Says nothing about whether publishing is allowed right now. That is
     * a separate switch, and a campaign still reaches the platform paused.
     */
    public function isConfigured(): bool;

    /**
     * Fields that can be changed on a live campaign.
     *
     * Everything else needs replacing rather than editing. On Meta an ad's
     * creative is immutable, so changing a headline means a new creative and a
     * new ad rather than an update.
     *
     * @return list<string>
     */
    public function editableAfterPublish(): array;

    /**
     * Supported image and video aspect ratios on this platform.
     *
     * @return list<string> e.g. ['1:1', '9:16', '1.91:1']
     */
    public function supportedAssetRatios(): array;
}
