<?php

namespace App\Campaigns;

/**
 * The pairings Meta enforces between an objective, what it optimises toward,
 * and the pixel event it counts.
 *
 * Kept in one place because getting a pair wrong is not a validation error you
 * can reason about from the message: sending PURCHASE under a leads objective
 * comes back as "conversion event unavailable", subcode 2446814.
 */
class Objectives
{
    /**
     * Five of Meta's six ODAX objectives.
     *
     * OUTCOME_APP_PROMOTION is deliberately absent. It needs a promoted app
     * object naming a store listing, there is no field for one anywhere in this
     * product, and an objective that cannot reach a publishable ad set is worse
     * as a button than as an absence.
     */
    public const SUPPORTED = [
        'OUTCOME_SALES', 'OUTCOME_LEADS', 'OUTCOME_TRAFFIC',
        'OUTCOME_ENGAGEMENT', 'OUTCOME_AWARENESS',
    ];

    private const OPTIMIZATION_GOAL = [
        'OUTCOME_SALES' => 'OFFSITE_CONVERSIONS',
        'OUTCOME_LEADS' => 'OFFSITE_CONVERSIONS',
        'OUTCOME_TRAFFIC' => 'LINK_CLICKS',
        'OUTCOME_ENGAGEMENT' => 'POST_ENGAGEMENT',
        'OUTCOME_AWARENESS' => 'REACH',
    ];

    private const CONVERSION_EVENT = [
        'OUTCOME_SALES' => 'PURCHASE',
        'OUTCOME_LEADS' => 'LEAD',
        'OUTCOME_TRAFFIC' => null,
        'OUTCOME_ENGAGEMENT' => null,
        'OUTCOME_AWARENESS' => null,
    ];

    /**
     * Pixel event names as Meta reports them, against the enum an ad set takes.
     *
     * Two different vocabularies for one thing: a pixel fires "CompleteRegistration"
     * and promoted_object.custom_event_type wants COMPLETE_REGISTRATION. Only the
     * standard events are here, because a custom event has no enum member and is
     * published as OTHER.
     */
    private const EVENT_TYPE = [
        'Purchase' => 'PURCHASE',
        'Lead' => 'LEAD',
        'CompleteRegistration' => 'COMPLETE_REGISTRATION',
        'AddToCart' => 'ADD_TO_CART',
        'AddToWishlist' => 'ADD_TO_WISHLIST',
        'InitiateCheckout' => 'INITIATED_CHECKOUT',
        'AddPaymentInfo' => 'ADD_PAYMENT_INFO',
        'ViewContent' => 'CONTENT_VIEW',
        'PageView' => 'PAGE_VIEW',
        'Search' => 'SEARCH',
        'Subscribe' => 'SUBSCRIBE',
        'StartTrial' => 'START_TRIAL',
        'Donate' => 'DONATE',
        'Contact' => 'CONTACT',
        'CustomizeProduct' => 'CUSTOMIZE_PRODUCT',
        'FindLocation' => 'FIND_LOCATION',
        'Schedule' => 'SCHEDULE',
        'SubmitApplication' => 'SUBMIT_APPLICATION',
    ];

    /**
     * The enum value for an event a pixel reported firing.
     *
     * A custom event is a real choice and becomes OTHER, which is what Meta
     * takes for anything outside its own list.
     */
    public static function eventType(string $pixelEventName): string
    {
        return self::EVENT_TYPE[$pixelEventName] ?? 'OTHER';
    }

    /** The pixel event name an enum value came from, for saying it back. */
    public static function eventName(?string $eventType): ?string
    {
        if ($eventType === null) {
            return null;
        }

        return array_search($eventType, self::EVENT_TYPE, true) ?: null;
    }

    /**
     * Whether the ad set names what it is promoting.
     *
     * A pixel objective names the pixel and the event; the rest name the Page.
     * REACH names neither: Meta rejects a promoted object against it, because
     * there is no object being optimised toward, only people being counted.
     *
     * @var array<string, bool>
     */
    private const NEEDS_PROMOTED_OBJECT = [
        'OFFSITE_CONVERSIONS' => true,
        'LINK_CLICKS' => true,
        'POST_ENGAGEMENT' => true,
        'REACH' => false,
    ];

    public const CALLS_TO_ACTION = [
        'SHOP_NOW', 'LEARN_MORE', 'SIGN_UP', 'GET_OFFER', 'SUBSCRIBE', 'CONTACT_US', 'BOOK_TRAVEL',
    ];

    /**
     * LOWEST_COST_WITHOUT_CAP spends the budget freely. COST_CAP holds an
     * average cost per result, which is how a target CPL or CPA is expressed.
     */
    public const BID_STRATEGIES = ['LOWEST_COST_WITHOUT_CAP', 'COST_CAP', 'LOWEST_COST_WITH_BID_CAP'];

    public const DEVICES = ['mobile', 'desktop'];

    public const PLACEMENTS = ['facebook', 'instagram', 'messenger', 'audience_network', 'threads'];

    /**
     * What a person calls it.
     *
     * A button reading OUTCOME_LEADS asks the reader to know Meta's vocabulary,
     * which is the thing offering choices is meant to remove. Falls back to the
     * raw value so a new objective is still usable before anyone names it.
     */
    public static function label(string $objective): string
    {
        return match ($objective) {
            'OUTCOME_SALES' => 'Sales',
            'OUTCOME_LEADS' => 'Leads',
            'OUTCOME_TRAFFIC' => 'Website Traffic',
            'OUTCOME_ENGAGEMENT' => 'Other',
            'OUTCOME_AWARENESS' => 'Brand Awareness',
            'OUTCOME_APP_PROMOTION' => 'App promotion',
            default => ucfirst(strtolower(str_replace(['OUTCOME_', '_'], ['', ' '], $objective))),
        };
    }

    /** One line on what picking it means, since the difference is not obvious. */
    public static function hint(string $objective): ?string
    {
        return match ($objective) {
            'OUTCOME_SALES' => 'Optimise for purchases. Needs a pixel.',
            'OUTCOME_LEADS' => 'Optimise for form fills and sign-ups. Needs a pixel.',
            'OUTCOME_TRAFFIC' => 'Optimise for clicks to the landing page.',
            'OUTCOME_ENGAGEMENT' => 'Optimise for reactions, comments and shares rather than clicks.',
            'OUTCOME_AWARENESS' => 'Optimise for how many people see it, not what they do next.',
            default => null,
        };
    }

    public static function supports(?string $objective): bool
    {
        return in_array($objective, self::SUPPORTED, true);
    }

    public static function optimizationGoal(string $objective): string
    {
        return self::OPTIMIZATION_GOAL[$objective] ?? 'LINK_CLICKS';
    }

    public static function conversionEvent(?string $objective): ?string
    {
        return self::CONVERSION_EVENT[$objective] ?? null;
    }

    /** Whether this optimisation goal takes a promoted object at all. */
    public static function needsPromotedObject(string $optimizationGoal): bool
    {
        return self::NEEDS_PROMOTED_OBJECT[$optimizationGoal] ?? true;
    }
}
