<?php
/**
 * Unified SMS provider.
 *
 * Supported transports:
 * - local: a USB GSM modem through Gammu.
 * - api: an HTTP SMS provider.
 *
 * The old csv/email/log-only modes are intentionally not send transports.
 */

require_once __DIR__ . '/settings_store.php';

if (!function_exists('sms_provider_default_gammu_binary')) {
    function sms_provider_default_gammu_binary(): string
    {
        return dirname(__DIR__) . DIRECTORY_SEPARATOR . 'runtime'
            . DIRECTORY_SEPARATOR . 'gammu' . DIRECTORY_SEPARATOR . 'gammu.exe';
    }
}

if (!function_exists('sms_provider_managed_config_path')) {
    function sms_provider_managed_config_path(): string
    {
        $programData = trim((string)(getenv('ProgramData') ?: 'C:\\ProgramData'));
        return rtrim($programData, '\\/') . '\\NetworkCustomers\\gammu\\gammurc';
    }
}

if (!function_exists('sms_provider_defaults')) {
    function sms_provider_defaults(): array
    {
        return [
            'enabled' => '0',
            'mode' => 'api',
            'sender_id' => '',
            'default_country_code' => '963',
            'api_international_numbers' => '1',
            'api_url' => '',
            'method' => 'POST',
            'body_type' => 'json',
            'auth_type' => 'bearer',
            'token' => '',
            'username' => '',
            'password' => '',
            'api_key_name' => 'api_key',
            'api_key_value' => '',
            'param_map' => ['to'=>'to', 'text'=>'text', 'sender'=>'sender'],
            'extra_params' => '',
            'timeout' => 20,
            'local_gammu_path' => sms_provider_default_gammu_binary(),
            'local_config_source' => 'managed',
            'local_config_path' => sms_provider_managed_config_path(),
            'local_port' => 'COM5',
            'local_connection' => 'at',
            'local_timeout' => 90,
            'local_autolen' => 120,
            'test_phone' => '',
            'queue_confirmation_required' => '1',
        ];
    }
}

if (!function_exists('sms_provider_normalize_config')) {
    function sms_provider_normalize_config($raw): array
    {
        if (!is_array($raw)) $raw = [];
        $defaults = sms_provider_defaults();

        // Import the nested configuration written by some older builds.
        $legacyApi = is_array($raw['api'] ?? null) ? $raw['api'] : [];
        $legacyMap = [
            'api_url' => 'url',
            'method' => 'method',
            'body_type' => 'content_type',
            'auth_type' => 'auth_type',
            'token' => 'token',
            'username' => 'username',
            'password' => 'password',
            'timeout' => 'timeout',
        ];
        foreach ($legacyMap as $target => $source) {
            if ((!isset($raw[$target]) || $raw[$target] === '') && array_key_exists($source, $legacyApi)) {
                $raw[$target] = $legacyApi[$source];
            }
        }
        if (!isset($raw['param_map']) || !is_array($raw['param_map'])) {
            $raw['param_map'] = [
                'to' => (string)($legacyApi['to_param'] ?? 'to'),
                'text' => (string)($legacyApi['text_param'] ?? 'text'),
                'sender' => (string)($legacyApi['sender_param'] ?? 'sender'),
            ];
        }
        if ((!isset($raw['api_key_name']) || $raw['api_key_name'] === '') && isset($legacyApi['token_param'])) {
            $raw['api_key_name'] = (string)$legacyApi['token_param'];
        }

        $cfg = array_merge($defaults, $raw);
        $cfg['param_map'] = array_merge($defaults['param_map'], is_array($raw['param_map'] ?? null) ? $raw['param_map'] : []);

        $cfg['enabled'] = (string)($cfg['enabled'] ?? '0') === '1' ? '1' : '0';
        $cfg['mode'] = in_array((string)($cfg['mode'] ?? ''), ['local', 'api'], true) ? (string)$cfg['mode'] : 'api';
        $cfg['method'] = strtoupper((string)($cfg['method'] ?? 'POST'));
        if (!in_array($cfg['method'], ['GET', 'POST'], true)) $cfg['method'] = 'POST';
        $cfg['body_type'] = strtolower((string)($cfg['body_type'] ?? 'json'));
        if (!in_array($cfg['body_type'], ['json', 'form'], true)) $cfg['body_type'] = 'json';
        $cfg['auth_type'] = strtolower((string)($cfg['auth_type'] ?? 'bearer'));
        if (!in_array($cfg['auth_type'], ['bearer', 'basic', 'param', 'none'], true)) $cfg['auth_type'] = 'bearer';
        $cfg['api_international_numbers'] = (string)($cfg['api_international_numbers'] ?? '1') === '1' ? '1' : '0';
        $cfg['queue_confirmation_required'] = (string)($cfg['queue_confirmation_required'] ?? '1') === '1' ? '1' : '0';
        $cfg['default_country_code'] = preg_replace('/\D+/', '', (string)($cfg['default_country_code'] ?? '963'));
        $cfg['timeout'] = max(5, min(60, (int)($cfg['timeout'] ?? 20)));
        $cfg['local_timeout'] = max(15, min(180, (int)($cfg['local_timeout'] ?? 90)));
        $cfg['local_autolen'] = max(1, min(255, (int)($cfg['local_autolen'] ?? 120)));

        foreach ([
            'sender_id','api_url','token','username','password','api_key_name','api_key_value',
            'extra_params','local_gammu_path','local_config_path','local_port','local_connection',
            'test_phone',
        ] as $key) {
            $cfg[$key] = trim((string)($cfg[$key] ?? ''));
        }
        foreach (['to','text','sender'] as $key) {
            $value = trim((string)($cfg['param_map'][$key] ?? $defaults['param_map'][$key]));
            $cfg['param_map'][$key] = preg_match('/^[A-Za-z0-9_.-]{1,80}$/', $value) ? $value : $defaults['param_map'][$key];
        }

        // Gammu is an application runtime, not a user-selected external program.
        // Always migrate legacy installations away from Program Files/user gammurc paths.
        $cfg['local_gammu_path'] = sms_provider_default_gammu_binary();
        $cfg['local_config_source'] = 'managed';
        $cfg['local_config_path'] = sms_provider_managed_config_path();

        return $cfg;
    }
}

if (!function_exists('sms_provider_write_managed_config')) {
    function sms_provider_write_managed_config(array $cfg, ?string $targetPath = null): array
    {
        $cfg = sms_provider_normalize_config($cfg);
        $path = trim((string)($targetPath ?? $cfg['local_config_path']));
        $port = strtoupper((string)$cfg['local_port']);
        $connection = strtolower((string)$cfg['local_connection']);
        if (!preg_match('/^COM[0-9]{1,3}$/', $port)) {
            return ['ok'=>false, 'error'=>'منفذ المودم يجب أن يكون بصيغة COM5 مثلاً.'];
        }
        if (!preg_match('/^[a-z0-9_-]{1,32}$/', $connection)) {
            return ['ok'=>false, 'error'=>'نوع اتصال Gammu غير صالح.'];
        }
        if ($path === '') $path = sms_provider_managed_config_path();
        $dir = dirname($path);
        if (!is_dir($dir) && !@mkdir($dir, 0775, true) && !is_dir($dir)) {
            return ['ok'=>false, 'error'=>'تعذر إنشاء مجلد إعداد Gammu: ' . $dir];
        }
        $content = "[gammu]\r\nport = {$port}\r\nconnection = {$connection}\r\ngammucoding = utf8\r\n";
        $tmp = $path . '.tmp-' . bin2hex(random_bytes(4));
        if (@file_put_contents($tmp, $content, LOCK_EX) === false || !@rename($tmp, $path)) {
            @unlink($tmp);
            return ['ok'=>false, 'error'=>'تعذر كتابة ملف إعداد Gammu: ' . $path];
        }
        return ['ok'=>true, 'path'=>$path, 'port'=>$port, 'connection'=>$connection];
    }
}

if (!function_exists('sms_provider_run_process')) {
    function sms_provider_run_process(array $command, int $timeoutSeconds, string $stdin = ''): array
    {
        if (!function_exists('proc_open')) {
            return ['ok'=>false, 'exit_code'=>-1, 'output'=>'', 'error'=>'تشغيل البرامج الخارجية غير متاح في PHP.'];
        }
        $descriptors = [
            0 => ['pipe', 'r'],
            1 => ['pipe', 'w'],
            2 => ['pipe', 'w'],
        ];
        $pipes = [];
        $process = @proc_open($command, $descriptors, $pipes, null, null, [
            'bypass_shell' => true,
            'suppress_errors' => true,
        ]);
        if (!is_resource($process)) {
            return ['ok'=>false, 'exit_code'=>-1, 'output'=>'', 'error'=>'تعذر تشغيل Gammu من المسار المحدد.'];
        }

        if ($stdin !== '') {
            $offset = 0;
            $length = strlen($stdin);
            while ($offset < $length) {
                $written = @fwrite($pipes[0], substr($stdin, $offset));
                if (!is_int($written) || $written <= 0) break;
                $offset += $written;
            }
        }
        @fclose($pipes[0]);
        @stream_set_blocking($pipes[1], false);
        @stream_set_blocking($pipes[2], false);
        $stdout = '';
        $stderr = '';
        $timedOut = false;
        $exitCode = -1;
        $started = microtime(true);

        while (true) {
            $stdout .= (string)@stream_get_contents($pipes[1]);
            $stderr .= (string)@stream_get_contents($pipes[2]);
            $status = @proc_get_status($process);
            if (!is_array($status) || empty($status['running'])) {
                if (is_array($status) && isset($status['exitcode'])) $exitCode = (int)$status['exitcode'];
                break;
            }
            if ((microtime(true) - $started) >= $timeoutSeconds) {
                $timedOut = true;
                @proc_terminate($process);
                break;
            }
            usleep(100000);
        }

        $stdout .= (string)@stream_get_contents($pipes[1]);
        $stderr .= (string)@stream_get_contents($pipes[2]);
        @fclose($pipes[1]);
        @fclose($pipes[2]);
        $closedCode = @proc_close($process);
        if ($exitCode < 0 && is_int($closedCode)) $exitCode = $closedCode;
        $output = trim($stdout . ($stderr !== '' ? "\n" . $stderr : ''));

        if ($timedOut) {
            return ['ok'=>false, 'exit_code'=>-1, 'output'=>$output, 'error'=>'انتهت مهلة الاتصال بمودم USB.'];
        }
        return [
            'ok' => $exitCode === 0,
            'exit_code' => $exitCode,
            'output' => $output,
            'error' => $exitCode === 0 ? '' : ($output !== '' ? $output : 'أعاد Gammu رمز خطأ ' . $exitCode),
        ];
    }
}

if (!function_exists('sms_provider_parse_identify_output')) {
    function sms_provider_parse_identify_output(string $output): array
    {
        $details = [];
        foreach (preg_split('/\R/u', $output) ?: [] as $line) {
            if (!str_contains($line, ':')) continue;
            [$key, $value] = array_map('trim', explode(':', $line, 2));
            if ($key !== '' && $value !== '') $details[$key] = $value;
        }
        return $details;
    }
}

if (!function_exists('sms_provider_windows_serial_ports')) {
    /** Enumerate Windows COM devices without changing their configuration. */
    function sms_provider_windows_serial_ports(): array
    {
        if (PHP_OS_FAMILY !== 'Windows') return [];

        $systemRoot = rtrim((string)(getenv('SystemRoot') ?: 'C:\\Windows'), '\\/');
        $powershell = $systemRoot . '\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
        $ports = [];
        if (is_file($powershell)) {
            $script = <<<'POWERSHELL'
$items = @(Get-CimInstance Win32_PnPEntity -ErrorAction SilentlyContinue | ForEach-Object {
  if ($_.Name -match '\((COM[0-9]+)\)') {
    [PSCustomObject]@{ port=$Matches[1]; name=[string]$_.Name; device_id=[string]$_.PNPDeviceID }
  }
}); ConvertTo-Json -InputObject $items -Compress
POWERSHELL;
            $result = sms_provider_run_process([
                $powershell, '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', $script,
            ], 8);
            $decoded = json_decode((string)($result['output'] ?? ''), true);
            if (is_array($decoded)) {
                if (isset($decoded['port'])) $decoded = [$decoded];
                foreach ($decoded as $row) {
                    if (!is_array($row)) continue;
                    $port = strtoupper(trim((string)($row['port'] ?? '')));
                    if (!preg_match('/^COM[0-9]{1,3}$/', $port)) continue;
                    $ports[$port] = [
                        'port' => $port,
                        'name' => trim((string)($row['name'] ?? $port)),
                        'device_id' => trim((string)($row['device_id'] ?? '')),
                    ];
                }
            }
        }

        // Registry fallback works on editions where the CIM service is unavailable.
        if (!$ports) {
            $regExe = $systemRoot . '\\System32\\reg.exe';
            if (is_file($regExe)) {
                $result = sms_provider_run_process([
                    $regExe, 'query', 'HKLM\\HARDWARE\\DEVICEMAP\\SERIALCOMM', '/s',
                ], 5);
                foreach (preg_split('/\R/u', (string)($result['output'] ?? '')) ?: [] as $line) {
                    if (!preg_match('/REG_SZ\s+(COM[0-9]{1,3})\s*$/i', trim($line), $match)) continue;
                    $port = strtoupper($match[1]);
                    $ports[$port] = ['port'=>$port, 'name'=>$port, 'device_id'=>''];
                }
            }
        }

        $modemWords = '/modem|gsm|mobile|huawei|zte|quectel|sierra|simcom|simtech|telit|fibocom|u-blox/i';
        $bluetoothWords = '/bluetooth|standard serial over bluetooth/i';
        foreach ($ports as &$row) {
            $haystack = (string)$row['name'] . ' ' . (string)$row['device_id'];
            $row['likely_modem'] = preg_match($modemWords, $haystack) === 1;
            $row['_score'] = ($row['likely_modem'] ? 100 : 0)
                - (preg_match($bluetoothWords, $haystack) === 1 ? 100 : 0)
                - (int)preg_replace('/\D+/', '', (string)$row['port']);
        }
        unset($row);
        uasort($ports, static fn(array $a, array $b): int => $b['_score'] <=> $a['_score']);
        foreach ($ports as &$row) unset($row['_score']);
        unset($row);
        return array_values($ports);
    }
}

if (!function_exists('sms_provider_detect_local_modem')) {
    /** Locate the actual SMS modem by probing enumerated COM devices with Gammu. */
    function sms_provider_detect_local_modem(array $cfg): array
    {
        $cfg = sms_provider_normalize_config($cfg);
        $ports = sms_provider_windows_serial_ports();
        if (!$ports) {
            return ['ok'=>false, 'verified'=>false, 'ports'=>[], 'message'=>'لم يعثر ويندوز على أي منفذ COM متصل. يمكنك إدخال المنفذ يدوياً.'];
        }

        $current = strtoupper((string)$cfg['local_port']);
        usort($ports, static function (array $a, array $b) use ($current): int {
            if ($a['port'] === $current) return -1;
            if ($b['port'] === $current) return 1;
            return ((int)!empty($b['likely_modem'])) <=> ((int)!empty($a['likely_modem']));
        });
        $exe = (string)$cfg['local_gammu_path'];
        if (!is_file($exe)) {
            return ['ok'=>false, 'verified'=>false, 'ports'=>$ports, 'suggested_port'=>$ports[0]['port'], 'message'=>'تم اكتشاف منافذ COM، لكن Gammu غير موجود للتحقق من المودم.'];
        }

        foreach (array_slice($ports, 0, 8) as $device) {
            $tempDir = rtrim(sys_get_temp_dir(), '\\/') . DIRECTORY_SEPARATOR
                . 'network-customers-gammu-detect-' . bin2hex(random_bytes(5));
            $configPath = $tempDir . DIRECTORY_SEPARATOR . 'gammurc';
            $candidate = $cfg;
            $candidate['local_port'] = (string)$device['port'];
            $candidate['local_connection'] = 'at';
            $written = sms_provider_write_managed_config($candidate, $configPath);
            if (empty($written['ok'])) continue;
            try {
                $result = sms_provider_run_process([$exe, '-c', $configPath, 'identify'], 4);
            } finally {
                if (is_file($configPath)) @unlink($configPath);
                if (is_dir($tempDir)) @rmdir($tempDir);
            }
            if (!empty($result['ok'])) {
                return [
                    'ok'=>true,
                    'verified'=>true,
                    'port'=>(string)$device['port'],
                    'connection'=>'at',
                    'device'=>$device,
                    'details'=>sms_provider_parse_identify_output((string)($result['output'] ?? '')),
                    'ports'=>$ports,
                    'message'=>'تم اكتشاف مودم الرسائل والتحقق منه تلقائياً على ' . $device['port'] . '.',
                ];
            }
        }

        return [
            'ok'=>false,
            'verified'=>false,
            'ports'=>$ports,
            'suggested_port'=>$ports[0]['port'],
            'message'=>'تم اكتشاف منافذ COM، لكن لم يستجب أي منها كـمودم GSM. اختر منفذاً من القائمة أو اكتبه يدوياً.',
        ];
    }
}

if (!function_exists('sms_provider_utf8_text')) {
    function sms_provider_utf8_text(string $message): string
    {
        $message = preg_replace('/^\xEF\xBB\xBF/', '', $message) ?? $message;
        if (preg_match('//u', $message) !== 1) {
            throw new RuntimeException('نص الرسالة ليس UTF-8 صالحاً.');
        }
        return str_replace("\0", '', $message);
    }
}

if (!function_exists('sms_provider_prepare_local_utf8_batch')) {
    function sms_provider_prepare_local_utf8_batch(
        string $exe,
        string $config,
        string $to,
        string $message,
        int $autolen
    ): array {
        foreach ([$exe, $config] as $path) {
            if ($path === '' || str_contains($path, '"') || preg_match('/[\r\n]/', $path)) {
                throw new RuntimeException('مسار تشغيل Gammu غير صالح.');
            }
        }
        if (!preg_match('/^[0-9]{5,20}$/', $to)) {
            throw new RuntimeException('رقم المستقبل غير صالح لمسار Gammu المحلي.');
        }

        $message = preg_replace('/\R/u', ' ', sms_provider_utf8_text($message)) ?? $message;
        // A literal quote can terminate CMD's quoted -text argument. Use a
        // typographic quote so the visible SMS remains equivalent and inert.
        $message = str_replace('"', '”', $message);

        $tempRoot = rtrim(sys_get_temp_dir(), '\\/')
            . DIRECTORY_SEPARATOR . 'network-customers-gammu-' . bin2hex(random_bytes(8));
        if (!@mkdir($tempRoot, 0700, true) && !is_dir($tempRoot)) {
            throw new RuntimeException('تعذر إنشاء مجلد Gammu المؤقت.');
        }

        $messagePath = $tempRoot . DIRECTORY_SEPARATOR . 'message.txt';
        $batchPath = $tempRoot . DIRECTORY_SEPARATOR . 'send.cmd';
        if (@file_put_contents($messagePath, $message, LOCK_EX) === false) {
            @rmdir($tempRoot);
            throw new RuntimeException('تعذر تجهيز نص رسالة Gammu.');
        }

        $batch = "@echo off\r\n"
            . "chcp 65001 >nul\r\n"
            . "setlocal EnableExtensions DisableDelayedExpansion\r\n"
            . "set \"MSG=\"\r\n"
            . "set /p \"MSG=\"<\"{$messagePath}\"\r\n"
            . "\"{$exe}\" -c \"{$config}\" sendsms TEXT {$to} -unicode -text \"%MSG%\" -autolen "
            . max(1, min(255, $autolen)) . "\r\n"
            . "exit /b %errorlevel%\r\n";
        if (@file_put_contents($batchPath, $batch, LOCK_EX) === false) {
            @unlink($messagePath);
            @rmdir($tempRoot);
            throw new RuntimeException('تعذر تجهيز مشغل Gammu المؤقت.');
        }

        $comspec = trim((string)(getenv('ComSpec') ?: 'C:\\Windows\\System32\\cmd.exe'));
        return [
            'command' => [$comspec, '/D', '/S', '/C', $batchPath],
            'temp_root' => $tempRoot,
            'message_path' => $messagePath,
            'batch_path' => $batchPath,
        ];
    }
}

if (!function_exists('sms_provider_cleanup_local_utf8_batch')) {
    function sms_provider_cleanup_local_utf8_batch(array $prepared): void
    {
        foreach (['batch_path', 'message_path'] as $key) {
            $path = (string)($prepared[$key] ?? '');
            if ($path !== '' && is_file($path)) @unlink($path);
        }
        $root = (string)($prepared['temp_root'] ?? '');
        if ($root !== '' && is_dir($root)) @rmdir($root);
    }
}

class SmsProviderSender
{
    private mysqli $conn;
    private array $cfg;

    public function __construct(mysqli $conn, ?array $overrideConfig = null)
    {
        $this->conn = $conn;
        $raw = $overrideConfig ?? setting_get($conn, 'sms_provider', []);
        $this->cfg = sms_provider_normalize_config($raw);
    }

    public function config(): array { return $this->cfg; }
    public function isEnabled(): bool { return $this->cfg['enabled'] === '1'; }
    public function mode(): string { return (string)$this->cfg['mode']; }

    public function isConfigured(): bool
    {
        if (!$this->isEnabled()) return false;
        if ($this->mode() === 'api') {
            return filter_var((string)$this->cfg['api_url'], FILTER_VALIDATE_URL) !== false;
        }
        return is_file((string)$this->cfg['local_gammu_path'])
            && preg_match('/^COM[0-9]{1,3}$/', strtoupper((string)$this->cfg['local_port'])) === 1;
    }

    public function normalizeTo(string $to, ?string $mode = null): string
    {
        $mode = $mode ?: $this->mode();
        $digits = preg_replace('/\D+/', '', trim($to));
        if (str_starts_with($digits, '00')) $digits = substr($digits, 2);
        if ($mode === 'api' && $this->cfg['api_international_numbers'] === '1') {
            $country = (string)$this->cfg['default_country_code'];
            if ($country !== '' && str_starts_with($digits, '0')) $digits = $country . substr($digits, 1);
        }
        return $digits;
    }

    public function localHealth(): array
    {
        $exe = (string)$this->cfg['local_gammu_path'];
        if ($exe === '' || !is_file($exe)) {
            return ['ok'=>false, 'error'=>'ملف gammu.exe غير موجود في المسار المحدد.', 'stage'=>'binary'];
        }
        $config = $this->localConfigPath(true);
        if ($config === '') {
            return ['ok'=>false, 'error'=>'ملف إعداد Gammu غير موجود أو تعذر إنشاؤه.', 'stage'=>'config'];
        }
        $result = sms_provider_run_process([$exe, '-c', $config, 'identify'], min(30, (int)$this->cfg['local_timeout']));
        if (empty($result['ok'])) {
            return [
                'ok'=>false,
                'error'=>$this->friendlyLocalError((string)($result['error'] ?? 'تعذر فحص المودم.')),
                'stage'=>'device',
                'exit_code'=>(int)($result['exit_code'] ?? -1),
            ];
        }
        return [
            'ok'=>true,
            'mode'=>'local',
            'message'=>'تم الاتصال بمودم USB عبر Gammu بنجاح.',
            'details'=>sms_provider_parse_identify_output((string)($result['output'] ?? '')),
        ];
    }

    public function sendText(string $to, string $message, array $context = []): array
    {
        $message = trim($message);
        $toNormalized = $this->normalizeTo($to);
        if ($toNormalized === '') return ['ok'=>false, 'error'=>'رقم المستقبل غير صحيح.'];
        if ($message === '') return ['ok'=>false, 'error'=>'نص الرسالة فارغ.'];
        if (!$this->isEnabled()) return ['ok'=>false, 'error'=>'نظام SMS غير مفعّل من الإعدادات.'];
        if (!$this->isConfigured()) return ['ok'=>false, 'error'=>'إعدادات طريقة إرسال SMS المختارة غير مكتملة.'];

        return $this->mode() === 'local'
            ? $this->sendViaLocal($toNormalized, $message)
            : $this->sendViaApi($toNormalized, $message);
    }

    private function localConfigPath(bool $createManaged): string
    {
        if ($createManaged) {
            $written = sms_provider_write_managed_config($this->cfg);
            return !empty($written['ok']) ? (string)$written['path'] : '';
        }
        $path = (string)$this->cfg['local_config_path'];
        return $path !== '' && is_file($path) ? $path : '';
    }

    private function sendViaLocal(string $to, string $message): array
    {
        $exe = (string)$this->cfg['local_gammu_path'];
        $config = $this->localConfigPath(true);
        if ($exe === '' || !is_file($exe)) return ['ok'=>false, 'error'=>'ملف gammu.exe غير موجود.'];
        if ($config === '') return ['ok'=>false, 'error'=>'ملف إعداد Gammu غير متاح.'];

        $lockPath = rtrim(sys_get_temp_dir(), '\\/') . DIRECTORY_SEPARATOR . 'network-customers-gammu.lock';
        $lock = @fopen($lockPath, 'c+');
        if (!$lock) return ['ok'=>false, 'error'=>'تعذر إنشاء قفل مودم USB.'];
        $locked = false;
        $deadline = microtime(true) + 15;
        do {
            $locked = @flock($lock, LOCK_EX | LOCK_NB);
            if (!$locked) usleep(150000);
        } while (!$locked && microtime(true) < $deadline);
        if (!$locked) {
            @fclose($lock);
            return ['ok'=>false, 'error'=>'مودم USB مشغول برسالة أخرى؛ ستتم إعادة المحاولة من الطابور.'];
        }

        $prepared = [];
        try {
            $prepared = sms_provider_prepare_local_utf8_batch(
                $exe,
                $config,
                $to,
                $message,
                (int)$this->cfg['local_autolen']
            );
            $result = sms_provider_run_process(
                (array)$prepared['command'],
                (int)$this->cfg['local_timeout']
            );
        } finally {
            sms_provider_cleanup_local_utf8_batch($prepared);
            @flock($lock, LOCK_UN);
            @fclose($lock);
        }

        if (empty($result['ok'])) {
            return [
                'ok'=>false,
                'mode'=>'local',
                'error'=>$this->friendlyLocalError((string)($result['error'] ?? 'فشل الإرسال عبر Gammu.')),
                'exit_code'=>(int)($result['exit_code'] ?? -1),
            ];
        }

        $output = (string)($result['output'] ?? '');
        $messageId = '';
        if (preg_match('/(?:reference|مرجع)\s*[=:]\s*([A-Za-z0-9._-]+)/iu', $output, $match)) {
            $messageId = (string)$match[1];
        }
        return [
            'ok'=>true,
            'mode'=>'local',
            'message_id'=>$messageId,
            'provider'=>'gammu',
        ];
    }

    private function sendViaApi(string $to, string $message): array
    {
        $url = (string)$this->cfg['api_url'];
        if (!filter_var($url, FILTER_VALIDATE_URL) || !preg_match('#^https?://#i', $url)) {
            return ['ok'=>false, 'error'=>'رابط API غير صالح.'];
        }
        if (!function_exists('curl_init')) return ['ok'=>false, 'error'=>'امتداد cURL غير متاح في PHP.'];

        $map = $this->cfg['param_map'];
        $payload = [
            (string)$map['to'] => $to,
            (string)$map['text'] => $message,
        ];
        if ($this->cfg['sender_id'] !== '') $payload[(string)$map['sender']] = (string)$this->cfg['sender_id'];

        $extra = $this->cfg['extra_params'];
        if (is_string($extra) && trim($extra) !== '') {
            $decoded = json_decode($extra, true);
            if (!is_array($decoded)) return ['ok'=>false, 'error'=>'الخيارات الإضافية يجب أن تكون JSON صالحاً.'];
            foreach ($decoded as $key => $value) {
                if ((string)$key !== '') $payload[(string)$key] = $value;
            }
        }

        $headers = ['Accept: application/json'];
        $query = [];
        if ($this->cfg['auth_type'] === 'bearer' && $this->cfg['token'] !== '') {
            $headers[] = 'Authorization: Bearer ' . $this->cfg['token'];
        } elseif ($this->cfg['auth_type'] === 'basic') {
            $headers[] = 'Authorization: Basic ' . base64_encode($this->cfg['username'] . ':' . $this->cfg['password']);
        } elseif ($this->cfg['auth_type'] === 'param' && $this->cfg['api_key_value'] !== '') {
            $query[(string)$this->cfg['api_key_name']] = (string)$this->cfg['api_key_value'];
        }

        if ($query) $url .= (str_contains($url, '?') ? '&' : '?') . http_build_query($query);
        $method = (string)$this->cfg['method'];
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => min(10, (int)$this->cfg['timeout']),
            CURLOPT_TIMEOUT => (int)$this->cfg['timeout'],
            CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_MAXREDIRS => 0,
        ]);
        if ($method === 'POST') {
            curl_setopt($ch, CURLOPT_POST, true);
            if ($this->cfg['body_type'] === 'json') {
                $headers[] = 'Content-Type: application/json; charset=utf-8';
                curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
            } else {
                $headers[] = 'Content-Type: application/x-www-form-urlencoded';
                curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload));
            }
        } else {
            $url .= (str_contains($url, '?') ? '&' : '?') . http_build_query($payload);
            curl_setopt($ch, CURLOPT_URL, $url);
        }
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

        $response = curl_exec($ch);
        $errno = curl_errno($ch);
        $curlError = curl_error($ch);
        $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        if ($errno) return ['ok'=>false, 'error'=>"تعذر الاتصال بمزوّد SMS: {$curlError}", 'curl_errno'=>$errno];

        $decoded = json_decode((string)$response, true);
        $ok = $httpCode >= 200 && $httpCode < 300;
        if (!$ok) {
            $providerError = '';
            if (is_array($decoded)) {
                $providerError = (string)($decoded['error']['message'] ?? $decoded['error'] ?? $decoded['message'] ?? '');
            }
            if ($providerError === '') $providerError = 'رفض مزوّد SMS الطلب برمز HTTP ' . $httpCode;
            return ['ok'=>false, 'error'=>$providerError, 'http'=>$httpCode, 'data'=>$decoded ?? (string)$response];
        }

        $messageId = '';
        if (is_array($decoded)) {
            $messageId = (string)($decoded['message_id'] ?? $decoded['id'] ?? $decoded['data']['id'] ?? '');
        }
        return ['ok'=>true, 'mode'=>'api', 'http'=>$httpCode, 'message_id'=>$messageId, 'data'=>$decoded ?? (string)$response];
    }

    private function friendlyLocalError(string $error): string
    {
        $lower = strtolower($error);
        if (str_contains($lower, "doesn't exist") || str_contains($lower, 'does not exist') || str_contains($lower, 'error opening device')) {
            return 'تعذر فتح منفذ مودم USB. تحقق من توصيل المودم ورقم COM وعدم استخدامه من برنامج آخر.';
        }
        if (str_contains($lower, 'permission') || str_contains($lower, 'access is denied')) {
            return 'لا يملك خادم البرنامج صلاحية الوصول إلى مودم USB أو ملف إعداد Gammu.';
        }
        return trim($error) !== '' ? trim($error) : 'فشل الاتصال بمودم USB عبر Gammu.';
    }

}

if (!function_exists('sms_provider_send')) {
    function sms_provider_send(mysqli $conn, string $to, string $message, array $context = []): array
    {
        try {
            $sender = new SmsProviderSender($conn);
            return $sender->sendText($to, $message, $context);
        } catch (Throwable $e) {
            return ['ok'=>false, 'error'=>'تعذر تشغيل مزوّد SMS: ' . $e->getMessage()];
        }
    }
}
