/**
 * Platform Services Manager
 * Orchestrates platform services (MongoDB, SmartStorage, and others) and their resources
 */
import { MongoDBProvider } from './providers/mongodb.js';
import { SmartStorageProvider } from './providers/smartstorage.js';
import { CoreTrafficProvider } from './providers/coretraffic.js';
import { ClickHouseProvider } from './providers/clickhouse.js';
import { MariaDBProvider } from './providers/mariadb.js';
import { ValkeyProvider } from './providers/valkey.js';
import * as plugins from '../../plugins.js';
import { logger } from '../../logging.js';
import { getErrorMessage } from '../../utils/error.js';
import { credentialEncryption } from '../encryption.js';
export class PlatformServicesManager {
    oneboxRef;
    providers = new Map();
    constructor(oneboxRef) {
        this.oneboxRef = oneboxRef;
    }
    /**
     * Initialize the platform services manager
     */
    async init() {
        // Initialize encryption
        await credentialEncryption.init();
        // Register providers
        this.registerProvider(new MongoDBProvider(this.oneboxRef));
        this.registerProvider(new SmartStorageProvider(this.oneboxRef));
        this.registerProvider(new CoreTrafficProvider(this.oneboxRef));
        this.registerProvider(new ClickHouseProvider(this.oneboxRef));
        this.registerProvider(new MariaDBProvider(this.oneboxRef));
        this.registerProvider(new ValkeyProvider(this.oneboxRef));
        await this.reconcileLegacyServiceSecrets();
        logger.info(`Platform services manager initialized with ${this.providers.size} providers`);
    }
    async reconcileLegacyServiceSecrets() {
        for (const service of await this.oneboxRef.database.getAllServices()) {
            const injectedEnvVars = await this.getInjectedEnvVars(service.id);
            const confidentialInjectedValues = Object.entries(injectedEnvVars)
                .filter(([envKeyArg, valueArg]) => (valueArg.length > 0
                && /(PASSWORD|SECRET|TOKEN|KEY|CREDENTIAL)/.test(envKeyArg)))
                .map(([, valueArg]) => valueArg);
            if (Object.keys(injectedEnvVars).length === 0)
                continue;
            const publicEnvVars = { ...(service.envVars || {}) };
            const serviceSecrets = await this.oneboxRef.database.serviceSecrets.get(service.id);
            let changed = false;
            for (const [envKey, envValue] of Object.entries(publicEnvVars)) {
                const isCanonicalExactValue = injectedEnvVars[envKey] === envValue;
                const containsConfidentialValue = confidentialInjectedValues.some((secretValueArg) => {
                    const escapedSecretValue = secretValueArg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                    return new RegExp(`(?<![A-Za-z0-9_])${escapedSecretValue}(?![A-Za-z0-9_])`).test(envValue);
                });
                if (!isCanonicalExactValue && !containsConfidentialValue)
                    continue;
                if (serviceSecrets[envKey] !== undefined && serviceSecrets[envKey] !== envValue) {
                    throw new Error(`Legacy platform secret conflicts with encrypted key ${envKey} for ${service.name}`);
                }
                serviceSecrets[envKey] = envValue;
                delete publicEnvVars[envKey];
                changed = true;
            }
            if (!changed)
                continue;
            await this.oneboxRef.database.serviceSecrets.set(service.id, serviceSecrets);
            await this.oneboxRef.database.updateService(service.id, { envVars: publicEnvVars });
            logger.info(`Moved legacy platform credentials into the encrypted bundle for ${service.name}`);
        }
    }
    /**
     * Register a platform service provider
     */
    registerProvider(provider) {
        this.providers.set(provider.type, provider);
        logger.debug(`Registered platform service provider: ${provider.displayName}`);
    }
    /**
     * Get a provider by type
     */
    getProvider(type) {
        return this.providers.get(type);
    }
    /**
     * Get all registered providers
     */
    getAllProviders() {
        return Array.from(this.providers.values());
    }
    /**
     * Ensure a platform service is running, deploying it if necessary
     */
    async ensureRunning(type) {
        const provider = this.providers.get(type);
        if (!provider) {
            throw new Error(`Unknown platform service type: ${type}`);
        }
        // Check if platform service exists in database
        let platformService = await this.oneboxRef.database.getPlatformServiceByType(type);
        if (!platformService) {
            // Create platform service record
            logger.info(`Creating new ${provider.displayName} platform service...`);
            const config = await provider.getDefaultConfig();
            platformService = await this.oneboxRef.database.createPlatformService({
                name: `onebox-${type}`,
                type,
                status: 'stopped',
                config,
                createdAt: Date.now(),
                updatedAt: Date.now(),
            });
        }
        // Check if already running
        let needsDeploy = platformService.status !== 'running';
        if (platformService.status === 'running') {
            // Verify it's actually healthy
            const isHealthy = await provider.healthCheck();
            if (isHealthy) {
                logger.debug(`${provider.displayName} is already running and healthy`);
                return platformService;
            }
            logger.warn(`${provider.displayName} reports running but health check failed, will redeploy...`);
            // Mark status as needing redeploy - container may have been recreated with different credentials
            await this.oneboxRef.database.updatePlatformService(platformService.id, {
                status: 'stopped',
            });
            needsDeploy = true;
        }
        // Deploy if needed
        if (needsDeploy) {
            logger.info(`Starting ${provider.displayName} platform service...`);
            try {
                await this.oneboxRef.database.updatePlatformService(platformService.id, {
                    status: 'starting',
                });
                const containerId = await provider.deployContainer();
                // Wait for health check to pass
                const healthy = await this.waitForHealthy(type, 60000); // 60 second timeout
                if (healthy) {
                    await this.oneboxRef.database.updatePlatformService(platformService.id, {
                        status: 'running',
                        containerId,
                    });
                    logger.success(`${provider.displayName} platform service is now running`);
                }
                else {
                    await this.oneboxRef.database.updatePlatformService(platformService.id, {
                        status: 'failed',
                    });
                    throw new Error(`${provider.displayName} failed to start within timeout`);
                }
                // Refresh platform service from database
                platformService = (await this.oneboxRef.database.getPlatformServiceByType(type));
            }
            catch (error) {
                logger.error(`Failed to start ${provider.displayName}: ${getErrorMessage(error)}`);
                await this.oneboxRef.database.updatePlatformService(platformService.id, {
                    status: 'failed',
                });
                throw error;
            }
        }
        return platformService;
    }
    /**
     * Wait for a platform service to become healthy
     */
    async waitForHealthy(type, timeoutMs) {
        const provider = this.providers.get(type);
        if (!provider) {
            logger.warn(`waitForHealthy: no provider for type ${type}`);
            return false;
        }
        logger.info(`waitForHealthy: starting health check loop for ${type} (timeout: ${timeoutMs}ms)`);
        const startTime = Date.now();
        const checkInterval = 2000; // Check every 2 seconds
        let checkCount = 0;
        while (Date.now() - startTime < timeoutMs) {
            checkCount++;
            logger.info(`waitForHealthy: health check attempt #${checkCount} for ${type}`);
            const isHealthy = await provider.healthCheck();
            if (isHealthy) {
                logger.info(`waitForHealthy: ${type} became healthy after ${checkCount} attempts`);
                return true;
            }
            await new Promise((resolve) => setTimeout(resolve, checkInterval));
        }
        logger.warn(`waitForHealthy: ${type} did not become healthy after ${checkCount} attempts (${timeoutMs}ms)`);
        return false;
    }
    /**
     * Stop a platform service
     */
    async stopPlatformService(type) {
        const provider = this.providers.get(type);
        if (!provider) {
            throw new Error(`Unknown platform service type: ${type}`);
        }
        const platformService = await this.oneboxRef.database.getPlatformServiceByType(type);
        if (!platformService) {
            logger.warn(`Platform service ${type} not found`);
            return;
        }
        if (!platformService.containerId) {
            logger.warn(`Platform service ${type} has no container ID`);
            return;
        }
        logger.info(`Stopping ${provider.displayName} platform service...`);
        await this.oneboxRef.database.updatePlatformService(platformService.id, {
            status: 'stopping',
        });
        try {
            await provider.stopContainer(platformService.containerId);
            await this.oneboxRef.database.updatePlatformService(platformService.id, {
                status: 'stopped',
                containerId: undefined,
            });
            logger.success(`${provider.displayName} platform service stopped`);
        }
        catch (error) {
            logger.error(`Failed to stop ${provider.displayName}: ${getErrorMessage(error)}`);
            await this.oneboxRef.database.updatePlatformService(platformService.id, {
                status: 'failed',
            });
            throw error;
        }
    }
    /**
     * Provision platform resources for a user service based on its requirements
     */
    async persistProvisionedResource(serviceArg, platformServiceArg, providerArg, resultArg) {
        let encryptedCreds;
        try {
            encryptedCreds = await credentialEncryption.encrypt(resultArg.credentials);
            await this.oneboxRef.database.createPlatformResource({
                platformServiceId: platformServiceArg.id,
                serviceId: serviceArg.id,
                resourceType: resultArg.type,
                resourceName: resultArg.name,
                credentialsEncrypted: encryptedCreds,
                createdAt: Date.now(),
            });
        }
        catch (error) {
            const failures = [`ownership persistence: ${getErrorMessage(error)}`];
            try {
                await providerArg.deprovisionResource({
                    platformServiceId: platformServiceArg.id,
                    serviceId: serviceArg.id,
                    resourceType: resultArg.type,
                    resourceName: resultArg.name,
                    credentialsEncrypted: '',
                    createdAt: Date.now(),
                }, resultArg.credentials);
            }
            catch (cleanupError) {
                failures.push(`compensating deprovision: ${getErrorMessage(cleanupError)}`);
                try {
                    encryptedCreds ||= await credentialEncryption.encrypt(resultArg.credentials);
                    await this.oneboxRef.database.persistPlatformResourceRecovery({
                        platformServiceId: platformServiceArg.id,
                        serviceId: serviceArg.id,
                        resourceType: resultArg.type,
                        resourceName: resultArg.name,
                        credentialsEncrypted: encryptedCreds,
                        createdAt: Date.now(),
                    });
                }
                catch (recoveryError) {
                    failures.push(`recovery ownership persistence: ${getErrorMessage(recoveryError)}`);
                }
            }
            throw new AggregateError(failures, `Failed to persist ownership for ${resultArg.type} ${resultArg.name}`);
        }
    }
    async provisionForService(service) {
        const requirements = service.platformRequirements;
        if (!requirements) {
            return {};
        }
        const allEnvVars = {};
        // Provision MongoDB if requested
        if (requirements.mongodb) {
            logger.info(`Provisioning MongoDB for service '${service.name}'...`);
            // Ensure MongoDB is running
            const mongoService = await this.ensureRunning('mongodb');
            const provider = this.providers.get('mongodb');
            // Provision database
            const result = await provider.provisionResource(service);
            await this.persistProvisionedResource(service, mongoService, provider, result);
            // Merge env vars
            Object.assign(allEnvVars, result.envVars);
            logger.success(`MongoDB provisioned for service '${service.name}'`);
        }
        // Provision S3 object storage if requested
        if (requirements.s3) {
            logger.info(`Provisioning S3 storage for service '${service.name}'...`);
            // Ensure smartstorage (corestore) is running
            const storageService = await this.ensureRunning('smartstorage');
            const provider = this.providers.get('smartstorage');
            // Provision bucket with scoped tenant credentials
            const result = await provider.provisionResource(service);
            await this.persistProvisionedResource(service, storageService, provider, result);
            // Merge env vars
            Object.assign(allEnvVars, result.envVars);
            logger.success(`S3 storage provisioned for service '${service.name}'`);
        }
        // Provision ClickHouse if requested
        if (requirements.clickhouse) {
            logger.info(`Provisioning ClickHouse for service '${service.name}'...`);
            // Ensure ClickHouse is running
            const clickhouseService = await this.ensureRunning('clickhouse');
            const provider = this.providers.get('clickhouse');
            // Provision database
            const result = await provider.provisionResource(service);
            await this.persistProvisionedResource(service, clickhouseService, provider, result);
            // Merge env vars
            Object.assign(allEnvVars, result.envVars);
            logger.success(`ClickHouse provisioned for service '${service.name}'`);
        }
        // Provision Valkey if requested
        if (requirements.valkey) {
            logger.info(`Provisioning Valkey for service '${service.name}'...`);
            // Ensure Valkey is running
            const valkeyService = await this.ensureRunning('valkey');
            const provider = this.providers.get('valkey');
            // Provision cache resource
            const result = await provider.provisionResource(service);
            await this.persistProvisionedResource(service, valkeyService, provider, result);
            // Merge env vars
            Object.assign(allEnvVars, result.envVars);
            logger.success(`Valkey provisioned for service '${service.name}'`);
        }
        // Provision MariaDB if requested
        if (requirements.mariadb) {
            logger.info(`Provisioning MariaDB for service '${service.name}'...`);
            // Ensure MariaDB is running
            const mariadbService = await this.ensureRunning('mariadb');
            const provider = this.providers.get('mariadb');
            // Provision database
            const result = await provider.provisionResource(service);
            await this.persistProvisionedResource(service, mariadbService, provider, result);
            // Merge env vars
            Object.assign(allEnvVars, result.envVars);
            logger.success(`MariaDB provisioned for service '${service.name}'`);
        }
        return allEnvVars;
    }
    /**
     * Cleanup platform resources when a user service is deleted
     */
    async cleanupForService(serviceId) {
        const resources = await this.oneboxRef.database.getPlatformResourcesByService(serviceId);
        const failures = [];
        for (const resource of resources) {
            try {
                const platformService = await this.oneboxRef.database.getPlatformServiceById(resource.platformServiceId);
                if (!platformService) {
                    const errorMessage = `Platform service ${resource.platformServiceId} not found for resource ${resource.id}`;
                    logger.error(errorMessage);
                    failures.push(errorMessage);
                    continue;
                }
                const provider = this.providers.get(platformService.type);
                if (!provider) {
                    const errorMessage = `Provider not found for type ${platformService.type}`;
                    logger.error(errorMessage);
                    failures.push(errorMessage);
                    continue;
                }
                // Decrypt credentials
                const credentials = await credentialEncryption.decrypt(resource.credentialsEncrypted);
                // Deprovision the resource
                logger.info(`Cleaning up ${resource.resourceType} '${resource.resourceName}'...`);
                await provider.deprovisionResource(resource, credentials);
                // Delete resource record
                await this.oneboxRef.database.deletePlatformResource(resource.id);
                logger.success(`Cleaned up ${resource.resourceType} '${resource.resourceName}'`);
            }
            catch (error) {
                const errorMessage = getErrorMessage(error);
                logger.error(`Failed to cleanup resource ${resource.id}: ${errorMessage}`);
                failures.push(`${resource.resourceType} ${resource.resourceName}: ${errorMessage}`);
            }
        }
        if (failures.length > 0) {
            throw new Error(`Failed to cleanup ${failures.length} platform resource(s): ${failures.join('; ')}`);
        }
    }
    /**
     * Get injected environment variables for a service
     */
    async getInjectedEnvVars(serviceId) {
        const resources = await this.oneboxRef.database.getPlatformResourcesByService(serviceId);
        const allEnvVars = {};
        for (const resource of resources) {
            const platformService = await this.oneboxRef.database.getPlatformServiceById(resource.platformServiceId);
            if (!platformService)
                continue;
            const provider = this.providers.get(platformService.type);
            if (!provider)
                continue;
            const credentials = await credentialEncryption.decrypt(resource.credentialsEncrypted);
            const mappings = provider.getEnvVarMappings();
            for (const mapping of mappings) {
                if (credentials[mapping.credentialPath]) {
                    allEnvVars[mapping.envVar] = credentials[mapping.credentialPath];
                }
            }
        }
        return allEnvVars;
    }
    /**
     * Get all platform services with their status
     */
    async getAllPlatformServices() {
        return await this.oneboxRef.database.getAllPlatformServices();
    }
    /**
     * One-time migration of every MinIO-backed bucket resource onto
     * smartstorage (corestore): provisions a scoped bucket per service,
     * stream-copies all objects, swaps the service env vars (container
     * recreate), and replaces the resource record. MinIO itself is left
     * running as rollback until it is decommissioned explicitly.
     */
    async migrateS3ResourcesToSmartstorage() {
        const migrated = [];
        const errors = [];
        const minioPlatformService = await this.oneboxRef.database.getPlatformServiceByType('minio');
        if (!minioPlatformService) {
            return { migrated, errors: ['no minio platform service present — nothing to migrate'] };
        }
        const smartStorageService = await this.ensureRunning('smartstorage');
        const smartStorageProvider = this.providers.get('smartstorage');
        const resources = await this.oneboxRef.database.getPlatformResourcesByPlatformService(minioPlatformService.id);
        for (const resource of resources) {
            if (resource.resourceType !== 'bucket')
                continue;
            try {
                const services = await this.oneboxRef.database.getAllServices();
                const service = services.find((serviceArg) => serviceArg.id === resource.serviceId);
                if (!service) {
                    errors.push(`resource ${resource.resourceName}: owning service ${resource.serviceId} not found`);
                    continue;
                }
                logger.info(`Migrating S3 resource '${resource.resourceName}' of service '${service.name}' to smartstorage...`);
                const provisioned = await smartStorageProvider.provisionResource(service);
                const oldCredentials = await credentialEncryption.decrypt(resource.credentialsEncrypted);
                const objectsCopied = await this.copyS3Bucket(oldCredentials, resource.platformServiceId, provisioned.credentials, smartStorageService.id);
                // Persist the new resource record before touching the old one
                const encryptedCreds = await credentialEncryption.encrypt(provisioned.credentials);
                await this.oneboxRef.database.createPlatformResource({
                    platformServiceId: smartStorageService.id,
                    serviceId: service.id,
                    resourceType: provisioned.type,
                    resourceName: provisioned.name,
                    credentialsEncrypted: encryptedCreds,
                    createdAt: Date.now(),
                });
                await this.oneboxRef.database.deletePlatformResource(resource.id);
                // Swap env vars on the service and recreate its container. Apply the
                // provider mapping AND rewrite any env var still holding an old
                // credential value — apps installed via appstore templates read
                // arbitrary env names (e.g. S3_ACCESSKEY) that no mapping covers.
                const newPublicEnvVars = { ...(service.envVars || {}) };
                const newSecretEnvVars = {
                    ...(await this.oneboxRef.database.serviceSecrets.get(service.id)),
                    ...provisioned.envVars,
                };
                const valueRewrites = new Map([
                    [oldCredentials.endpoint || '', provisioned.credentials.endpoint],
                    [(oldCredentials.endpoint || '').replace(/^https?:\/\//, ''), provisioned.credentials.endpoint.replace(/^https?:\/\//, '')],
                    [oldCredentials.bucket || '', provisioned.credentials.bucket],
                    [oldCredentials.accessKey || '', provisioned.credentials.accessKey],
                    [oldCredentials.secretKey || '', provisioned.credentials.secretKey],
                ]);
                for (const [envKey, envValue] of Object.entries(newPublicEnvVars)) {
                    const replacement = valueRewrites.get(envValue);
                    if (replacement && envValue) {
                        delete newPublicEnvVars[envKey];
                        newSecretEnvVars[envKey] = replacement;
                    }
                }
                for (const envKey of Object.keys(provisioned.envVars)) {
                    delete newPublicEnvVars[envKey];
                }
                await this.oneboxRef.services.updateService(service.name, {
                    envVars: newPublicEnvVars,
                    secretEnvVars: newSecretEnvVars,
                });
                migrated.push({
                    serviceName: service.name,
                    oldBucket: resource.resourceName,
                    newBucket: provisioned.name,
                    objectsCopied,
                });
                logger.success(`Migrated '${service.name}' to smartstorage bucket '${provisioned.name}' (${objectsCopied} objects)`);
            }
            catch (error) {
                const errorMessage = getErrorMessage(error);
                logger.error(`Failed to migrate resource ${resource.resourceName}: ${errorMessage}`);
                errors.push(`${resource.resourceName}: ${errorMessage}`);
            }
        }
        // Decommission MinIO once nothing references it anymore: stop the
        // container and drop the platform service record. The data directory is
        // intentionally left on disk as a manual rollback artifact.
        if (errors.length === 0) {
            const remaining = await this.oneboxRef.database.getPlatformResourcesByPlatformService(minioPlatformService.id);
            if (remaining.length === 0) {
                if (minioPlatformService.containerId) {
                    try {
                        await this.oneboxRef.docker.stopContainer(minioPlatformService.containerId);
                    }
                    catch (error) {
                        logger.warn(`MinIO container stop during decommission: ${getErrorMessage(error)}`);
                    }
                }
                await this.oneboxRef.database.deletePlatformService(minioPlatformService.id);
                logger.success('MinIO platform service decommissioned (data directory kept on disk)');
            }
        }
        return { migrated, errors };
    }
    /**
     * Stream-copies every object from one S3 bucket to another through the
     * host-published ports and verifies the object count afterwards.
     */
    async copyS3Bucket(sourceCredentials, sourcePlatformServiceId, targetCredentials, targetPlatformServiceId) {
        const backupManager = this.oneboxRef.backupManager;
        const sourceInfo = await backupManager.getReachableS3ConnectionInfo(sourceCredentials, sourcePlatformServiceId);
        const targetInfo = await backupManager.getReachableS3ConnectionInfo(targetCredentials, targetPlatformServiceId);
        const sourceClient = backupManager.createS3Client(sourceInfo);
        const targetClient = backupManager.createS3Client(targetInfo);
        let copied = 0;
        let continuationToken;
        do {
            const listResponse = await sourceClient.send(new plugins.awsS3.ListObjectsV2Command({
                Bucket: sourceInfo.bucket,
                ContinuationToken: continuationToken,
            }));
            for (const object of listResponse.Contents || []) {
                if (!object.Key)
                    continue;
                const getResponse = await sourceClient.send(new plugins.awsS3.GetObjectCommand({ Bucket: sourceInfo.bucket, Key: object.Key }));
                const bodyBytes = await getResponse.Body?.transformToByteArray();
                await targetClient.send(new plugins.awsS3.PutObjectCommand({
                    Bucket: targetInfo.bucket,
                    Key: object.Key,
                    Body: bodyBytes,
                    ContentType: getResponse.ContentType,
                    Metadata: getResponse.Metadata,
                }));
                copied++;
            }
            continuationToken = listResponse.IsTruncated ? listResponse.NextContinuationToken : undefined;
        } while (continuationToken);
        // Verify object counts match between source and target
        const countObjects = async (client, bucket) => {
            let count = 0;
            let token;
            do {
                const page = await client.send(new plugins.awsS3.ListObjectsV2Command({ Bucket: bucket, ContinuationToken: token }));
                count += page.KeyCount || page.Contents?.length || 0;
                token = page.IsTruncated ? page.NextContinuationToken : undefined;
            } while (token);
            return count;
        };
        const [sourceCount, targetCount] = await Promise.all([
            countObjects(sourceClient, sourceInfo.bucket),
            countObjects(targetClient, targetInfo.bucket),
        ]);
        if (targetCount < sourceCount) {
            throw new Error(`object count mismatch after copy: source ${sourceCount}, target ${targetCount}`);
        }
        return copied;
    }
    /**
     * Get resources for a specific user service
     */
    async getResourcesForService(serviceId) {
        const resources = await this.oneboxRef.database.getPlatformResourcesByService(serviceId);
        const result = [];
        for (const resource of resources) {
            const platformService = await this.oneboxRef.database.getPlatformServiceById(resource.platformServiceId);
            if (!platformService)
                continue;
            const credentials = await credentialEncryption.decrypt(resource.credentialsEncrypted);
            result.push({
                resource,
                platformService,
                credentials,
            });
        }
        return result;
    }
}
//# sourceMappingURL=data:application/json;base64, 