/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { Storage } from '@google-cloud/storage';
import { gzipSync, gunzipSync } from 'node:zlib';
import * as tar from 'tar';
import * as fse from 'fs-extra';
import { promises as fsPromises, createReadStream } from 'node:fs';
import { tmpdir } from '@google/gemini-cli-core';
import { join } from 'node:path';
import { logger } from '../utils/logger.js';
import { setTargetDir } from '../config/config.js';
import { getPersistedState } from '../types.js';
import { v4 as uuidv4 } from 'uuid';
const getTmpArchiveFilename = (taskId) => `task-${taskId}-workspace-${uuidv4()}.tar.gz`;
// Validate the taskId to prevent path traversal attacks by ensuring it only contains safe characters.
const isTaskIdValid = (taskId) => {
    // Allow only alphanumeric characters, dashes, and underscores, and ensure it's not empty.
    const validTaskIdRegex = /^[a-zA-Z0-9_-]+$/;
    return validTaskIdRegex.test(taskId);
};
export class GCSTaskStore {
    storage;
    bucketName;
    bucketInitialized;
    constructor(bucketName) {
        if (!bucketName) {
            throw new Error('GCS bucket name is required.');
        }
        this.storage = new Storage();
        this.bucketName = bucketName;
        logger.info(`GCSTaskStore initializing with bucket: ${this.bucketName}`);
        // Prerequisites: user account or service account must have storage admin IAM role
        // and the bucket name must be unique.
        this.bucketInitialized = this.initializeBucket();
    }
    async initializeBucket() {
        try {
            const [buckets] = await this.storage.getBuckets();
            const exists = buckets.some((bucket) => bucket.name === this.bucketName);
            if (!exists) {
                logger.info(`Bucket ${this.bucketName} does not exist in the list. Attempting to create...`);
                try {
                    await this.storage.createBucket(this.bucketName);
                    logger.info(`Bucket ${this.bucketName} created successfully.`);
                }
                catch (createError) {
                    logger.info(`Failed to create bucket ${this.bucketName}: ${createError}`);
                    throw new Error(`Failed to create GCS bucket ${this.bucketName}: ${createError}`);
                }
            }
            else {
                logger.info(`Bucket ${this.bucketName} exists.`);
            }
        }
        catch (error) {
            logger.info(`Error during bucket initialization for ${this.bucketName}: ${error}`);
            throw new Error(`Failed to initialize GCS bucket ${this.bucketName}: ${error}`);
        }
    }
    async ensureBucketInitialized() {
        await this.bucketInitialized;
    }
    getObjectPath(taskId, type) {
        if (!isTaskIdValid(taskId)) {
            throw new Error(`Invalid taskId: ${taskId}`);
        }
        return `tasks/${taskId}/${type}.tar.gz`;
    }
    async save(task) {
        await this.ensureBucketInitialized();
        const taskId = task.id;
        const persistedState = getPersistedState(
        // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
        task.metadata);
        if (!persistedState) {
            throw new Error(`Task ${taskId} is missing persisted state in metadata.`);
        }
        const workDir = process.cwd();
        const metadataObjectPath = this.getObjectPath(taskId, 'metadata');
        const workspaceObjectPath = this.getObjectPath(taskId, 'workspace');
        const dataToStore = task.metadata;
        try {
            const jsonString = JSON.stringify(dataToStore);
            const compressedMetadata = gzipSync(Buffer.from(jsonString));
            const metadataFile = this.storage
                .bucket(this.bucketName)
                .file(metadataObjectPath);
            await metadataFile.save(compressedMetadata, {
                contentType: 'application/gzip',
            });
            logger.info(`Task ${taskId} metadata saved to GCS: gs://${this.bucketName}/${metadataObjectPath}`);
            if (await fse.pathExists(workDir)) {
                const entries = await fsPromises.readdir(workDir);
                if (entries.length > 0) {
                    const tmpArchiveFile = join(tmpdir(), getTmpArchiveFilename(taskId));
                    try {
                        await tar.c({
                            gzip: true,
                            file: tmpArchiveFile,
                            cwd: workDir,
                            portable: true,
                        }, entries);
                        if (!(await fse.pathExists(tmpArchiveFile))) {
                            throw new Error(`tar.c command failed to create ${tmpArchiveFile}`);
                        }
                        const workspaceFile = this.storage
                            .bucket(this.bucketName)
                            .file(workspaceObjectPath);
                        const sourceStream = createReadStream(tmpArchiveFile);
                        const destStream = workspaceFile.createWriteStream({
                            contentType: 'application/gzip',
                            resumable: true,
                        });
                        await new Promise((resolve, reject) => {
                            sourceStream.on('error', (err) => {
                                logger.error(`Error in source stream for ${tmpArchiveFile}:`, err);
                                // Attempt to close destStream if source fails
                                if (!destStream.destroyed) {
                                    destStream.destroy(err);
                                }
                                reject(err);
                            });
                            destStream.on('error', (err) => {
                                logger.error(`Error in GCS dest stream for ${workspaceObjectPath}:`, err);
                                reject(err);
                            });
                            destStream.on('finish', () => {
                                logger.info(`GCS destStream finished for ${workspaceObjectPath}`);
                                resolve();
                            });
                            logger.info(`Piping ${tmpArchiveFile} to GCS object ${workspaceObjectPath}`);
                            sourceStream.pipe(destStream);
                        });
                        logger.info(`Task ${taskId} workspace saved to GCS: gs://${this.bucketName}/${workspaceObjectPath}`);
                    }
                    catch (error) {
                        logger.error(`Error during workspace save process for ${taskId}:`, error);
                        throw error;
                    }
                    finally {
                        logger.info(`Cleaning up temporary file: ${tmpArchiveFile}`);
                        try {
                            if (await fse.pathExists(tmpArchiveFile)) {
                                await fse.remove(tmpArchiveFile);
                                logger.info(`Successfully removed temporary file: ${tmpArchiveFile}`);
                            }
                            else {
                                logger.warn(`Temporary file not found for cleanup: ${tmpArchiveFile}`);
                            }
                        }
                        catch (removeError) {
                            logger.error(`Error removing temporary file ${tmpArchiveFile}:`, removeError);
                        }
                    }
                }
                else {
                    logger.info(`Workspace directory ${workDir} is empty, skipping workspace save for task ${taskId}.`);
                }
            }
            else {
                logger.info(`Workspace directory ${workDir} not found, skipping workspace save for task ${taskId}.`);
            }
        }
        catch (error) {
            logger.error(`Failed to save task ${taskId} to GCS:`, error);
            throw error;
        }
    }
    async load(taskId) {
        await this.ensureBucketInitialized();
        const metadataObjectPath = this.getObjectPath(taskId, 'metadata');
        const workspaceObjectPath = this.getObjectPath(taskId, 'workspace');
        try {
            const metadataFile = this.storage
                .bucket(this.bucketName)
                .file(metadataObjectPath);
            const [metadataExists] = await metadataFile.exists();
            if (!metadataExists) {
                logger.info(`Task ${taskId} metadata not found in GCS.`);
                return undefined;
            }
            const [compressedMetadata] = await metadataFile.download();
            const jsonData = gunzipSync(compressedMetadata).toString();
            const loadedMetadata = JSON.parse(jsonData);
            logger.info(`Task ${taskId} metadata loaded from GCS.`);
            const persistedState = getPersistedState(loadedMetadata);
            if (!persistedState) {
                throw new Error(`Loaded metadata for task ${taskId} is missing internal persisted state.`);
            }
            const agentSettings = persistedState._agentSettings;
            const workDir = setTargetDir(agentSettings);
            await fse.ensureDir(workDir);
            const workspaceFile = this.storage
                .bucket(this.bucketName)
                .file(workspaceObjectPath);
            const [workspaceExists] = await workspaceFile.exists();
            if (workspaceExists) {
                const tmpArchiveFile = join(tmpdir(), getTmpArchiveFilename(taskId));
                try {
                    await workspaceFile.download({ destination: tmpArchiveFile });
                    await tar.x({ file: tmpArchiveFile, cwd: workDir });
                    logger.info(`Task ${taskId} workspace restored from GCS to ${workDir}`);
                }
                finally {
                    if (await fse.pathExists(tmpArchiveFile)) {
                        await fse.remove(tmpArchiveFile);
                    }
                }
            }
            else {
                logger.info(`Task ${taskId} workspace archive not found in GCS.`);
            }
            return {
                id: taskId,
                contextId: loadedMetadata._contextId || uuidv4(),
                kind: 'task',
                status: {
                    state: persistedState._taskState,
                    timestamp: new Date().toISOString(),
                },
                metadata: loadedMetadata,
                history: [],
                artifacts: [],
            };
        }
        catch (error) {
            logger.error(`Failed to load task ${taskId} from GCS:`, error);
            throw error;
        }
    }
}
export class NoOpTaskStore {
    realStore;
    constructor(realStore) {
        this.realStore = realStore;
    }
    async save(task) {
        logger.info(`[NoOpTaskStore] save called for task ${task.id} - IGNORED`);
        return Promise.resolve();
    }
    async load(taskId) {
        logger.info(`[NoOpTaskStore] load called for task ${taskId}, delegating to real store.`);
        return this.realStore.load(taskId);
    }
}
//# sourceMappingURL=gcs.js.map