<?php

namespace App\Campaigns\Publishing;

use App\Models\Campaign;
use App\Services\Platforms\LinkedInAdapter;
use Illuminate\Support\Facades\Log;
use Throwable;

/**
 * LinkedIn's implementation of campaign publishing.
 */
class LinkedInPublisher implements PlatformPublisher
{
    public function __construct(
        private readonly LinkedInAdapter $adapter,
    ) {}

    /**
     * @param  array<string,mixed>  $created
     * @return array<string,mixed>
     */
    public function create(Campaign $campaign, string $actor, array &$created): array
    {
        $campaignId = $this->adapter->createCampaign($campaign);
        $created['campaign_id'] = $campaignId;
        $campaign->update(['external_campaign_id' => $campaignId]);

        $created['ads'] = [];

        foreach ($campaign->ads()->orderBy('id')->get() as $ad) {
            $creativeId = $this->adapter->createAd($campaign, $ad);
            $ad->update([
                'external_creative_id' => $creativeId,
                'external_ad_id' => $creativeId,
                'status' => 'published',
            ]);
            $created['ads'][] = [
                'ad_row' => $ad->id,
                'creative_id' => $creativeId,
                'ad_id' => $creativeId,
            ];
        }

        $this->adapter->publish($campaign);

        return $created;
    }

    /**
     * Undo whatever was created before the failure.
     *
     * @param  array<string,mixed>  $created
     */
    public function rollBack(Campaign $campaign, array $created, string $actor): void
    {
        foreach ($campaign->ads()->get() as $ad) {
            if ($ad->status === 'published' || $ad->external_creative_id) {
                $ad->update([
                    'external_creative_id' => null,
                    'external_ad_id' => null,
                    'status' => 'draft',
                ]);
            }
        }

        // Remove what was created, newest first.
        //
        // This was an empty try block with a comment saying cleanup "could" go
        // here, so a publish that failed on ad two of three left a real
        // LinkedIn campaign and a real creative behind while our rows went back
        // to draft. The record said nothing had happened and the account said
        // otherwise, and external_campaign_id stayed set, so a retry
        // short-circuited at createCampaign() and never rebuilt the rest.
        //
        // Creatives before the campaign, because a campaign cannot be removed
        // while things hang off it. Each failure is logged and the next one
        // still tried: a rollback that stops at the first error leaves more
        // behind than one that keeps going.
        $adAccountId = $campaign->ad_account_id ?: config('platforms.linkedin.ad_account_id');

        if (blank($adAccountId)) {
            return;
        }

        foreach (array_reverse((array) ($created['ads'] ?? [])) as $ad) {
            $this->remove($adAccountId, 'creative', $ad['creative_id'] ?? null, $campaign);
        }

        if (! empty($created['campaign_id'])) {
            $this->remove($adAccountId, 'campaign', $created['campaign_id'], $campaign);

            $campaign->update(['external_campaign_id' => null]);
        }
    }

    /**
     * Delete one thing, and carry on if it will not go.
     *
     * Reported rather than swallowed: something left on the account is worth
     * knowing about, because the next attempt will build alongside it.
     */
    private function remove(string $adAccountId, string $type, ?string $id, Campaign $campaign): void
    {
        if (blank($id)) {
            return;
        }

        try {
            $this->adapter->delete($adAccountId, $type, $id);
        } catch (Throwable $e) {
            Log::warning('LinkedIn rollback left something behind', [
                'campaign' => $campaign->id,
                'type' => $type,
                'id' => $id,
                'error' => $e->getMessage(),
            ]);
        }
    }

    /**
     * Push live edits to LinkedIn.
     *
     * @param  array<string, array{from: mixed, to: mixed}>  $changes
     */
    /**
     * Send approved changes to the live campaign.
     *
     * This was an empty body under the comment "Live updates for LinkedIn".
     * CampaignUpdater moves its snapshot forward on a clean return, so every
     * edit to a published LinkedIn campaign was recorded as applied and none of
     * them was sent: the budget, the bid and the dates on LinkedIn stayed as
     * first published while our rows and the chat said they had changed. A
     * buyer raising a daily budget saw the new number everywhere except where
     * the money is spent.
     *
     * The work is the adapter's, which already knows how to shape a budget and
     * a bid for LinkedIn and is where create() builds the same fields.
     *
     * @param  array<string, array{from: mixed, to: mixed}>  $changes
     */
    public function update(Campaign $campaign, array $changes, string $actor): void
    {
        if ($changes === []) {
            return;
        }

        $this->adapter->updateCampaign($campaign, $changes);
    }
}
