<?php

namespace App\Jobs;

use App\Models\BlockNumber;
use App\Models\CampaignTransactionLog;
use App\Models\CreditManage;
use App\Models\CutoffDetail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\RcsSendMessage;
use App\Models\RcsSendNumber;
use App\Models\SmartCutoff;
use App\Models\User;
use App\Models\RcsToken;
use App\Models\RcsGateway;
use App\Models\PlanAssign;
use App\Models\RoutingPlan;
use App\Models\RcsTemplate;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Pool as GuzzlePool;
use GuzzleHttp\Psr7\Request as GuzzleRequest;

class RcsSendChunkJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    // Queue configuration
    public $tries = 3;
    public $backoff = [30, 60];
    public $timeout = 3600;
    public $memory = 512;

    protected $numbers;
    protected $message;
    protected $userId;
    protected $requestMessage;
    protected $variables;
    protected $routeId;
    protected $creditPerNumber;
    protected $gatewayName;
    protected $endPoint;
    protected $accessToken;
    protected $clientId;
    protected $gatewayId;
    protected $isVariable;
    protected $gatewayTemplateId;
    protected $cardTitle;
    protected $urlLink;
    protected $buttonText;
    protected $imagePath;

    // Store the actual data instead of IDs
    protected $storedGatewayName;
    protected $storedEndpoint;
    protected $storedClientId;
    protected $storedAccessToken;
    protected $storedBotType;
    protected $tokenId;
    protected $storedTokenId;

    // Chunk specific properties
    protected $chunkNumber;
    protected $totalChunks;

    public function __construct(
        array $numbers, 
        RcsSendMessage $message, 
        int $userId, 
        string $requestMessage, 
        int $routeId, 
        int $creditPerNumber = 1,
        int $chunkNumber,
        int $totalChunks,
        array $variables = []
    ) {
        $this->numbers = $numbers;
        $this->message = $message;
        $this->userId = $userId;
        $this->requestMessage = $requestMessage;
        $this->variables = $variables;
        $this->routeId = $routeId;
        $this->creditPerNumber = $creditPerNumber;
        $this->storedBotType = $message->bot_type;
        $this->chunkNumber = $chunkNumber;
        $this->totalChunks = $totalChunks;

        $this->cardTitle = $message->card_title ?? null;
        $this->urlLink = $message->url_link ?? null;
        $this->buttonText = $message->button_text ?? null;
        $this->imagePath = $message->image_path ?? null;
        $this->isVariable = 0; // Will be set from rcs_templates in fetchAndStoreGatewayDetails()

        // Fetch and store gateway details in constructor to avoid race conditions
        $this->fetchAndStoreGatewayDetails();
    }

    protected function fetchAndStoreGatewayDetails()
    {
        try {
            // Read-only lookups - no locks needed on reference data
            $planAssignDetail = PlanAssign::where('user_ID', $this->userId)
                                ->where('isActive', 1)
                                ->first();

            if(!$planAssignDetail)
            {
                throw new \Exception('Plan assign details not found');
            }

            $plan_ID = $planAssignDetail->plan_ID;

            $routingPlanDetail = RoutingPlan::where('plan_ID', $plan_ID)
                                ->where('route_ID', $this->routeId)
                                ->where('isActive', 1)
                                ->first();

            if(!$routingPlanDetail)
            {
                throw new \Exception('Routing plan details not found');
            }

            $gateway_ID = $routingPlanDetail->gateway_ID;

            $rcsGatewayDetail = RcsGateway::where('id', $gateway_ID)
                                ->where('isActive', 1)
                                ->first();

            if(!$rcsGatewayDetail)
            {
                throw new \Exception('RCS gateway details not found');
            }

            $this->storedGatewayName = $rcsGatewayDetail->gateway_name;
            $this->storedEndpoint = $rcsGatewayDetail->end_point;
            $this->gatewayId = $gateway_ID;

            // Fetch template first — if it already remembers which specific token it
            // belongs to, use that instead of guessing between multiple matching tokens.
            $templateDetail = RcsTemplate::where('template_name', $this->message->template_name)
                ->where('bot_type', $this->storedBotType)
                ->where('user_ID', $this->userId)
                ->where('isActive', 1)
                ->first();

            if($templateDetail && $templateDetail->token_id)
            {
                $rcsTokenDetail = RcsToken::where('id', $templateDetail->token_id)
                                ->where('isActive', 1)
                                ->first();
            }
            else
            {
                $rcsTokenDetail = RcsToken::where('user_ID', $this->userId)
                                ->where('bot_type', $this->storedBotType)
                                ->where('gateway_id', $this->gatewayId)
                                ->where('isActive', 1)
                                ->first();
            }

            if(!$rcsTokenDetail)
            {
                throw new \Exception('RCS token details not found');
            }

            $this->storedClientId = $rcsTokenDetail->client_id;
            $this->storedAccessToken = $rcsTokenDetail->access_token;
            $this->storedTokenId = $rcsTokenDetail->id;

            // Set the instance variables from stored data
            $this->gatewayName = $this->storedGatewayName;
            $this->endPoint = $this->storedEndpoint;
            $this->clientId = $this->storedClientId;
            $this->accessToken = $this->storedAccessToken;
            $this->tokenId = $this->storedTokenId;

            $this->isVariable = $templateDetail->is_variable ?? 0;
            $this->gatewayTemplateId = $templateDetail->template_id ?? null;

            Log::info('[RCS][CHUNK] Gateway & Template details resolved', [
                'user_id' => $this->userId,
                'gateway_name' => $this->gatewayName,
                'endpoint' => $this->endPoint,
                'client_id_botId' => $this->clientId,
                'bot_type' => $this->storedBotType,
                'template_name' => $this->message->template_name,
                'is_variable' => $this->isVariable,
                'template_found_in_db' => $templateDetail ? true : false,
                'template_id' => $templateDetail->id ?? null,
            ]);

            $this->cardTitle = $this->message->card_title ?? null;
            $this->urlLink = $this->message->url_link ?? null;
            $this->buttonText = $this->message->button_text ?? null;
            $this->imagePath = $this->imagePath ?? $this->message->image_path ?? null;
        }
        catch (\Exception $e)
        {
            throw $e;
        }
    }

    public function handle(): void
    {
        // Use chunk-specific lock.
        //
        // This MUST NOT use the default (file) cache store. Cache::add()/forget()
        // on the file driver race each other across concurrent chunk jobs: one
        // process removes a cache file/directory while another is opening it,
        // throwing "fopen: No such file or directory" *before* any logging, which
        // killed the job and silently dropped that chunk's numbers.
        //
        // Cache::store('database')->lock() uses Laravel's DatabaseLock, which
        // acquires via a unique-key INSERT — atomicity is guaranteed by the
        // database, not the filesystem — and automatically reclaims locks whose
        // expiration has passed, so a crashed job cannot deadlock the chunk.
        $lockKey = 'rcs_chunk_processing_' . $this->message->id . '_' . $this->chunkNumber;
        $lock = Cache::store('database')->lock($lockKey, 7200);
        if (!$lock->get()) {
            Log::warning('RcsSendChunkJob: Chunk already processing or processed', [
                'message_id' => $this->message->id,
                'chunk_number' => $this->chunkNumber,
                'job_id' => $this->message->job_id
            ]);
            return;
        }

        try {
            if(!$this->gatewayName || !$this->endPoint || !$this->accessToken || !$this->clientId) 
            {
                $this->fetchAndStoreGatewayDetails();
                
                if(!$this->gatewayName || !$this->endPoint || !$this->accessToken || !$this->clientId) 
                {
                    Log::error('RCS Chunk Job: Missing gateway details after fetch attempt', [
                        'user_id' => $this->userId,
                        'route_id' => $this->routeId,
                        'gateway_name' => $this->gatewayName,
                        'endpoint' => $this->endPoint,
                        'chunk_number' => $this->chunkNumber
                    ]);
                    return;
                }
            }

            $parent_id = User::find($this->userId)?->created_by ?? $this->userId;

            // Log chunk job start
            Log::info('RcsSendChunkJob started', [
                'job_id' => $this->message->job_id,
                'user_id' => $this->userId,
                'route_id' => $this->routeId,
                'chunk_number' => $this->chunkNumber,
                'total_chunks' => $this->totalChunks,
                'numbers_in_chunk' => count($this->numbers),
                'gateway' => $this->gatewayName
            ]);

            // Cut-Off Details (apply proportionally for chunks)
            $cutOffDetail = CutoffDetail::where('user_ID', $this->userId)
                            ->where('cutoff_route_ID', $this->routeId)
                            ->where('isActive', 1)
                            ->first();

            $throughput = null;
            $min_cut_value = null;
            $cutoff_status = null;

            if($cutOffDetail)
            {
                $throughput = $cutOffDetail->throughput;
                $min_cut_value = $cutOffDetail->min_cut_value;
                $cutoff_status = $cutOffDetail->cutoff_status;
            }

            // === PASS 1: Validate numbers and collect for block check ===
            $validNumbers = [];
            $numbersToFetch = [];

            foreach($this->numbers as $number)
            {
                $number = trim($number);

                if(empty($number) || !is_numeric($number))
                {
                    continue;
                }

                $originalNumber = $number;
                $normalizedNumber = $originalNumber;

                if(str_starts_with($normalizedNumber, '+91'))
                {
                    $normalizedNumber = substr($normalizedNumber, 3);
                }

                if(strlen($normalizedNumber) !== 10 || !is_numeric($normalizedNumber))
                {
                    continue;
                }

                if(!str_starts_with($originalNumber, '+91'))
                {
                    $number = '+91'.$originalNumber;
                }
                else
                {
                    $number = $originalNumber;
                }

                if(strlen($number) > 13)
                {
                    continue;
                }

                $validNumbers[] = ['number' => $number, 'original' => $originalNumber];
                $numbersToFetch[] = $number;
                $numbersToFetch[] = $originalNumber;
            }

            $uniqueNumbersToFetch = array_unique($numbersToFetch);
            unset($numbersToFetch);

            // Fetch block numbers using pluck for speed
            $blockNumbers = [];
            if(!empty($uniqueNumbersToFetch))
            {
                $chunkSize = 5000;

                foreach(array_chunk($uniqueNumbersToFetch, $chunkSize) as $chunk)
                {
                    $chunkResults = BlockNumber::whereIn('mobile_number', $chunk)
                                        ->where('user_ID', $this->userId)
                                        ->pluck('status', 'mobile_number')
                                        ->toArray();

                    foreach($chunkResults as $mobile => $status) {
                        $blockNumbers[$this->normalizeNumber($mobile)] = $status;
                    }
                }
            }

            unset($uniqueNumbersToFetch);

            // Count valid numbers for cutoff calculation
            $validNumbersCount = 0;
            foreach($validNumbers as $entry)
            {
                $normalizedNum = $this->normalizeNumber($entry['number']);
                $effectiveBlockStatus = $blockNumbers[$normalizedNum] ?? ($blockNumbers[$this->normalizeNumber($entry['original'])] ?? null);
                if($effectiveBlockStatus !== 1) {
                    $validNumbersCount++;
                }
            }

            // Apply cutoff logic for this chunk
            $applyCutoff = false;
            $cutoffPercentage = null;
            $numbersToMarkAsCutoff = 0;

            if($cutOffDetail && $cutoff_status && $throughput)
            {
                $throughputParts = explode('-', $throughput);

                if(count($throughputParts) === 2)
                {
                    $minPercent = (int)trim($throughputParts[0]);
                    $maxPercent = (int)trim($throughputParts[1]);
                    $cutoffPercentage = rand($minPercent, $maxPercent);
                    $numbersToMarkAsCutoff = (int)ceil(($cutoffPercentage / 100) * $validNumbersCount);
                    $applyCutoff = ($numbersToMarkAsCutoff > 0);
                }
            }

            // === PASS 2: Build records and process list in a single loop ===
            $cutoffCounter = 0;
            $numbersToProcess = [];
            $records = [];
            $msgCredit = $this->creditPerNumber;
            $now = now();

            foreach($validNumbers as $entry)
            {
                $number = $entry['number'];
                $originalNumber = $entry['original'];

                $status = "Submitted";
                $isPicked = 0;

                $normalizedCurrentNumber = $this->normalizeNumber($number);
                $effectiveBlockStatus = $blockNumbers[$normalizedCurrentNumber] ?? ($blockNumbers[$this->normalizeNumber($originalNumber)] ?? null);

                $masterJobId = $this->generateRandomAlphaNumeric(20);

                if($applyCutoff && $effectiveBlockStatus !== 1 && $cutoffCounter < $numbersToMarkAsCutoff)
                {
                    $status = $cutoff_status;
                    $isPicked = 1;
                    $cutoffCounter++;
                }
                elseif($effectiveBlockStatus === 1)
                {
                    continue;
                }
                elseif($effectiveBlockStatus === 0)
                {
                    $status = 'Blacklisted';
                    $isPicked = 1;
                }
                else
                {
                    $numbersToProcess[] = [
                        'number' => $number,
                        'is_picked' => $isPicked,
                        'master_job_id' => $masterJobId
                    ];
                }

                $records[] = [
                    'message_id' => $this->message->id,
                    'mobile_number' => $number,
                    'master_job_id' => $masterJobId,
                    'msg_job_id' => $this->message->job_id,
                    'parent_id' => $parent_id,
                    'message' => $this->requestMessage,
                    'msgcredit' => $msgCredit,
                    'status' => $status,
                    'route' => $this->message->route,
                    'userid' => $this->userId,
                    'is_picked' => $isPicked,
                    'service_id' => $this->gatewayName,
                    'created_by' => $this->userId,
                    'created_at' => $now,
                    'updated_at' => null,
                ];
            }

            unset($validNumbers, $blockNumbers);

            if(!empty($records))
            {
                Log::info('RCS Chunk - Inserting records', [
                    'chunk_number' => $this->chunkNumber,
                    'record_count' => count($records),
                    'numbers_to_process' => count($numbersToProcess),
                    'gateway' => $this->gatewayName
                ]);

                // Insert records in larger batches for speed
                $insertBatchSize = 3000;
                $totalInserted = 0;

                foreach(array_chunk($records, $insertBatchSize) as $chunk)
                {
                    RcsSendNumber::insert($chunk);
                    $totalInserted += count($chunk);
                }

                // Atomically update the numbers_count for this chunk
                $this->updateMessageCount(count($records));

                // Process numbers for API calls
                $this->processNumbers($numbersToProcess);

                // Adjust credits: refund difference between input numbers and actual valid records
                $inputCount = count($this->numbers);
                $actualCount = count($records);
                if($actualCount < $inputCount)
                {
                    $creditDifference = $this->creditPerNumber * ($inputCount - $actualCount);

                    if($creditDifference > 0)
                    {
                        // Atomically refund excess credits
                        $creditManageDetail = CreditManage::where('route_id', $this->routeId)
                                            ->where('userid', $this->userId)
                                            ->where('isActive', 1)
                                            ->first();

                        if($creditManageDetail)
                        {
                            $creditManageDetail->increment('balance', $creditDifference);

                            // Atomically reduce msg_credit on the message
                            RcsSendMessage::where('id', $this->message->id)
                                ->update([
                                    'msg_credit' => DB::raw('msg_credit - ' . $creditDifference)
                                ]);

                            Log::info('RCS Chunk - Credit adjusted', [
                                'chunk_number' => $this->chunkNumber,
                                'input_numbers' => $inputCount,
                                'actual_records' => $actualCount,
                                'credit_refunded' => $creditDifference
                            ]);
                        }
                    }
                }

                Log::info('RcsSendChunkJob completed', [
                    'job_id' => $this->message->job_id,
                    'chunk_number' => $this->chunkNumber,
                    'records_inserted' => $totalInserted,
                ]);
            } 
            else 
            {
                Log::warning('RcsSendChunkJob: No valid records to insert for chunk', [
                    'job_id' => $this->message->job_id,
                    'chunk_number' => $this->chunkNumber,
                    'total_input_numbers' => count($this->numbers)
                ]);
            }
            
        } finally {
            // Release chunk lock
            $lock->release();
        }
    }

    protected function updateMessageCount(int $count): void
    {
        if ($count <= 0) {
            return;
        }

        // Use atomic update to increment numbers_count
        $updated = RcsSendMessage::where('id', $this->message->id)
            ->update([
                'numbers_count' => DB::raw('COALESCE(numbers_count, 0) + ' . $count),
                'updated_at' => now()
            ]);
        
        if ($updated) {
            Log::debug('RCS message count updated from chunk', [
                'message_id' => $this->message->id,
                'chunk_number' => $this->chunkNumber,
                'increment_by' => $count
            ]);
        }
    }

    protected function processNumbers($numbersToProcess)
    {
        $toSend = array_filter($numbersToProcess, fn($n) => $n['is_picked'] == 0);

        if(empty($toSend)) {
            return;
        }

        if(!$this->gatewayName || !$this->endPoint || !$this->accessToken || !$this->clientId) {
            Log::warning('[RCS][CHUNK] Missing gateway details, skipping API calls', [
                'chunk_number' => $this->chunkNumber,
                'gateway' => $this->gatewayName
            ]);
            return;
        }

        $toSendArray = array_values($toSend);
        // Vi RBM support confirmed the account's real limit: 100 TPS, shared
        // across the whole aggregator (not exclusive to this sub-account).
        // 10 is the highest concurrency directly measured on this account: a
        // chunk run alone at concurrency 10 sustained ~64 req/sec with 99.3%
        // accepted and zero 429s — 64% of the confirmed ceiling, leaving
        // margin for other sub-aggregator traffic sharing the same 100 TPS.
        // Only safe because the global send lock below already guarantees
        // just one chunk is ever sending at a time — this number would be
        // wrong without that lock (see the 40k/50k test campaigns that hit
        // heavy 429s at this same concurrency before the lock existed).
        $concurrency = 10;

        // $concurrency above only bounds THIS job's own Guzzle Pool. Vi's rate
        // limit applies to the whole account, and several chunk jobs send at
        // the same time with no coordination between them — real in-flight
        // load is (parallel chunk jobs x concurrency), not just concurrency.
        // That's what drove the 429 bursts on the 50k and 40k test campaigns
        // (chunks running alone measured ~99% accepted; the same chunks running
        // four-wide measured ~36%, same per-job concurrency both times).
        //
        // This lock makes chunk jobs take turns sending instead of overlapping.
        // Keyed per gateway so gateways other than Vi aren't slowed by it.
        // Same DatabaseLock mechanism as the chunk-processing lock above (line
        // ~225), which is already proven atomic under real worker contention.
        $globalLockKey = 'rcs_global_send_lock_' . $this->gatewayName;
        $globalLock = Cache::store('database')->lock($globalLockKey, 600);
        try {
            $globalLock->block(3600);
        } catch (\Illuminate\Contracts\Cache\LockTimeoutException $e) {
            Log::error('[RCS][CHUNK] Timed out waiting for global send lock', [
                'chunk_number' => $this->chunkNumber,
                'gateway' => $this->gatewayName,
            ]);
            return;
        }

        try {

        Log::info('[RCS][CHUNK] Sending API requests with streaming pool', [
            'chunk_number' => $this->chunkNumber,
            'total_to_send' => count($toSendArray),
            'concurrency' => $concurrency,
            'gateway' => $this->gatewayName,
            'job_id'           => $this->message->job_id,
        ]);

        $totalSuccess = 0;
        $totalFail = 0;
        $has401 = false;
        $firstException = null;
        $firstErrorStatus = null;
        $firstErrorBody = null;
        // Rejections the gateway returned synchronously, grouped by err code
        // (e.g. '429', '404-A', 'NETWORK_ERROR') so a batched UPDATE can be run
        // per group instead of leaving these numbers stuck at 'Submitted' forever.
        $failedNumbers = [];

        // Single Guzzle client - reuses TCP connections and SSL sessions across ALL requests
        $client = new GuzzleClient([
            'connect_timeout' => 15,
            'timeout' => 30,
            'http_errors' => false, // Don't throw on 4xx/5xx, handle in fulfilled callback
            'headers' => match($this->gatewayName) {
                'pinnacle' => [
                    'Content-Type' => 'application/json',
                    'apikey' => $this->accessToken,
                    'botid' => $this->clientId,
                ],
                default => [
                    'Content-Type' => 'application/json',
                    'Authorization' => 'Bearer ' . $this->accessToken,
                ],
            },
            'curl' => [
                CURLOPT_TCP_KEEPALIVE => 1,
                CURLOPT_TCP_KEEPIDLE => 30,
            ],
        ]);

        // Generator yields requests one by one - Guzzle streams them with concurrency limit
        $requests = function () use ($toSendArray) {
            foreach($toSendArray as $index => $numberData) {
                $payload = $this->buildApiPayload($numberData['number'], $numberData['master_job_id']);
                if($payload) {
                    yield $index => new GuzzleRequest(
                        'POST',
                        $payload['url'],
                        [], // Headers set on client level
                        json_encode($payload['data'])
                    );
                }
            }
        };

        try {
            $pool = new GuzzlePool($client, $requests(), [
                'concurrency' => $concurrency,
                'fulfilled' => function ($response, $index) use (&$totalSuccess, &$totalFail, &$has401, &$firstErrorStatus, &$firstErrorBody, &$failedNumbers, $toSendArray) {
                    $status = $response->getStatusCode();
                    if($status >= 200 && $status < 300) {
                        $totalSuccess++;
                    } else {
                        $totalFail++;
                        if($status === 401) {
                            $has401 = true;
                        }
                        if($firstErrorBody === null) {
                            $firstErrorStatus = $status;
                            $firstErrorBody = substr((string) $response->getBody(), 0, 2000);
                        }
                        if(isset($toSendArray[$index]['master_job_id'])) {
                            $failedNumbers[(string) $status][] = $toSendArray[$index]['master_job_id'];
                        }
                    }
                },
                'rejected' => function ($reason, $index) use (&$totalFail, &$firstException, &$failedNumbers, $toSendArray) {
                    $totalFail++;
                    if($firstException === null) {
                        $firstException = [
                            'message' => $reason->getMessage(),
                            'code' => $reason->getCode(),
                            'type' => get_class($reason)
                        ];
                    }
                    if(isset($toSendArray[$index]['master_job_id'])) {
                        $failedNumbers['NETWORK_ERROR'][] = $toSendArray[$index]['master_job_id'];
                    }
                },
            ]);

            $pool->promise()->wait();
        } catch(\Throwable $e) {
            Log::error('[RCS][CHUNK][POOL] Pool error', [
                'chunk_number' => $this->chunkNumber,
                'error' => $e->getMessage()
            ]);
        }

        // Log failures if any
        if($totalFail > 0) {
            Log::warning('[RCS][CHUNK][API] Pool has failures', [
                'chunk_number' => $this->chunkNumber,
                'success' => $totalSuccess,
                'failed' => $totalFail,
                'first_error_status' => $firstErrorStatus,
                'first_error_body' => $firstErrorBody,
                'first_exception' => $firstException
            ]);
        }

        // Handle 401 - refresh token and retry ALL requests
        if($has401) {
            Log::info('[RCS][CHUNK] Token expired, refreshing and retrying all', [
                'chunk_number' => $this->chunkNumber
            ]);

            $this->refreshToken();

            $retrySuccess = 0;
            $retryFail = 0;
            // Retry re-sends every request, so its outcome is authoritative —
            // discard whatever the primary pass collected and start fresh.
            $failedNumbers = [];

            $retryClient = new GuzzleClient([
                'connect_timeout' => 15,
                'timeout' => 30,
                'http_errors' => false,
                'headers' => match($this->gatewayName) {
                    'pinnacle' => [
                        'Content-Type' => 'application/json',
                        'apikey' => $this->accessToken,
                        'botid' => $this->clientId,
                    ],
                    default => [
                        'Content-Type' => 'application/json',
                        'Authorization' => 'Bearer ' . $this->accessToken,
                    ],
                },
                'curl' => [
                    CURLOPT_TCP_KEEPALIVE => 1,
                    CURLOPT_TCP_KEEPIDLE => 30,
                ],
            ]);

            $retryRequests = function () use ($toSendArray) {
                foreach($toSendArray as $index => $numberData) {
                    $payload = $this->buildApiPayload($numberData['number'], $numberData['master_job_id']);
                    if($payload) {
                        yield $index => new GuzzleRequest(
                            'POST',
                            $payload['url'],
                            [],
                            json_encode($payload['data'])
                        );
                    }
                }
            };

            try {
                $retryPool = new GuzzlePool($retryClient, $retryRequests(), [
                    'concurrency' => $concurrency,
                    'fulfilled' => function ($response, $index) use (&$retrySuccess, &$retryFail, &$failedNumbers, $toSendArray) {
                        $status = $response->getStatusCode();
                        if($status >= 200 && $status < 300) {
                            $retrySuccess++;
                        } else {
                            $retryFail++;
                            if(isset($toSendArray[$index]['master_job_id'])) {
                                $failedNumbers[(string) $status][] = $toSendArray[$index]['master_job_id'];
                            }
                        }
                    },
                    'rejected' => function ($reason, $index) use (&$retryFail, &$failedNumbers, $toSendArray) {
                        $retryFail++;
                        if(isset($toSendArray[$index]['master_job_id'])) {
                            $failedNumbers['NETWORK_ERROR'][] = $toSendArray[$index]['master_job_id'];
                        }
                    },
                ]);

                $retryPool->promise()->wait();
            } catch(\Throwable $e) {
                Log::error('[RCS][CHUNK][POOL] Retry pool error', [
                    'chunk_number' => $this->chunkNumber,
                    'error' => $e->getMessage()
                ]);
            }

            // Replace totals with retry results
            $totalSuccess = $retrySuccess;
            $totalFail = $retryFail;

            Log::info('[RCS][CHUNK][API] Retry results', [
                'chunk_number' => $this->chunkNumber,
                'retry_success' => $retrySuccess,
                'retry_failed' => $retryFail
            ]);
        }

        // Record synchronous rejections (429 rate-limit, 400/403/404, network
        // errors, etc.) that would otherwise leave these numbers stuck at
        // 'Submitted' forever — they were never accepted by the gateway, so no
        // async DLR will ever arrive to correct the status later.
        // NOTE: rcs_send_numbers has no err_code column (unlike send_numbers for
        // SMS), so the specific gateway code is recorded in the log line below
        // rather than the row. Adding a column would be a schema change.
        $totalMarkedFailed = 0;
        $failedByCode = [];
        foreach($failedNumbers as $errCode => $masterJobIds) {
            $failedByCode[$errCode] = count($masterJobIds);
            foreach(array_chunk($masterJobIds, 500) as $chunk) {
                $totalMarkedFailed += DB::table('rcs_send_numbers')
                    ->whereIn('master_job_id', $chunk)
                    ->where('status', 'Submitted')
                    ->update([
                        'status'     => 'Failed',
                        'updated_at' => now(),
                    ]);
            }
        }

        Log::info('[RCS][CHUNK] All API requests processed', [
            'chunk_number' => $this->chunkNumber,
            'failed_by_code' => $failedByCode,
            'total_sent' => count($toSendArray),
            'total_success' => $totalSuccess,
            'total_failed' => $totalFail,
            'marked_failed_in_db' => $totalMarkedFailed
        ]);

        } finally {
            $globalLock->release();
        }
    }

    protected function buildApiPayload($number, $masterJobId): ?array
    {
        return match($this->gatewayName) {
            'jio'        => $this->buildJioPayload($number, $masterJobId, []),
            'vi'         => $this->buildViPayload($number, $masterJobId, []),
            'valuefirst' => $this->buildValueFirstPayload($number, $masterJobId, []),
            'pinnacle'   => $this->buildPinnaclePayload($number, $masterJobId, []),
            default      => null
        };
    }

    /* ========================== JIO API ========================== */
    protected function buildJioPayload($number, $masterJobId, $headers): array
    {
        $url = rtrim($this->endPoint, '/') . "/v1/messaging/users/{$number}/assistantMessages/async?messageId={$masterJobId}";

        if($this->message->template_type === 'rich_card')
        {
            $imgUrl = null;

            if($this->message->image_path)
            {
                $imgUrl = filter_var($this->message->image_path, FILTER_VALIDATE_URL)
                    ? $this->message->image_path
                    : url($this->message->image_path);
            }
            elseif($this->imagePath)
            {
                $imgUrl = filter_var($this->imagePath, FILTER_VALIDATE_URL)
                    ? $this->imagePath
                    : url($this->imagePath);
            }

            if($imgUrl && !$this->urlLink)
            {
                $data = [
                    'content' => [
                        'richCardDetails' => [
                            'standalone' => [
                                'cardOrientation' => "VERTICAL",
                                'content' => [
                                    'cardTitle' => $this->cardTitle,
                                    'cardDescription' => urldecode($this->requestMessage),
                                    'cardMedia' => [
                                        'mediaHeight' => "TALL",
                                        'contentInfo' => ['fileUrl' => $imgUrl]
                                    ]
                                ]
                            ]
                        ]
                    ]
                ];
            }
            elseif($imgUrl && $this->urlLink)
            {
                $data = [
                    'content' => [
                        'richCardDetails' => [
                            'standalone' => [
                                'cardOrientation' => "VERTICAL",
                                'content' => [
                                    'cardTitle' => $this->cardTitle,
                                    'cardDescription' => urldecode($this->requestMessage),
                                    'cardMedia' => [
                                        'mediaHeight' => "TALL",
                                        'contentInfo' => ['fileUrl' => $imgUrl]
                                    ],
                                    'suggestions' => [
                                        [
                                            'action' => [
                                                'plainText' => $this->buttonText,
                                                'postBack' => ['data' => " "],
                                                'openUrl' => ['url' => $this->urlLink]
                                            ]
                                        ]
                                    ]
                                ]
                            ]
                        ]
                    ]
                ];
            }
            else
            {
                $data = ['content' => ['plainText' => urldecode($this->requestMessage)]];
            }
        }
        else
        {
            $data = ['content' => ['plainText' => urldecode($this->requestMessage)]];
        }

        return ['url' => $url, 'data' => $data, 'headers' => $headers];
    }

    /* ========================== VI (DOTGO) API ========================== */
    protected function buildViPayload($number, $masterJobId, $headers): array
    {
        $url = rtrim($this->endPoint, '/') .
            "/rcs/v1/phones/{$number}/agentMessages/async?sendGipLink=false&messageId={$masterJobId}&botId={$this->clientId}";

        if($this->isVariable == 0)
        {
            $data = [
                'contentMessage' => [
                    'templateMessage' => [
                        'templateCode' => $this->message->template_name
                    ]
                ]
            ];
        }
        else
        {
            // Prefer the real values submitted with the send; only guess from the
            // resolved text if the request didn't carry them (older client, etc.).
            $customParams = !empty($this->variables)
                ? $this->variables
                : $this->extractCustomParams($this->message->message, $this->requestMessage);

            $data = [
                'contentMessage' => [
                    'templateMessage' => [
                        'templateCode' => $this->message->template_name,
                        // Vi's API expects customParams as a JSON-encoded string, not a nested object
                        // (per their docs: "customParams":"{\"name\":\"user1\"}").
                        'customParams' => json_encode($customParams, JSON_UNESCAPED_UNICODE)
                    ]
                ]
            ];
        }

        return ['url' => $url, 'data' => $data, 'headers' => $headers];
    }

    /* ========================== VALUEFIRST API ========================== */
    protected function buildValueFirstPayload($number, $masterJobId, $headers): array
    {
        // Prefer the real values submitted with the send; only guess from the
        // resolved text if the request didn't carry them (older client, etc.).
        $customParams = !empty($this->variables)
            ? $this->variables
            : $this->extractCustomParams($this->message->message, $this->requestMessage);

        $data = [
            'apiver' => '1.0',
            'rcs' => [
                'messages' => [
                    [
                        'id' => $masterJobId,
                        'msgtype' => $this->message->template_type === 'rich_card' ? 'rich_card' : 'text',
                        'template_name' => $this->message->template_name,
                        'addresses' => [
                            [
                                'seq' => $masterJobId,
                                'to' => $number,
                                'from' => $this->clientId,
                                'custom_params' => $this->isVariable
                                    ? $customParams
                                    : null
                            ]
                        ]
                    ]
                ]
            ]
        ];

        return ['url' => $this->endPoint, 'data' => $data, 'headers' => $headers];
    }

    /* ========================== PINNACLE API ========================== */
    protected function buildPinnaclePayload($number, $masterJobId, $headers): array
    {
        // rcs_gateway.end_point for pinnacle should be stored as "https://rcsapi.pinnacle.in/api"
        $url = rtrim($this->endPoint, '/') . '/v1/send-message';

        $variables = [];

        if($this->isVariable)
        {
            // Prefer the real values submitted with the send; only guess from the
            // resolved text if the request didn't carry them (older client, etc.).
            $customParams = !empty($this->variables)
                ? $this->variables
                : $this->extractCustomParams($this->message->message, $this->requestMessage);

            foreach($customParams as $key => $value)
            {
                $variables[] = ['key' => $key, 'value' => $value];
            }
        }

        $data = [
            'category' => match(strtolower($this->storedBotType)) {
                'promo' => 'promotional',
                'trans' => 'transactional',
                default => strtolower($this->storedBotType),
            },
            'correlationId' => $masterJobId,
            'messages' => [
                [
                    'to' => $number,
                    'templateId' => $this->gatewayTemplateId ?: $this->message->template_name,
                    'variables' => $variables,
                    'isSMSFallbackRequired' => false,
                ]
            ]
        ];

        return ['url' => $url, 'data' => $data, 'headers' => $headers];
    }

    protected function refreshToken()
    {
        try
        {
            if($this->gatewayName === 'jio')
            {
                $tokenEndpoint = 'https://tgs.businessmessaging.jio.com/v1/oauth/token';

                $response = Http::asForm()->post($tokenEndpoint, [
                    'grant_type' => 'client_credentials',
                    'client_id' => $this->clientId,
                    'client_secret' => $this->getClientSecret(),
                    'scope' => 'read'
                ]);
            }
            elseif($this->gatewayName === 'vi')
            {
                $authHeader = 'Basic ' . base64_encode($this->clientId . ':' . $this->getClientSecret());

                $response = Http::withHeaders([
                    'Authorization' => $authHeader,
                    'Content-Type' => 'application/x-www-form-urlencoded'
                ])->post('https://auth.virbm.in/auth/oauth/token?grant_type=client_credentials');
            }
            else
            {
                return;
            }

            if($response->successful())
            {
                $tokenData = $response->json();

                if(isset($tokenData['access_token']))
                {
                    $this->accessToken = $tokenData['access_token'];

                    RcsToken::where('id', $this->tokenId)
                            ->update([
                                'access_token' => $this->accessToken,
                                'updated_at' => now()
                            ]);

                    Log::info('[RCS][CHUNK][TOKEN] Token refreshed successfully', [
                        'chunk_number' => $this->chunkNumber,
                        'gateway' => $this->gatewayName
                    ]);
                }
            }
            else
            {
                Log::error('[RCS][CHUNK][TOKEN] Failed to refresh token', [
                    'chunk_number' => $this->chunkNumber,
                    'gateway' => $this->gatewayName,
                    'status' => $response->status()
                ]);
            }
        }
        catch(\Exception $e)
        {
            Log::error('[RCS][CHUNK][TOKEN] Exception during token refresh', [
                'chunk_number' => $this->chunkNumber,
                'error' => $e->getMessage()
            ]);
        }
    }

    protected function getClientSecret()
    {
        $tokenDetail = RcsToken::where('id', $this->tokenId)
                        ->where('isActive', 1)
                        ->first();

        return $tokenDetail ? $tokenDetail->client_secret : null;
    }

    protected function extractCustomParams($templateMessage, $requestMessage)
    {
        preg_match_all('/\{([^}]+)\}/', $templateMessage, $matches);
        
        $params = [];
        
        if(!empty($matches[1])) 
        {
            foreach($matches[1] as $paramName) 
            {
                $params[$paramName] = $this->getParamValue($paramName, $templateMessage, $requestMessage);
            }
        }
        
        return $params;
    }

    protected function getParamValue($paramName, $templateMessage, $requestMessage)
    {
        $pattern = $this->createExtractionPattern($templateMessage, $paramName);
        
        if(preg_match($pattern, $requestMessage, $matches)) 
        {
            return trim($matches[1]);
        }
        
        return $this->extractUsingDelimiters($paramName, $requestMessage);
    }

    protected function createExtractionPattern($templateMessage, $paramName)
    {
        $escapedTemplate = preg_quote($templateMessage, '/');
        
        $escapedTemplate = str_replace('\{' . $paramName . '\}', '(.+?)', $escapedTemplate);
        
        $escapedTemplate = preg_replace('/\\\\\{[^}]+\\\\\}/', '.+?', $escapedTemplate);
        
        return '/^' . $escapedTemplate . '$/';
    }

    protected function extractUsingDelimiters($paramName, $requestMessage)
    {
        $commonPatterns = [
            '/\b' . preg_quote($paramName, '/') . '\s+is\s+([^\s,.]+)/i',
            
            '/\b' . preg_quote($paramName, '/') . '\s*[:=]\s*([^\s,.]+)/i',
            
            '/\b' . preg_quote($paramName, '/') . '\s+([^\s,.]{2,50})/i',
            
            '/\b' . preg_quote($paramName, '/') . '\s+(\w+)/i'
        ];
        
        foreach($commonPatterns as $pattern) 
        {
            if(preg_match($pattern, $requestMessage, $matches)) 
            {
                return trim($matches[1]);
            }
        }
    }

    protected function normalizeNumber(string $number): string
    {
        $number = preg_replace('/[^0-9]/', '', $number);
        
        if(str_starts_with($number, '91') && strlen($number) > 10) 
        {
            return substr($number, 2);
        }
        elseif(str_starts_with($number, '+91')) 
        {
            return substr($number, 3);
        }

        return $number;
    }
    
    protected function generateRandomAlphaNumeric(int $length = 20): string
    {
        $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
        $charactersLength = strlen($characters);
        $randomString = '';
        
        for ($i = 0; $i < $length; $i++) {
            $randomString .= $characters[rand(0, $charactersLength - 1)];
        }
        
        return $randomString;
    }

    public function failed(\Throwable $exception): void
    {
        Log::error('RcsSendChunkJob failed', [
            'message_id' => $this->message->id ?? 'unknown',
            'chunk_number' => $this->chunkNumber,
            'job_id' => $this->message->job_id ?? 'unknown',
            'error' => $exception->getMessage(),
            'trace' => $exception->getTraceAsString()
        ]);
        
        // Release chunk lock on failure. failed() runs in a different process
        // than handle(), so the lock owner token won't match — forceRelease()
        // is required, otherwise the lock would linger until its expiration and
        // block any retry of this chunk.
        $lockKey = 'rcs_chunk_processing_' . ($this->message->id ?? 'unknown') . '_' . $this->chunkNumber;
        Cache::store('database')->lock($lockKey)->forceRelease();
    }
}