<?php

namespace App\Services\Meta;

use InvalidArgumentException;

/** One ad creative: an image or a video, a destination, and the words around it. */
readonly class CreativeSpec
{
    /**
     * @param  string|null  $imageHash  the hash Meta returned for an uploaded image
     * @param  string|null  $videoId  the id Meta returned for an uploaded video
     * @param  string|null  $thumbnailUrl  a still from the video, which Meta requires
     */
    public function __construct(
        public string $adAccountId,
        public string $pageId,
        public string $landingUrl,
        public string $headline,
        public string $primaryText,
        public string $callToAction,
        public string $name,
        public ?string $imageHash = null,
        public ?string $videoId = null,
        public ?string $thumbnailUrl = null,
        public string $description = '',
        public ?string $instagramUserId = null,
    ) {
        // A creative is one thing or the other. Neither meant a null image hash
        // reached Meta as the word "null"; both would be ambiguous. Refusing
        // here keeps the failure at the point the mistake was made rather than
        // after a campaign and ad set already exist on the account.
        if (blank($imageHash) === blank($videoId)) {
            throw new InvalidArgumentException(
                'An ad creative needs exactly one of an image hash or a video id.'
            );
        }

        if (filled($videoId) && blank($thumbnailUrl)) {
            throw new InvalidArgumentException(
                'A video creative needs a thumbnail. Meta refuses video_data without one.'
            );
        }
    }

    public function isVideo(): bool
    {
        return filled($this->videoId);
    }

    /**
     * The Graph API shape.
     *
     * Video is not a variation on the image payload, it is a different one.
     * Meta names the same ideas differently inside video_data: the headline is
     * title rather than name, the description is link_description, and the
     * destination moves into the call to action because there is no link field
     * to put it in.
     */
    public function storySpec(): array
    {
        return [
            'page_id' => $this->pageId,
            $this->isVideo() ? 'video_data' : 'link_data' => $this->isVideo()
                ? $this->videoData()
                : $this->linkData(),
        ];
    }

    /** @return array<string, mixed> */
    private function linkData(): array
    {
        return [
            'image_hash' => $this->imageHash,
            'link' => $this->landingUrl,
            'message' => $this->primaryText,
            'name' => $this->headline,
            'description' => $this->description,
            'call_to_action' => [
                'type' => $this->callToAction,
                'value' => ['link' => $this->landingUrl],
            ],
        ];
    }

    /** @return array<string, mixed> */
    private function videoData(): array
    {
        return array_filter([
            'video_id' => $this->videoId,
            'image_url' => $this->thumbnailUrl,
            'message' => $this->primaryText,
            'title' => $this->headline,
            'link_description' => $this->description,
            'call_to_action' => [
                'type' => $this->callToAction,
                'value' => ['link' => $this->landingUrl],
            ],
        ], fn (mixed $value): bool => $value !== '' && $value !== null);
    }

    /** The MCP shape, which is flat and uses different names for the same things. */
    public function mcpArguments(): array
    {
        return array_filter([
            'ad_account_id' => ltrim(str_replace('act_', '', $this->adAccountId), '_'),
            'page_id' => $this->pageId,
            'image_hash' => $this->imageHash,
            'video_id' => $this->videoId,
            'thumbnail_url' => $this->thumbnailUrl,
            'link_url' => $this->landingUrl,
            'message' => $this->primaryText,
            'headline' => $this->headline,
            'description' => $this->description,
            'call_to_action_type' => $this->callToAction,
            'name' => $this->name,
            ...$this->instagramFields(),
        ], fn (mixed $value): bool => $value !== null);
    }

    /**
     * Who the ad appears as on Instagram.
     *
     * Sits beside object_story_spec rather than inside it, which is why it is
     * returned separately. Omitted entirely when there is no account, because
     * Meta reads a null as an explicit unset and rejects it.
     *
     * @return array<string, string>
     */
    public function instagramFields(): array
    {
        return $this->instagramUserId ? ['instagram_user_id' => $this->instagramUserId] : [];
    }
}
