<?php

namespace classes;

/**
 * Additive schema bootstrap for application release management.
 *
 * The legacy API does not have a centralized migration runner, so these
 * migrations must be safe to call from request handlers, tests, and cron.
 */
class release_manager_schema_bootstrap
{
    private static bool $initialized = false;
    private static ?bool $tablesExist = null;

    public static function ensureTables(): void
    {
        if (self::$initialized) {
            return;
        }

        global $db;

        $queries = [
            "CREATE TABLE IF NOT EXISTS release_channels (
                id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                slug VARCHAR(64) NOT NULL,
                name VARCHAR(128) NOT NULL,
                description TEXT NULL,
                enabled TINYINT(1) NOT NULL DEFAULT 1,
                default_channel TINYINT(1) NOT NULL DEFAULT 0,
                rollout_percent DECIMAL(5,2) NOT NULL DEFAULT 0.00,
                frontend_base_url VARCHAR(512) NULL,
                api_base_url VARCHAR(512) NULL,
                replay_enabled TINYINT(1) NOT NULL DEFAULT 0,
                capture_level VARCHAR(32) NOT NULL DEFAULT 'metadata',
                retention_days INT NOT NULL DEFAULT 14,
                metadata_json LONGTEXT NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                deleted_at DATETIME NULL,
                UNIQUE KEY uq_release_channels_slug (slug),
                INDEX idx_release_channels_enabled (enabled, deleted_at),
                INDEX idx_release_channels_default (default_channel, deleted_at)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",

            "CREATE TABLE IF NOT EXISTS release_versions (
                id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                app VARCHAR(16) NOT NULL,
                repository VARCHAR(255) NULL,
                branch VARCHAR(128) NULL,
                commit_sha VARCHAR(64) NULL,
                tag VARCHAR(128) NULL,
                version_label VARCHAR(128) NULL,
                build_url VARCHAR(512) NULL,
                artifact_url VARCHAR(512) NULL,
                deployed_url VARCHAR(512) NULL,
                status VARCHAR(32) NOT NULL DEFAULT 'discovered',
                metadata_json LONGTEXT NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                deployed_at DATETIME NULL,
                INDEX idx_release_versions_app_status (app, status),
                INDEX idx_release_versions_commit (commit_sha),
                INDEX idx_release_versions_repo_branch (repository, branch)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",

            "CREATE TABLE IF NOT EXISTS release_channel_versions (
                id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                channel_id BIGINT UNSIGNED NOT NULL,
                frontend_version_id BIGINT UNSIGNED NULL,
                api_version_id BIGINT UNSIGNED NULL,
                deployment_id BIGINT UNSIGNED NULL,
                service_set_id BIGINT UNSIGNED NULL,
                bundle_id BIGINT UNSIGNED NULL,
                actor_user_id INT NULL,
                active TINYINT(1) NOT NULL DEFAULT 1,
                activated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                INDEX idx_release_channel_versions_channel_active (channel_id, active),
                INDEX idx_release_channel_versions_frontend (frontend_version_id),
                INDEX idx_release_channel_versions_api (api_version_id),
                INDEX idx_release_channel_versions_deployment (deployment_id),
                INDEX idx_release_channel_versions_service_set (service_set_id),
                INDEX idx_release_channel_versions_bundle (bundle_id)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",

            "CREATE TABLE IF NOT EXISTS release_assignments (
                id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                subject_type VARCHAR(16) NOT NULL,
                subject_id VARCHAR(64) NOT NULL,
                channel_id BIGINT UNSIGNED NOT NULL,
                reason VARCHAR(255) NULL,
                expires_at DATETIME NULL,
                actor_user_id INT NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                deleted_at DATETIME NULL,
                INDEX idx_release_assignments_subject (subject_type, subject_id, deleted_at, expires_at),
                INDEX idx_release_assignments_channel (channel_id, deleted_at)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",

            "CREATE TABLE IF NOT EXISTS release_deployment_targets (
                id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                channel_id BIGINT UNSIGNED NOT NULL,
                app VARCHAR(16) NOT NULL,
                coolify_instance_id BIGINT UNSIGNED NULL,
                coolify_service_uuid VARCHAR(128) NULL,
                repository VARCHAR(255) NOT NULL,
                branch VARCHAR(128) NOT NULL DEFAULT 'main',
                auto_deploy TINYINT(1) NOT NULL DEFAULT 1,
                health_url VARCHAR(512) NULL,
                deploy_context_json LONGTEXT NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                deleted_at DATETIME NULL,
                INDEX idx_release_targets_channel_app (channel_id, app, deleted_at),
                INDEX idx_release_targets_repo_branch (repository, branch, auto_deploy, deleted_at),
                INDEX idx_release_targets_coolify (coolify_instance_id, coolify_service_uuid)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",

            "CREATE TABLE IF NOT EXISTS release_auto_sync_events (
                id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                channel_id BIGINT UNSIGNED NOT NULL,
                app VARCHAR(16) NOT NULL,
                repository VARCHAR(255) NOT NULL,
                branch VARCHAR(128) NOT NULL,
                commit_sha VARCHAR(64) NOT NULL,
                status VARCHAR(32) NOT NULL DEFAULT 'pending',
                source VARCHAR(64) NULL,
                workflow_url VARCHAR(512) NULL,
                gate_operation_id BIGINT UNSIGNED NULL,
                sync_operation_id BIGINT UNSIGNED NULL,
                deployment_id BIGINT UNSIGNED NULL,
                error_message TEXT NULL,
                metadata_json LONGTEXT NULL,
                received_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                gate_passed_at DATETIME NULL,
                synced_at DATETIME NULL,
                promoted_at DATETIME NULL,
                failed_at DATETIME NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                UNIQUE KEY uq_release_auto_sync_event (channel_id, app, repository, branch, commit_sha),
                INDEX idx_release_auto_sync_channel_status (channel_id, status, updated_at),
                INDEX idx_release_auto_sync_gate (gate_operation_id),
                INDEX idx_release_auto_sync_sync (sync_operation_id),
                INDEX idx_release_auto_sync_deployment (deployment_id)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",

            "CREATE TABLE IF NOT EXISTS release_service_sets (
                id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                channel_id BIGINT UNSIGNED NULL,
                name VARCHAR(128) NOT NULL,
                slug VARCHAR(64) NOT NULL,
                mode VARCHAR(32) NOT NULL DEFAULT 'attach_existing',
                source_service_set_id BIGINT UNSIGNED NULL,
                frontend_target_id BIGINT UNSIGNED NULL,
                api_target_id BIGINT UNSIGNED NULL,
                database_coolify_target_id BIGINT UNSIGNED NULL,
                redis_coolify_target_id BIGINT UNSIGNED NULL,
                minio_coolify_target_id BIGINT UNSIGNED NULL,
                status VARCHAR(32) NOT NULL DEFAULT 'needs_configuration',
                health_json LONGTEXT NULL,
                metadata_json LONGTEXT NULL,
                actor_user_id INT NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                deleted_at DATETIME NULL,
                UNIQUE KEY uq_release_service_sets_slug (slug),
                INDEX idx_release_service_sets_channel (channel_id, deleted_at),
                INDEX idx_release_service_sets_source (source_service_set_id),
                INDEX idx_release_service_sets_status (status, deleted_at),
                INDEX idx_release_service_sets_frontend_target (frontend_target_id),
                INDEX idx_release_service_sets_api_target (api_target_id),
                INDEX idx_release_service_sets_database_target (database_coolify_target_id),
                INDEX idx_release_service_sets_redis_target (redis_coolify_target_id),
                INDEX idx_release_service_sets_minio_target (minio_coolify_target_id)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",

            "CREATE TABLE IF NOT EXISTS release_deployments (
                id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                channel_id BIGINT UNSIGNED NOT NULL,
                target_id BIGINT UNSIGNED NULL,
                version_id BIGINT UNSIGNED NULL,
                service_set_id BIGINT UNSIGNED NULL,
                bundle_id BIGINT UNSIGNED NULL,
                deployment_kind VARCHAR(32) NOT NULL DEFAULT 'single_app',
                app VARCHAR(16) NOT NULL,
                active_channel_app_key VARCHAR(96) NULL,
                provider VARCHAR(32) NOT NULL DEFAULT 'coolify',
                repository VARCHAR(255) NULL,
                branch VARCHAR(128) NULL,
                commit_sha VARCHAR(64) NULL,
                status VARCHAR(32) NOT NULL DEFAULT 'queued',
                provider_operation_id VARCHAR(128) NULL,
                deployment_url VARCHAR(512) NULL,
                actor_user_id INT NULL,
                requested_payload_json LONGTEXT NULL,
                result_json LONGTEXT NULL,
                error_message TEXT NULL,
                started_at DATETIME NULL,
                completed_at DATETIME NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                INDEX idx_release_deployments_channel_status (channel_id, status),
                INDEX idx_release_deployments_target (target_id),
                INDEX idx_release_deployments_version (version_id),
                INDEX idx_release_deployments_service_set (service_set_id),
                INDEX idx_release_deployments_bundle (bundle_id),
                INDEX idx_release_deployments_kind (deployment_kind),
                INDEX idx_release_deployments_commit (commit_sha)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",

            "CREATE TABLE IF NOT EXISTS release_bundles (
                id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                channel_id BIGINT UNSIGNED NOT NULL,
                service_set_id BIGINT UNSIGNED NOT NULL,
                version_label VARCHAR(128) NULL,
                frontend_version_id BIGINT UNSIGNED NULL,
                api_version_id BIGINT UNSIGNED NULL,
                frontend_deployment_id BIGINT UNSIGNED NULL,
                api_deployment_id BIGINT UNSIGNED NULL,
                frontend_repository VARCHAR(255) NULL,
                frontend_branch VARCHAR(128) NULL,
                frontend_commit_sha VARCHAR(64) NULL,
                api_repository VARCHAR(255) NULL,
                api_branch VARCHAR(128) NULL,
                api_commit_sha VARCHAR(64) NULL,
                status VARCHAR(32) NOT NULL DEFAULT 'draft',
                deployment_result_json LONGTEXT NULL,
                metadata_json LONGTEXT NULL,
                actor_user_id INT NULL,
                deployed_at DATETIME NULL,
                promoted_at DATETIME NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                deleted_at DATETIME NULL,
                INDEX idx_release_bundles_channel_status (channel_id, status, deleted_at),
                INDEX idx_release_bundles_service_set (service_set_id, status, deleted_at),
                INDEX idx_release_bundles_frontend_version (frontend_version_id),
                INDEX idx_release_bundles_api_version (api_version_id),
                INDEX idx_release_bundles_frontend_deployment (frontend_deployment_id),
                INDEX idx_release_bundles_api_deployment (api_deployment_id)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",

            "CREATE TABLE IF NOT EXISTS release_replay_targets (
                id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                target_type VARCHAR(16) NOT NULL,
                target_id VARCHAR(64) NULL,
                channel_id BIGINT UNSIGNED NULL,
                capture_level VARCHAR(32) NOT NULL DEFAULT 'full_redacted',
                enabled TINYINT(1) NOT NULL DEFAULT 1,
                expires_at DATETIME NULL,
                actor_user_id INT NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                deleted_at DATETIME NULL,
                INDEX idx_release_replay_target (target_type, target_id, enabled, deleted_at, expires_at),
                INDEX idx_release_replay_channel (channel_id, enabled, deleted_at, expires_at)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",

            "CREATE TABLE IF NOT EXISTS release_timeline_sessions (
                id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                trace_id VARCHAR(64) NOT NULL,
                session_hash VARCHAR(64) NULL,
                principal_type VARCHAR(16) NULL,
                principal_id VARCHAR(64) NULL,
                customer_number INT NULL,
                channel_id BIGINT UNSIGNED NULL,
                channel_slug VARCHAR(64) NULL,
                frontend_version_id BIGINT UNSIGNED NULL,
                api_version_id BIGINT UNSIGNED NULL,
                device_type VARCHAR(16) NULL,
                browser_name VARCHAR(64) NULL,
                browser_version VARCHAR(64) NULL,
                os_name VARCHAR(64) NULL,
                os_version VARCHAR(64) NULL,
                viewport_width INT NULL,
                viewport_height INT NULL,
                device_pixel_ratio DECIMAL(6,3) NULL,
                frontend_version_label VARCHAR(128) NULL,
                frontend_commit_sha VARCHAR(128) NULL,
                api_version_label VARCHAR(128) NULL,
                api_commit_sha VARCHAR(128) NULL,
                last_route_path VARCHAR(255) NULL,
                user_agent VARCHAR(512) NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                last_seen_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                UNIQUE KEY uq_release_timeline_trace (trace_id),
                INDEX idx_release_timeline_principal (principal_type, principal_id),
                INDEX idx_release_timeline_channel (channel_id, channel_slug),
                INDEX idx_release_timeline_device (device_type),
                INDEX idx_release_timeline_release (frontend_version_label, api_version_label),
                INDEX idx_release_timeline_customer (customer_number),
                INDEX idx_release_timeline_last_seen (last_seen_at)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",

            "CREATE TABLE IF NOT EXISTS release_timeline_events (
                id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                timeline_session_id BIGINT UNSIGNED NULL,
                trace_id VARCHAR(64) NOT NULL,
                event_type VARCHAR(64) NOT NULL,
                severity VARCHAR(16) NOT NULL DEFAULT 'info',
                module_key VARCHAR(64) NULL,
                route_path VARCHAR(255) NULL,
                component VARCHAR(255) NULL,
                request_id VARCHAR(128) NULL,
                occurred_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                payload_json LONGTEXT NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                INDEX idx_release_timeline_events_session (timeline_session_id, occurred_at),
                INDEX idx_release_timeline_events_trace (trace_id, occurred_at),
                INDEX idx_release_timeline_events_type (event_type, severity),
                INDEX idx_release_timeline_events_module (module_key, occurred_at)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",

            "CREATE TABLE IF NOT EXISTS release_module_health_snapshots (
                id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                channel_id BIGINT UNSIGNED NULL,
                module_key VARCHAR(64) NOT NULL,
                status VARCHAR(32) NOT NULL,
                reason VARCHAR(512) NULL,
                payload_json LONGTEXT NULL,
                checked_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                INDEX idx_release_module_health_module (module_key, checked_at),
                INDEX idx_release_module_health_channel (channel_id, module_key, checked_at)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",

            "CREATE TABLE IF NOT EXISTS release_operation_runs (
                id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                operation_type VARCHAR(64) NOT NULL,
                subject_type VARCHAR(64) NULL,
                subject_id VARCHAR(128) NULL,
                channel_id BIGINT UNSIGNED NULL,
                app VARCHAR(16) NULL,
                status VARCHAR(32) NOT NULL DEFAULT 'queued',
                title VARCHAR(255) NULL,
                summary TEXT NULL,
                solution_hint TEXT NULL,
                actor_user_id INT NULL,
                context_json LONGTEXT NULL,
                started_at DATETIME NULL,
                completed_at DATETIME NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                INDEX idx_release_operation_runs_channel (channel_id, created_at),
                INDEX idx_release_operation_runs_subject (subject_type, subject_id, created_at),
                INDEX idx_release_operation_runs_type_status (operation_type, status),
                INDEX idx_release_operation_runs_created (created_at)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",

            "CREATE TABLE IF NOT EXISTS release_operation_steps (
                id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                operation_run_id BIGINT UNSIGNED NOT NULL,
                step_key VARCHAR(64) NOT NULL,
                label VARCHAR(255) NOT NULL,
                status VARCHAR(32) NOT NULL DEFAULT 'queued',
                message TEXT NULL,
                diagnostic TEXT NULL,
                solution_hint TEXT NULL,
                context_json LONGTEXT NULL,
                started_at DATETIME NULL,
                completed_at DATETIME NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                INDEX idx_release_operation_steps_run (operation_run_id, id),
                INDEX idx_release_operation_steps_status (status)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",

            "CREATE TABLE IF NOT EXISTS release_audit_logs (
                id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                channel_id BIGINT UNSIGNED NULL,
                deployment_id BIGINT UNSIGNED NULL,
                action VARCHAR(64) NOT NULL,
                actor_user_id INT NULL,
                severity VARCHAR(16) NOT NULL DEFAULT 'info',
                context_json LONGTEXT NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                INDEX idx_release_audit_channel (channel_id, created_at),
                INDEX idx_release_audit_deployment (deployment_id),
                INDEX idx_release_audit_action (action)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
        ];

        foreach ($queries as $sql) {
            $db->query($sql);
        }

        self::ensureColumn('release_channel_versions', 'service_set_id', 'BIGINT UNSIGNED NULL AFTER deployment_id');
        self::ensureColumn('release_channel_versions', 'bundle_id', 'BIGINT UNSIGNED NULL AFTER service_set_id');
        self::ensureColumn('release_deployments', 'service_set_id', 'BIGINT UNSIGNED NULL AFTER version_id');
        self::ensureColumn('release_deployments', 'bundle_id', 'BIGINT UNSIGNED NULL AFTER service_set_id');
        self::ensureColumn('release_deployments', 'deployment_kind', "VARCHAR(32) NOT NULL DEFAULT 'single_app' AFTER bundle_id");
        self::ensureColumn('release_deployments', 'active_channel_app_key', 'VARCHAR(96) NULL AFTER app');
        self::ensureColumn('release_timeline_sessions', 'device_type', 'VARCHAR(16) NULL AFTER api_version_id');
        self::ensureColumn('release_timeline_sessions', 'browser_name', 'VARCHAR(64) NULL AFTER device_type');
        self::ensureColumn('release_timeline_sessions', 'browser_version', 'VARCHAR(64) NULL AFTER browser_name');
        self::ensureColumn('release_timeline_sessions', 'os_name', 'VARCHAR(64) NULL AFTER browser_version');
        self::ensureColumn('release_timeline_sessions', 'os_version', 'VARCHAR(64) NULL AFTER os_name');
        self::ensureColumn('release_timeline_sessions', 'viewport_width', 'INT NULL AFTER os_version');
        self::ensureColumn('release_timeline_sessions', 'viewport_height', 'INT NULL AFTER viewport_width');
        self::ensureColumn('release_timeline_sessions', 'device_pixel_ratio', 'DECIMAL(6,3) NULL AFTER viewport_height');
        self::ensureColumn('release_timeline_sessions', 'frontend_version_label', 'VARCHAR(128) NULL AFTER device_pixel_ratio');
        self::ensureColumn('release_timeline_sessions', 'frontend_commit_sha', 'VARCHAR(128) NULL AFTER frontend_version_label');
        self::ensureColumn('release_timeline_sessions', 'api_version_label', 'VARCHAR(128) NULL AFTER frontend_commit_sha');
        self::ensureColumn('release_timeline_sessions', 'api_commit_sha', 'VARCHAR(128) NULL AFTER api_version_label');
        self::ensureColumn('release_timeline_sessions', 'last_route_path', 'VARCHAR(255) NULL AFTER api_commit_sha');
        self::ensureIndex('release_timeline_sessions', 'idx_release_timeline_device', 'device_type');
        self::ensureIndex('release_timeline_sessions', 'idx_release_timeline_release', 'frontend_version_label, api_version_label');
        self::ensureUniqueIndex('release_deployments', 'uniq_release_deployments_active_channel_app', 'active_channel_app_key');

        self::ensureModuleConfigDefault('ReleaseManager', 'enabled', 'true', 'bool');
        self::ensureModuleConfigDefault('ReleaseManager', 'github_webhook_secret', '', 'string');
        self::ensureModuleConfigDefault('ReleaseManager', 'release_gate_token', '', 'string');
        self::ensureModuleConfigDefault('ReleaseManager', 'release_gate_required_for_promotion', 'true', 'bool');
        self::ensureModuleConfigDefault('ReleaseManager', 'github_token', '', 'string');
        self::ensureModuleConfigDefault('ReleaseManager', 'github_api_url', 'https://api.github.com', 'string');
        self::ensureModuleConfigDefault('ReleaseManager', 'default_retention_days', '14', 'int');

        self::ensureDefaultChannels();

        self::$initialized = true;
        self::$tablesExist = true;
    }

    public static function tablesExist(): bool
    {
        if (self::$tablesExist !== null) {
            return self::$tablesExist;
        }

        global $db;

        foreach ([
            'release_channels',
            'release_versions',
            'release_channel_versions',
            'release_assignments',
            'release_auto_sync_events',
            'release_service_sets',
            'release_deployments',
            'release_bundles',
            'release_operation_runs',
            'release_operation_steps',
            'release_timeline_sessions',
            'release_timeline_events',
        ] as $table) {
            $tableSql = $db->escape_string($table);
            $result = $db->query("SHOW TABLES LIKE '$tableSql'");
            if ($result === false || $result->num_rows === 0) {
                self::$tablesExist = false;
                return false;
            }
        }

        self::$tablesExist = true;
        return true;
    }

    private static function ensureDefaultChannels(): void
    {
        global $db;

        $channels = [
            ['stable', 'Stable', 'Default production channel.', 1, 1, 'metadata'],
            ['canary', 'Canary', 'Earliest production validation channel.', 1, 0, 'metadata'],
            ['beta', 'Beta', 'Broader pre-stable rollout channel.', 1, 0, 'metadata'],
            ['internal', 'Internal', 'Internal staff and superuser validation channel.', 1, 0, 'metadata'],
        ];

        foreach ($channels as [$slug, $name, $description, $enabled, $default, $captureLevel]) {
            $db->query(sprintf(
                "INSERT IGNORE INTO release_channels (slug, name, description, enabled, default_channel, capture_level)
                 VALUES ('%s', '%s', '%s', %d, %d, '%s')",
                $db->escape_string($slug),
                $db->escape_string($name),
                $db->escape_string($description),
                (int)$enabled,
                (int)$default,
                $db->escape_string($captureLevel)
            ));
        }
    }

    private static function ensureModuleConfigDefault(string $module, string $variable, string $value, string $type): void
    {
        global $db;

        $moduleSql = $db->escape_string($module);
        $variableSql = $db->escape_string($variable);
        $result = $db->query("SELECT value FROM module_config WHERE module = '$moduleSql' AND variable = '$variableSql' LIMIT 1");
        if ($result !== false && $result->num_rows > 0) {
            return;
        }

        $valueSql = $db->escape_string($value);
        $typeSql = $db->escape_string($type);
        $db->query("INSERT INTO module_config (module, variable, value, type) VALUES ('$moduleSql', '$variableSql', '$valueSql', '$typeSql')");
    }

    private static function ensureColumn(string $table, string $column, string $definition): void
    {
        global $db;

        $table = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
        $column = preg_replace('/[^a-zA-Z0-9_]/', '', $column);
        if ($table === '' || $column === '') {
            return;
        }

        $result = $db->query("SHOW COLUMNS FROM `$table` LIKE '$column'");
        if ($result !== false && $result->num_rows > 0) {
            return;
        }

        $db->query("ALTER TABLE `$table` ADD COLUMN `$column` $definition");
    }

    private static function ensureIndex(string $table, string $index, string $columns): void
    {
        global $db;

        $table = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
        $index = preg_replace('/[^a-zA-Z0-9_]/', '', $index);
        if ($table === '' || $index === '') {
            return;
        }

        $indexSql = $db->escape_string($index);
        $result = $db->query("SHOW INDEX FROM `$table` WHERE Key_name = '$indexSql'");
        if ($result !== false && $result->num_rows > 0) {
            return;
        }

        $db->query("ALTER TABLE `$table` ADD INDEX `$index` ($columns)");
    }

    private static function ensureUniqueIndex(string $table, string $index, string $columns): void
    {
        global $db;

        $table = preg_replace('/[^a-zA-Z0-9_]/', '', $table);
        $index = preg_replace('/[^a-zA-Z0-9_]/', '', $index);
        if ($table === '' || $index === '') {
            return;
        }

        $indexSql = $db->escape_string($index);
        $result = $db->query("SHOW INDEX FROM `$table` WHERE Key_name = '$indexSql'");
        if ($result !== false && $result->num_rows > 0) {
            return;
        }

        $db->query("ALTER TABLE `$table` ADD UNIQUE KEY `$index` ($columns)");
    }
}
