<?php
namespace EasyChat\Library\Messenger;

use EasyChat\Library\RequestContext;
use EasyChat\Library\SafeRuntime;
use PhpWA\Auth\ClientPayloadFactory;
use PhpWA\Auth\CompanionDeviceConfig;
use PhpWA\Client;
use PhpWA\Client\ProtocolConnection;
use PhpWA\Config\ClientOptions;
use PhpWA\Integration\GenerationRunGuard;
use PhpWA\Runtime\PrivateFileAccountLockProvider;
use PhpWA\Runtime\RunCheckpoint;
use PhpWA\Storage\AesGcmKeyringCipher;
use PhpWA\Storage\PdoAccountStore;
use PhpWA\Storage\PdoSchema;
use PhpWA\Transport\Noise\NoiseSocket;
use PhpWA\Transport\StreamConnectorInterface;
use PhpWA\Transport\WebSocket\WebSocketClient;
use PhpWA\Transport\WebSocket\WebSocketClientOptions;







final class WhatsAppPersonalRuntime {
    private const KEY_FILE_FORMAT = 1;

    public static function requireAutoload(): void {
        if (class_exists('PhpWA\\Client', false)) {
            return;
        }
        $candidates = [
            DIR_LIBRARY . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'phpwa' . DIRECTORY_SEPARATOR . 'autoload.php',
             
             
            dirname(DIR_LIBRARY, 2) . DIRECTORY_SEPARATOR . 'w' . DIRECTORY_SEPARATOR . 'autoload.php',
        ];
        $autoload = '';
        foreach ($candidates as $candidate) {
            if (is_file($candidate) && !is_link($candidate)) {
                $autoload = $candidate;
                break;
            }
        }
        if ($autoload === '') {
            throw new \RuntimeException('whatsapp_runtime_missing');
        }
        require_once $autoload;
        if (!class_exists('PhpWA\\Client')) {
            throw new \RuntimeException('whatsapp_runtime_invalid');
        }
    }

    public static function privateRoot(): string {
        static $resolved = '';
        if ($resolved !== '') {
            return $resolved;
        }
        $configured = defined('EASYCHAT_PRIVATE_STORAGE_DIR')
            ? trim((string)EASYCHAT_PRIVATE_STORAGE_DIR)
            : trim((string)getenv('EASYCHAT_PRIVATE_STORAGE_DIR'));
        $candidates = [];
        if ($configured !== '') {
            $candidates[] = rtrim($configured, '/\\') . DIRECTORY_SEPARATOR . 'phpwa';
        }
        $site = rtrim((string)SITE_PATH, '/\\');
        $public_roots = self::publicRoots($site);
        $inferred_public_root = $public_roots ? reset($public_roots) : '';
        if (is_string($inferred_public_root) && $inferred_public_root !== '') {
            $candidates[] = dirname($inferred_public_root)
                . DIRECTORY_SEPARATOR . '.easychat-private'
                . DIRECTORY_SEPARATOR . 'phpwa';
        }

        foreach (array_unique($candidates) as $candidate) {
            if (!self::absolutePath($candidate)) {
                continue;
            }
            if (self::insideAnyRoot($candidate, $public_roots)) {
                continue;
            }
            $previous = umask(0077);
            try {
                if (!is_dir($candidate) && !SafeRuntime::mkdir($candidate, 0700, true)) {
                    continue;
                }
            } finally {
                umask($previous);
            }
            if (!is_dir($candidate) || is_link($candidate) || !is_writable($candidate)) {
                continue;
            }
            $real_candidate = realpath($candidate);
            if (!is_string($real_candidate) || self::insideAnyRoot($real_candidate, $public_roots)) {
                continue;
            }
            if (!SafeRuntime::chmod($candidate, 0700) || SafeRuntime::permissions($candidate) !== 0700) {
                continue;
            }
            self::writeDenyFile($candidate);
            return $resolved = $candidate;
        }
        throw new \RuntimeException('whatsapp_private_storage_unavailable');
    }

    public static function client(
        array $channel,
        int $generation,
        callable $generation_reader,
        callable $heartbeat,
        ?StreamConnectorInterface $connector = null
    ): Client {
        self::requireAutoload();
        $channel_id = (int)($channel['channel_id'] ?? 0);
        if ($channel_id <= 0 || $generation <= 0) {
            throw new \InvalidArgumentException('whatsapp_channel_invalid');
        }
         
         
         
         
         
         
        self::publishGeneration($channel_id, $generation);
        $root = self::privateRoot();
        $keys = self::keys($root);
        $pdo = self::pdo();
        $prefix = self::protocolPrefix();
        self::ensureProtocolSchema($pdo, $prefix);
        $store = new PdoAccountStore(
            $pdo,
            new AesGcmKeyringCipher('primary', ['primary' => $keys['storage']]),
            $keys['index'],
            $prefix
        );
        $guard = new GenerationRunGuard(
            $generation,
            static function () use ($generation_reader): int {
                return max(0, (int)call_user_func($generation_reader));
            },
            static function (RunCheckpoint $checkpoint) use ($heartbeat): void {
                call_user_func($heartbeat, $checkpoint);
            }
        );
        $account_id = 'easychat:whatsapp-personal:channel:' . $channel_id;
        $work = $root . DIRECTORY_SEPARATOR . 'channels' . DIRECTORY_SEPARATOR . $channel_id
            . DIRECTORY_SEPARATOR . 'generation-' . $generation;
        $companion = new CompanionDeviceConfig();
        $web_socket = new WebSocketClientOptions();
        $connection_factory = null;
        if ($connector !== null) {
            $connection_factory = static function () use ($connector, $companion, $web_socket): ProtocolConnection {
                return new ProtocolConnection(
                    new NoiseSocket(new WebSocketClient($web_socket, $connector)),
                    new ClientPayloadFactory($companion)
                );
            };
        }
        $options = new ClientOptions(
            $account_id,
            $work,
            null,
            $store,
            new PrivateFileAccountLockProvider($root . DIRECTORY_SEPARATOR . 'locks'),
            $companion,
            $web_socket,
            15.0,
            268435456,
            $connection_factory
        );
        return new Client($options->withRunGuard($guard, 1.0));
    }

    public static function publishGeneration(int $channel_id, int $generation): void {
        if ($channel_id <= 0 || $generation <= 0) {
            throw new \InvalidArgumentException('whatsapp_generation_invalid');
        }
        $dir = self::privateRoot() . DIRECTORY_SEPARATOR . 'generations';
        if (!is_dir($dir) && !SafeRuntime::mkdir($dir, 0700, true)) {
            throw new \RuntimeException('whatsapp_generation_store_unavailable');
        }
        if (!SafeRuntime::chmod($dir, 0700) || SafeRuntime::permissions($dir) !== 0700) {
            throw new \RuntimeException('whatsapp_generation_store_permissions_invalid');
        }
        self::writePrivateFile($dir . DIRECTORY_SEPARATOR . 'channel-' . $channel_id, (string)$generation . "\n");
    }

    public static function generation(int $channel_id): int {
        if ($channel_id <= 0) {
            return 0;
        }
        $path = self::privateRoot() . DIRECTORY_SEPARATOR . 'generations' . DIRECTORY_SEPARATOR . 'channel-' . $channel_id;
        $value = is_file($path) && !is_link($path)
            ? SafeRuntime::call(function () use ($path) { return file_get_contents($path, false, null, 0, 32); }, false)
            : false;
        return is_string($value) && preg_match('/^[1-9][0-9]{0,9}\s*$/D', $value) === 1 ? (int)$value : 0;
    }

     
    public static function keys(?string $root = null): array {
        $root = $root !== null ? $root : self::privateRoot();
        $path = rtrim($root, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'keys.json';
        $lock_path = rtrim($root, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'keys.lock';
        $lock = SafeRuntime::call(function () use ($lock_path) { return fopen($lock_path, 'c+b'); }, false);
        $locked = is_resource($lock)
            && SafeRuntime::call(function () use ($lock) { return flock($lock, LOCK_EX); }, false);
        if (!is_resource($lock) || !$locked) {
            if (is_resource($lock)) fclose($lock);
            throw new \RuntimeException('whatsapp_key_lock_unavailable');
        }
        if (!SafeRuntime::chmod($lock_path, 0600) || SafeRuntime::permissions($lock_path) !== 0600) {
            flock($lock, LOCK_UN);
            fclose($lock);
            throw new \RuntimeException('whatsapp_key_lock_permissions_invalid');
        }
        try {
             
             
            if (!is_file($path)) {
                $payload = json_encode([
                    'format' => self::KEY_FILE_FORMAT,
                    'storage' => base64_encode(random_bytes(32)),
                    'index' => base64_encode(random_bytes(32)),
                    'created_at' => time(),
                ], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
                self::writePrivateFile($path, (string)$payload);
            }
        } finally {
            flock($lock, LOCK_UN);
            fclose($lock);
        }
        $stat = SafeRuntime::call(function () use ($path) { return lstat($path); }, false);
        if (!is_array($stat) || is_link($path) || ((int)$stat['mode'] & 0077) !== 0 || (int)$stat['nlink'] !== 1) {
            throw new \RuntimeException('whatsapp_key_file_permissions_invalid');
        }
        $raw = SafeRuntime::call(function () use ($path) { return file_get_contents($path, false, null, 0, 4096); }, false);
        $data = is_string($raw) ? json_decode($raw, true) : null;
        $storage = is_array($data) ? base64_decode((string)($data['storage'] ?? ''), true) : false;
        $index = is_array($data) ? base64_decode((string)($data['index'] ?? ''), true) : false;
        if (($data['format'] ?? null) !== self::KEY_FILE_FORMAT || !is_string($storage) || strlen($storage) !== 32
            || !is_string($index) || strlen($index) !== 32 || hash_equals($storage, $index)) {
            throw new \RuntimeException('whatsapp_key_file_invalid');
        }
        return ['storage' => $storage, 'index' => $index];
    }

    private static function pdo(): \PDO {
        if (!class_exists('PDO') || !in_array('mysql', \PDO::getAvailableDrivers(), true)) {
            throw new \RuntimeException('whatsapp_pdo_mysql_missing');
        }
        $dsn = 'mysql:host=' . DB_HOSTNAME . ';port=' . (int)DB_PORT . ';dbname=' . DB_DATABASE . ';charset=utf8mb4';
        return new \PDO($dsn, DB_USERNAME, DB_PASSWORD, [
            \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
            \PDO::ATTR_EMULATE_PREPARES => false,
            \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
        ]);
    }

    private static function protocolPrefix(): string {
        $base = function_exists('easychat_db_prefix') ? easychat_db_prefix() : (defined('DB_PREFIX') ? DB_PREFIX : 'chat_');
        $base = preg_replace('/[^A-Za-z0-9_]/', '', (string)$base);
        $prefix = substr((string)$base, 0, 24) . 'phpwa_';
        PdoSchema::assertPrefix($prefix);
        return $prefix;
    }

    private static function ensureProtocolSchema(\PDO $pdo, string $prefix): void {
        static $ready = [];
        $key = DB_DATABASE . ':' . $prefix;
        if (!empty($ready[$key])) {
            return;
        }
        $tables = array_map(function (string $suffix) use ($prefix): string {
            return $prefix . $suffix;
        }, ['accounts', 'state', 'dedupe', 'outbox', 'retries']);
        $placeholders = implode(',', array_fill(0, count($tables), '?'));
        $statement = $pdo->prepare(
            'SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = ? AND table_name IN (' . $placeholders . ')'
        );
        $statement->execute(array_merge([DB_DATABASE], $tables));
        if ((int)$statement->fetchColumn() !== count($tables)) {
            PdoSchema::install($pdo, $prefix);
        }
        $ready[$key] = true;
    }

    private static function writePrivateFile(string $path, string $contents): void {
        $temp = $path . '.tmp.' . bin2hex(random_bytes(8));
        $previous = umask(0077);
        try {
            $handle = SafeRuntime::call(function () use ($temp) { return fopen($temp, 'xb'); }, false);
        } finally {
            umask($previous);
        }
        if (!is_resource($handle)) {
            throw new \RuntimeException('whatsapp_private_file_create_failed');
        }
        try {
            try {
                $length = strlen($contents);
                $written = 0;
                while ($written < $length) {
                    $chunk = fwrite($handle, substr($contents, $written));
                    if ($chunk === false || $chunk === 0) {
                        throw new \RuntimeException('whatsapp_private_file_write_failed');
                    }
                    $written += $chunk;
                }
                if (!fflush($handle)) {
                    throw new \RuntimeException('whatsapp_private_file_flush_failed');
                }
            } finally {
                fclose($handle);
            }
        } catch (\Throwable $error) {
            SafeRuntime::unlink($temp);
            throw $error;
        }
        if (!SafeRuntime::chmod($temp, 0600)
            || SafeRuntime::permissions($temp) !== 0600
            || !SafeRuntime::rename($temp, $path)) {
            SafeRuntime::unlink($temp);
            throw new \RuntimeException('whatsapp_private_file_publish_failed');
        }
    }

    private static function writeDenyFile(string $directory): void {
        $path = rtrim($directory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '.htaccess';
        if (!is_file($path)) {
            self::writePrivateFile($path, "Require all denied\nDeny from all\n");
        }
    }

     
    private static function publicRoots(string $site): array {
        $roots = [];
        $server = RequestContext::server();
        $document_root = trim((string)($server['DOCUMENT_ROOT'] ?? ''));
        if (self::absolutePath($document_root)) {
            $roots[] = $document_root;
        }
        $cursor = $site;
        $public_names = ['public_html', 'www', 'htdocs', 'httpdocs', 'html', 'public'];
        while (self::absolutePath($cursor) && dirname($cursor) !== $cursor) {
            if (in_array(strtolower(basename($cursor)), $public_names, true)) {
                $roots[] = $cursor;
                break;
            }
            $cursor = dirname($cursor);
        }
        $normalized = [];
        foreach ($roots as $root) {
            $root = realpath($root) ?: self::normalizeAbsolutePath($root);
            if ($root !== '') {
                $normalized[$root] = true;
            }
        }
        return array_keys($normalized);
    }

    private static function insideAnyRoot(string $path, array $roots): bool {
        $path = realpath($path) ?: self::normalizeAbsolutePath($path);
        if ($path === '') {
            return true;
        }
        foreach ($roots as $root) {
            $root = rtrim((string)$root, DIRECTORY_SEPARATOR);
            if ($root !== '' && ($path === $root || strpos($path, $root . DIRECTORY_SEPARATOR) === 0)) {
                return true;
            }
        }
        return false;
    }

    private static function normalizeAbsolutePath(string $path): string {
        if (!self::absolutePath($path)) {
            return '';
        }
        $drive = preg_match('/^([A-Za-z]:)[\\\\\/]/', $path, $match) === 1 ? strtoupper($match[1]) : '';
        $parts = [];
        foreach (preg_split('#[\\\\/]#', $path) ?: [] as $part) {
            if ($part === '' || $part === '.') continue;
            if ($part === '..') {
                array_pop($parts);
            } else {
                $parts[] = $part;
            }
        }
        if ($drive !== '' && isset($parts[0]) && strcasecmp($parts[0], $drive) === 0) {
            array_shift($parts);
            return $drive . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $parts);
        }
        return DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $parts);
    }

    private static function absolutePath(string $path): bool {
        return $path !== '' && ($path[0] === DIRECTORY_SEPARATOR || preg_match('/^[A-Za-z]:[\\\\\/]/', $path) === 1)
            && strpos($path, "\0") === false;
    }
}
