"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.S3ComponentsStore = void 0;
const logger_1 = require("../../../utils/logger");
const error_handler_1 = require("../../../utils/error.handler");
const platform_utils_1 = require("../platform.utils");
/**
 * S3 Components Store implementation
 *
 * This store downloads components from an AWS S3 bucket.
 * It expects components to be organized in the bucket with the following structure:
 *
 * bucket/
 * ├── component1/
 * │   ├── v1.0.0/
 * │   │   ├── component1-v1.0.0-win32.zip
 * │   │   ├── component1-v1.0.0-darwin.dmg
 * │   │   └── component1-v1.0.0-darwin-arm64.dmg
 * │   └── v1.1.0/
 * │       ├── component1-v1.1.0-win32.zip
 * │       └── component1-v1.1.0-darwin.dmg
 * └── component2/
 *     └── v2.0.0/
 *         └── component2-v2.0.0-win32.zip
 *
 * Authentication is handled through AWS credentials (environment variables, IAM roles, etc.)
 */
class S3ComponentsStore {
    logger = logger_1.Logger.getInstance("s3.store");
    bucketName;
    region;
    prefix;
    accessKeyId;
    secretAccessKey;
    constructor(config) {
        this.bucketName = config.bucketName;
        this.region = config.region;
        this.prefix = config.prefix;
        this.accessKeyId = config.accessKeyId;
        this.secretAccessKey = config.secretAccessKey;
        this.validateConfiguration();
    }
    getInfo() {
        const prefixInfo = this.prefix ? ` (prefix: ${this.prefix})` : '';
        return `S3 Components Store: s3://${this.bucketName}${prefixInfo} (${this.region})`;
    }
    async getAll() {
        this.logger.debug(`Scanning S3 bucket for components: ${this.bucketName}`);
        try {
            const objects = await this.listAllObjects();
            return this.parseS3ObjectsToComponents(objects);
        }
        catch (error) {
            throw new error_handler_1.CLIError(`Failed to list components from S3 bucket: ${error instanceof Error ? error.message : error}`, {
                code: error_handler_1.ErrorCode.NETWORK_ERROR,
                cause: error,
                suggestions: [
                    'Check your AWS credentials are properly configured',
                    'Verify the S3 bucket exists and you have read permissions',
                    'Check your network connection',
                    'Ensure the AWS region is correct'
                ]
            });
        }
    }
    async download(name, version) {
        this.logger.info(`Downloading component ${name}@${version} from S3...`);
        try {
            // Find the specific object for this component and version
            const objectKey = await this.findComponentObject(name, version);
            if (!objectKey) {
                throw new error_handler_1.CLIError(`Component ${name}@${version} not found in S3 bucket`, {
                    code: error_handler_1.ErrorCode.NOT_FOUND,
                    suggestions: [
                        `Check that component '${name}' version '${version}' exists in the S3 bucket`,
                        'Use the browse command to see available components',
                        'Verify the component naming convention matches the expected format'
                    ]
                });
            }
            // Download the object
            const data = await this.downloadObject(objectKey);
            const filename = this.extractFilenameFromKey(objectKey);
            this.logger.debug(`Downloaded ${name}@${version} (${filename}) from S3 successfully!`);
            return {
                name: name,
                data: data,
                filename: filename,
                actualVersion: this.extractVersionFromKey(objectKey)
            };
        }
        catch (error) {
            if (error instanceof error_handler_1.CLIError) {
                throw error;
            }
            throw new error_handler_1.CLIError(`Failed to download component from S3: ${error instanceof Error ? error.message : error}`, {
                code: error_handler_1.ErrorCode.NETWORK_ERROR,
                cause: error,
                suggestions: [
                    'Check your AWS credentials and permissions',
                    'Verify the S3 bucket and object exist',
                    'Check your network connection'
                ]
            });
        }
    }
    /**
     * Validate the S3 configuration
     */
    validateConfiguration() {
        if (!this.bucketName) {
            throw new error_handler_1.CLIError('S3 bucket name is required', {
                code: error_handler_1.ErrorCode.INVALID_CONFIG,
                suggestions: [
                    'Set the bucketName in your S3 components store configuration',
                    'Check your cli-config.json components.storeS3Config.bucketName setting'
                ]
            });
        }
        if (!this.region) {
            throw new error_handler_1.CLIError('S3 region is required', {
                code: error_handler_1.ErrorCode.INVALID_CONFIG,
                suggestions: [
                    'Set the region in your S3 components store configuration',
                    'Check your cli-config.json components.storeS3Config.region setting'
                ]
            });
        }
        // Check for AWS credentials if not using IAM roles
        if (!this.accessKeyId && !process.env['AWS_ACCESS_KEY_ID']) {
            this.logger.warn('No AWS Access Key ID found. Assuming IAM role or other AWS credential provider is configured.');
        }
    }
    /**
     * List all objects in the S3 bucket
     */
    async listAllObjects() {
        const objects = [];
        let continuationToken;
        do {
            const response = await this.listObjectsV2(continuationToken);
            if (response.Contents) {
                objects.push(...response.Contents);
            }
            continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined;
        } while (continuationToken);
        return objects;
    }
    /**
     * List objects in S3 bucket using ListObjectsV2 API
     */
    async listObjectsV2(continuationToken) {
        const url = this.buildS3ApiUrl('');
        const params = new URLSearchParams({
            'list-type': '2',
            'max-keys': '1000'
        });
        if (this.prefix) {
            params.append('prefix', this.prefix);
        }
        if (continuationToken) {
            params.append('continuation-token', continuationToken);
        }
        const requestUrl = `${url}?${params.toString()}`;
        const headers = await this.createAuthHeaders('GET', '', requestUrl);
        const response = await fetch(requestUrl, {
            method: 'GET',
            headers: headers
        });
        if (!response.ok) {
            throw new Error(`S3 API error: ${response.status} ${response.statusText}`);
        }
        const xmlText = await response.text();
        return this.parseListObjectsResponse(xmlText);
    }
    /**
     * Download an object from S3
     */
    async downloadObject(key) {
        const url = this.buildS3ApiUrl(key);
        const headers = await this.createAuthHeaders('GET', key, url);
        this.logger.debug(`Downloading S3 object: ${key}`);
        const response = await fetch(url, {
            method: 'GET',
            headers: headers
        });
        if (!response.ok) {
            throw new Error(`Failed to download S3 object: ${response.status} ${response.statusText}`);
        }
        const buffer = await response.arrayBuffer();
        return new Uint8Array(buffer);
    }
    /**
     * Find the S3 object key for a specific component and version
     */
    async findComponentObject(name, version) {
        const objects = await this.listAllObjects();
        // Look for objects that match the component naming pattern
        const targetObjects = objects.filter(obj => {
            if (!obj.Key)
                return false;
            const key = obj.Key;
            const platform = platform_utils_1.ComponentPlatformUtils.getCurrentPlatform();
            // Handle "latest" version by finding the most recent version
            if (version === 'latest') {
                return key.includes(`${name}/`) && this.matchesPlatform(key, platform);
            }
            else {
                return key.includes(`${name}/v${version}/`) && this.matchesPlatform(key, platform);
            }
        });
        if (targetObjects.length === 0) {
            return null;
        }
        // If multiple objects match, prefer exact platform matches
        const exactMatch = targetObjects.find(obj => {
            const key = obj.Key;
            const platform = platform_utils_1.ComponentPlatformUtils.getCurrentPlatform();
            return key.includes(`-${platform}.`) || key.includes(`-${platform}-`);
        });
        if (exactMatch) {
            return exactMatch.Key;
        }
        // If looking for "latest", find the most recent by sorting
        if (version === 'latest') {
            targetObjects.sort((a, b) => {
                const aVersion = this.extractVersionFromKey(a.Key);
                const bVersion = this.extractVersionFromKey(b.Key);
                return platform_utils_1.ComponentPlatformUtils.compareVersions(bVersion, aVersion); // Descending order
            });
        }
        return targetObjects[0]?.Key || null;
    }
    /**
     * Parse S3 objects into Component array
     */
    parseS3ObjectsToComponents(objects) {
        const components = [];
        for (const obj of objects) {
            if (!obj.Key)
                continue;
            const component = this.parseComponentFromKey(obj.Key);
            if (component) {
                components.push(component);
            }
        }
        return components;
    }
    /**
     * Parse component information from S3 object key
     */
    parseComponentFromKey(key) {
        // Expected format: [prefix/]componentName/vVersion/componentName-vVersion-platform.ext
        const parts = key.split('/');
        if (parts.length < 3) {
            return null;
        }
        // Remove prefix if it exists
        const relevantParts = this.prefix ? parts.slice(this.prefix.split('/').length) : parts;
        if (relevantParts.length < 3) {
            return null;
        }
        const componentName = relevantParts[0];
        const versionDir = relevantParts[1]; // e.g., "v1.0.0"
        const filename = relevantParts[relevantParts.length - 1];
        // Extract version from directory name
        const version = versionDir.startsWith('v') ? versionDir.substring(1) : versionDir;
        // Extract platform from filename using the utility
        const platformInfo = platform_utils_1.ComponentPlatformUtils.parseComponentFilename(filename);
        return {
            name: componentName,
            version: version,
            platform: platformInfo.platform || platform_utils_1.ComponentPlatformUtils.getCurrentPlatform(),
            downloadUrl: this.buildS3ApiUrl(key),
            filename: filename,
            prerelease: false // S3 store does not have prerelease info;
        };
    }
    /**
     * Check if a key matches the current platform
     */
    matchesPlatform(key, _platform) {
        // Extract the filename from the key
        const filename = key.split('/').pop() || key;
        return platform_utils_1.ComponentPlatformUtils.matchesCurrentPlatform(filename);
    }
    /**
     * Extract version from S3 object key
     */
    extractVersionFromKey(key) {
        const versionMatch = key.match(/\/v([^\/]+)\//);
        return versionMatch ? versionMatch[1] : '0.0.0';
    }
    /**
     * Extract filename from S3 object key
     */
    extractFilenameFromKey(key) {
        return key.split('/').pop() || key;
    }
    /**
     * Build S3 API URL for a given object key
     */
    buildS3ApiUrl(key) {
        const encodedKey = encodeURIComponent(key).replace(/%2F/g, '/');
        return `https://${this.bucketName}.s3.${this.region}.amazonaws.com/${encodedKey}`;
    }
    /**
     * Create AWS Signature Version 4 authentication headers
     */
    async createAuthHeaders(_method, _key, _url) {
        const accessKeyId = this.accessKeyId || process.env['AWS_ACCESS_KEY_ID'];
        const secretAccessKey = this.secretAccessKey || process.env['AWS_SECRET_ACCESS_KEY'];
        const sessionToken = process.env['AWS_SESSION_TOKEN'];
        if (!accessKeyId || !secretAccessKey) {
            // Return empty headers if no credentials - assuming IAM role or other auth
            this.logger.debug('No AWS credentials found, assuming IAM role authentication');
            return {};
        }
        const headers = {
            'Host': `${this.bucketName}.s3.${this.region}.amazonaws.com`
        };
        if (sessionToken) {
            headers['X-Amz-Security-Token'] = sessionToken;
        }
        // For simplicity, this implementation assumes public bucket or IAM role auth
        // In a full implementation, you would implement AWS Signature Version 4
        // For now, we'll try without authentication and let AWS handle it
        return headers;
    }
    /**
     * Parse S3 ListObjects XML response
     */
    parseListObjectsResponse(xmlText) {
        const response = {
            Contents: []
        };
        // Simple XML parsing for S3 ListObjects response
        const contentsMatches = xmlText.match(/<Contents>[\s\S]*?<\/Contents>/g);
        if (contentsMatches) {
            for (const contentMatch of contentsMatches) {
                const keyMatch = contentMatch.match(/<Key>(.*?)<\/Key>/);
                const lastModifiedMatch = contentMatch.match(/<LastModified>(.*?)<\/LastModified>/);
                const sizeMatch = contentMatch.match(/<Size>(.*?)<\/Size>/);
                if (keyMatch) {
                    const s3Object = {
                        Key: keyMatch[1]
                    };
                    if (lastModifiedMatch) {
                        s3Object.LastModified = new Date(lastModifiedMatch[1]);
                    }
                    if (sizeMatch) {
                        s3Object.Size = parseInt(sizeMatch[1]);
                    }
                    response.Contents.push(s3Object);
                }
            }
        }
        // Check if response is truncated
        const truncatedMatch = xmlText.match(/<IsTruncated>(.*?)<\/IsTruncated>/);
        if (truncatedMatch && truncatedMatch[1] === 'true') {
            response.IsTruncated = true;
            const tokenMatch = xmlText.match(/<NextContinuationToken>(.*?)<\/NextContinuationToken>/);
            if (tokenMatch) {
                response.NextContinuationToken = tokenMatch[1];
            }
        }
        return response;
    }
}
exports.S3ComponentsStore = S3ComponentsStore;
//# sourceMappingURL=s3.store.js.map