<?php

namespace App\Campaigns\Platforms;

use App\Models\Campaign;

/**
 * Meta's rules, as the API actually enforces them.
 *
 * Most of these were learned from rejections rather than documentation, so the
 * reason each one exists is recorded next to it.
 */
class MetaRules implements PlatformRules
{
    public function key(): string
    {
        return 'meta';
    }

    public function name(): string
    {
        return 'Meta';
    }

    public function objectives(): array
    {
        return ['OUTCOME_SALES', 'OUTCOME_LEADS', 'OUTCOME_TRAFFIC'];
    }

    public function bidStrategies(): array
    {
        return ['LOWEST_COST_WITHOUT_CAP', 'COST_CAP', 'LOWEST_COST_WITH_BID_CAP'];
    }

    public function placements(): array
    {
        return ['facebook', 'instagram', 'messenger', 'audience_network', 'threads'];
    }

    public function callsToAction(): array
    {
        return ['SHOP_NOW', 'LEARN_MORE', 'SIGN_UP', 'GET_OFFER', 'SUBSCRIBE', 'CONTACT_US', 'BOOK_TRAVEL'];
    }

    public function minimumDailyBudget(): float
    {
        return 5.00;
    }

    public function optimizationGoal(string $objective): ?string
    {
        return match ($objective) {
            'OUTCOME_SALES', 'OUTCOME_LEADS' => 'OFFSITE_CONVERSIONS',
            'OUTCOME_TRAFFIC' => 'LINK_CLICKS',
            default => null,
        };
    }

    /**
     * Sending PURCHASE under a leads objective is rejected as "conversion event
     * unavailable" (subcode 2446814), so the event is derived rather than asked for.
     */
    public function conversionEvent(string $objective): ?string
    {
        return match ($objective) {
            'OUTCOME_SALES' => 'PURCHASE',
            'OUTCOME_LEADS' => 'LEAD',
            default => null,
        };
    }

    public function requiresPixel(string $objective): bool
    {
        return $this->conversionEvent($objective) !== null;
    }

    /** @return list<string> */
    public function additionalProblems(Campaign $campaign): array
    {
        $problems = [];

        // Naming a publisher platform switches off automatic placement, and Meta
        // then rejects the ad set unless positions within each platform are named
        // too. The spec fills those in, but only for platforms it knows.
        $unknown = array_diff((array) ($campaign->placements ?? []), $this->placements());

        if ($unknown !== []) {
            $problems[] = 'placement not supported on Meta: '.implode(', ', $unknown);
        }

        // A schedule that ends before it starts is accepted by our own checks but
        // rejected on create, after the campaign already exists.
        if ($campaign->starts_at && $campaign->ends_at && $campaign->ends_at->lte($campaign->starts_at)) {
            $problems[] = 'end date must be after the start date';
        }

        return $problems;
    }

    /**
     * Budget, bid and schedule can be updated in place. Creative cannot: Meta
     * treats an ad's creative as immutable, so changing copy or image means a
     * new creative and a new ad rather than an edit.
     *
     * @return list<string>
     */
    public function editableAfterPublish(): array
    {
        return ['name', 'budget', 'bid_strategy', 'bid_amount', 'starts_at', 'ends_at'];
    }
}
