import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, HeadObjectCommand, ListBucketsCommand, } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { ServiceBubble } from '../../../types/service-bubble-class.js';
import { CredentialType } from '@bubblelab/shared-schemas';
import { S3ParamsSchema, S3ResultSchema, } from './s3.schema.js';
import { parseS3Credential, isBase64 } from './s3.utils.js';
/**
 * S3 Storage Service Bubble
 *
 * S3-compatible storage operations for file management.
 * Works with AWS S3, MinIO, DigitalOcean Spaces, Backblaze B2, and other
 * S3-compatible providers via a configurable endpoint.
 *
 * Uses a single bundled credential (S3_CRED) containing:
 * - accessKeyId, secretAccessKey (required)
 * - endpoint (optional, for non-AWS providers)
 * - region (optional, defaults to us-east-1)
 */
export class S3Bubble extends ServiceBubble {
    static service = 's3';
    static authType = 'apikey';
    static bubbleName = 's3-storage';
    static type = 'service';
    static schema = S3ParamsSchema;
    static resultSchema = S3ResultSchema;
    static shortDescription = 'S3-compatible storage operations for file management';
    static longDescription = `
    A comprehensive storage bubble for S3-compatible storage providers.
    Works with AWS S3, MinIO, DigitalOcean Spaces, Backblaze B2, and more.
    Use cases:
    - Generate presigned upload URLs for client-side file uploads
    - Get secure download URLs for file retrieval with authentication
    - Delete files from S3 buckets
    - Update/replace files in S3 buckets (supports base64 encoded content for binary files like images)
    - Manage file access with time-limited URLs
  `;
    static alias = 's3';
    s3Client = null;
    constructor(params = {
        operation: 'getUploadUrl',
        bucketName: 'my-bucket',
        fileName: 'example.txt',
    }, context, instanceId) {
        super(params, context, instanceId);
    }
    chooseCredential() {
        const { credentials } = this.params;
        if (!credentials || typeof credentials !== 'object') {
            return undefined;
        }
        return credentials[CredentialType.S3_CRED];
    }
    initializeS3Client(regionOverride) {
        const credentialValue = this.chooseCredential();
        if (!credentialValue) {
            throw new Error('S3 credentials not found. Provide an S3_CRED credential.');
        }
        const creds = parseS3Credential(credentialValue);
        // Region: explicit override > per-operation > credential > default
        const region = regionOverride ||
            this.params.region ||
            creds.region ||
            'us-east-1';
        const clientConfig = {
            region,
            credentials: {
                accessKeyId: creds.accessKeyId,
                secretAccessKey: creds.secretAccessKey,
            },
        };
        // Only set endpoint for non-AWS providers
        if (creds.endpoint) {
            clientConfig.endpoint = creds.endpoint;
            clientConfig.forcePathStyle = true;
        }
        this.s3Client = new S3Client(clientConfig);
        return creds;
    }
    /**
     * Extract the correct region from an S3 PermanentRedirect error.
     * AWS returns the correct endpoint in the error message, e.g.
     * "...endpoint: gymii-test.s3.us-east-2.amazonaws.com"
     */
    extractRegionFromRedirectError(error) {
        // AWS SDK v3 PermanentRedirect errors include region in multiple places
        const err = error;
        // Check if this is a PermanentRedirect error
        if (err?.name !== 'PermanentRedirect' &&
            err?.Code !== 'PermanentRedirect') {
            return undefined;
        }
        // 1. Check $response headers for x-amz-bucket-region
        const response = err?.$response;
        const headers = response?.headers;
        if (headers?.['x-amz-bucket-region']) {
            return headers['x-amz-bucket-region'];
        }
        // 2. Extract region from Endpoint in error message or body
        const message = error instanceof Error ? error.message : String(error);
        const regionFromEndpoint = message.match(/s3[.-]([a-z0-9-]+)\.amazonaws\.com/);
        if (regionFromEndpoint?.[1]) {
            return regionFromEndpoint[1];
        }
        // 3. Check Endpoint field on the error object itself
        const endpoint = err?.Endpoint;
        if (endpoint) {
            const match = endpoint.match(/s3[.-]([a-z0-9-]+)\.amazonaws\.com/);
            if (match?.[1])
                return match[1];
        }
        return undefined;
    }
    async testCredential() {
        const credential = this.chooseCredential();
        if (!credential) {
            throw new Error('S3 credentials not provided');
        }
        const creds = parseS3Credential(credential);
        if (!creds.accessKeyId || !creds.secretAccessKey) {
            throw new Error('S3 credential is missing accessKeyId or secretAccessKey');
        }
        this.initializeS3Client();
        if (!this.s3Client) {
            throw new Error('Failed to initialize S3 client');
        }
        // Actually call S3 to verify the credentials work — let errors propagate
        // so the validator can surface vendor-specific messages
        await this.s3Client.send(new ListBucketsCommand({}));
        return true;
    }
    async executeOperation() {
        const { operation } = this.params;
        switch (operation) {
            case 'getUploadUrl':
                return await this.getUploadUrl(this.params);
            case 'getFile':
                return await this.getFile(this.params);
            case 'deleteFile':
                return await this.deleteFile(this.params);
            case 'updateFile':
                return await this.updateFile(this.params);
            case 'getMultipleUploadUrls':
                return await this.getMultipleUploadUrls(this.params);
            default:
                throw new Error(`Unsupported operation: ${operation}`);
        }
    }
    async performAction(context) {
        void context;
        const { operation } = this.params;
        try {
            this.initializeS3Client();
            if (!this.s3Client) {
                throw new Error('Failed to initialize S3 client');
            }
            try {
                const result = await this.executeOperation();
                return result;
            }
            catch (error) {
                // Auto-retry on region mismatch (PermanentRedirect)
                const redirectRegion = this.extractRegionFromRedirectError(error);
                if (redirectRegion) {
                    console.log(`[S3Bubble] Region redirect detected, retrying with region: ${redirectRegion}`);
                    this.initializeS3Client(redirectRegion);
                    const result = await this.executeOperation();
                    return result;
                }
                throw error;
            }
        }
        catch (error) {
            // Surface S3-specific error names (e.g., NotFound, NoSuchKey, AccessDenied)
            const err = error;
            const errorName = err?.name || err?.Code || '';
            const errorMessage = error instanceof Error ? error.message : '';
            // Map common S3 error names to human-readable messages
            const s3ErrorMessages = {
                NotFound: 'The specified file does not exist',
                NoSuchKey: 'The specified file does not exist',
                NoSuchBucket: 'The specified bucket does not exist',
                AccessDenied: 'Access denied — check your S3 credentials and bucket permissions',
                PermanentRedirect: 'Bucket is in a different region — set the correct region',
                InvalidAccessKeyId: 'The AWS access key ID is invalid',
                SignatureDoesNotMatch: 'The secret access key is invalid',
            };
            const friendlyMessage = s3ErrorMessages[errorName];
            const displayError = friendlyMessage ||
                (errorMessage && errorMessage !== errorName
                    ? `${errorName}: ${errorMessage}`
                    : '') ||
                errorName ||
                'Unknown error occurred';
            return {
                operation,
                success: false,
                error: displayError,
            };
        }
    }
    async getUploadUrl(params) {
        if (!this.s3Client)
            throw new Error('S3 client not initialized');
        // Generate secure filename with timestamp and optional userId for isolation
        const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
        const fileExtension = params.fileName.split('.').pop() || 'bin';
        const baseName = params.fileName.replace(/\.[^/.]+$/, '');
        const sanitizedBaseName = baseName.replace(/[^a-zA-Z0-9-_]/g, '_');
        const userPrefix = params.userId ? `${params.userId}/` : '';
        const secureFileName = `${userPrefix}${timestamp}-${sanitizedBaseName}.${fileExtension}`;
        const command = new PutObjectCommand({
            Bucket: params.bucketName,
            Key: secureFileName,
            ContentType: params.contentType,
        });
        const uploadUrl = await getSignedUrl(this.s3Client, command, {
            expiresIn: params.expirationMinutes * 60,
        });
        return {
            operation: 'getUploadUrl',
            success: true,
            uploadUrl,
            fileName: secureFileName,
            contentType: params.contentType,
            error: '',
        };
    }
    async getFile(params) {
        if (!this.s3Client)
            throw new Error('S3 client not initialized');
        // Check if file exists and get metadata
        const headCommand = new HeadObjectCommand({
            Bucket: params.bucketName,
            Key: params.fileName,
        });
        const metadata = await this.s3Client.send(headCommand);
        // Generate presigned download URL
        const command = new GetObjectCommand({
            Bucket: params.bucketName,
            Key: params.fileName,
        });
        const downloadUrl = await getSignedUrl(this.s3Client, command, {
            expiresIn: params.expirationMinutes * 60,
        });
        return {
            operation: 'getFile',
            success: true,
            downloadUrl,
            fileUrl: downloadUrl,
            fileName: params.fileName,
            fileSize: metadata.ContentLength,
            contentType: metadata.ContentType,
            lastModified: metadata.LastModified?.toISOString(),
            error: '',
        };
    }
    async deleteFile(params) {
        if (!this.s3Client)
            throw new Error('S3 client not initialized');
        const command = new DeleteObjectCommand({
            Bucket: params.bucketName,
            Key: params.fileName,
        });
        await this.s3Client.send(command);
        return {
            operation: 'deleteFile',
            success: true,
            fileName: params.fileName,
            deleted: true,
            error: '',
        };
    }
    /** Check if a fileName already has a secure prefix (timestamp-UUID pattern) */
    isSecureFileName(fileName) {
        // Matches patterns like "2026-02-28T00-42-37-653Z-b2f0c3bd-7d29-492c-bea1-394042d33ee2-..."
        // or with userId prefix like "alice/2026-02-28T00-42-37-653Z-..."
        const nameWithoutUserPrefix = fileName.includes('/')
            ? fileName.split('/').slice(1).join('/')
            : fileName;
        return /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z-[0-9a-f]{8}-/.test(nameWithoutUserPrefix);
    }
    async updateFile(params) {
        if (!this.s3Client)
            throw new Error('S3 client not initialized');
        let key;
        if (this.isSecureFileName(params.fileName)) {
            // Already a secure filename (from a previous operation) — use as-is
            key = params.fileName;
        }
        else {
            // Generate new secure filename
            const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
            const fileExtension = params.fileName.split('.').pop() || 'bin';
            const baseName = params.fileName.replace(/\.[^/.]+$/, '');
            const sanitizedBaseName = baseName.replace(/[^a-zA-Z0-9-_]/g, '_');
            const userPrefix = params.userId ? `${params.userId}/` : '';
            key = `${userPrefix}${timestamp}-${crypto.randomUUID()}-${sanitizedBaseName}.${fileExtension}`;
        }
        // Handle base64 encoded content
        let bodyContent;
        if (isBase64(params.fileContent)) {
            const base64Data = params.fileContent.replace(/^data:[^;]+;base64,/, '');
            bodyContent = Buffer.from(base64Data, 'base64');
        }
        else {
            bodyContent = params.fileContent;
        }
        const command = new PutObjectCommand({
            Bucket: params.bucketName,
            Key: key,
            ContentType: params.contentType,
            Body: bodyContent,
        });
        await this.s3Client.send(command);
        return {
            operation: 'updateFile',
            success: true,
            fileName: key,
            updated: true,
            contentType: params.contentType,
            error: '',
        };
    }
    async getMultipleUploadUrls(params) {
        if (!this.s3Client)
            throw new Error('S3 client not initialized');
        const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
        const userPrefix = params.userId ? `${params.userId}/` : '';
        // Generate secure PDF filename
        const pdfExtension = params.pdfFileName.split('.').pop() || 'pdf';
        const pdfBaseName = params.pdfFileName
            .replace(/\.[^/.]+$/, '')
            .replace(/[^a-zA-Z0-9-_]/g, '_');
        const securePdfFileName = `${userPrefix}${timestamp}-${pdfBaseName}.${pdfExtension}`;
        // Generate PDF upload URL
        const pdfCommand = new PutObjectCommand({
            Bucket: params.bucketName,
            Key: securePdfFileName,
            ContentType: 'application/pdf',
        });
        const pdfUploadUrl = await getSignedUrl(this.s3Client, pdfCommand, {
            expiresIn: params.expirationMinutes * 60,
        });
        // Generate page image upload URLs
        const pageUploadUrls = [];
        for (let pageNum = 1; pageNum <= params.pageCount; pageNum++) {
            const pageFileName = `${userPrefix}${timestamp}-${pdfBaseName}_page${pageNum}.jpeg`;
            const pageCommand = new PutObjectCommand({
                Bucket: params.bucketName,
                Key: pageFileName,
                ContentType: 'image/jpeg',
            });
            const pageUploadUrl = await getSignedUrl(this.s3Client, pageCommand, {
                expiresIn: params.expirationMinutes * 60,
            });
            pageUploadUrls.push({
                pageNumber: pageNum,
                uploadUrl: pageUploadUrl,
                fileName: pageFileName,
            });
        }
        return {
            operation: 'getMultipleUploadUrls',
            success: true,
            pdfUploadUrl,
            pdfFileName: securePdfFileName,
            pageUploadUrls,
            error: '',
        };
    }
}
//# sourceMappingURL=s3.js.map