<?php

namespace App\Campaigns\Platforms;

use App\Models\Campaign;
use App\Models\PlatformConnection;

/**
 * LinkedIn's rules, as the LinkedIn Campaign Manager API enforces them.
 */
class LinkedInRules implements PlatformRules
{
    public function key(): string
    {
        return 'linkedin';
    }

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

    /**
     * @return list<string>
     */
    public function objectives(): array
    {
        return [
            'WEBSITE_VISITS',
            'LEAD_GENERATION',
            'BRAND_AWARENESS',
            'ENGAGEMENT',
            'VIDEO_VIEWS',
            'JOB_APPLY',
            'WEBSITE_CONVERSIONS',
        ];
    }

    public function objectiveFor(string $outcome): ?string
    {
        return match (strtoupper($outcome)) {
            'SALES', 'CONVERSIONS', 'CONVERSION' => 'WEBSITE_CONVERSIONS',
            'LEADS', 'LEAD' => 'LEAD_GENERATION',
            'TRAFFIC', 'VISITS' => 'WEBSITE_VISITS',
            'AWARENESS', 'REACH' => 'BRAND_AWARENESS',
            'ENGAGEMENT' => 'ENGAGEMENT',
            'VIEWS', 'VIDEO' => 'VIDEO_VIEWS',
            'JOB_APPLY', 'JOBS' => 'JOB_APPLY',
            default => null,
        };
    }

    /**
     * @return array{label: string, hint: string}
     */
    public function describeObjective(string $objective): array
    {
        return match ($objective) {
            'WEBSITE_VISITS' => ['label' => 'Website visits', 'hint' => 'Drive traffic to your landing page or website.'],
            'LEAD_GENERATION' => ['label' => 'Lead generation', 'hint' => 'Capture leads using pre-filled LinkedIn forms.'],
            'BRAND_AWARENESS' => ['label' => 'Brand awareness', 'hint' => 'Reach the maximum number of relevant professionals.'],
            'ENGAGEMENT' => ['label' => 'Engagement', 'hint' => 'Increase clicks, social actions, and company page followers.'],
            'VIDEO_VIEWS' => ['label' => 'Video views', 'hint' => 'Maximize views and exposure for your video ads.'],
            'JOB_APPLY' => ['label' => 'Job applicants', 'hint' => 'Drive relevant applicants to your job postings.'],
            'WEBSITE_CONVERSIONS' => ['label' => 'Website conversions', 'hint' => 'Drive valuable actions on your website.'],
            default => ['label' => ucfirst(strtolower(str_replace('_', ' ', $objective))), 'hint' => ''],
        };
    }

    /**
     * @return list<string>
     */
    public function campaignTypes(): array
    {
        return [];
    }

    /**
     * @return array{label: string, hint: string}
     */
    public function describeCampaignType(string $type): array
    {
        return ['label' => $type, 'hint' => ''];
    }

    public function suggestedCampaignType(?Campaign $campaign = null): ?array
    {
        return null;
    }

    /**
     * @return list<string>
     */
    public function bidStrategyFor(string $intent, ?string $objective = null): ?string
    {
        return match (strtoupper($intent)) {
            'AUTOMATIC' => 'AUTOMATED_BID',
            'TARGET_COST' => 'TARGET_COST',
            'BID_CAP' => 'MAXIMUM_BID',
            default => null,
        };
    }

    public function bidStrategies(): array
    {
        return [
            'AUTOMATED_BID',
            'MAXIMUM_BID',
            'TARGET_COST',
        ];
    }

    /**
     * @return list<array{value: string, label: string, hint: string, sends: string}>
     */
    public function bidStrategyOptions(): array
    {
        return [
            [
                'value' => 'AUTOMATED_BID',
                'label' => 'Automated bid (Maximum delivery)',
                'hint' => 'Spend the budget for as many results as possible automatically.',
                'sends' => 'Bid automatically for maximum delivery',
            ],
            [
                'value' => 'TARGET_COST',
                'label' => 'Target cost',
                'hint' => 'Hold the average cost per result near a target number you name.',
                'sends' => 'Use target cost bidding',
            ],
            [
                'value' => 'MAXIMUM_BID',
                'label' => 'Maximum bid',
                'hint' => 'Set a cap on the maximum bid for each result or click.',
                'sends' => 'Use maximum bid cap',
            ],
        ];
    }

    public function bidStrategyNeedsTarget(string $strategy): bool
    {
        return $strategy !== 'AUTOMATED_BID';
    }

    /**
     * @return list<string>
     */
    public function placements(): array
    {
        return [
            'linkedin_feed',
            'linkedin_audience_network',
        ];
    }

    /**
     * @return list<string>
     */
    public function callsToAction(): array
    {
        return [
            'LEARN_MORE',
            'APPLY',
            'DOWNLOAD',
            'SIGN_UP',
            'SUBSCRIBE',
            'REGISTER',
            'JOIN',
            'ATTEND',
            'REQUEST_DEMO',
            'VIEW_QUOTE',
        ];
    }

    /**
     * LinkedIn does not require housing/credit/employment declarations in this format.
     *
     * @return list<string>
     */
    public function specialAdCategories(): array
    {
        return [];
    }

    /**
     * @return list<array{value: string, label: string, hint: string, sends: string, badge?: string|null}>
     */
    public function trackingOptions(): array
    {
        return [
            [
                'value' => 'utm_source=linkedin&utm_medium=paid_social&utm_campaign=arb',
                'label' => 'Standard tagging',
                'hint' => 'Source and medium parameters for LinkedIn.',
                'badge' => 'Suggested',
                'sends' => 'Use the standard UTM tagging',
            ],
        ];
    }

    public function defaultAdAccountId(): ?string
    {
        return config('platforms.linkedin.ad_account_id') ? (string) config('platforms.linkedin.ad_account_id') : null;
    }

    /**
     * LinkedIn Campaign Manager enforces a minimum daily budget of $10 USD.
     */
    public function minimumDailyBudget(): float
    {
        return 10.00;
    }

    /**
     * @return list<array{value: string, label: string, hint: string, sends: string}>
     */
    public function budgetModes(): array
    {
        return [
            [
                'value' => 'campaign:daily',
                'label' => 'Daily',
                'hint' => 'Spend this much every day until stopped. LinkedIn requires at least $10/day.',
                'sends' => 'Use a daily campaign budget',
            ],
        ];
    }

    /**
     * Recommended character limits for sponsored content before truncation.
     *
     * @return array<string, int>
     */
    public function copyLimits(): array
    {
        return [
            'headline' => 70,
            'description' => 100,
            'primary_text' => 600,
            'introductory_text' => 600,
        ];
    }

    /**
     * @return list<string>
     */
    public function placementsFor(string $objective): array
    {
        return $this->placements();
    }

    public function optimizationGoal(string $objective): ?string
    {
        return match ($objective) {
            'WEBSITE_VISITS' => 'CLICKS',
            'LEAD_GENERATION' => 'LEAD_GENERATION',
            'BRAND_AWARENESS' => 'REACH',
            'ENGAGEMENT' => 'ENGAGEMENT',
            'VIDEO_VIEWS' => 'VIDEO_VIEWS',
            'WEBSITE_CONVERSIONS' => 'CONVERSIONS',
            default => 'CLICKS',
        };
    }

    public function conversionEvent(string $objective): ?string
    {
        return match ($objective) {
            'LEAD_GENERATION' => 'LEAD',
            'WEBSITE_VISITS' => 'PAGE_VIEW',
            'WEBSITE_CONVERSIONS' => 'CONVERSION',
            default => null,
        };
    }

    /**
     * No, because nothing in this product can set one for LinkedIn.
     *
     * This returned true for LEAD_GENERATION and WEBSITE_CONVERSIONS, and
     * PublishGate blocks on a blank pixel_id, and steps() has no pixel step,
     * and choose_pixel only speaks to Meta. So a LinkedIn lead-gen campaign was
     * refused at publish for a field no click could ever fill, and the buyer
     * had no way forward at all.
     *
     * A requirement nothing can satisfy is worse than no requirement: it stops
     * the campaign without protecting anything. The same trap is recorded in
     * PublishGate for the campaign name, which blocked on a value nobody was
     * ever asked for.
     *
     * Restoring this means three things together, not one: an Insight Tag
     * chosen through a step, choose_pixel taught to list LinkedIn's conversion
     * rules, and the publisher sending them. Until then a LinkedIn conversion
     * campaign runs without conversion tracking, which is the state every
     * LinkedIn campaign is in today, and it is at least a state the buyer can
     * reach.
     */
    public function requiresPixel(string $objective): bool
    {
        return false;
    }

    public function requiresCreativeAsset(?Campaign $campaign = null): bool
    {
        return true;
    }

    public function requiresPage(): bool
    {
        return false;
    }

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

        $unknown = array_diff((array) ($campaign->placements ?? []), $this->placements());
        if ($unknown !== []) {
            $problems[] = 'placement not supported on LinkedIn: '.implode(', ', $unknown);
        }

        if ($campaign->starts_at && $campaign->ends_at && $campaign->ends_at->lte($campaign->starts_at)) {
            $problems[] = 'end date must be after the start date';
        }

        if (in_array($campaign->objective, ['WEBSITE_VISITS', 'WEBSITE_CONVERSIONS', 'JOB_APPLY'], true) && blank($campaign->landing_url)) {
            $problems[] = 'a destination URL is required for the '.$campaign->objective.' objective on LinkedIn';
        }

        foreach ($campaign->ads as $index => $ad) {
            $label = $campaign->ads->count() > 1 ? 'ad '.($index + 1).': ' : '';
            if (mb_strlen((string) $ad->headline) > 200) {
                $problems[] = "{$label}headline exceeds LinkedIn's 200-character limit";
            }
            if (mb_strlen((string) $ad->primary_text) > 3000) {
                $problems[] = "{$label}primary text exceeds LinkedIn's 3,000-character limit";
            }
        }

        return $problems;
    }

    /**
     * @return list<array{key: string, label: string, tool: string, offers: bool, ask?: string}>
     */
    public function steps(): array
    {
        return [
            ['key' => 'ad_account', 'label' => 'ad account', 'tool' => 'list_ad_accounts', 'offers' => true,
                'ask' => 'Which LinkedIn ad account should this campaign run on?'],
            ['key' => 'objective', 'label' => 'objective', 'tool' => 'choose_objective', 'offers' => true,
                'ask' => 'What should this campaign optimise for?'],
            ['key' => 'bidding', 'label' => 'bid strategy', 'tool' => 'set_bidding', 'offers' => true,
                'ask' => 'How should it bid?'],
            ['key' => 'targeting', 'label' => 'countries and placements', 'tool' => 'set_targeting', 'offers' => true,
                'ask' => 'Where should this run, and on which devices?'],
            // No click tracking step, the same as Meta and Google. The value is
            // written at creation from trackingOptions(), so asking cost a
            // question and an answer to arrive at what was going to be used
            // anyway. The buyer is told what was set and can change it.
        ];
    }

    /**
     * @return list<string>
     */
    public function isConfigured(): bool
    {
        // Check database connection first, fall back to config token.
        return filled(PlatformConnection::shared('linkedin')?->access_token ?: config('platforms.linkedin.token'));
    }

    public function editableAfterPublish(): array
    {
        return ['name', 'budget', 'bid_strategy', 'bid_amount', 'starts_at', 'ends_at'];
    }

    public function supportedAssetRatios(): array
    {
        return ['1.91:1', '1:1', '4:5'];
    }
}
