<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Models\SendMessage;
use App\Models\SendNumber;
use App\Models\User;
use App\Models\BlockNumber;
use App\Models\CampaignTransactionLog;
use App\Models\CreditManage;
use App\Models\DndNumber;
use App\Models\CutoffDetail;
use App\Models\OtpSend;
use App\Models\SenderDetail;
use App\Models\SendSms;
use App\Models\SmartCutoff;
use App\Models\SpamKeyword;
use App\Models\SpamDrop;
use App\Models\SmsGateway;
use App\Models\UrlTrackingKey;

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

    public $timeout = 3600; // 1 hour timeout
    public $tries = 3; // Max 3 attempts
    public $maxExceptions = 2;

    protected $numbers;
    protected $message;
    protected $userId;
    protected $requestMessage;
    protected $contentTypeId;
    protected $dndCheckId;
    protected $routeId;
    protected $gatewayDistribution;
    
    // Chunk size for bulk inserts
    protected $chunkSize = 2000;

    public function __construct(array $numbers, SendMessage $message, int $userId, string $requestMessage, int $contentTypeId, int $dndCheckId, int $routeId, array $gatewayDistribution = [])
    {
        $this->numbers = $numbers;
        $this->message = $message;
        $this->userId = $userId;
        $this->requestMessage = $requestMessage;
        $this->contentTypeId = $contentTypeId;
        $this->dndCheckId = $dndCheckId;
        $this->routeId = $routeId;
        $this->gatewayDistribution = $gatewayDistribution;
    }

    public function handle(): void
    {
        try {
            // Process the entire batch of numbers (this job already receives a chunk)
            $this->processBatch($this->numbers);

        } catch (\Throwable $e) {
            // Catch \Throwable (not just \Exception) so TypeError/Error from bad
            // gateway config or empty arrays is always logged before the job fails.
            Log::error('SendNumbersJob failed', [
                'job_id'  => $this->message->job_id,
                'user_id' => $this->userId,
                'error'   => $e->getMessage(),
                'trace'   => $e->getTraceAsString()
            ]);

            throw $e;
        }
    }

    protected function processBatch(array $numbers): void
    {
        DB::beginTransaction();

        try {
            $this->processNumbers($numbers);
            DB::commit();
        } catch (\Throwable $e) {
            DB::rollBack();
            throw $e;
        }
    }
    
    protected function processNumbers(array $numbersToProcess): void
    {
        $parent_id = User::find($this->userId)?->created_by ?? $this->userId;
        $tm_ID = User::find($this->userId)?->tm_ID;
        $pe_id = $this->message->pe_id;
        $template_id = $this->message->template_id;
        $metadata = "?smpp?PEID=$pe_id&TID=$template_id";
        
        $gatewayDistribution = $this->gatewayDistribution;
        $gatewayNames = array_column($gatewayDistribution, 'name');
        $serviceIdValue = implode(',', $gatewayNames);

        // Meta Data
        $gatewayTypeId = $this->getGatewayTypeId($gatewayNames);
        
        if($gatewayTypeId == 1) {
            $tm_val = $pe_id . "," . $tm_ID;
            $tmid_hash = hash('sha256', $tm_val);
            $metadata = "?smpp?PEID=$pe_id&TID=$template_id&TMID=$tmid_hash";
        } elseif($gatewayTypeId !== null) {
            $tm_ID_val = $pe_id . "," . $tm_ID;
            $metadata = "?smpp?PEID=$pe_id&TID=$template_id&TMID=$tm_ID_val";
        }

        // Fetch sender_ID
        $senderDetail = SenderDetail::where('id', $this->message->senderid)->first();
        $sender_ID = $senderDetail ? $senderDetail->sender_ID : null;

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

        $throughput = $cutOffDetail?->throughput;
        $min_cut_value = $cutOffDetail?->min_cut_value;
        $cutoff_status = $cutOffDetail?->cutoff_status;

        // Spam Keyword Details
        $spamKeywords = $this->getSpamKeywords();
        $foundSpamKeywords = $this->findSpamKeywords($this->requestMessage, $spamKeywords);
        $containsSpamKeyword = !empty($foundSpamKeywords);

        // Prepare DND and Block numbers - Use only numbers from this batch
        $dndNumbers = $this->getDndNumbers($numbersToProcess);
        $blockNumbers = $this->getBlockNumbers($numbersToProcess);

        // Calculate valid numbers for cutoff - Use only numbers from this batch
        $validNumbersCount = $this->countValidNumbers($numbersToProcess, $blockNumbers);
        
        // Determine cutoff logic
        $cutoffDetails = $this->calculateCutoffDetails($cutOffDetail, $validNumbersCount, $throughput);
        $applyCutoff = $cutoffDetails['apply'];
        $cutoffPercentage = $cutoffDetails['percentage'];
        $numbersToMarkAsCutoff = $cutoffDetails['numbers_to_mark'];
        
        $cutoffCounter = 0;

        // Prepare gateway distribution - Based on batch size
        $gatewayIterator = $this->prepareGatewayIterator($gatewayDistribution, count($numbersToProcess));
        $trackingData = $this->getTrackingData();
        
        // Process numbers in chunks
        $records = [];
        $tempSendSmsData = [];
        $recordIndex = 0;
        $processedCount = 0;

        // Initialize gateway iterator with proper reset
        $gatewayIterator = $this->prepareGatewayIterator($gatewayDistribution, count($numbersToProcess));

        // CRITICAL FIX: Use $numbersToProcess instead of $this->numbers
        foreach ($numbersToProcess as $number) {
            $number = trim($number);

            if (!$this->isValidNumberFormat($number)) {
                continue;
            }

            $processedNumber = $this->normalizePhoneNumber($number);
            
            if (!$processedNumber) {
                continue;
            }

            list($normalizedCurrentNumber, $normalizedOriginalNumber) = $this->getNormalizedNumbers($processedNumber, $number);
            
            $effectiveBlockStatus = $this->getBlockStatus($blockNumbers, $normalizedCurrentNumber, $normalizedOriginalNumber);

            // Skip processing if blocked
            if ($effectiveBlockStatus === 1) {
                continue;
            }

            $charCount = mb_strlen($this->requestMessage); // Use requestMessage instead of message->message
            $isUnicode = preg_match('/[^\x00-\x7F]/', $this->requestMessage);
            $limit = $isUnicode ? 70 : 160;
            $msgCredit = ceil($charCount / $limit);

            $statusData = $this->determineStatus(
                $pe_id,
                $containsSpamKeyword,
                $applyCutoff,
                $cutoffCounter,
                $numbersToMarkAsCutoff,
                $effectiveBlockStatus,
                $normalizedCurrentNumber,
                $normalizedOriginalNumber,
                $dndNumbers,
                $cutoff_status
            );

            $status = $statusData['status'];
            $isPicked = $statusData['is_picked'];
            $errCode = $statusData['err_code'];
            $statusId = $statusData['status_id'];

            if ($statusData['increment_cutoff']) {
                $cutoffCounter++;
            }

            $processedMessage = $this->buildProcessedMessage($processedNumber, $trackingData);
            $unicodeType = $isUnicode ? 1 : 0;
            $msgdata = $isUnicode ? rawurlencode($processedMessage) : $processedMessage;

             // Get the next gateway - check if iterator is valid
            if (!$gatewayIterator->valid()) {
                // Recreate iterator if exhausted
                $gatewayIterator = $this->prepareGatewayIterator($gatewayDistribution, count($numbersToProcess));
            }

            // Get the next gateway
            $currentGateway = $gatewayIterator->current();
            $gatewayIterator->next();

            // Prepare SendSms data BEFORE building the record so is_picked is correct
            if (!empty($sender_ID)) {
                $tempSendSmsData[$recordIndex] = [
                    'momt'      => 'MT',
                    'sender'    => $sender_ID,
                    'receiver'  => $processedNumber,
                    'msgdata'   => $processedMessage,
                    'smsc_id'   => $currentGateway,
                    'sms_type'  => '2',
                    'coding'    => '0',
                    'dlr_mask'  => '19',
                    'charset'   => 'utf8',
                    'boxc_id'   => 'sqlbox',
                    'meta_data' => $metadata,
                    'udh'       => '0',
                    'priority'  => '0',
                ];

                $isPicked = 1;
            } else {
                // ERROR-level: this prevents ALL send_sms entries for this job.
                Log::error('SendNumbersJob: sender_ID is null — no send_sms entries will be created', [
                    'job_id'           => $this->message->job_id,
                    'message_senderid' => $this->message->senderid,
                    'sender_detail'    => $senderDetail ? 'found (but sender_ID field empty)' : 'not found in sender_details',
                ]);
            }

            $record = [
                'message_id' => $this->message->id,
                'msgdata' => $msgdata,
                'mobile_number' => $processedNumber,
                'senderid' => $this->message->senderid,
                'route' => $this->message->route,
                'userids' => $this->userId,
                'send_msg' => $processedMessage,
                'master_job_id' => generateRandomAlphaNumeric(20),
                'msg_job_id' => $this->message->job_id,
                'parent_id' => $parent_id,
                'char_count' => $charCount,
                'msgcredit' => $msgCredit,
                'service_id' => $currentGateway,
                'status' => $status,
                'is_picked' => $isPicked,
                'metadata' => $metadata,
                'err_code' => $errCode,
                'status_id' => $statusId,
                'unicode_type' => $unicodeType,
                'created_by' => $this->userId,
                'created_at' => now(),
                'updated_at' => null,
            ];

            $records[] = $record;
            $recordIndex++;
            $processedCount++;

            // Insert in chunks to avoid "too many placeholders" error
            if (count($records) >= $this->chunkSize) {
                $this->insertChunk($records, $tempSendSmsData);
                $records = [];
                $tempSendSmsData = [];
            }
        }

        // Insert any remaining records
        if (!empty($records)) {
            $this->insertChunk($records, $tempSendSmsData);
        }

        // Post-processing - Only update message once per job
        $this->postProcess($cutOffDetail, $applyCutoff, $cutoffPercentage, $numbersToMarkAsCutoff, 
                          $containsSpamKeyword, $foundSpamKeywords, $this->requestMessage, 
                          $parent_id, $msgCredit, $processedCount);
    }
    
    // protected function insertChunk(array &$records, array &$tempSendSmsData): void
    // {
    //     if (empty($records)) {
    //         return;
    //     }

    //     try {
    //         // Check for duplicates before inserting
    //         $this->checkForDuplicates($records);
            
    //         // Get the last ID before insertion
    //         $lastId = SendNumber::max('id') ?? 0;
            
    //         // Insert the chunk
    //         SendNumber::insert($records);
            
    //         // Get the first inserted ID for this chunk
    //         $firstInsertedId = $lastId + 1;
            
    //         // Prepare SendSms records for this chunk
    //         $sendSmsRecords = [];
    //         $currentId = $firstInsertedId;
            
    //         foreach ($tempSendSmsData as $index => $smsData) {
    //             $smsData['dlr_url'] = $currentId;
    //             $sendSmsRecords[] = $smsData;
    //             $currentId++;
    //         }
            
    //         // Insert SendSms records
    //         if (!empty($sendSmsRecords)) {
    //             if ($this->message->route == "OTP") {
    //                 OtpSend::insert($sendSmsRecords);
    //             } else {
    //                 SendSms::insert($sendSmsRecords);
    //             }
    //         }
            
    //     } catch (\Exception $e) {
    //         throw $e;
    //     }
    // }

    protected function insertChunk(array &$records, array &$tempSendSmsData): void
    {
        if (empty($records)) {
            return;
        }

        try {

            // Check for duplicates before inserting
            $this->checkForDuplicates($records);

            // If all records were duplicates, nothing to insert
            if (empty($records)) {
                Log::warning('[SendNumbersJob] insertChunk() — all records were duplicates, skipping', [
                    'job_id' => $this->message->job_id,
                ]);
                return;
            }

            Log::info('[SendNumbersJob] insertChunk() — attempting SendNumber::insert', [
                'job_id'       => $this->message->job_id,
                'records_count'=> count($records),
                'first_record' => $records[0] ?? null,
            ]);

            SendNumber::insert($records);
            $firstInsertedId = (int) DB::selectOne('SELECT LAST_INSERT_ID() AS id')->id;

            Log::info('[SendNumbersJob] insertChunk() — SendNumber::insert done', [
                'job_id'         => $this->message->job_id,
                'firstInsertedId'=> $firstInsertedId,
                'inserted'       => count($records),
            ]);

            // Prepare SendSms records for this chunk
            $sendSmsRecords = [];
            $currentId = $firstInsertedId;

            foreach ($tempSendSmsData as $index => $smsData) {

                if (empty($smsData['smsc_id'])) {
                    // Always increment currentId even when skipping so that subsequent
                    // entries still map to the correct send_numbers row id (dlr_url).
                    Log::warning('SendNumbersJob: smsc_id (gateway name) is empty — skipping send_sms entry', [
                        'job_id'     => $this->message->job_id,
                        'record_idx' => $index,
                        'current_id' => $currentId,
                    ]);
                    $currentId++;
                    continue;
                }

                $smsData['dlr_url'] = $currentId;
                $sendSmsRecords[] = $smsData;
                $currentId++;
            }
            
            // Insert SendSms records
            if (!empty($sendSmsRecords)) {
                Log::info('[SendNumbersJob] insertChunk() — attempting SendSms::insert', [
                    'job_id'       => $this->message->job_id,
                    'records_count'=> count($sendSmsRecords),
                    'first_record' => $sendSmsRecords[0] ?? null,
                ]);

                try {
                    if ($this->message->route == "OTP") {

                        OtpSend::insert($sendSmsRecords);

                        // Verify insertion
                        $insertedCount = OtpSend::where('dlr_url', '>=', $firstInsertedId)
                            ->where('dlr_url', '<', $firstInsertedId + count($records))
                            ->count();

                        Log::info('[SendNumbersJob] insertChunk() — OtpSend::insert done', [
                            'job_id'         => $this->message->job_id,
                            'inserted'       => count($sendSmsRecords),
                            'verified_count' => $insertedCount,
                        ]);
                    } else {
                        SendSms::insert($sendSmsRecords);

                        Log::info('[SendNumbersJob] insertChunk() — SendSms::insert done', [
                            'job_id'   => $this->message->job_id,
                            'inserted' => count($sendSmsRecords),
                        ]);
                    }
                } catch (\Exception $e) {
                    Log::error('[SendNumbersJob] insertChunk() — SendSms/OtpSend::insert FAILED', [
                        'job_id' => $this->message->job_id,
                        'error'  => $e->getMessage(),
                    ]);
                    throw $e;
                }
            } else {
                Log::warning('[SendNumbersJob] insertChunk() — sendSmsRecords is EMPTY, nothing inserted into send_sms', [
                    'job_id'                => $this->message->job_id,
                    'temp_data_count'       => count($tempSendSmsData),
                    'first_temp_data_entry' => $tempSendSmsData ? reset($tempSendSmsData) : null,
                ]);
            }
            
        } catch (\Exception $e) {
            throw $e;
        }
    }
    
    protected function checkForDuplicates(array &$records): void
    {
        // Extract mobile numbers from records
        $mobileNumbers = array_column($records, 'mobile_number');
        
        // Check if any of these numbers already exist for this message
        $existingNumbers = SendNumber::where('message_id', $this->message->id)
            ->whereIn('mobile_number', $mobileNumbers)
            ->pluck('mobile_number')
            ->toArray();
        
        if (!empty($existingNumbers)) {
            // Filter out duplicate records
            $records = array_filter($records, function ($record) use ($existingNumbers) {
                return !in_array($record['mobile_number'], $existingNumbers);
            });
            
            // Log the duplicates
            Log::warning('Duplicate numbers filtered out', [
                'job_id' => $this->message->job_id,
                'duplicate_count' => count($existingNumbers),
                'duplicate_numbers' => array_slice($existingNumbers, 0, 10)
            ]);
        }
    }
    
    protected function postProcess($cutOffDetail, bool $applyCutoff, ?int $cutoffPercentage, 
                                  int $numbersToMarkAsCutoff, bool $containsSpamKeyword, 
                                  array $foundSpamKeywords, string $processedMessage, 
                                  $parent_id, int $msgCredit, int $processedCount): void
    {
        // Cut Off
        if ($applyCutoff && $processedCount > 0) {
            SmartCutoff::create([
                'user_ID' => $this->userId,
                'job_id' => $this->message->job_id,
                'msg_count' => $processedCount,
                'throughput' => $cutOffDetail->throughput,
                'percent' => $cutoffPercentage,
                'route_ID' => $this->routeId,
                'min_value' => $cutOffDetail->min_cut_value,
                'parent_id' => $parent_id,
                'created_by' => $this->userId,
                'created_at' => now(),
                'updated_at' => null,
            ]);
        }

        // Spam Keyword
        if ($containsSpamKeyword && $processedCount > 0) {
            SpamDrop::create([
                'user_ID' => $this->userId,
                'message' => $processedMessage,
                'job_ID' => $this->message->job_id,
                'spam_keyword' => implode(', ', $foundSpamKeywords),
                'total_num_count' => $processedCount,
                'route_ID' => $this->routeId,
                'sent_at' => now(),
                'status' => 'pending',
                'created_by' => $this->userId,
                'created_at' => now(),
                'updated_at' => null,
            ]);
        }

        // Update numbers_count on the send_messages record with actual processed count
        $this->message->update(['numbers_count' => $processedCount]);

        Log::info('[SendNumbersJob] postProcess() completed', [
            'job_id'         => $this->message->job_id,
            'message_id'     => $this->message->id,
            'processedCount' => $processedCount,
        ]);

        // Credit adjustment - Use atomic update to avoid race conditions
        //  if ($processedCount > 0) {
        //     $updated = DB::table('send_messages')
        //         ->where('id', $this->message->id)
        //         ->update([
        //             'numbers_count' => DB::raw('COALESCE(numbers_count, 0) + ' . $processedCount),
        //             'updated_at'    => now(),
        //         ]);
        //  }
    }

    // Helper methods...
    
    protected function getGatewayTypeId(array $gatewayNames): ?int
    {
        foreach ($gatewayNames as $gatewayName) {
            $smsGateway = SmsGateway::where('gateway_name', $gatewayName)->first();
            if ($smsGateway) {
                return $smsGateway->gateway_type_ID;
            }
        }
        return null;
    }
    
    protected function getSpamKeywords(): array
    {
        $spamKeywordDetail = SpamKeyword::where('route_ID', $this->routeId)
            ->where('isActive', 1)
            ->first();

        if ($spamKeywordDetail && !empty($spamKeywordDetail->keywords)) {
            $keywords = array_map('trim', explode(',', $spamKeywordDetail->keywords));
            return array_filter($keywords);
        }

        return [];
    }
    
    protected function getDndNumbers(array $numbersToProcess): array
    {
        $allNumbers = [];
        
        // Collect all possible number variations from the batch
        foreach ($numbersToProcess as $number) {
            $number = trim($number);
            if ($this->isValidNumberFormat($number)) {
                $allNumbers[] = $number;
                $processed = $this->normalizePhoneNumber($number);
                if ($processed) {
                    $allNumbers[] = $processed;
                }
            }
        }

        $uniqueNumbers = array_unique($allNumbers);
        
        if (empty($uniqueNumbers)) {
            return [];
        }
        
        // Process in chunks to avoid too many placeholders
        $chunkSize = 5000;
        $dndNumbers = [];
        
        foreach (array_chunk($uniqueNumbers, $chunkSize) as $chunk) {
            $chunkResult = DndNumber::whereIn('mobile_number', $chunk)
                ->pluck('mobile_number')
                ->map(fn($num) => $this->normalizeNumber($num))
                ->toArray();
            
            $dndNumbers = array_merge($dndNumbers, $chunkResult);
        }
        
        return array_unique($dndNumbers);
    }
    
    protected function getBlockNumbers(array $numbersToProcess): array
    {
        $allNumbers = [];
        
        foreach ($numbersToProcess as $number) {
            $number = trim($number);
            if ($this->isValidNumberFormat($number)) {
                $allNumbers[] = $number;
                $processed = $this->normalizePhoneNumber($number);
                if ($processed) {
                    $allNumbers[] = $processed;
                }
            }
        }

        $uniqueNumbers = array_unique($allNumbers);
        
        if (empty($uniqueNumbers)) {
            return [];
        }
        
        // Process in chunks to avoid too many placeholders
        $chunkSize = 5000;
        $blockNumbers = [];
        
        foreach (array_chunk($uniqueNumbers, $chunkSize) as $chunk) {
            $chunkResult = BlockNumber::whereIn('mobile_number', $chunk)
                ->where('user_ID', $this->userId)
                ->get()
                ->mapWithKeys(function ($item) {
                    return [$this->normalizeNumber($item->mobile_number) => $item->status];
                })
                ->toArray();
            
            $blockNumbers = array_merge($blockNumbers, $chunkResult);
        }
        
        return $blockNumbers;
    }
    
    protected function countValidNumbers(array $numbersToProcess, array $blockNumbers): int
    {
        $validCount = 0;
        
        foreach ($numbersToProcess as $number) {
            $number = trim($number);
            
            if (!$this->isValidNumberFormat($number)) {
                continue;
            }
            
            $processedNumber = $this->normalizePhoneNumber($number);
            if (!$processedNumber) {
                continue;
            }
            
            list($normalizedCurrentNumber, $normalizedOriginalNumber) = $this->getNormalizedNumbers($processedNumber, $number);
            
            $effectiveBlockStatus = $this->getBlockStatus($blockNumbers, $normalizedCurrentNumber, $normalizedOriginalNumber);
            
            if ($effectiveBlockStatus !== 1) {
                $validCount++;
            }
        }
        
        return $validCount;
    }
    
    protected function calculateCutoffDetails($cutOffDetail, int $validNumbersCount, ?string $throughput): array
    {
        $applyCutoff = $cutOffDetail && 
            $cutOffDetail->min_cut_value !== null && 
            $cutOffDetail->min_cut_value < $validNumbersCount &&
            $throughput && 
            $cutOffDetail->cutoff_status;

        $cutoffPercentage = null;
        $numbersToMarkAsCutoff = 0;

        if ($applyCutoff) {
            $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);
            }
        }

        return [
            'apply' => $applyCutoff,
            'percentage' => $cutoffPercentage,
            'numbers_to_mark' => $numbersToMarkAsCutoff
        ];
    }
    
    protected function prepareGatewayIterator(array $gatewayDistribution, int $batchSize): \Generator
    {
        $totalPercent = array_sum(array_column($gatewayDistribution, 'percent'));
        $gatewayCounts = [];

        // Reset array pointer
        reset($gatewayDistribution);
        
        if (count($gatewayDistribution) > 1) {
            $remainingNumbers = $batchSize;
            
            for ($i = 0; $i < count($gatewayDistribution) - 1; $i++) {
                $gateway = $gatewayDistribution[$i];
                $countForGateway = round(($gateway['percent'] / $totalPercent) * $batchSize);
                $gatewayCounts[$gateway['name']] = $countForGateway;
                $remainingNumbers -= $countForGateway;
            }
            
            $lastGateway = end($gatewayDistribution);
            $gatewayCounts[$lastGateway['name']] = $remainingNumbers;
            reset($gatewayDistribution);
        } else {
            $gatewayCounts[$gatewayDistribution[0]['name']] = $batchSize;
        }
        
        foreach ($gatewayDistribution as $gateway) {
            $gatewayName = $gateway['name'];
            $count = $gatewayCounts[$gatewayName];
            
            for ($i = 0; $i < $count; $i++) {
                yield $gatewayName;
            }
        }
        
        // Infinite fallback
        while (true) {
            yield end($gatewayDistribution)['name'];
        }
    }
    
    protected function getTrackingData(): array
    {
        return UrlTrackingKey::where('job_id', $this->message->job_id)
            ->get()
            ->keyBy('mobile_number')
            ->map(function ($item) {
                return [
                    'sub_track_key' => $item->sub_track_key,
                    'tracking_key' => $item->tracking_key
                ];
            })
            ->toArray();
    }
    
    protected function isValidNumberFormat(string $number): bool
    {
        return !empty($number) && is_numeric($number);
    }
    
    protected function normalizePhoneNumber(string $number): ?string
    {
        $originalNumber = $number;
        $normalizedNumber = $originalNumber;
        
        if (str_starts_with($normalizedNumber, '+91')) {
            $normalizedNumber = substr($normalizedNumber, 3);
        }
        
        if (strlen($normalizedNumber) !== 10 || !is_numeric($normalizedNumber)) {
            return null;
        }
        
        if (!str_starts_with($originalNumber, '+91')) {
            return '+91' . $originalNumber;
        }
        
        return $originalNumber;
    }
    
    protected function getNormalizedNumbers(string $processedNumber, string $originalNumber): array
    {
        $normalizedCurrentNumber = $this->normalizeNumber($processedNumber);
        $normalizedOriginalNumber = $this->normalizeNumber($originalNumber);
        
        return [$normalizedCurrentNumber, $normalizedOriginalNumber];
    }
    
    protected function getBlockStatus(array $blockNumbers, string $normalizedCurrentNumber, string $normalizedOriginalNumber): ?int
    {
        $effectiveBlockStatus = $blockNumbers[$normalizedCurrentNumber] ?? null;
        
        if ($effectiveBlockStatus === null) {
            $effectiveBlockStatus = $blockNumbers[$normalizedOriginalNumber] ?? null;
        }
        
        return $effectiveBlockStatus;
    }
    
    protected function determineStatus(
        ?string $pe_id,
        bool $containsSpamKeyword,
        bool $applyCutoff,
        int $cutoffCounter,
        int $numbersToMarkAsCutoff,
        ?int $effectiveBlockStatus,
        string $normalizedCurrentNumber,
        string $normalizedOriginalNumber,
        array $dndNumbers,
        ?string $cutoff_status
    ): array {
        $status = "Submitted";
        $isPicked = 0;
        $errCode = null;
        $statusId = 0;
        $incrementCutoff = false;

        if (empty($pe_id)) {
            $isPicked = 1;
            $status = 'Failed';
            $errCode = 'PID';
            $statusId = 2;
        } elseif ($containsSpamKeyword) {
            $isPicked = -2;
        } elseif ($applyCutoff && $effectiveBlockStatus !== 1 && $cutoffCounter < $numbersToMarkAsCutoff) {
            $status = $cutoff_status;
            $isPicked = 1;
            $incrementCutoff = true;
        } elseif ($effectiveBlockStatus === 1) {
            // Already filtered out
        } elseif ($this->contentTypeId === 2) {
            if (in_array($normalizedCurrentNumber, $dndNumbers) || 
                in_array($normalizedOriginalNumber, $dndNumbers) ||
                ($effectiveBlockStatus === 0)) {
                $status = 'DND';
                $isPicked = 1;
            }
        } elseif ($this->dndCheckId === 2) {
            if (in_array($normalizedCurrentNumber, $dndNumbers) || 
                in_array($normalizedOriginalNumber, $dndNumbers)) {
                $status = 'DND';
                $isPicked = 1;
            }
        } elseif ($effectiveBlockStatus === 0) {
            $status = 'Blacklisted';
            $isPicked = 1;
        }

        return [
            'status' => $status,
            'is_picked' => $isPicked,
            'err_code' => $errCode,
            'status_id' => $statusId,
            'increment_cutoff' => $incrementCutoff
        ];
    }
    
    protected function buildProcessedMessage(string $number, array $trackingData): string
    {
        $templateContent = $this->message->message ?? '';
        $isAllStatic = $templateContent !== '' && strpos($templateContent, '{#var#}') === false;

        if ($isAllStatic) {
            // ALL-STATIC template (no {#var#}): the DLT registered template has a specific
            // static URL. Replacing that URL with a per-number tracking URL causes a
            // byte-for-byte DLT mismatch → operator silently drops the message.
            // For ALL-STATIC templates, always send the exact template content as registered.
            // To use URL tracking, the DLT template must declare {#var#} for the URL slot.
            return $templateContent;
        }

        // Template has {#var#}: build the final message FROM the template (not from
        // requestMessage) so the SMSC receives the DLT-registered whitespace/newlines.
        // Sending the user's flat requestMessage causes operator-side DLT mismatch.

        if ($this->message->url_status == 1 && isset($trackingData[$number])) {
            // URL tracking: substitute {#var#} with per-number tracking URL.
            $trackingInfo = $trackingData[$number];
            $trackingUrl  = "https://b1y.info/{$trackingInfo['sub_track_key']}/{$trackingInfo['tracking_key']}";
            return $this->replaceFirstVar($templateContent, $trackingUrl);
        }

        // No URL tracking: extract the value(s) the user placed in {#var#} slots
        // from requestMessage and inject them into the template structure.
        return $this->injectVarsIntoTemplate($templateContent, $this->requestMessage);
    }

    /**
     * Replace the first {#var#} occurrence in $template with $value safely
     * (handles values containing $ or \ without regex substitution issues).
     */
    protected function replaceFirstVar(string $template, string $value): string
    {
        $pos = strpos($template, '{#var#}');
        if ($pos === false) {
            return $template;
        }
        return substr($template, 0, $pos) . $value . substr($template, $pos + 7);
    }

    /**
     * Extract variable values from the user's flat requestMessage (whitespace-normalized)
     * and inject them into the template, preserving the template's original newlines.
     */
    protected function injectVarsIntoTemplate(string $template, string $requestMessage): string
    {
        $normalize = fn(string $s): string => trim(preg_replace('/\s+/u', ' ', $s));

        // Split template on each {#var#}
        $parts            = preg_split('/\{#var#\}/u', $template);
        $placeholderCount = count($parts) - 1;

        if ($placeholderCount === 0) {
            return $requestMessage;
        }

        $normalizedMsg = $normalize($requestMessage);
        $cursor        = 0;
        $result        = $template;

        for ($i = 0; $i < $placeholderCount; $i++) {
            $prefix = $normalize($parts[$i]);
            $suffix = $normalize($parts[$i + 1] ?? '');

            // Advance cursor past the prefix text
            if ($prefix !== '') {
                $prefixPos = strpos($normalizedMsg, $prefix, $cursor);
                if ($prefixPos !== false) {
                    $cursor = $prefixPos + strlen($prefix);
                    // Skip any whitespace between prefix and variable value
                    while ($cursor < strlen($normalizedMsg) && $normalizedMsg[$cursor] === ' ') {
                        $cursor++;
                    }
                }
            }

            // Extract the variable value: everything up to the next static suffix
            if ($suffix !== '') {
                $endPos  = strpos($normalizedMsg, $suffix, $cursor);
                $varValue = $endPos !== false
                    ? rtrim(substr($normalizedMsg, $cursor, $endPos - $cursor))
                    : substr($normalizedMsg, $cursor);
                $cursor  = ($endPos !== false) ? $endPos : strlen($normalizedMsg);
            } else {
                $varValue = substr($normalizedMsg, $cursor);
                $cursor   = strlen($normalizedMsg);
            }

            // Safely replace the first {#var#} in template (no regex — avoids $ / \ issues)
            $result = $this->replaceFirstVar($result, $varValue);
        }

        return $result;
    }

    protected function replaceAnyUrlWithTracking(string $message, string $number, array $trackingData): string
    {
        if (!isset($trackingData[$number])) {
            return $message;
        }

        $trackingInfo = $trackingData[$number];
        $subTrackKey  = $trackingInfo['sub_track_key'];
        $trackingKey  = $trackingInfo['tracking_key'];
        $newUrl = "https://b1y.info/{$subTrackKey}/{$trackingKey}";

        // Replace the first URL found in the template with the per-number tracking URL
        return preg_replace('/(https?:\/\/\S+)/iu', $newUrl, $message, 1);
    }

    protected function replaceUrlParameters(string $message, string $number, array $trackingData): string
    {
        if (!isset($trackingData[$number])) {
            return $message;
        }

        $trackingInfo = $trackingData[$number];
        $subTrackKey = $trackingInfo['sub_track_key'];
        $trackingKey = $trackingInfo['tracking_key'];

        $pattern = '/(https?:\/\/b1y\.info\/)[^\/\s]+\/[^\/\s]+/i';

        return preg_replace_callback($pattern, function ($matches) use ($subTrackKey, $trackingKey) {
            $baseUrl = $matches[1];
            return $baseUrl . $subTrackKey . '/' . $trackingKey;
        }, $message);
    }
    
    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 findSpamKeywords(string $message, array $spamKeywords): array
    {
        $foundKeywords = [];

        if (empty($spamKeywords)) {
            return $foundKeywords;
        }

        $message = strtolower($message);
        
        foreach ($spamKeywords as $keyword) {
            $keyword = strtolower(trim($keyword));
            
            if (empty($keyword)) {
                continue;
            }
            
            if (preg_match('/\b' . preg_quote($keyword, '/') . '\b/i', $message)) {
                $foundKeywords[] = $keyword;
            }
        }
        
        return $foundKeywords;
    }
}