<?php

namespace App\Agent\Tools\Campaign;

use App\Agent\Tools\Campaign\Concerns\ActsOnFocusedCampaign;
use App\Agent\Workspace;
use App\Campaigns\Platforms\Platforms;
use App\Models\ArtifactFieldSource;
use App\Services\LinkedIn\LinkedInException;
use App\Services\LinkedIn\LinkedInRestApi;
use App\Services\LinkedIn\LinkedInUrn;
use App\Services\Meta\Meta;
use App\Services\Meta\MetaException;
use App\Services\Platforms\Support\Guardrails;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;

/**
 * Confirm an ad account exists, is usable, and is one we are allowed to touch.
 *
 * Must succeed before Pages, pixels or uploads: all three are account scoped.
 * Changing the account clears them, because a Page id from one account is
 * meaningless on another and silently keeping it is how an ad ends up posted
 * by the wrong brand.
 */
class VerifyAdAccount implements Tool
{
    use ActsOnFocusedCampaign;

    public function __construct(
        private readonly Workspace $workspace,
        private readonly Meta $meta,
        private readonly ?LinkedInRestApi $linkedInApi = null,
    ) {}

    public function description(): Stringable|string
    {
        return 'Verify an ad account by id and attach it to the campaign in focus. '
            .'Call this directly when the user names an account; only list accounts when they have not.';
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'ad_account_id' => $schema->string()
                ->description('The ad account id, with or without the act_ prefix.')
                ->required(),
        ];
    }

    public function handle(Request $request): Stringable|string
    {
        if (! $campaign = $this->focused()) {
            return $this->noCampaign();
        }

        if (! in_array($campaign->platform, ['meta', 'linkedin'], true)) {
            return $this->json([
                'ok' => false,
                'error' => sprintf(
                    '%s is not connected yet, so an ad account cannot be verified for it.',
                    app(Platforms::class)->for($campaign)->name(),
                ),
            ]);
        }

        if ($campaign->platform === 'linkedin') {
            return $this->handleLinkedIn($campaign, trim((string) ($request->all()['ad_account_id'] ?? '')));
        }

        $id = trim((string) ($request->all()['ad_account_id'] ?? ''));
        $id = str_starts_with($id, 'act_') ? $id : 'act_'.ltrim($id, '_');

        try {
            $account = $this->meta->on($campaign->id, 'agent')->adAccount($id);
        } catch (MetaException $e) {
            return $this->json(['ok' => false, 'error' => $e->getMessage()]);
        }

        if ((int) ($account['account_status'] ?? 0) !== 1) {
            return $this->json([
                'ok' => false,
                'error' => "Ad account {$id} is not active (status {$account['account_status']}).",
            ]);
        }

        $changed = $campaign->ad_account_id && $campaign->ad_account_id !== $account['id'];

        $campaign->update([
            'ad_account_id' => $account['id'],
            'currency' => $account['currency'] ?? $campaign->currency,
            // Account scoped, so they cannot survive a change of account.
            ...$changed ? ['page_id' => null, 'pixel_id' => null] : [],
        ]);

        $this->workspace->recordSource($campaign, 'ad_account_id', ArtifactFieldSource::PLATFORM_RETURNED);

        if ($changed) {
            $this->workspace->forgetSources($campaign, 'page_id', 'pixel_id');
        }

        return $this->json(array_filter([
            'ok' => true,
            'ad_account_id' => $account['id'],
            'name' => $account['name'] ?? null,
            'currency' => $account['currency'] ?? null,
            'cleared_because_account_changed' => $changed ? ['page_id', 'pixel_id'] : null,
        ], fn ($v) => $v !== null));
    }

    private function handleLinkedIn($campaign, string $rawId): Stringable|string
    {
        // Checked here rather than by LinkedIn.
        //
        // bare() returned anything without a colon unchanged, so a Meta-style
        // act_1950320145707342 - or an account name typed instead of an id - was
        // put straight into rest/adAccounts/{id}. LinkedIn replies "Failed to
        // convert ... to java.lang.Long", which reached the chat verbatim: a Java
        // type error, in front of a media buyer, about a value they could have
        // fixed in a second if anything had said which value it was.
        $bareId = LinkedInUrn::numericId($rawId);

        if ($bareId === null) {
            return $this->json([
                'ok' => false,
                'error' => $rawId === ''
                    ? 'A LinkedIn ad account id is required.'
                    : sprintf(
                        '[%s] is not a LinkedIn ad account id. LinkedIn ids are numbers, either on '
                        .'their own or as urn:li:sponsoredAccount:<number>. An act_ prefix is Meta\'s.',
                        $rawId,
                    ),
                'note' => 'Ask the user for the LinkedIn account id, or call list_ad_accounts to put '
                    .'the ones we can reach on screen. Do not repeat this message verbatim.',
            ]);
        }

        // Asked of the guard rather than re-derived here.
        //
        // This enumerated permittedAccounts() and compared ids, and a wildcard
        // is a rule rather than an account: with linkedin:* set the list was
        // ['*'], no real id equals that, and every LinkedIn account was refused
        // on the one platform that had deliberately been opened to all of them.
        // The guard already answers this question, wildcard included, and it is
        // the same answer the publish gate will give later - which the local
        // copy was free to disagree with, and did.
        try {
            app(Guardrails::class)->assertAccountAllowed($bareId, 'linkedin');
        } catch (\Throwable $e) {
            return $this->json(['ok' => false, 'error' => $e->getMessage()]);
        }

        $api = $this->linkedInApi ?? app(LinkedInRestApi::class);

        try {
            $account = $api->adAccount($bareId);
        } catch (LinkedInException|\Throwable $e) {
            return $this->json(['ok' => false, 'error' => $e->getMessage()]);
        }

        if (($account['status'] ?? '') !== 'ACTIVE') {
            return $this->json([
                'ok' => false,
                'error' => "Ad account {$bareId} is not active (status ".($account['status'] ?? 'unknown').').',
            ]);
        }

        $changed = $campaign->ad_account_id && $campaign->ad_account_id !== $bareId;

        $campaign->update([
            'ad_account_id' => $bareId,
            'currency' => $account['currency'] ?? $campaign->currency ?? 'USD',
            ...$changed ? ['page_id' => null, 'pixel_id' => null] : [],
        ]);

        $this->workspace->recordSource($campaign, 'ad_account_id', ArtifactFieldSource::PLATFORM_RETURNED);

        if ($changed) {
            $this->workspace->forgetSources($campaign, 'page_id', 'pixel_id');
        }

        return $this->json(array_filter([
            'ok' => true,
            'ad_account_id' => $bareId,
            'name' => $account['name'] ?? null,
            'currency' => $account['currency'] ?? null,
            'cleared_because_account_changed' => $changed ? ['page_id', 'pixel_id'] : null,
        ], fn ($v) => $v !== null));
    }
}
