<?php
namespace EasyChat\Library\Messenger;

use EasyChat\Library\SafeRuntime;
use EasyChat\Model\Messenger;
use PhpWA\Client\SessionEvent;

 
final class WhatsAppPersonalAuthService {
    private const QR_LIFETIME = 180;
    private const WORKER_TIMEOUT = 210;
    private const HEARTBEAT_LEASE = 75;
    private const PROVIDER_TRANSPORT_HOST = 'web.whatsapp.com';
    private const PROVIDER_TRANSPORT_TIMEOUT = 5.0;

    private $registry;
    private $model;
    private $lifecycle;
    private $store;

    public function __construct($registry) {
        $this->registry = $registry;
        $this->model = new Messenger($registry);
        $this->lifecycle = new WhatsAppPersonalLifecycle($registry);
         
         
         
        $this->store = null;
    }

    public function state(array $channel): array {
        $settings = $this->settings($channel);
        $auth = is_array($settings['auth'] ?? null) ? $settings['auth'] : [];
        $status = (string)($auth['status'] ?? 'idle');
        $expires = (int)($auth['expires_at'] ?? 0);
        if ($expires > 0 && $expires < time() && !in_array($status, ['connected', 'error', 'expired'], true)) {
            $status = 'expired';
        }
        $heartbeat = (int)($auth['worker_heartbeat_at'] ?? 0);
        $created = (int)($auth['created_at'] ?? 0);
        if (in_array($status, ['qr_starting', 'worker_ready', 'qr_pending', 'pairing'], true)
            && (($heartbeat > 0 && time() - $heartbeat > self::HEARTBEAT_LEASE)
                || ($heartbeat <= 0 && $created > 0 && time() - $created > 15))) {
            $status = 'error';
            $auth['error_code'] = 'qr_worker_stalled';
        }
        $public = [
            'status' => $status,
            'expires_at' => $expires,
            'error_code' => (string)($auth['error_code'] ?? ''),
            'message' => $this->message($status, (string)($auth['error_code'] ?? '')),
        ];
        $attempt = (string)($auth['attempt_id'] ?? '');
        $generation = (int)($auth['generation'] ?? 0);
        if (in_array($status, ['qr_pending', 'pairing'], true) && $attempt !== '' && $generation > 0) {
            try {
                $qr = $this->store()->load((int)$channel['channel_id'], $generation, $attempt);
                if (!empty($qr['qr'])) {
                    $public['qr_svg'] = $this->qrSvg((string)$qr['qr']);
                }
            } catch (\Throwable $e) {
                $public['status'] = 'error';
                $public['error_code'] = 'private_storage_unavailable';
                $public['message'] = $this->message('error', 'private_storage_unavailable');
            }
        }
        return $public;
    }

    public function startQr(array $channel): array {
        $recovered = $this->resumePairedAttempt($channel);
        if ($recovered !== null) {
            return $recovered;
        }
        $current = $this->state($channel);
        if (in_array((string)($current['status'] ?? ''), ['qr_starting', 'worker_ready', 'qr_pending', 'pairing'], true)) {
             
             
            return ['success' => true, 'pending' => true, 'state' => $current, 'reused' => true];
        }
        $readiness = $this->readiness();
        if (empty($readiness['success'])) {
            return ['success' => false, 'error' => (string)$readiness['error'], 'message' => (string)$readiness['message']];
        }
        try {
            $context = $this->lifecycle->beginAuthentication($channel);
            $spawn = $this->spawn($context, $readiness);
            if (empty($spawn['success'])) {
                $this->lifecycle->updateAuth(
                    (int)$context['channel']['channel_id'],
                    (int)$context['generation'],
                    (string)$context['attempt_id'],
                    ['status' => 'error', 'error_code' => (string)$spawn['error'], 'worker_heartbeat_at' => time()]
                );
                return $spawn;
            }
            $deadline = microtime(true) + 4.0;
            do {
                usleep(200000);
                $fresh = $this->model->getChannelById((int)$context['channel']['channel_id']);
                $state = $this->state($fresh ?: $context['channel']);
                if (!in_array((string)$state['status'], ['qr_starting', 'worker_ready'], true)) {
                    return ['success' => (string)$state['status'] !== 'error', 'state' => $state];
                }
            } while (microtime(true) < $deadline);
            $fresh = $this->model->getChannelById((int)$context['channel']['channel_id']);
            return [
                'success' => true,
                'pending' => true,
                'state' => $this->state($fresh ?: $context['channel']),
            ];
        } catch (\Throwable $e) {
            return ['success' => false, 'error' => 'session_prepare_failed', 'message' => 'Не удалось подготовить защищённую сессию WhatsApp.'];
        }
    }

    public function check(array $channel): array {
        $state = $this->state($channel);
        return [
            'success' => !in_array((string)$state['status'], ['error', 'expired'], true),
            'pending' => in_array((string)$state['status'], ['qr_starting', 'worker_ready', 'qr_pending', 'pairing'], true),
            'state' => $state,
        ];
    }

    public function clear(array $channel): array {
        try {
            $this->lifecycle->clear($channel);
            return ['success' => true];
        } catch (\Throwable $e) {
            return [
                'success' => false,
                'error' => 'session_clear_failed',
                'message' => 'Предыдущая попытка ещё завершается. Повторите через несколько секунд.',
            ];
        }
    }

    public function setEnabled(array $channel, bool $enabled): array {
        return $this->lifecycle->setReceivingEnabled((int)$channel['channel_id'], $enabled);
    }

    public function runQrWorker(int $channel_id, string $attempt_id): array {
        if ($channel_id <= 0 || preg_match('/^[a-f0-9]{24}$/D', $attempt_id) !== 1) {
            return ['success' => false, 'error' => 'invalid_arguments'];
        }
        $channel = $this->model->getChannelById($channel_id);
        if (!$channel || (string)($channel['provider'] ?? '') !== WhatsAppPersonalProvider::PROVIDER) {
            return ['success' => false, 'error' => 'stale_attempt'];
        }
        $auth = is_array($this->settings($channel)['auth'] ?? null) ? $this->settings($channel)['auth'] : [];
        $generation = (int)($auth['generation'] ?? 0);
        if (!hash_equals((string)($auth['attempt_id'] ?? ''), $attempt_id) || $generation <= 0) {
            return ['success' => false, 'error' => 'stale_attempt'];
        }
        $lock = $this->workerLock($channel_id, $attempt_id);
        if (!is_resource($lock)) {
            return ['success' => true, 'pending' => true, 'duplicate' => true];
        }
        try {
            $this->touch($channel_id, $generation, $attempt_id, ['status' => 'worker_ready']);
            $client = $this->lifecycle->client($channel, $generation);
            if ($client->isPaired()) {
                return $this->completePairedAttempt($channel, $generation, $attempt_id);
            }
            $paired = false;
            $client->onQr(function (array $codes) use ($channel_id, $generation, $attempt_id): void {
                $qr = isset($codes[0]) && is_string($codes[0]) ? $codes[0] : '';
                if ($qr === '') throw new \RuntimeException('qr_payload_missing');
                $expires = time() + self::QR_LIFETIME;
                $this->store()->save($channel_id, $generation, $attempt_id, $qr, $expires);
                $this->touch($channel_id, $generation, $attempt_id, [
                    'status' => 'qr_pending', 'expires_at' => $expires, 'error_code' => '',
                ]);
            });
            $client->onSessionEvent(function (SessionEvent $event) use (&$paired, $channel_id, $generation, $attempt_id): void {
                if ($event->type() === SessionEvent::PAIRING_SUCCESS) {
                    $paired = true;
                    $this->touch($channel_id, $generation, $attempt_id, ['status' => 'pairing']);
                } elseif ($event->type() === SessionEvent::DISCONNECTED && $event->credentialResetRequired()) {
                    throw new \RuntimeException('authorization_rejected');
                } else {
                    $this->touch($channel_id, $generation, $attempt_id, []);
                }
            });
            $first = $client->runFor(175.0, 2000);
            if (!$first->lockAcquired()) {
                return ['success' => true, 'pending' => true, 'duplicate' => true];
            }
            if (!$paired && !$client->isPaired()) {
                $fresh = $this->model->getChannelById($channel_id) ?: $channel;
                $fresh_settings = $this->settings($fresh);
                $fresh_auth = is_array($fresh_settings['auth'] ?? null)
                    ? $fresh_settings['auth']
                    : [];
                if ((int)($fresh_auth['expires_at'] ?? 0) > time()) {
                     
                     
                     
                    $this->touch($channel_id, $generation, $attempt_id, [
                        'status' => 'error', 'error_code' => 'provider_unreachable',
                    ]);
                    return ['success' => false, 'error' => 'provider_unreachable'];
                }
                $this->touch($channel_id, $generation, $attempt_id, ['status' => 'expired', 'error_code' => 'qr_expired']);
                return ['success' => false, 'error' => 'qr_expired'];
            }

            return $this->completePairedAttempt($channel, $generation, $attempt_id);
        } catch (\Throwable $e) {
            $code = $this->workerError($e);
             
             
            error_log(
                'EASYCHAT_WHATSAPP_QR_ERROR=' . $code
                . ';exception=' . $this->safeExceptionClass($e)
            );
            $this->touch($channel_id, $generation, $attempt_id, ['status' => 'error', 'error_code' => $code]);
            return ['success' => false, 'error' => $code];
        } finally {
            flock($lock, LOCK_UN);
            fclose($lock);
        }
    }

    



    private function completePairedAttempt(array $channel, int $generation, string $attempt_id): array {
        $channel_id = (int)$channel['channel_id'];
        $fresh = $this->model->getChannelById($channel_id) ?: $channel;
        $online = $this->lifecycle->client($fresh, $generation);
        $this->touch($channel_id, $generation, $attempt_id, ['status' => 'pairing', 'error_code' => '']);
        $online->onSessionEvent(function (SessionEvent $event) use ($channel_id, $generation, $attempt_id): void {
            if ($event->type() === SessionEvent::DISCONNECTED && $event->credentialResetRequired()) {
                throw new \RuntimeException('authorization_rejected');
            }
            $this->touch($channel_id, $generation, $attempt_id, []);
        });
        try {
            $identity = $online->withOpenSession(static function ($ready) {
                return $ready->identity();
            }, 35.0, 800, 0.0);
        } catch (\PhpWA\Exception\AccountBusyException $error) {
            return ['success' => true, 'pending' => true, 'duplicate' => true];
        }
        if (!$identity || !$online->isReady()) {
            throw new \RuntimeException('paired_reconnect_failed');
        }
        $completed = $this->lifecycle->completeAuthentication($channel_id, $generation, $attempt_id, [
            'display_name' => (string)($identity->name() ?: 'Личный WhatsApp'),
            'pn_jid' => $identity->id(),
            'lid_jid' => (string)($identity->lid() ?? ''),
        ]);
        if (!$completed) return ['success' => false, 'error' => 'stale_attempt'];
        $this->store()->delete($channel_id, $generation, $attempt_id);
        return ['success' => true];
    }

     
    private function resumePairedAttempt(array $channel): ?array {
        $settings = $this->settings($channel);
        $auth = is_array($settings['auth'] ?? null) ? $settings['auth'] : [];
        $attempt_id = (string)($auth['attempt_id'] ?? '');
        $generation = (int)($auth['generation'] ?? 0);
        $heartbeat = (int)($auth['worker_heartbeat_at'] ?? 0);
        if ((string)($auth['status'] ?? '') !== 'pairing'
            || $generation <= 0
            || preg_match('/^[a-f0-9]{24}$/D', $attempt_id) !== 1
            || ($heartbeat > 0 && time() - $heartbeat <= self::HEARTBEAT_LEASE)) {
            return null;
        }
        try {
            if (!$this->lifecycle->client($channel, $generation)->isPaired()) {
                return null;
            }
        } catch (\Throwable $error) {
            return null;
        }
        $readiness = $this->readiness();
        if (empty($readiness['success'])) {
            return ['success' => false, 'error' => (string)$readiness['error'], 'message' => (string)$readiness['message']];
        }
        $spawn = $this->spawn([
            'channel' => $channel,
            'generation' => $generation,
            'attempt_id' => $attempt_id,
        ], $readiness);
        if (empty($spawn['success'])) {
            return $spawn;
        }
        return ['success' => true, 'pending' => true, 'state' => [
            'status' => 'pairing',
            'expires_at' => (int)($auth['expires_at'] ?? 0),
            'error_code' => '',
            'message' => $this->message('pairing', ''),
        ], 'recovered' => true];
    }

    private function touch(int $channel_id, int $generation, string $attempt, array $patch): void {
        $patch['worker_heartbeat_at'] = time();
        if (!$this->lifecycle->updateAuth($channel_id, $generation, $attempt, $patch)) {
            throw new \RuntimeException('stale_attempt');
        }
    }

    private function readiness(): array {
        if (!function_exists('exec')) {
            return ['success' => false, 'error' => 'exec_disabled', 'message' => 'Хостинг запретил запуск фонового QR-процесса.'];
        }
        $web_missing = [];
        if (!class_exists('PDO') || !in_array('mysql', \PDO::getAvailableDrivers(), true)) {
            $web_missing[] = 'pdo_mysql';
        }
        if ($web_missing) {
            return [
                'success' => false,
                'error' => 'web_runtime_capability_missing',
                'message' => 'В PHP сайта отсутствуют расширения: ' . implode(', ', $web_missing) . '.',
            ];
        }
        try {
            WhatsAppPersonalRuntime::privateRoot();
        } catch (\Throwable $e) {
            return [
                'success' => false,
                'error' => 'private_storage_unavailable',
                'message' => 'Сервер не смог создать закрытое хранилище вне публичной папки сайта.',
            ];
        }
        $transport = $this->providerTransportReadiness();
        if (empty($transport['success'])) {
            return $transport;
        }
        $script = SITE_PATH . DIRECTORY_SEPARATOR . 'cli' . DIRECTORY_SEPARATOR . 'whatsapp_qr_auth_worker.php';
        $php = SchedulerCliSupport::detectPhpCli($script, $this->requiredCliExtensions());
        if (empty($php['success'])) {
            return ['success' => false, 'error' => (string)($php['error'] ?? 'php_cli_unavailable'), 'message' => (string)($php['message'] ?? 'Не найден подходящий PHP CLI.')];
        }
        $timeout = $this->timeoutBinary();
        if ($timeout === '') {
            return ['success' => false, 'error' => 'worker_supervisor_unavailable', 'message' => 'Сервер не подтвердил безопасное ограничение времени QR-процесса.'];
        }
        $nohup = $this->nohupBinary();
        if ($nohup === '') {
            return ['success' => false, 'error' => 'worker_spawn_unavailable', 'message' => 'Сервер не подтвердил безопасный запуск фонового QR-процесса.'];
        }
        return ['success' => true, 'php' => $php, 'timeout' => $timeout, 'nohup' => $nohup, 'script' => $script];
    }

    private function spawn(array $context, array $readiness): array {
        $log_dir = WhatsAppPersonalRuntime::privateRoot() . DIRECTORY_SEPARATOR . 'worker-logs';
        if (!is_dir($log_dir) && !SafeRuntime::mkdir($log_dir, 0700, true)) {
            return ['success' => false, 'error' => 'worker_log_unavailable', 'message' => 'Не удалось подготовить журнал QR-процесса.'];
        }
        if (!SafeRuntime::chmod($log_dir, 0700) || SafeRuntime::permissions($log_dir) !== 0700) {
            return ['success' => false, 'error' => 'worker_log_unavailable', 'message' => 'Не удалось защитить папку журналов QR-процесса.'];
        }
        $log_suffix = SafeRuntime::call(static function (): string {
            return bin2hex(random_bytes(4));
        }, '');
        if (preg_match('/^[a-f0-9]{8}$/D', (string)$log_suffix) !== 1) {
            return ['success' => false, 'error' => 'worker_log_unavailable', 'message' => 'Не удалось подготовить журнал QR-процесса.'];
        }
        $log = $log_dir . DIRECTORY_SEPARATOR . 'qr-' . (int)$context['channel']['channel_id']
            . '-' . $context['attempt_id'] . '-' . $log_suffix . '.log';
        $handle = SafeRuntime::call(function () use ($log) { return fopen($log, 'xb'); }, false);
        if (!is_resource($handle)) {
            return ['success' => false, 'error' => 'worker_log_unavailable', 'message' => 'Не удалось подготовить журнал QR-процесса.'];
        }
        fclose($handle);
        if (!SafeRuntime::chmod($log, 0600) || SafeRuntime::permissions($log) !== 0600) {
            SafeRuntime::unlink($log);
            return ['success' => false, 'error' => 'worker_log_unavailable', 'message' => 'Не удалось защитить журнал QR-процесса.'];
        }
        $this->pruneWorkerLogs($log_dir, $log);
        $command = escapeshellarg((string)$readiness['nohup']) . ' ' . escapeshellarg((string)$readiness['timeout'])
            . ' -s TERM -k 5 ' . self::WORKER_TIMEOUT
            . ' ' . escapeshellarg((string)$readiness['php']['binary'])
            . ' ' . escapeshellarg((string)$readiness['script'])
            . ' ' . (int)$context['channel']['channel_id']
            . ' ' . escapeshellarg((string)$context['attempt_id'])
            . ' < /dev/null > /dev/null 2>> ' . escapeshellarg($log) . ' &';
        $output = [];
        $code = 0;
        $started = SafeRuntime::call(function () use ($command, &$output, &$code) {
            exec($command, $output, $code);
            return true;
        }, false);
        return $started && $code === 0
            ? ['success' => true]
            : ['success' => false, 'error' => 'worker_spawn_failed', 'message' => 'Не удалось запустить QR-процесс WhatsApp.'];
    }

    private function workerLock(int $channel_id, string $attempt_id) {
        $path = WhatsAppPersonalRuntime::privateRoot() . DIRECTORY_SEPARATOR . 'auth-worker-' . $channel_id . '-' . $attempt_id . '.lock';
        $handle = SafeRuntime::call(function () use ($path) { return fopen($path, 'c+b'); }, false);
        $locked = is_resource($handle)
            && SafeRuntime::call(function () use ($handle) { return flock($handle, LOCK_EX | LOCK_NB); }, false);
        if (!is_resource($handle) || !$locked) {
            if (is_resource($handle)) fclose($handle);
            return null;
        }
        if (!SafeRuntime::chmod($path, 0600) || SafeRuntime::permissions($path) !== 0600) {
            flock($handle, LOCK_UN);
            fclose($handle);
            return null;
        }
        return $handle;
    }

    private function timeoutBinary(): string {
        foreach (['/usr/bin/timeout', '/bin/timeout'] as $candidate) {
            $real = realpath($candidate);
            if ($real && is_file($real) && is_executable($real)
                && preg_match('#^/(?:usr/)?bin/timeout$#D', $real) === 1) return $real;
        }
        return '';
    }

    private function nohupBinary(): string {
        foreach (['/usr/bin/nohup', '/bin/nohup'] as $candidate) {
            $real = realpath($candidate);
            if ($real && is_file($real) && is_executable($real)
                && preg_match('#^/(?:usr/)?bin/nohup$#D', $real) === 1) return $real;
        }
        return '';
    }

    private function qrSvg(string $value): string {
        try {
            require_once DIR_LIBRARY . DIRECTORY_SEPARATOR . 'qrcode' . DIRECTORY_SEPARATOR . 'loader.php';
            $svg = \splitbrain\phpQRCode\QRCode::svg($value, ['ecl' => 'M']);
            return 'data:image/svg+xml;base64,' . base64_encode($svg);
        } catch (\Throwable $e) {
            return '';
        }
    }

    private function settings(array $channel): array {
        $settings = json_decode((string)($channel['settings_json'] ?? '{}'), true);
        return is_array($settings) ? $settings : [];
    }

    private function workerError(\Throwable $error): string {
        if ($error instanceof \PhpWA\Exception\TransportException) return 'provider_unreachable';
        $message = strtolower(get_class($error) . ' ' . $error->getMessage());
        if (strpos($message, 'stale_attempt') !== false || strpos($message, 'generation') !== false) return 'stale_attempt';
        if (strpos($message, 'authorization') !== false || strpos($message, 'logged out') !== false) return 'authorization_rejected';
        if (strpos($message, 'storage') !== false || strpos($message, 'pdo') !== false) return 'storage_unavailable';
        if (strpos($message, 'timeout') !== false || strpos($message, 'expired') !== false) return 'qr_expired';
        return 'qr_worker_failed';
    }

    private function safeExceptionClass(\Throwable $error): string {
        $class = str_replace('\\', '/', get_class($error));
        $class = basename($class);
        return preg_match('/^[A-Za-z][A-Za-z0-9_]{0,79}$/D', $class) === 1
            ? $class
            : 'Throwable';
    }

    private function message(string $status, string $error): string {
        $messages = [
            'idle' => 'Отсканируйте QR-код, чтобы подключить личный WhatsApp.',
            'qr_starting' => 'Готовим QR-код.', 'worker_ready' => 'Подключаемся к WhatsApp.',
            'qr_pending' => 'Отсканируйте QR-код в WhatsApp.', 'pairing' => 'Завершаем защищённый вход.',
            'connected' => 'Личный WhatsApp подключён.', 'expired' => 'QR-код устарел. Начните вход заново.',
            'error' => $error === 'qr_worker_stalled'
                ? 'QR-процесс остановился. Начните вход заново.'
                : ($error === 'provider_unreachable'
                    ? $this->providerUnavailableMessage()
                : ($error === 'private_storage_unavailable'
                    ? 'Сервер не подготовил защищённое хранилище. Проверьте требования хостинга.'
                    : 'Не удалось подключить WhatsApp. Повторите вход.')),
        ];
        return $messages[$status] ?? $messages['idle'];
    }

    private function providerTransportReadiness(): array {
        $stream = null;
        try {
            WhatsAppPersonalRuntime::requireAutoload();
            $connector = $this->lifecycle->proxyConnector();
            if ($connector === null) {
                $connector = new \PhpWA\Transport\DirectStreamConnector();
            }
            $stream = $connector->connect(
                self::PROVIDER_TRANSPORT_HOST,
                443,
                true,
                \PhpWA\Runtime\Deadline::after(self::PROVIDER_TRANSPORT_TIMEOUT)
            );
            return ['success' => true];
        } catch (\Throwable $error) {
            return [
                'success' => false,
                'error' => 'provider_unreachable',
                'message' => $this->providerUnavailableMessage(),
            ];
        } finally {
            if (is_resource($stream)) {
                fclose($stream);
            }
        }
    }

    private function providerUnavailableMessage(): string {
        try {
            if ($this->lifecycle->proxyConnector() !== null) {
                return 'Прокси не может подключиться к WhatsApp. Проверьте маршрут WhatsApp в настройках proxy.';
            }
        } catch (\Throwable $error) {
        }
        return 'Сервер не может подключиться к WhatsApp. Разрешите исходящий HTTPS-доступ к web.whatsapp.com.';
    }

    private function store(): WhatsAppPersonalAuthStore {
        if (!$this->store instanceof WhatsAppPersonalAuthStore) {
            $this->store = new WhatsAppPersonalAuthStore();
        }
        return $this->store;
    }

    private function requiredCliExtensions(): array {
        return ['curl', 'pdo_mysql', 'zlib'];
    }

     
    private function pruneWorkerLogs(string $directory, string $active): void {
        $files = glob($directory . DIRECTORY_SEPARATOR . 'qr-*.log');
        if (!is_array($files)) {
            return;
        }
        usort($files, static function (string $left, string $right): int {
            $right_mtime = (int)SafeRuntime::call(function () use ($right) { return filemtime($right); }, 0);
            $left_mtime = (int)SafeRuntime::call(function () use ($left) { return filemtime($left); }, 0);
            return $right_mtime <=> $left_mtime;
        });
        $now = time();
        foreach ($files as $index => $file) {
            if ($file === $active || is_link($file)) {
                continue;
            }
            $modified = (int)SafeRuntime::call(function () use ($file) { return filemtime($file); }, 0);
            if ($index >= 12 || $modified <= 0 || $modified < $now - 86400) {
                SafeRuntime::unlink($file);
            }
        }
    }
}
