<?php
declare(strict_types=1);

final class BotService
{
    private ChildDatabaseManager $childDatabases;

    public function __construct(
        private Repository $repo,
        private Security $security,
        private array $config
    ) {
        $this->childDatabases = new ChildDatabaseManager($config);
    }

    public function registerChildBot(int $actorUserId, string|int $ownerTelegramId, string $token, ?int $planId = null, ?int $months = null, ?int $price = null): array
    {
        $token = trim($token);
        if (!Telegram::isValidTokenFormat($token)) throw new RuntimeException('توکن ربات معتبر نیست.');

        $ownerTelegramId = digits_only((string)$ownerTelegramId);
        if ($ownerTelegramId === '') throw new RuntimeException('آیدی عددی مالک ربات معتبر نیست.');

        $owner = $this->repo->upsertUser(['telegram_id' => $ownerTelegramId], 'customer');
        $actor = $this->repo->findUserById($actorUserId) ?: [];
        $resellerId = null;
        if (($actor['role'] ?? '') === 'reseller') {
            $reseller = $this->repo->resellerByUserId($actorUserId);
            $resellerId = $reseller ? (int)$reseller['id'] : null;
        }

        $plan = $planId ? $this->repo->getPlan($planId) : null;
        $useMonths = $months ?: (int)($plan['months'] ?? 1);
        $usePrice = $price ?? (int)($plan['price'] ?? 0);
        $expiresAt = date('Y-m-d H:i:s', strtotime('+' . max(1, $useMonths) . ' months'));

        $tg = new Telegram($token);
        $info = $tg->getMe();
        $secret = Security::randomSecret(24);
        $childDb = null;
        $deployment = null;
        $registeredBotId = null;

        try {
            // Universal zero-input isolation: every child receives a private
            // table namespace inside the configured MySQL database. No cPanel,
            // CREATE DATABASE, CREATE USER or GRANT privilege is required.
            $childDb = $this->childDatabases->provisionForSecret($secret);
            $deployment = $this->deployMirzaEhsannetSource(
                $secret,
                $token,
                (string)$ownerTelegramId,
                $info,
                $childDb
            );

            usleep(600000);
            $tg->setWebhook($deployment['webhook_url'], null, [
                'message', 'edited_message', 'callback_query', 'chat_member', 'my_chat_member',
                'inline_query', 'pre_checkout_query', 'shipping_query'
            ]);
            try {
                $tg->setMenuButton($deployment['panel_url'], 'پنل ربات');
            } catch (Throwable $e) {
                app_log('Child menu button failed', ['error' => $e->getMessage(), 'url' => $deployment['panel_url']]);
            }

            $bot = $this->repo->registerBot(
                $actorUserId,
                (int)$owner['id'],
                $this->security->encrypt($token),
                $secret,
                $info,
                $planId,
                $resellerId,
                $usePrice,
                $expiresAt
            );
            $registeredBotId = (int)$bot['id'];
            $this->repo->setBotDeployment(
                $registeredBotId,
                $deployment['public_path'],
                $deployment['webhook_url'],
                (string)$childDb['name'],
                (string)$childDb['user'],
                (string)$childDb['grant_host'],
                (string)$childDb['provider'],
                (string)$childDb['storage_path']
            );
            return $this->repo->botById($registeredBotId) ?: $bot;
        } catch (Throwable $e) {
            // Registration is all-or-nothing. Do not leave a webhook, public copy,
            // database, database user, subscription or bot metadata behind.
            try { $tg->deleteWebhook(); } catch (Throwable $cleanupError) {
                app_log('Rollback child deleteWebhook failed', ['error' => $cleanupError->getMessage()]);
            }
            if ($registeredBotId !== null) {
                try { $this->repo->deleteBotById($registeredBotId); } catch (Throwable $cleanupError) {
                    app_log('Rollback child metadata failed', ['bot_id' => $registeredBotId, 'error' => $cleanupError->getMessage()]);
                }
            }
            $targetDir = $this->targetDirectory($secret);
            if (is_dir($targetDir)) $this->removeDirectory($targetDir);
            if (is_array($childDb) && !empty($childDb['name'])) {
                try {
                    $this->childDatabases->drop(
                        (string)$childDb['name'],
                        (string)($childDb['user'] ?? ''),
                        (string)($childDb['grant_host'] ?? ''),
                        $secret,
                        (string)($childDb['provider'] ?? '')
                    );
                } catch (Throwable $cleanupError) {
                    app_log('Rollback child database failed', ['database' => $childDb['name'], 'error' => $cleanupError->getMessage()]);
                }
            }
            throw $e;
        }
    }

    private function deployMirzaEhsannetSource(string $secret, string $token, string $ownerTelegramId, array $botInfo, array $childDb): array
    {
        $templateDir = dirname(__DIR__) . '/templates/mirza_ehsannet';
        if (!is_dir($templateDir)) throw new RuntimeException('قالب ربات میرزا ساز احسان نت داخل پکیج پیدا نشد.');

        $targetBase = dirname(__DIR__) . '/public/generated_bots';
        if (!is_dir($targetBase) && !mkdir($targetBase, 0755, true)) {
            throw new RuntimeException('ساخت پوشه generated_bots ممکن نشد. سطح دسترسی هاست را بررسی کنید.');
        }
        $targetDir = $this->targetDirectory($secret);
        if (is_dir($targetDir)) $this->removeDirectory($targetDir);
        $this->copyDirectory($templateDir, $targetDir);

        foreach (['installer', 'install.sh', 'README.md', 'LICENSE'] as $unsafe) {
            $path = $targetDir . '/' . $unsafe;
            if (is_dir($path)) $this->removeDirectory($path);
            elseif (is_file($path)) @unlink($path);
        }

        $dbHost = (string)($childDb['host'] ?? 'localhost');
        $dbPort = (int)($childDb['port'] ?? 3306);
        $dbName = (string)($childDb['name'] ?? '');
        $dbUser = (string)($childDb['user'] ?? '');
        $dbPass = (string)($childDb['pass'] ?? '');
        $tablePrefix = (string)($childDb['table_prefix'] ?? '');
        if ($dbName === '' || $dbUser === '' || $tablePrefix === '') throw new RuntimeException('اطلاعات فضای دیتابیس اختصاصی ربات کامل نیست.');
        if (!preg_match('/^[a-z][a-z0-9_]{8,30}_$/', $tablePrefix)) throw new RuntimeException('پیشوند جدول اختصاصی ربات نامعتبر است.');

        $appUrl = rtrim((string)($this->config['app_url'] ?? ''), '/');
        if ($appUrl === '' || !preg_match('~^https://~i', $appUrl)) throw new RuntimeException('آدرس app_url باید HTTPS باشد.');

        $publicPath = 'generated_bots/' . $secret;
        $webhookUrl = $appUrl . '/' . $publicPath . '/index.php';
        $panelUrl = $appUrl . '/' . $publicPath . '/panel/login.php';
        $domainHosts = preg_replace('~^https?://~i', '', $appUrl . '/' . $publicPath);
        $botUsername = (string)($botInfo['username'] ?? '');

        $configPhp = "<?php\n" .
            "// Auto-generated isolated child configuration.\n" .
            "// Every SQL query is restricted to this bot's private table namespace.\n" .
            "require_once __DIR__ . '/tenant_db.php';\n" .
            '$dbname = ' . var_export($dbName, true) . ";\n" .
            '$usernamedb = ' . var_export($dbUser, true) . ";\n" .
            '$passworddb = ' . var_export($dbPass, true) . ";\n" .
            '$dbhost = ' . var_export($dbHost, true) . ";\n" .
            '$dbport = ' . var_export($dbPort, true) . ";\n" .
            '$tenant_table_prefix = ' . var_export($tablePrefix, true) . ";\n" .
            '$GLOBALS[\'tenant_table_prefix\'] = $tenant_table_prefix;' . "\n" .
            'mysqli_report(MYSQLI_REPORT_OFF);' . "\n" .
            '$connect = new TenantMysqli($dbhost, $usernamedb, $passworddb, $dbname, (int)$dbport, null, $tenant_table_prefix);' . "\n" .
            'if ($connect->connect_errno) { error_log("Child database connection failed"); die("database error"); }' . "\n" .
            '$connect->set_charset("utf8mb4");' . "\n" .
            '$options = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false];' . "\n" .
            '$dsn = "mysql:host={$dbhost};port={$dbport};dbname={$dbname};charset=utf8mb4";' . "\n" .
            'try { $pdo = new TenantPDO($dsn, $usernamedb, $passworddb, $options, $tenant_table_prefix); } catch (PDOException $e) { error_log("Child database connection failed: " . $e->getMessage()); die("database error"); }' . "\n" .
            '$APIKEY = ' . var_export($token, true) . ";\n" .
            '$adminnumber = ' . var_export($ownerTelegramId, true) . ";\n" .
            '$domainhosts = ' . var_export($domainHosts, true) . ";\n" .
            '$usernamebot = ' . var_export($botUsername, true) . ";\n" .
            '$new_marzban = true;' . "\n";


        if (file_put_contents($targetDir . '/config.php', $configPhp, LOCK_EX) === false) {
            throw new RuntimeException('ساخت کانفیگ اختصاصی ربات ناموفق بود.');
        }
        @chmod($targetDir . '/config.php', 0640);

        $this->installMirzaEhsannetTables($targetDir);
        $this->ensureChildOwnerHasFullAccess($ownerTelegramId, $childDb);

        return [
            'webhook_url' => $webhookUrl,
            'panel_url' => $panelUrl,
            'public_path' => $publicPath,
        ];
    }

    private function ensureChildOwnerHasFullAccess(string $ownerTelegramId, array $childDb): void
    {
        $ownerTelegramId = digits_only($ownerTelegramId);
        if ($ownerTelegramId === '') throw new RuntimeException('آیدی عددی مالک ربات معتبر نیست.');

        try {
            $pdo = Db::make($childDb);
            $tablePrefix = (string)($childDb['table_prefix'] ?? '');
            if (!preg_match('/^[a-z][a-z0-9_]{8,30}_$/', $tablePrefix)) throw new RuntimeException('پیشوند جدول اختصاصی ربات نامعتبر است.');
            $adminTable = ChildDatabaseManager::quoteIdentifier($tablePrefix . 'admin');
            $pdo->exec("CREATE TABLE IF NOT EXISTS {$adminTable} (
                id_admin varchar(500) PRIMARY KEY NOT NULL,
                username varchar(1000) NOT NULL,
                password varchar(1000) NOT NULL,
                rule varchar(500) NOT NULL
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci");

            $username = 'owner_' . $ownerTelegramId;
            $password = bin2hex(random_bytes(10));
            $stmt = $pdo->prepare("INSERT INTO {$adminTable} (id_admin, username, password, rule)
                VALUES (:id_admin, :username, :password, 'administrator')
                ON DUPLICATE KEY UPDATE username=VALUES(username), rule='administrator'");
            $stmt->execute([':id_admin' => $ownerTelegramId, ':username' => $username, ':password' => $password]);
        } catch (Throwable $e) {
            app_log('Child owner admin upsert failed', [
                'owner_telegram_id' => $ownerTelegramId,
                'child_database' => $childDb['name'] ?? null,
                'child_table_prefix' => $childDb['table_prefix'] ?? null,
                'error' => $e->getMessage(),
            ]);
            throw new RuntimeException('ثبت مالک به عنوان ادمین کامل ربات انجام نشد: ' . $e->getMessage(), 0, $e);
        }
    }

    private function installMirzaEhsannetTables(string $targetDir): void
    {
        $tableFile = $targetDir . '/table.php';
        if (!is_file($tableFile)) throw new RuntimeException('فایل جدول‌های قالب ربات پیدا نشد.');

        global $pdo, $connect, $dbname, $usernamedb, $passworddb, $dbhost, $dbport, $options, $dsn;
        global $APIKEY, $adminnumber, $domainhosts, $usernamebot, $new_marzban, $tenant_table_prefix;

        $oldCwd = getcwd();
        $oldGlobals = [];
        foreach (['pdo','connect','dbname','usernamedb','passworddb','dbhost','dbport','options','dsn','APIKEY','adminnumber','domainhosts','usernamebot','new_marzban','tenant_table_prefix'] as $key) {
            $oldGlobals[$key] = array_key_exists($key, $GLOBALS) ? $GLOBALS[$key] : null;
        }

        try {
            chdir($targetDir);
            if (!defined('MIRZA_BUILDER_SKIP_TABLE_WEBHOOK')) define('MIRZA_BUILDER_SKIP_TABLE_WEBHOOK', true);
            ob_start();
            require $tableFile;
            ob_end_clean();
            $this->assertChildSchemaInstalled($pdo);
        } catch (Throwable $e) {
            if (ob_get_level() > 0) ob_end_clean();
            app_log('Mirza source table install failed', ['error' => $e->getMessage(), 'dir' => $targetDir]);
            throw new RuntimeException('نصب جدول‌های ربات میرزا ساز احسان نت انجام نشد: ' . $e->getMessage(), 0, $e);
        } finally {
            foreach ($oldGlobals as $key => $value) {
                if ($value === null) unset($GLOBALS[$key]); else $GLOBALS[$key] = $value;
            }
            if ($oldCwd !== false) @chdir($oldCwd);
        }
    }

    private function assertChildSchemaInstalled(mixed $pdo): void
    {
        if (!$pdo instanceof PDO) {
            throw new RuntimeException('اتصال PDO دیتابیس ربات بعد از نصب در دسترس نیست.');
        }

        $prefix = (string)($GLOBALS['tenant_table_prefix'] ?? '');
        if (!preg_match('/^[a-z][a-z0-9_]{8,30}_$/', $prefix)) {
            throw new RuntimeException('پیشوند جدول اختصاصی پس از نصب در دسترس نیست.');
        }
        $required = ['user', 'setting', 'admin', 'marzban_panel', 'product', 'invoice'];
        $stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name=?');
        $missing = [];
        foreach ($required as $table) {
            $stmt->execute([$prefix . $table]);
            if ((int)$stmt->fetchColumn() !== 1) $missing[] = $table;
        }
        $countStmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=DATABASE() AND LEFT(table_name, CHAR_LENGTH(?)) = ?');
        $countStmt->execute([$prefix, $prefix]);
        $total = (int)$countStmt->fetchColumn();
        if ($missing || $total < 20) {
            throw new RuntimeException(
                'نصب دیتابیس ربات ناقص است. جدول‌های مفقود: ' . ($missing ? implode(', ', $missing) : 'ندارد') .
                ' | تعداد جدول‌های ساخته‌شده: ' . $total
            );
        }
    }

    private function targetDirectory(string $secret): string
    {
        return dirname(__DIR__) . '/public/generated_bots/' . $secret;
    }

    private function copyDirectory(string $src, string $dst): void
    {
        $dir = opendir($src);
        if ($dir === false) throw new RuntimeException('امکان خواندن پوشه قالب وجود ندارد.');
        if (!is_dir($dst) && !mkdir($dst, 0755, true)) {
            closedir($dir);
            throw new RuntimeException('امکان ساخت پوشه مقصد وجود ندارد.');
        }
        while (($file = readdir($dir)) !== false) {
            if ($file === '.' || $file === '..') continue;
            $source = $src . '/' . $file;
            $dest = $dst . '/' . $file;
            if (is_dir($source)) $this->copyDirectory($source, $dest);
            else {
                if (!copy($source, $dest)) {
                    closedir($dir);
                    throw new RuntimeException('کپی فایل قالب ناموفق بود: ' . $file);
                }
                @chmod($dest, 0644);
            }
        }
        closedir($dir);
        @chmod($dst, 0755);
    }

    private function removeDirectory(string $dir): void
    {
        if (is_link($dir)) { @unlink($dir); return; }
        if (!is_dir($dir)) return;
        $items = scandir($dir);
        if (!$items) return;
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') continue;
            $path = $dir . '/' . $item;
            if (is_dir($path)) $this->removeDirectory($path); else @unlink($path);
        }
        @rmdir($dir);
    }

    /**
     * Rebuilds a legacy/shared child with the current isolated template.
     * Historical mixed data is intentionally not copied because legacy rows do
     * not contain a reliable bot identifier.
     */
    public function rebuildLegacyChildIsolation(int $actorUserId, int $botId): array
    {
        $bot = $this->repo->botById($botId);
        if (!$bot) throw new RuntimeException('ربات پیدا نشد.');
        if (($bot['isolation_mode'] ?? '') === 'table_namespace' && ($bot['child_db_provider'] ?? '') === 'table_namespace') {
            throw new RuntimeException('این ربات از قبل روی فضای داده اختصاصی قرار دارد.');
        }

        $secret = (string)($bot['webhook_secret'] ?? '');
        $token = $this->security->decrypt((string)$bot['bot_token_enc']);
        if (!Telegram::isValidTokenFormat($token)) throw new RuntimeException('توکن ذخیره‌شده ربات معتبر نیست.');

        $targetDir = $this->targetDirectory($secret);
        $backupDir = dirname(__DIR__) . '/storage/migration_backups/bot_' . $botId . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4));
        $backupMade = false;
        $childDb = null;

        try {
            if (is_dir($targetDir)) {
                $parent = dirname($backupDir);
                if (!is_dir($parent) && !mkdir($parent, 0700, true) && !is_dir($parent)) {
                    throw new RuntimeException('ساخت پوشه بکاپ مهاجرت ممکن نشد.');
                }
                if (!rename($targetDir, $backupDir)) throw new RuntimeException('بکاپ پوشه فعلی ربات انجام نشد.');
                $backupMade = true;
            }

            $childDb = $this->childDatabases->provisionForSecret($secret);
            $botInfo = [
                'username' => (string)($bot['bot_username'] ?? ''),
                'first_name' => (string)($bot['bot_name'] ?? ''),
            ];
            $deployment = $this->deployMirzaEhsannetSource(
                $secret,
                $token,
                (string)$bot['owner_telegram_id'],
                $botInfo,
                $childDb
            );

            $tg = new Telegram($token);
            $tg->setWebhook($deployment['webhook_url'], null, [
                'message', 'edited_message', 'callback_query', 'chat_member', 'my_chat_member',
                'inline_query', 'pre_checkout_query', 'shipping_query'
            ]);
            try { $tg->setMenuButton($deployment['panel_url'], 'پنل ربات'); } catch (Throwable) {}

            $this->repo->setBotDeployment(
                $botId,
                $deployment['public_path'],
                $deployment['webhook_url'],
                (string)$childDb['name'],
                (string)$childDb['user'],
                '',
                'table_namespace',
                (string)$childDb['storage_path']
            );
            $this->repo->audit($actorUserId, 'rebuild_legacy_child_isolation', [
                'bot_id' => $botId,
                'table_prefix' => $childDb['table_prefix'],
            ]);

            if ($backupMade && is_dir($backupDir)) $this->removeDirectory($backupDir);
            return $this->repo->botById($botId) ?: $bot;
        } catch (Throwable $e) {
            if (is_dir($targetDir)) $this->removeDirectory($targetDir);
            if ($backupMade && is_dir($backupDir)) @rename($backupDir, $targetDir);
            if (is_array($childDb)) {
                try {
                    $this->childDatabases->drop(
                        (string)$childDb['name'],
                        (string)$childDb['user'],
                        '',
                        $secret,
                        'table_namespace'
                    );
                } catch (Throwable $cleanupError) {
                    app_log('Legacy isolation cleanup failed', ['bot_id' => $botId, 'error' => $cleanupError->getMessage()]);
                }
            }
            throw $e;
        }
    }

    public function deleteChildBot(int $actorUserId, int $botId): array
    {
        $bot = $this->repo->botById($botId);
        if (!$bot) throw new RuntimeException('ربات پیدا نشد.');

        try {
            $token = $this->security->decrypt((string)$bot['bot_token_enc']);
            if (Telegram::isValidTokenFormat($token)) (new Telegram($token))->deleteWebhook();
        } catch (Throwable $e) {
            app_log('Child deleteWebhook failed', ['bot_id' => $botId, 'error' => $e->getMessage()]);
        }

        $childDbName = trim((string)($bot['child_db_name'] ?? ''));
        $childProvider = trim((string)($bot['child_db_provider'] ?? ''));
        $isNamespaceBot = $childProvider === 'table_namespace' || (string)($bot['isolation_mode'] ?? '') === 'table_namespace';
        if ($childDbName !== '' || $isNamespaceBot) {
            // Drop data before metadata so a failed DROP can be retried safely.
            // Namespace metadata can be recovered from the webhook secret even
            // if an older partial deployment left child_db_name empty.
            $databaseForDrop = $childDbName !== ''
                ? $childDbName
                : $this->childDatabases->databaseNameForSecret((string)$bot['webhook_secret']);
            $this->childDatabases->drop(
                $databaseForDrop,
                (string)($bot['child_db_user'] ?? ''),
                (string)($bot['child_db_user_host'] ?? ''),
                (string)$bot['webhook_secret'],
                $childProvider !== '' ? $childProvider : 'table_namespace'
            );
            $childDbName = $databaseForDrop;
        }

        $deploymentPath = trim((string)($bot['deployment_path'] ?? ''));
        if ($deploymentPath !== '' && preg_match('~^generated_bots/[A-Za-z0-9_\-]+$~', $deploymentPath)) {
            $dir = dirname(__DIR__) . '/public/' . $deploymentPath;
            if (is_dir($dir)) $this->removeDirectory($dir);
        }

        $deleted = $this->repo->deleteBotById($botId);
        $this->repo->audit($actorUserId, 'delete_bot', [
            'bot_id' => $botId,
            'bot_username' => $bot['bot_username'] ?? null,
            'child_namespace_deleted' => $childDbName !== '',
        ]);
        return $deleted ?: $bot;
    }

    public function registerMainWebhook(): array
    {
        $secret = (string)($this->config['main_webhook_secret'] ?? '');
        if ($secret === '') throw new RuntimeException('main_webhook_secret خالی است.');
        $url = rtrim((string)$this->config['app_url'], '/') . '/bot.php?secret=' . $secret;
        $tg = new Telegram((string)$this->config['main_bot_token']);
        $result = $tg->setWebhook($url, $secret);
        try {
            $tg->setMenuButton(rtrim((string)$this->config['app_url'], '/') . '/index.php', 'پنل مدیریت');
        } catch (Throwable $e) {
            app_log('Main menu button failed', ['error' => $e->getMessage()]);
        }
        return $result;
    }
}
