<?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);

        // The group the adapter made or reused, so rollback can remove it.
        // createCampaign() returns only the campaign id, so nothing here ever
        // knew a group had been created and rollback left it on the account.
        if ($groupId = $campaign->refresh()->external_ids['campaign_group_id'] ?? null) {
            $created['campaign_group_id'] = $groupId;
        }

        $created['campaign_id'] = $campaignId;
        $campaign->update(['external_campaign_id' => $campaignId]);

        $created['ads'] = [];

        // An ad that already has a creative on LinkedIn is left alone.
        //
        // This created one for every row every time, so a second publish - a
        // retry after a partial failure, or a creative swap that adds a row -
        // built a duplicate creative for each ad that was already live. LinkedIn
        // accepts that without complaint: two creatives, two ads, the same copy
        // twice on the same campaign, and the second id overwrote the first in our
        // own row so the original was left running with nothing pointing at it.
        //
        // Read from the database rather than a loaded relation, because the rows
        // were written by the previous publish and a relation loaded before it
        // would still say they were drafts.
        foreach ($campaign->ads()->orderBy('id')->get() as $ad) {
            if (filled($ad->external_creative_id)) {
                continue;
            }

            $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,
            ];
        }

        // Nothing is started here.
        //
        // This called $this->adapter->publish(), which sets the campaign ACTIVE,
        // so on LinkedIn alone publishing meant going live - and the only reason it
        // did not was a switch rewriting the status further down. Meta and Google
        // have never done this. A campaign is built and parked on every platform,
        // and starting it is its own approved decision.
        return $created;
    }

    /**
     * Undo whatever was created before the failure.
     *
     * @param  array<string,mixed>  $created
     */
    public function rollBack(Campaign $campaign, array $created, string $actor): void
    {
        // Only the ads this publish created.
        //
        // This walked every ad on the campaign and reset any that looked
        // published, which on a campaign that had published successfully before
        // meant clearing the ids of live LinkedIn creatives this attempt never
        // touched. The creative kept running and nothing local pointed at it any
        // more: not editable, not removable, not visible as ours. Acting on the
        // campaign's current state instead of on what this attempt actually did.
        //
        // $created is the record of this attempt, so it is the only thing here
        // entitled to be undone.
        $rows = collect((array) ($created['ads'] ?? []))
            ->pluck('ad_row')
            ->filter()
            ->all();

        if ($rows !== []) {
            $campaign->ads()->whereIn('id', $rows)->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]);
        }

        // The group last, because the campaign hangs off it, and only one this
        // publish actually created. A group reused from an earlier successful
        // publish is not ours to remove.
        if (! empty($created['campaign_group_id'])) {
            $this->remove($adAccountId, 'campaign_group', $created['campaign_group_id'], $campaign);

            $campaign->update([
                'external_ids' => collect((array) ($campaign->external_ids ?? []))
                    ->except('campaign_group_id')
                    ->all(),
            ]);
        }
    }

    /**
     * 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);
    }

    /**
     * Start the group, the campaign and every creative under it.
     *
     * Delegated to the adapter for the same reason create() is: the payload shapes
     * and the three different words LinkedIn uses for "live" are platform detail.
     * The creative ids come from our own rows rather than from the account, so a
     * creative we did not build is not switched on by us.
     */
    public function activate(Campaign $campaign, string $actor): void
    {
        $this->adapter->activate($campaign, $campaign->ads()
            ->whereNotNull('external_creative_id')
            ->pluck('external_creative_id')
            ->map(strval(...))
            ->all());
    }

    /** And stop it, at the campaign. */
    public function pause(Campaign $campaign, string $actor): void
    {
        $this->adapter->pause($campaign);
    }
}
