<?php

namespace App\Services\Meta;

use App\Exceptions\MetaMarketingApiException;
use App\Models\CustomerQualityAudienceMember;
use App\Models\CustomerQualitySyncLog;
use App\Services\Customer\CustomerQualityService;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Schema;
use Throwable;

class MetaCustomerAudienceSyncService
{
    private const VALUE_BASED_SCHEMA = ['EMAIL', 'PHONE', 'EXTERN_ID', 'COUNTRY', 'LOOKALIKE_VALUE'];

    private array $diagnostics = [];

    public function __construct(
        private readonly CustomerQualityService $customerQualityService
    ) {}

    public function sync(string $audienceKey, string $mode = 'dry_run', ?array $metricKeys = null, ?int $adminId = null): CustomerQualitySyncLog
    {
        $syncAllEligible = $metricKeys === null;

        if ($mode === 'dry_run') {
            return $this->dryRun($audienceKey, $metricKeys, $syncAllEligible, $adminId);
        }

        $plan = $this->customerQualityService->audiencePlan($audienceKey, $metricKeys, $syncAllEligible);
        $dryRun = $this->customerQualityService->latestValidDryRun(
            $audienceKey,
            $plan['scope_hash'],
            $plan['checksum']
        );

        if (! $dryRun) {
            throw new \RuntimeException('A matching, non-expired dry run with scope hash and checksum is required before Meta upload.');
        }

        if ($mode === 'test_upload') {
            return $this->testUploadFromDryRun($dryRun->id, $audienceKey, $metricKeys, $adminId);
        }

        return $this->syncLiveFromDryRun($dryRun->id, $audienceKey, $metricKeys, $adminId);
    }

    public function dryRun(string $audienceKey, ?array $metricKeys = null, bool $syncAllEligible = true, ?int $adminId = null): CustomerQualitySyncLog
    {
        $plan = $this->customerQualityService->audiencePlan($audienceKey, $metricKeys, $syncAllEligible);

        return CustomerQualitySyncLog::query()->create([
            'audience_key' => $audienceKey,
            'audience_name' => $plan['audience_name'],
            'mode' => 'dry_run',
            'status' => 'dry_run',
            'scope_hash' => $plan['scope_hash'],
            'checksum' => $plan['checksum'],
            'expires_at' => $plan['expires_at'],
            'eligible_count' => $plan['eligible_count'],
            'add_count' => $plan['add_count'],
            'remove_count' => $plan['remove_count'],
            'skipped_count' => $plan['skipped_count'],
            'skipped_reasons' => $plan['skipped_reasons'],
            'skipped_rows' => $plan['skipped_rows'],
            'meta_response' => [
                'schema' => self::VALUE_BASED_SCHEMA,
                'candidate_count' => $plan['candidate_count'],
                'scope_hash' => $plan['scope_hash'],
                'checksum' => $plan['checksum'],
                'expires_at' => $plan['expires_at']->toDateTimeString(),
                'skipped_reasons' => $plan['skipped_reasons'],
                'skipped_rows' => $plan['skipped_rows'],
                'add_preview' => $this->previewRows($plan['add_rows']),
                'remove_preview' => $this->previewRows($plan['remove_rows']),
                'dataset_snapshot' => collect($plan['rows'])->pluck('phone_hash')->filter()->sort()->values()->all(),
                'proposed_remove_count' => $plan['proposed_remove_count'],
                'suppressed_remove_count' => $plan['suppressed_remove_count'],
                'removal_enabled' => $plan['removal_enabled'],
                'removal_percentage' => $plan['removal_percentage'],
            ],
            'synced_by_admin_id' => $adminId,
            'started_at' => now(),
            'finished_at' => now(),
        ]);
    }

    public function testUploadFromDryRun(int $dryRunLogId, string $audienceKey, ?array $metricKeys = null, ?int $adminId = null): CustomerQualitySyncLog
    {
        [$plan, $dryRun] = $this->validatedDryRunPlan($dryRunLogId, $audienceKey, $metricKeys);
        $testRows = array_slice($plan['add_rows'] ?: $plan['rows'], 0, 1);

        if ($testRows === []) {
            throw new \RuntimeException('Test upload requires at least one eligible customer row.');
        }

        $log = $this->newUploadLog('test_upload', $plan, $dryRun, $adminId);
        $this->diagnostics = [];

        try {
            $audience = $this->resolveAudience($audienceKey);
            $testRow = $testRows[0];
            $trackedCustomer = [
                'metric_key' => (string) data_get($testRow, 'metric_key', ''),
                'phone_hash' => (string) data_get($testRow, 'phone_hash', ''),
                'external_id_hash' => (string) data_get($testRow, 'external_id_hash', ''),
            ];
            $addResponses = $this->sendUsers($audience['id'], $testRows, 'add');
            $log->fill([
                'meta_audience_id' => $audience['id'],
                'meta_response' => array_replace((array) $log->meta_response, [
                    'audience_resolution' => $audience,
                    'test_customer' => $trackedCustomer,
                    'add_responses' => $addResponses,
                ]),
            ])->save();
            $addAcceptance = null;
            $addAcceptanceError = null;
            try {
                $addAcceptance = $this->assertUploadAccepted('Test upload', $addResponses, count($testRows));
                $log->fill([
                    'meta_response' => array_replace((array) $log->meta_response, [
                        'add_acceptance' => $addAcceptance,
                    ]),
                ])->save();
            } catch (Throwable $exception) {
                $addAcceptanceError = $exception;
            }
            $cleanupResponses = $this->sendUsers($audience['id'], $testRows, 'remove');
            $log->fill([
                'meta_response' => array_replace((array) $log->meta_response, [
                    'cleanup_responses' => $cleanupResponses,
                ]),
            ])->save();
            $cleanupAcceptance = $this->assertUploadAccepted('Test upload cleanup', $cleanupResponses, count($testRows));
            if ($addAcceptanceError) {
                throw $addAcceptanceError;
            }
            $status = ['state' => 'not_checked', 'is_ready' => false];

            try {
                $status = $this->fetchAndRememberAudienceStatus($audienceKey, $audience['id']);
            } catch (Throwable $exception) {
                $error = $exception instanceof MetaMarketingApiException
                    ? $exception->logContext()
                    : ['message' => $exception->getMessage()];
                $this->diagnostics[] = [
                    'stage' => 'post_test_audience_status',
                    'warning' => 'Test upload and cleanup were accepted; audience status lookup failed independently.',
                    'meta_error' => $error,
                ];
            }
            $log->fill([
                'meta_audience_id' => $audience['id'],
                'status' => 'test_passed',
                'meta_response' => array_replace((array) $log->meta_response, [
                    'audience_resolution' => $audience,
                    'test_row_count' => count($testRows),
                    'test_customer' => $trackedCustomer,
                    'add_responses' => $addResponses,
                    'add_acceptance' => $addAcceptance,
                    'cleanup_responses' => $cleanupResponses,
                    'cleanup_acceptance' => $cleanupAcceptance,
                    'audience_status' => $status,
                    'diagnostics' => $this->diagnostics,
                ]),
                'finished_at' => now(),
            ])->save();

            return $log;
        } catch (Throwable $throwable) {
            $this->failLog($log, $throwable);
            throw $throwable;
        }
    }

    public function syncLiveFromDryRun(int $dryRunLogId, string $audienceKey, ?array $metricKeys = null, ?int $adminId = null): CustomerQualitySyncLog
    {
        $metaWriteEligibility = $this->customerQualityService->metaWriteEligibility();
        if (! data_get($metaWriteEligibility, 'allowed', false)) {
            throw new \RuntimeException(
                'Live Meta Sync safety prerequisites are not satisfied. '
                .implode(' ', (array) data_get($metaWriteEligibility, 'reasons', []))
            );
        }

        [$plan, $dryRun] = $this->validatedDryRunPlan($dryRunLogId, $audienceKey, $metricKeys);

        if (! $this->customerQualityService->hasSuccessfulTestUpload($audienceKey, $plan['scope_hash'], $plan['checksum'])) {
            throw new \RuntimeException('A successful accepted-and-cleaned test upload is required for this exact dry-run checksum before Live Meta Sync.');
        }

        if (CustomerQualitySyncLog::query()->where('audience_key', $audienceKey)->where('mode', 'live')->where('checksum', $plan['checksum'])->whereIn('status', ['running', 'processing', 'ready', 'completed'])->exists()) {
            throw new \RuntimeException('This exact audience checksum already has a live upload or an in-progress live job.');
        }

        $maxRemoval = (float) data_get($this->customerQualityService->settings(), 'max_removal_percentage', 20);
        if ($plan['removal_enabled'] && $plan['removal_percentage'] > $maxRemoval) {
            throw new \RuntimeException(sprintf('Removal safety limit exceeded: %.2f%% proposed, %.2f%% allowed.', $plan['removal_percentage'], $maxRemoval));
        }

        $log = $this->newUploadLog('live', $plan, $dryRun, $adminId);
        $this->diagnostics = [];

        try {
            $audience = $this->resolveAudience($audienceKey);
            $responses = [];
            $acceptance = [];

            if ($plan['add_rows'] !== []) {
                $responses['add'] = $this->sendUsers($audience['id'], $plan['add_rows'], 'add');
                $log->fill([
                    'meta_response' => array_replace((array) $log->meta_response, ['responses' => $responses]),
                ])->save();
                $acceptance['add'] = $this->assertUploadAccepted('Live add upload', $responses['add'], count($plan['add_rows']));
                $log->fill([
                    'meta_response' => array_replace((array) $log->meta_response, ['acceptance' => $acceptance]),
                ])->save();
            }

            if ($plan['remove_rows'] !== []) {
                $responses['remove'] = $this->sendUsers($audience['id'], $plan['remove_rows'], 'remove');
                $log->fill([
                    'meta_response' => array_replace((array) $log->meta_response, ['responses' => $responses]),
                ])->save();
                $acceptance['remove'] = $this->assertUploadAccepted('Live remove upload', $responses['remove'], count($plan['remove_rows']));
                $log->fill([
                    'meta_response' => array_replace((array) $log->meta_response, ['acceptance' => $acceptance]),
                ])->save();
            }

            DB::transaction(function () use ($audienceKey, $plan) {
                foreach ($plan['rows'] as $row) {
                    CustomerQualityAudienceMember::query()->updateOrCreate(
                        [
                            'audience_key' => $audienceKey,
                            'phone_hash' => $row->phone_hash,
                        ],
                        [
                            'metric_key' => $row->metric_key,
                            'user_id' => $row->user_id,
                            'email_hash' => $row->email_hash ?: null,
                            'external_id_hash' => $row->external_id_hash ?: null,
                            'country_code' => $row->country_code ?: 'BD',
                            'lookalike_value' => $row->lookalike_value,
                            'last_seen_at' => now(),
                            'last_synced_at' => now(),
                        ]
                    );
                }

                if ($plan['remove_hashes'] !== []) {
                    CustomerQualityAudienceMember::query()
                        ->where('audience_key', $audienceKey)
                        ->whereIn('phone_hash', $plan['remove_hashes'])
                        ->delete();
                }
            });

            $status = ['state' => 'processing', 'is_ready' => false, 'status_lookup_failed' => false];
            try {
                $status = $this->fetchAndRememberAudienceStatus($audienceKey, $audience['id']);
            } catch (MetaMarketingApiException $exception) {
                $status['status_lookup_failed'] = true;
                $this->diagnostics[] = [
                    'stage' => 'post_live_audience_status',
                    'warning' => 'Live upload was accepted but audience status could not be read; log remains processing.',
                    'meta_error' => $exception->logContext(),
                ];
            }
            $liveStatus = in_array(data_get($status, 'state'), ['processing', 'ready', 'failed'], true)
                ? (string) data_get($status, 'state')
                : 'processing';
            $log->fill([
                'meta_audience_id' => $audience['id'],
                'status' => $liveStatus,
                'error_message' => $liveStatus === 'failed'
                    ? ((string) data_get($status, 'operation_description') ?: (string) data_get($status, 'delivery_description'))
                    : null,
                'meta_response' => array_replace((array) $log->meta_response, [
                    'audience_resolution' => $audience,
                    'responses' => $responses,
                    'acceptance' => $acceptance,
                    'audience_status' => $status,
                    'diagnostics' => $this->diagnostics,
                ]),
                'finished_at' => now(),
            ])->save();

            $this->customerQualityService->rememberAudienceSyncResult($audienceKey, [
                'last_successful_sync_at' => now()->toDateTimeString(),
                'last_add_count' => (int) $plan['add_count'],
                'last_remove_count' => (int) $plan['remove_count'],
                'last_failed_count' => 0,
                'last_checksum' => (string) $plan['checksum'],
                'meta_request_id' => (string) ($log->meta_trace_id ?? ''),
            ]);

            return $log;
        } catch (Throwable $throwable) {
            $this->failLog($log, $throwable);
            throw $throwable;
        }
    }

    public function refreshAudienceStatus(string $audienceKey): array
    {
        $this->diagnostics = [];
        $audience = $this->resolveAudience($audienceKey);
        $status = $this->fetchAndRememberAudienceStatus($audienceKey, $audience['id']);

        $liveLog = CustomerQualitySyncLog::query()
            ->where('audience_key', $audienceKey)
            ->where('mode', 'live')
            ->whereIn('status', ['processing', 'ready', 'failed'])
            ->latest('id')
            ->first();

        if ($liveLog) {
            $liveLog->fill([
                'status' => (string) data_get($status, 'state', 'processing'),
                'error_message' => data_get($status, 'state') === 'failed'
                    ? ((string) data_get($status, 'operation_description') ?: (string) data_get($status, 'delivery_description'))
                    : null,
                'meta_response' => array_replace((array) $liveLog->meta_response, [
                    'audience_status' => $status,
                    'diagnostics' => $this->diagnostics,
                ]),
                'finished_at' => now(),
            ])->save();
        }

        return $status;
    }

    public function resolveAudienceId(string $audienceKey): string
    {
        return $this->resolveAudience($audienceKey)['id'];
    }

    public function createBangladeshLookalikeAudience(string $audienceKey = CustomerQualityService::HIGH_AUDIENCE_KEY): string
    {
        $settings = $this->customerQualityService->settings();
        $configuredId = trim((string) data_get($settings, 'audiences.'.$audienceKey.'.lookalike_audience_id'));

        if (! (bool) data_get($settings, 'lookalike_sync_enabled', false)) {
            throw new \RuntimeException('Lookalike sync is disabled by the production safety settings.');
        }

        if ($configuredId === '') {
            throw new \RuntimeException('A validated Lookalike Audience ID is required; automatic Lookalike creation is disabled.');
        }

        $this->validateAudienceId($configuredId);

        return $configuredId;
    }

    private function resolveAudience(string $audienceKey): array
    {
        $settings = $this->customerQualityService->settings();
        $configuredId = trim((string) data_get($settings, 'audiences.'.$audienceKey.'.meta_audience_id'));
        if ($configuredId === '') {
            throw new \RuntimeException('A configured and validated Meta Audience ID is required. Automatic audience lookup/creation is disabled.');
        }

        $validated = $this->validateAudienceId($configuredId);

        return ['id' => $configuredId, 'source' => 'validated_saved_id', 'name' => (string) data_get($validated, 'name', '')];
    }

    private function findAudienceByName(string $audienceName): ?array
    {
        $target = $this->normalizeAudienceName($audienceName);

        foreach ($this->listCustomAudiences() as $audience) {
            if ($this->normalizeAudienceName((string) data_get($audience, 'name')) === $target) {
                $id = trim((string) data_get($audience, 'id'));
                if ($id !== '') {
                    return ['id' => $id, 'name' => (string) data_get($audience, 'name')];
                }
            }
        }

        return null;
    }

    private function validateAudienceId(string $audienceId): array
    {
        $this->assertMarketingConfig();
        $response = Http::timeout($this->timeout())->get($this->graphUrl('/'.$audienceId), [
            'fields' => 'id,name,subtype,account_id,operation_status,delivery_status,approximate_count_lower_bound,approximate_count_upper_bound',
            'access_token' => $this->accessToken(),
        ]);

        $this->assertSuccessfulResponse('Saved Meta Audience ID validation', $response);
        $body = (array) $response->json();

        if (trim((string) data_get($body, 'id')) !== $audienceId) {
            throw new \RuntimeException('Saved Meta Audience ID validation returned a different or empty Audience ID.');
        }

        $expectedAccountId = preg_replace('/\D+/', '', $this->adAccountId()) ?? '';
        $actualAccountId = preg_replace('/\D+/', '', (string) data_get($body, 'account_id')) ?? '';
        if ($actualAccountId !== '' && $expectedAccountId !== '' && $actualAccountId !== $expectedAccountId) {
            throw new \RuntimeException('The Meta Audience belongs to a different Ad Account.');
        }

        $body['ad_account_validation'] = $this->validateAdAccountOwnership();

        return $body;
    }

    private function validateAdAccountOwnership(): array
    {
        $response = Http::timeout($this->timeout())->get($this->graphUrl('/'.$this->adAccountId()), [
            'fields' => 'id,name,account_status,business{id,name}',
            'access_token' => $this->accessToken(),
        ]);
        $this->assertSuccessfulResponse('Meta Ad Account and Business validation', $response);
        $body = (array) $response->json();
        $configuredBusiness = preg_replace('/\D+/', '', (string) config('services.meta_marketing.business_id', '')) ?? '';
        $actualBusiness = preg_replace('/\D+/', '', (string) data_get($body, 'business.id')) ?? '';
        if ($configuredBusiness !== '' && $actualBusiness !== '' && $configuredBusiness !== $actualBusiness) {
            throw new \RuntimeException('The configured Meta Business does not own the configured Ad Account.');
        }

        return [
            'ad_account_id' => (string) data_get($body, 'id'),
            'ad_account_name' => (string) data_get($body, 'name'),
            'account_status' => data_get($body, 'account_status'),
            'business_id' => (string) data_get($body, 'business.id'),
            'business_name' => (string) data_get($body, 'business.name'),
            'validated_at' => now()->toDateTimeString(),
        ];
    }

    private function listCustomAudiences(): array
    {
        $url = $this->graphUrl('/'.$this->adAccountId().'/customaudiences');
        $query = [
            'fields' => 'id,name',
            'limit' => 500,
            'access_token' => $this->accessToken(),
        ];
        $audiences = [];

        for ($page = 0; $page < 50 && $url; $page++) {
            $response = Http::timeout($this->timeout())->get($url, $query);
            $this->assertSuccessfulResponse('Meta audience list', $response);
            $audiences = array_merge($audiences, (array) data_get($response->json(), 'data', []));

            $next = trim((string) data_get($response->json(), 'paging.next'));
            $url = str_starts_with($next, 'https://graph.facebook.com/') ? $next : '';
            $query = [];
        }

        return $audiences;
    }

    private function createValueBasedAudience(string $audienceName): string
    {
        $response = Http::asForm()
            ->timeout($this->timeout())
            ->post($this->graphUrl('/'.$this->adAccountId().'/customaudiences'), [
                'name' => $audienceName,
                'subtype' => 'CUSTOM',
                'customer_file_source' => 'USER_PROVIDED_ONLY',
                'is_value_based' => '1',
                'description' => 'Value-based audience synced from Ammar Customer Quality report.',
                'access_token' => $this->accessToken(),
            ]);

        $this->assertSuccessfulResponse('Meta value-based audience create', $response);
        $createdId = trim((string) data_get($response->json(), 'id'));

        if ($createdId === '') {
            throw new \RuntimeException('Meta value-based audience create returned no Audience ID.');
        }

        return $createdId;
    }

    private function fetchAndRememberAudienceStatus(string $audienceKey, string $audienceId): array
    {
        $response = Http::timeout($this->timeout())->get($this->graphUrl('/'.$audienceId), [
            'fields' => 'id,name,subtype,operation_status,delivery_status,approximate_count_lower_bound,approximate_count_upper_bound',
            'access_token' => $this->accessToken(),
        ]);

        if (! $response->successful()) {
            $firstError = MetaMarketingApiException::fromResponse('Meta audience detailed status lookup', $response);
            $this->diagnostics[] = [
                'stage' => 'audience_detailed_status_lookup',
                'warning' => 'Detailed status fields failed; minimal status fields were retried.',
                'meta_error' => $firstError->logContext(),
            ];
            $response = Http::timeout($this->timeout())->get($this->graphUrl('/'.$audienceId), [
                'fields' => 'id,name,operation_status,delivery_status',
                'access_token' => $this->accessToken(),
            ]);
        }

        $this->assertSuccessfulResponse('Meta audience status lookup', $response);
        $body = (array) $response->json();
        $operationCode = (int) data_get($body, 'operation_status.code', 0);
        $deliveryCode = data_get($body, 'delivery_status.code');
        $deliveryCode = $deliveryCode === null ? null : (int) $deliveryCode;
        $isReady = $operationCode === 200 && ($deliveryCode === null || $deliveryCode === 200);
        $isFailed = $operationCode >= 400 || ($deliveryCode !== null && $deliveryCode >= 400);
        $status = [
            'id' => (string) data_get($body, 'id', $audienceId),
            'name' => (string) data_get($body, 'name', ''),
            'state' => $isReady ? 'ready' : ($isFailed ? 'failed' : 'processing'),
            'is_ready' => $isReady,
            'operation_code' => $operationCode,
            'operation_description' => (string) data_get($body, 'operation_status.description', ''),
            'delivery_code' => $deliveryCode,
            'delivery_description' => (string) data_get($body, 'delivery_status.description', ''),
            'approximate_count_lower_bound' => data_get($body, 'approximate_count_lower_bound'),
            'approximate_count_upper_bound' => data_get($body, 'approximate_count_upper_bound'),
            'checked_at' => now()->toDateTimeString(),
        ];

        $this->customerQualityService->rememberAudienceStatus($audienceKey, $status);

        return $status;
    }

    private function sendUsers(string $audienceId, array $rows, string $operation): array
    {
        $this->assertMarketingConfig();
        $url = $this->graphUrl("/{$audienceId}/users");
        $responses = [];
        $payloadRows = $this->payloadRows($rows);
        $chunks = array_chunk($payloadRows, 5000);
        $sessionId = random_int(100000000, 999999999);

        foreach ($chunks as $index => $chunk) {
            if ($chunk === []) {
                continue;
            }

            $body = [
                'payload' => json_encode([
                    'schema' => self::VALUE_BASED_SCHEMA,
                    'data' => $chunk,
                    'session' => [
                        'session_id' => $sessionId,
                        'batch_seq' => $index + 1,
                        'last_batch_flag' => $index === array_key_last($chunks),
                        'estimated_num_total' => count($payloadRows),
                    ],
                ], JSON_UNESCAPED_SLASHES),
                'access_token' => $this->accessToken(),
            ];
            $request = Http::asForm()->timeout($this->timeout());
            $response = $operation === 'remove'
                ? $request->send('DELETE', $url, ['form_params' => $body])
                : $request->post($url, $body);

            $responses[] = [
                'operation' => $operation,
                'count' => count($chunk),
                'status' => $response->status(),
                'body' => $response->json(),
            ];
            $this->assertSuccessfulResponse("Meta audience {$operation} upload", $response);
        }

        return $responses;
    }

    private function assertUploadAccepted(string $operation, array $responses, int $expectedCount): array
    {
        $received = 0;
        $invalid = 0;
        $countsReported = false;
        $invalidCountsReported = false;
        $invalidSamples = [];

        foreach ($responses as $response) {
            $body = (array) data_get($response, 'body', []);
            if (array_key_exists('num_received', $body)) {
                $countsReported = true;
                $received += (int) data_get($body, 'num_received', 0);
            }

            if (array_key_exists('num_invalid_entries', $body) || array_key_exists('num_invalid', $body)) {
                $invalidCountsReported = true;
                $invalid += (int) data_get($body, 'num_invalid_entries', data_get($body, 'num_invalid', 0));
            }

            $samples = data_get($body, 'invalid_entry_samples', []);
            if (is_array($samples) && $samples !== []) {
                $invalidSamples = array_merge($invalidSamples, $samples);
            }
        }

        if (! $countsReported) {
            throw new \RuntimeException("{$operation} returned HTTP success but no num_received count.");
        }

        if (! $invalidCountsReported) {
            throw new \RuntimeException("{$operation} returned HTTP success but no invalid-entry count.");
        }

        if ($received < $expectedCount || $invalid > 0) {
            throw new \RuntimeException(sprintf(
                '%s was not fully accepted. Expected: %d, Received: %d, Invalid: %d.',
                $operation,
                $expectedCount,
                $received,
                $invalid
            ));
        }

        return [
            'expected_count' => $expectedCount,
            'received_count' => $received,
            'invalid_count' => $invalid,
            'invalid_entry_samples' => $invalidSamples,
            'accepted' => true,
        ];
    }

    private function payloadRows(array $rows): array
    {
        return collect($rows)
            ->map(function ($row) {
                if (blank(data_get($row, 'phone_hash'))) {
                    return null;
                }

                return [
                    (string) data_get($row, 'email_hash', ''),
                    (string) data_get($row, 'phone_hash', ''),
                    (string) data_get($row, 'external_id_hash', ''),
                    $this->customerQualityService->hashCountry((string) data_get($row, 'country_code', 'BD')),
                    (string) max(0.01, round((float) data_get($row, 'lookalike_value', 0), 2)),
                ];
            })
            ->filter()
            ->values()
            ->all();
    }

    private function previewRows(array $rows): array
    {
        return collect($rows)
            ->take(100)
            ->map(fn ($row) => [
                'metric_key' => (string) data_get($row, 'metric_key', ''),
                'phone' => (string) data_get($row, 'formatted_phone', data_get($row, 'normalized_phone', '')),
                'phone_hash' => substr((string) data_get($row, 'phone_hash', ''), 0, 16),
                'email_hash' => substr((string) data_get($row, 'email_hash', ''), 0, 16),
                'external_id_hash' => substr((string) data_get($row, 'external_id_hash', ''), 0, 16),
                'country_code' => (string) data_get($row, 'country_code', 'BD'),
                'lookalike_value' => (float) data_get($row, 'lookalike_value', 0),
                'quality_score' => (float) data_get($row, 'quality_score', 0),
            ])
            ->values()
            ->all();
    }

    private function validatedDryRunPlan(int $dryRunLogId, string $audienceKey, ?array $metricKeys): array
    {
        $syncAllEligible = $metricKeys === null;
        $plan = $this->customerQualityService->audiencePlan($audienceKey, $metricKeys, $syncAllEligible);
        $dryRun = CustomerQualitySyncLog::query()->find($dryRunLogId);

        if (! $dryRun
            || $dryRun->audience_key !== $audienceKey
            || $dryRun->mode !== 'dry_run'
            || $dryRun->status !== 'dry_run'
            || blank($dryRun->scope_hash)
            || blank($dryRun->checksum)
            || ! $dryRun->expires_at
            || $dryRun->expires_at->isPast()
            || $dryRun->scope_hash !== $plan['scope_hash']
            || $dryRun->checksum !== $plan['checksum']
        ) {
            throw new \RuntimeException('Dry run scope/checksum/expiry no longer matches this Meta upload request.');
        }

        return [$plan, $dryRun];
    }

    private function newUploadLog(string $mode, array $plan, CustomerQualitySyncLog $dryRun, ?int $adminId): CustomerQualitySyncLog
    {
        return CustomerQualitySyncLog::query()->create([
            'audience_key' => $plan['audience_key'],
            'audience_name' => $plan['audience_name'],
            'mode' => $mode,
            'status' => 'running',
            'scope_hash' => $plan['scope_hash'],
            'checksum' => $plan['checksum'],
            'expires_at' => $dryRun->expires_at,
            'eligible_count' => $plan['eligible_count'],
            'add_count' => $plan['add_count'],
            'remove_count' => $plan['remove_count'],
            'skipped_count' => $plan['skipped_count'],
            'skipped_reasons' => $plan['skipped_reasons'],
            'skipped_rows' => $plan['skipped_rows'],
            'meta_response' => [
                'dry_run_log_id' => $dryRun->id,
                'schema' => self::VALUE_BASED_SCHEMA,
                'skipped_reasons' => $plan['skipped_reasons'],
                'dataset_snapshot' => collect($plan['rows'])->pluck('phone_hash')->filter()->sort()->values()->all(),
            ],
            'synced_by_admin_id' => $adminId,
            'started_at' => now(),
        ]);
    }

    private function failLog(CustomerQualitySyncLog $log, Throwable $throwable): void
    {
        $metaError = $throwable instanceof MetaMarketingApiException
            ? $throwable->logContext()
            : ['message' => $throwable->getMessage()];

        $log->fill([
            'status' => 'failed',
            'error_message' => $throwable->getMessage(),
            'meta_http_status' => data_get($metaError, 'http_status'),
            'meta_error_code' => data_get($metaError, 'code'),
            'meta_error_subcode' => data_get($metaError, 'subcode'),
            'meta_trace_id' => data_get($metaError, 'trace_id'),
            'meta_response' => array_replace((array) $log->meta_response, [
                'error' => $metaError,
                'diagnostics' => $this->diagnostics,
            ]),
            'finished_at' => now(),
        ])->save();

        $this->customerQualityService->rememberAudienceSyncResult((string) $log->audience_key, [
            'last_failed_at' => now()->toDateTimeString(),
            'last_failed_count' => 1,
            'last_api_error' => $throwable->getMessage(),
            'meta_request_id' => (string) data_get($metaError, 'trace_id', ''),
        ]);
    }

    private function assertSuccessfulResponse(string $operation, Response $response): void
    {
        if (! $response->successful()) {
            throw MetaMarketingApiException::fromResponse($operation, $response);
        }
    }

    private function assertMarketingConfig(): void
    {
        $token = $this->accessToken();
        $accountId = $this->adAccountId();

        if ($token === '' || $this->looksLikePlaceholder($token)) {
            throw new \RuntimeException('A real Meta Marketing API access token is required in config/services.php.');
        }

        if (! preg_match('/^act_[0-9]+$/', $accountId) || $this->looksLikePlaceholder($accountId)) {
            throw new \RuntimeException('A real Meta ad account ID in act_123456789 format is required in config/services.php.');
        }

        if (! Schema::hasTable('customer_quality_audience_members') || ! Schema::hasTable('customer_quality_sync_logs')) {
            throw new \RuntimeException('Customer quality sync tables are missing. Please run migrations first.');
        }
    }

    private function normalizeAudienceName(string $name): string
    {
        return mb_strtolower(trim(preg_replace('/\s+/', ' ', $name) ?? $name));
    }

    private function looksLikePlaceholder(string $value): bool
    {
        $value = strtolower($value);

        return str_contains($value, 'your_')
            || str_contains($value, '_here')
            || str_contains($value, 'real_meta')
            || str_contains($value, 'placeholder');
    }

    private function graphUrl(string $path): string
    {
        $version = trim((string) config('services.meta_marketing.graph_version', 'v20.0')) ?: 'v20.0';
        $version = str_starts_with($version, 'v') ? $version : 'v'.$version;

        return 'https://graph.facebook.com/'.$version.$path;
    }

    private function accessToken(): string
    {
        return trim((string) config('services.meta_marketing.access_token', ''));
    }

    private function adAccountId(): string
    {
        $accountId = trim((string) config('services.meta_marketing.ad_account_id', ''));

        return $accountId !== '' && ! str_starts_with($accountId, 'act_')
            ? 'act_'.$accountId
            : $accountId;
    }

    private function timeout(): int
    {
        return max(5, (int) config('services.meta_marketing.timeout', 15));
    }
}
