<?php

namespace App\Services;

use App\Models\Asset;
use App\Models\TikTokConnection;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use RuntimeException;

class TikTokService
{
    /** @return array<string, mixed> */
    public function exchangeCode(string $code): array
    {
        $response = Http::asJson()
            ->connectTimeout(5)
            ->timeout(20)
            ->post($this->url('/open_api/v1.3/oauth2/access_token/'), [
                'app_id' => config('services.tiktok.app_id'),
                'secret' => config('services.tiktok.app_secret'),
                'auth_code' => $code,
                'grant_type' => 'authorized_code',
            ]);

        $response->throw();

        $providerCode = $response->json('code');
        if ($providerCode !== null && (string) $providerCode !== '0') {
            throw new RuntimeException('TikTok token exchange failed with provider code '.(string) $providerCode.'.');
        }

        $data = $response->json('data');
        if (! is_array($data)) {
            throw new RuntimeException('TikTok token exchange returned no data object.');
        }

        return $data;
    }

    /** @return list<array{advertiser_id: string, advertiser_name: string}> */
    public function advertisers(TikTokConnection $connection): array
    {
        $response = $this->request($connection)
            ->get($this->url('/open_api/v1.3/oauth2/advertiser/get/'), [
                'app_id' => config('services.tiktok.app_id'),
                'secret' => config('services.tiktok.app_secret'),
                'access_token' => $connection->access_token,
                'page' => 1,
                'page_size' => 100,
            ]);
        $response->throw();

        if ((string) $response->json('code', '0') !== '0') {
            $message = trim((string) $response->json('message'));

            throw new RuntimeException('TikTok advertiser lookup failed with code '.(string) $response->json('code').($message !== '' ? ': '.$message : '.'));
        }

        $payload = $response->json('data.list', []);

        return collect($payload)->map(fn (array $advertiser): array => [
            'advertiser_id' => (string) ($advertiser['advertiser_id'] ?? ''),
            'advertiser_name' => (string) ($advertiser['advertiser_name'] ?? 'TikTok advertiser'),
        ])->filter(fn (array $advertiser): bool => $advertiser['advertiser_id'] !== '')->values()->all();
    }

    /** @return list<array{pixel_id: string, pixel_name: string}> */
    public function pixels(TikTokConnection $connection): array
    {
        if (blank($connection->advertiser_id)) {
            throw new RuntimeException('Select a TikTok advertiser before listing pixels.');
        }

        $response = $this->request($connection)->get($this->url('/open_api/v1.3/pixel/list/'), [
            'advertiser_id' => $connection->advertiser_id,
            'page' => 1,
            'page_size' => 10,
        ]);
        $response->throw();

        if ((string) $response->json('code', '0') !== '0') {
            $message = trim((string) $response->json('message'));

            throw new RuntimeException('TikTok pixel lookup failed with code '.(string) $response->json('code').($message !== '' ? ': '.$message : '.'));
        }

        $payload = $response->json('data.list') ?? $response->json('data.pixels') ?? [];

        return collect(is_array($payload) ? $payload : [])->map(fn (array $pixel): array => [
            'pixel_id' => (string) ($pixel['pixel_id'] ?? $pixel['id'] ?? ''),
            'pixel_name' => (string) ($pixel['pixel_name'] ?? $pixel['name'] ?? 'TikTok pixel'),
        ])->filter(fn (array $pixel): bool => $pixel['pixel_id'] !== '')->values()->all();
    }

    /** @return array<string, mixed> */
    public function createPixel(TikTokConnection $connection, string $pixelName): array
    {
        if (blank($connection->advertiser_id)) {
            throw new RuntimeException('Select a TikTok advertiser before creating a pixel.');
        }

        return $this->post($connection, '/open_api/v1.3/pixel/create/', [
            'advertiser_id' => $connection->advertiser_id,
            'pixel_name' => $pixelName,
        ]);
    }

    /** @return list<array{identity_id: string, identity_type: string, identity_name: string, identity_authorized_bc_id: ?string}> */
    public function identities(TikTokConnection $connection): array
    {
        if (blank($connection->advertiser_id)) {
            throw new RuntimeException('Select a TikTok advertiser before listing identities.');
        }

        $response = $this->request($connection)->get($this->url('/open_api/v1.3/identity/get/'), [
            'advertiser_id' => $connection->advertiser_id,
            'page' => 1,
            'page_size' => 100,
        ]);
        $response->throw();

        if ((string) $response->json('code', '0') !== '0') {
            $message = trim((string) $response->json('message'));

            throw new RuntimeException('TikTok identity lookup failed with code '.$response->json('code').($message !== '' ? ': '.$message : '.'));
        }

        $payload = $response->json('data.identity_list')
            ?? $response->json('data.list')
            ?? $response->json('data.identities')
            ?? [];

        return collect(is_array($payload) ? $payload : [])->map(fn (array $identity): array => [
            'identity_id' => (string) ($identity['identity_id'] ?? ''),
            'identity_type' => (string) ($identity['identity_type'] ?? ''),
            'identity_name' => (string) ($identity['display_name'] ?? $identity['identity_name'] ?? $identity['identity_id'] ?? 'TikTok identity'),
            'identity_authorized_bc_id' => filled($identity['identity_authorized_bc_id'] ?? null) ? (string) $identity['identity_authorized_bc_id'] : null,
        ])->filter(fn (array $identity): bool => $identity['identity_id'] !== '' && $identity['identity_type'] !== '')->values()->all();
    }

    /** @return list<array{item_id: string, text: string, poster_url: ?string, duration: ?float}> */
    public function identityPosts(TikTokConnection $connection): array
    {
        if (blank($connection->identity_id) || blank($connection->identity_type)) {
            throw new RuntimeException('Select an authorized TikTok account before listing posts.');
        }

        $response = $this->request($connection)->get($this->url('/open_api/v1.3/identity/video/get/'), array_filter([
            'advertiser_id' => $connection->advertiser_id,
            'identity_type' => $connection->identity_type,
            'identity_id' => $connection->identity_id,
            'identity_authorized_bc_id' => $connection->identity_authorized_bc_id,
            'page' => 1,
            'page_size' => 100,
        ], fn (mixed $value): bool => filled($value)));
        $response->throw();

        if ((string) $response->json('code', '0') !== '0') {
            throw new RuntimeException('TikTok post lookup failed with code '.$response->json('code').'.');
        }

        return collect($response->json('data.video_list', []))->map(fn (array $post): array => [
            'item_id' => (string) ($post['item_id'] ?? ''),
            'text' => trim((string) ($post['text'] ?? '')),
            'poster_url' => filled(data_get($post, 'video_info.poster_url')) ? (string) data_get($post, 'video_info.poster_url') : null,
            'duration' => is_numeric(data_get($post, 'video_info.duration')) ? (float) data_get($post, 'video_info.duration') : null,
        ])->filter(fn (array $post): bool => $post['item_id'] !== '')->values()->all();
    }

    /**
     * @param  array<string, mixed>  $campaign
     * @param  array<string, mixed>  $adGroup
     * @param  array<string, mixed>  $creative
     * @return array{campaign_id: string, adgroup_id: string, ad_id: string}
     */
    public function createPausedAd(TikTokConnection $connection, array $campaign, array $adGroup, array $creative, ?string $existingCampaignId = null, ?string $existingAdGroupId = null, array &$created = []): array
    {
        $advertiserId = (string) $connection->advertiser_id;
        if ($advertiserId === '') {
            throw new RuntimeException('Select a TikTok advertiser before creating an ad.');
        }

        $campaignId = $existingCampaignId;
        if (blank($campaignId)) {
            $campaignData = $this->post($connection, '/open_api/v1.3/campaign/create/', [
                ...$campaign,
                'advertiser_id' => $advertiserId,
                'operation_status' => 'DISABLE',
            ]);
            $campaignId = $this->requiredId($campaignData, ['campaign_id'], 'campaign');
            $created['campaign_id'] = $campaignId;
        } else {
            $created['campaign_id'] = $campaignId;
        }

        $adGroupId = $existingAdGroupId;
        if (blank($adGroupId)) {
            try {
                $adGroup['schedule_start_time'] ??= now()->addMinutes(5)->format('Y-m-d H:i:s');
                $adGroupData = $this->post($connection, '/open_api/v1.3/adgroup/create/', [
                    ...$adGroup,
                    'advertiser_id' => $advertiserId,
                    'campaign_id' => $campaignId,
                    'operation_status' => 'DISABLE',
                ]);
                $adGroupId = $this->requiredId($adGroupData, ['adgroup_id'], 'ad group');
                $created['adgroup_id'] = $adGroupId;
            } catch (RuntimeException $exception) {
                throw new RuntimeException("TikTok created disabled campaign {$campaignId}, but ad group creation failed: {$exception->getMessage()}", previous: $exception);
            }
        } else {
            $created['adgroup_id'] = $adGroupId;
        }

        try {
            $adData = $this->post($connection, '/open_api/v1.3/ad/create/', [
                'advertiser_id' => $advertiserId,
                'adgroup_id' => $adGroupId,
                'creatives' => [[...$creative, 'operation_status' => 'DISABLE']],
            ]);
            $adId = $this->requiredId($adData, ['ad_id', 'ad_ids.0'], 'ad');
            $created['ad_id'] = $adId;
        } catch (RuntimeException $exception) {
            throw new RuntimeException("TikTok created disabled campaign {$campaignId} and ad group {$adGroupId}, but ad creation failed: {$exception->getMessage()}", previous: $exception);
        }

        return [
            'campaign_id' => $campaignId,
            'adgroup_id' => $adGroupId,
            'ad_id' => $adId,
        ];
    }

    /** @param array<string, mixed> $created */
    public function deleteCreated(TikTokConnection $connection, array $created): void
    {
        $adIds = collect($created['ads'] ?? [])->pluck('ad_id')->filter()->values()->all();
        if ($adIds !== []) {
            $this->post($connection, '/open_api/v1.3/ad/delete/', [
                'advertiser_id' => $connection->advertiser_id,
                'ad_ids' => array_map('strval', $adIds),
            ]);
        }

        if (filled($created['adgroup_id'] ?? null)) {
            $this->post($connection, '/open_api/v1.3/adgroup/delete/', [
                'advertiser_id' => $connection->advertiser_id,
                'adgroup_ids' => [(string) $created['adgroup_id']],
            ]);
        }

        if (filled($created['campaign_id'] ?? null)) {
            $this->post($connection, '/open_api/v1.3/campaign/delete/', [
                'advertiser_id' => $connection->advertiser_id,
                'campaign_ids' => [(string) $created['campaign_id']],
            ]);
        }
    }

    public function updateCampaignStatus(TikTokConnection $connection, string $campaignId, string $operationStatus): void
    {
        $this->post($connection, '/open_api/v1.3/campaign/status/update/', [
            'advertiser_id' => $connection->advertiser_id,
            'campaign_ids' => [$campaignId],
            'operation_status' => strtoupper($operationStatus),
        ]);
    }

    public function uploadCreative(TikTokConnection $connection, Asset $asset): string
    {
        $advertiserId = (string) $connection->advertiser_id;
        if ($advertiserId === '') {
            throw new RuntimeException('Select a TikTok advertiser before uploading a creative.');
        }

        $existingReference = $asset->refFor('tiktok', $advertiserId)?->reference();
        if (filled($existingReference)) {
            return (string) $existingReference;
        }

        $contents = Storage::disk($asset->disk ?: 'local')->get($asset->path);
        $fileName = $asset->original_name ?: basename($asset->path);
        $isVideo = $asset->kind === 'video';
        $fileField = $isVideo ? 'video_file' : 'image_file';
        $signatureField = $isVideo ? 'video_signature' : 'image_signature';
        $idField = $isVideo ? 'video_id' : 'image_id';
        $path = $isVideo ? '/open_api/v1.3/file/video/ad/upload/' : '/open_api/v1.3/file/image/ad/upload/';

        $response = $this->request($connection)
            ->attach($fileField, $contents, $fileName)
            ->post($this->url($path), [
                'advertiser_id' => $advertiserId,
                'file_name' => $fileName,
                $signatureField => md5($contents),
                'upload_type' => 'UPLOAD_BY_FILE',
            ]);

        $response->throw();
        if ((string) $response->json('code', '0') !== '0') {
            $message = trim((string) $response->json('message'));

            throw new RuntimeException('TikTok creative upload failed with code '.$response->json('code').($message !== '' ? ': '.$message : '.'));
        }

        $platformId = $response->json('data.'.$idField);
        if (! is_scalar($platformId) || (string) $platformId === '') {
            throw new RuntimeException("TikTok did not return a {$idField} for the selected creative.");
        }

        $asset->platformRefs()->updateOrCreate(
            ['platform' => 'tiktok', 'ad_account_id' => $advertiserId],
            $isVideo ? ['external_id' => (string) $platformId, 'external_hash' => null] : ['external_hash' => (string) $platformId, 'external_id' => null],
        );

        return (string) $platformId;
    }

    private function request(TikTokConnection $connection): PendingRequest
    {
        if (blank($connection->access_token)) {
            throw new RuntimeException('TikTok authorization is required.');
        }

        return Http::withHeaders(['Access-Token' => $connection->access_token])
            ->acceptJson()
            ->connectTimeout(5)
            ->timeout(20);
    }

    /**
     * @param  array<string, mixed>  $payload
     * @return array<string, mixed>
     */
    private function post(TikTokConnection $connection, string $path, array $payload): array
    {
        $response = $this->request($connection)
            ->asJson()
            ->post($this->url($path), $payload);

        $response->throw();

        if ((string) $response->json('code', '0') !== '0') {
            $message = trim((string) $response->json('message'));

            throw new RuntimeException('TikTok API rejected '.$path.' with code '.$response->json('code').($message !== '' ? ': '.$message : '.'));
        }

        $data = $response->json('data');
        if (! is_array($data)) {
            throw new RuntimeException('TikTok API returned no data object.');
        }

        return $data;
    }

    /** @param list<string> $paths */
    private function requiredId(array $data, array $paths, string $resource): string
    {
        foreach ($paths as $path) {
            $id = data_get($data, $path);
            if (is_scalar($id) && (string) $id !== '') {
                return (string) $id;
            }
        }

        throw new RuntimeException("TikTok did not return a {$resource} ID.");
    }

    private function url(string $path): string
    {
        return rtrim((string) config('services.tiktok.api_base_url'), '/').$path;
    }
}
