"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
const config_service_1 = require("../config/config.service");
const logger_1 = require("../../utils/logger");
const file_helper_1 = require("./file.helper");
const github_store_1 = require("./stores/github.store");
const local_store_1 = require("./stores/local.store");
const s3_store_1 = require("./stores/s3.store");
const version_selector_1 = require("./version.selector");
const fs_1 = require("fs");
const path_1 = require("path");
const component_config_1 = require("./component.config");
const colors_1 = require("../../utils/colors");
const clack = __importStar(require("@clack/prompts"));
/**
 * Service for managing components
 */
class ComponentsService {
    logger = logger_1.Logger.getInstance("components.service");
    componentsStore;
    getComponentsStore() {
        if (this.componentsStore) {
            return this.componentsStore;
        }
        const storeConfig = config_service_1.ConfigManager.config.components.store;
        // Determine which store to use based on first property defined
        if (storeConfig.github) {
            this.componentsStore = new github_store_1.GitHubComponentsStore(storeConfig.github.repo, storeConfig.github.usePrereleases || false);
        }
        else if (storeConfig.local) {
            this.componentsStore = new local_store_1.LocalComponentsStore(storeConfig.local.path);
        }
        else if (storeConfig.s3) {
            this.componentsStore = new s3_store_1.S3ComponentsStore(storeConfig.s3);
        }
        else {
            throw new Error(`No components store configured. Please configure github, local, or s3 store in components.store`);
        }
        this.logger.info(`Using components store: ${this.componentsStore.getInfo()}`);
        return this.componentsStore;
    }
    async listComponentsInThisProject() {
        // Logic to extract components from the definition
        const componentsList = config_service_1.ConfigManager.config.components.list;
        return Object.keys(componentsList).map(key => ({
            name: key,
            version: componentsList[key],
        }));
    }
    async browseComponentsFromStore() {
        this.logger.info(`Listing available components ...`);
        const all = await this.getComponentsStore().getAll();
        const byComponent = all.reduce((acc, component) => {
            let current = acc[component.name];
            if (!current) {
                current = {
                    versions: new Set(),
                };
                acc[component.name] = current;
            }
            current.versions.add(component.version);
            return acc;
        }, {});
        this.logger.debug(`Found ${Object.keys(byComponent).length} components:`);
        for (const [name, { versions }] of Object.entries(byComponent)) {
            this.logger.debug(`Component: ${name}, Versions: ${Array.from(versions).join(", ")}`);
        }
        return all;
    }
    async installComponent(name, version) {
        version = version ?? "latest";
        if (name.indexOf("@") >= 0) {
            const parts = name.split("@");
            name = parts[0];
            version = parts[1] || version;
        }
        // install the component
        const resolvedVersion = await this.installComponentCore(name, version);
        // Save the new component to the cnf
        this.logger.debug(`Updating iocd.cli.config.json to include ${name}@${resolvedVersion}...`);
        const componentsList = config_service_1.ConfigManager.config.components.list;
        componentsList[name] = resolvedVersion;
        await config_service_1.ConfigManager.saveComponents(componentsList);
        this.logger.debug(`Updated iocd.cli.config.json to include ${name}@${resolvedVersion}`);
    }
    async uninstallComponent(name) {
        this.logger.info(`Uninstalling component ${name}...`);
        // Remove from config first
        const componentsList = config_service_1.ConfigManager.config.components.list;
        if (componentsList[name]) {
            delete componentsList[name];
            await config_service_1.ConfigManager.saveComponents(componentsList);
            this.logger.debug(`Updated iocd.cli.config.json to remove ${name}`);
        }
        else {
            this.logger.warn(`Component ${name} not found in iocd.cli.config.json`);
        }
        // Now remove the files
        const fileService = new file_helper_1.FileHelper();
        await fileService.removeComponent(name);
        this.logger.info(`Component ${name} uninstalled successfully.`);
    }
    async installComponents() {
        this.logger.info("Installing all components described in iocd.cli.config.json...");
        // Use the ComponentsStore to download all components described in package.json under ioCDComponents
        const components = await this.listComponentsInThisProject();
        for (const component of components) {
            try {
                await this.installComponentCore(component.name, component.version);
            }
            catch (error) {
                const errorMsg = `Failed to install component ${component.name}@${component.version}: ${error}`;
                this.logger.error(errorMsg, error);
                if (component.name === "iocd") {
                    throw new Error("iocd component failed to install, aborting further installations...");
                }
            }
        }
        this.logger.info(`Components installation completed successfully.`);
    }
    async installComponentsForBuild() {
        this.logger.info("Installing components for build (excluding development-only components)...");
        // Get all components from configuration
        const components = await this.listComponentsInThisProject();
        // Get list of components to exclude from build
        const excludeFromBuild = config_service_1.ConfigManager.config.components.excludeFromBuild || [];
        this.logger.debug(`Components excluded from build: ${excludeFromBuild.join(", ")}`);
        // Filter out excluded components
        const buildComponents = components.filter(component => !excludeFromBuild.includes(component.name));
        for (const component of buildComponents) {
            try {
                await this.installComponentCore(component.name, component.version);
            }
            catch (error) {
                const errorMsg = `Failed to install component ${component.name}@${component.version}: ${error}`;
                this.logger.error(errorMsg, error);
                if (component.name === "iocd") {
                    throw new Error("iocd component failed to install, aborting further installations...");
                }
            }
        }
        this.logger.info(`Installation completed successfully.`);
    }
    /**
     * Pin all "latest" component versions to their currently installed specific versions
     * This ensures reproducible builds by replacing "latest" with actual version numbers
     */
    async pinComponentVersions() {
        this.logger.debug("Pinning component versions...");
        const componentsList = config_service_1.ConfigManager.config.components.list;
        const pinnedVersions = {};
        const changedComponents = [];
        for (const [componentName, currentVersion] of Object.entries(componentsList)) {
            if (currentVersion === "latest") {
                // Get the actual installed version by checking what's available
                try {
                    const availableComponents = await this.getComponentsStore().getAll();
                    const matchingComponents = availableComponents.filter(c => c.name === componentName);
                    if (matchingComponents.length > 0) {
                        // Use the version selector to get the latest version
                        const selectedComponent = (0, version_selector_1.selectVersion)("latest", matchingComponents);
                        if (selectedComponent) {
                            pinnedVersions[componentName] = selectedComponent.version;
                            changedComponents.push(`${componentName}: "latest" → "${selectedComponent.version}"`);
                            this.logger.debug(`Pinned ${componentName} from "latest" to "${selectedComponent.version}"`);
                        }
                        else {
                            // Keep as latest if no version could be resolved
                            pinnedVersions[componentName] = currentVersion;
                            this.logger.warn(`Could not resolve latest version for ${componentName}, keeping as "latest"`);
                        }
                    }
                    else {
                        // Component not found in store, keep current version
                        pinnedVersions[componentName] = currentVersion;
                        this.logger.warn(`Component ${componentName} not found in store, keeping version as "${currentVersion}"`);
                    }
                }
                catch (error) {
                    // On error, keep the current version
                    pinnedVersions[componentName] = currentVersion;
                    this.logger.error(`Error resolving version for ${componentName}: ${error}`);
                }
            }
            else {
                // Already pinned or specific version, keep as is
                pinnedVersions[componentName] = currentVersion;
            }
        }
        // Save the pinned versions if any changes were made
        if (changedComponents.length > 0) {
            await config_service_1.ConfigManager.saveComponents(pinnedVersions);
            this.logger.debug(`Component versions pinned successfully:`);
            changedComponents.forEach(change => this.logger.debug(`  • ${change}`));
        }
        else {
            this.logger.debug('All component versions are already pinned (no "latest" versions found)');
        }
    }
    async installComponentCore(name, version) {
        this.logger.info(`Installing component ${name}@${version}...`);
        const componentData = await this.getComponentsStore().download(name, version);
        const fileService = new file_helper_1.FileHelper();
        const componentDir = await fileService.storeComponent(componentData);
        // find component.json inside the componentDir
        const componentJsonPath = (0, path_1.join)(componentDir, "component.json");
        this.logger.debug(`Looking for component.json at ${componentJsonPath}...`);
        if ((0, fs_1.existsSync)(componentJsonPath)) {
            this.logger.debug(`Found component.json for ${name}@${version}, processing components config...`);
            await this.processComponentConfig(componentDir, componentJsonPath);
        }
        const actualVersion = componentData.actualVersion;
        this.logger.info((0, colors_1.green)(`Component ${name}@${actualVersion} installed successfully.`));
        return actualVersion;
    }
    async processComponentConfig(componentDir, componentJsonPath) {
        await (0, component_config_1.copyModifications)(componentDir, componentJsonPath);
    }
    /**
     * Downloads all available components from the current store to a local directory
     * in the local store format
     * @param outputDir Target directory for downloaded components
     * @param force Whether to overwrite existing files
     */
    async downloadAllComponents(outputDir, force = false) {
        this.logger.info(`Downloading all components to: ${outputDir}`);
        const storeConfig = config_service_1.ConfigManager.config.components.store;
        // Check if current store is local before trying to access it
        if (storeConfig.local) {
            throw new Error("Cannot download components from a local store. Please configure a different store (github or s3) to download from.");
        }
        // Create a special store instance for downloading that includes prereleases
        let store;
        if (storeConfig.github) {
            this.logger.info("Creating GitHub store with prereleases enabled for download");
            store = new github_store_1.GitHubComponentsStore(storeConfig.github.repo, true // Always include prereleases for download
            );
        }
        else if (storeConfig.s3) {
            store = new s3_store_1.S3ComponentsStore(storeConfig.s3);
        }
        else {
            throw new Error(`No valid components store configured for downloading. Please configure github or s3 store.`);
        }
        const allComponents = await store.getAll();
        if (allComponents.length === 0) {
            this.logger.info("No components found in the store");
            return;
        }
        this.logger.info(`Found ${allComponents.length} components to download`);
        // Create output directory
        if (!(0, fs_1.existsSync)(outputDir)) {
            (0, fs_1.mkdirSync)(outputDir, { recursive: true });
        }
        // Group components by name and version, but keep all platform variants
        const componentGroups = new Map();
        for (const component of allComponents) {
            if (!componentGroups.has(component.name)) {
                componentGroups.set(component.name, new Map());
            }
            const versionMap = componentGroups.get(component.name);
            if (!versionMap.has(component.version)) {
                versionMap.set(component.version, []);
            }
            versionMap.get(component.version).push(component);
        }
        const spinner = clack.spinner();
        spinner.start("Downloading components...");
        let downloadedCount = 0;
        let errorCount = 0;
        try {
            for (const [componentName, versions] of componentGroups) {
                for (const [version, variants] of versions) {
                    // Create directory structure: outputDir/componentName/version/
                    const versionDir = (0, path_1.join)(outputDir, componentName, `v${version}`);
                    if (!(0, fs_1.existsSync)(versionDir)) {
                        (0, fs_1.mkdirSync)(versionDir, { recursive: true });
                    }
                    // Download each variant (different platforms/architectures)
                    for (const variant of variants) {
                        try {
                            const targetFilePath = (0, path_1.join)(versionDir, variant.filename);
                            // Check if file already exists
                            if ((0, fs_1.existsSync)(targetFilePath) && !force) {
                                this.logger.debug(`Skipping existing file: ${variant.filename}`);
                                continue;
                            }
                            spinner.message(`Downloading ${componentName}@${version} (${variant.platform}-${variant.arch})...`);
                            // Download this specific variant by fetching from its URL directly
                            // Use proper retry logic for reliability
                            let response;
                            let retryCount = 0;
                            const maxRetries = 3;
                            while (retryCount <= maxRetries) {
                                try {
                                    response = await fetch(variant.downloadUrl);
                                    if (response.ok) {
                                        break;
                                    }
                                    else if (response.status >= 500 &&
                                        retryCount < maxRetries) {
                                        // Retry on server errors
                                        retryCount++;
                                        await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
                                        continue;
                                    }
                                    else {
                                        throw new Error(`Failed to download: ${response.status} ${response.statusText}`);
                                    }
                                }
                                catch (error) {
                                    if (retryCount < maxRetries) {
                                        retryCount++;
                                        await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
                                        continue;
                                    }
                                    throw error;
                                }
                            }
                            const buffer = await response.arrayBuffer();
                            // Write the file
                            (0, fs_1.writeFileSync)(targetFilePath, new Uint8Array(buffer));
                            downloadedCount++;
                            this.logger.debug(`Downloaded: ${targetFilePath}`);
                        }
                        catch (error) {
                            this.logger.warn(`Failed to download ${componentName}@${version} (${variant.platform}-${variant.arch}): ${error}`);
                            errorCount++;
                        }
                    }
                }
            }
            spinner.stop(`✅ Download complete! Downloaded ${downloadedCount} components`);
            if (errorCount > 0) {
                this.logger.warn(`${errorCount} downloads failed. Check logs for details.`);
            }
            clack.note(`Components downloaded to: ${outputDir}\n` +
                `Total files: ${downloadedCount}\n` +
                `Failed downloads: ${errorCount}\n\n` +
                "📦 Components Downloaded");
        }
        catch (error) {
            spinner.stop(`❌ Download failed: ${error}`);
            throw error;
        }
    }
}
exports.default = new ComponentsService();
//# sourceMappingURL=components.service.js.map