"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TemplateService = void 0;
const node_path_1 = require("node:path");
const node_fs_1 = require("node:fs");
const logger_1 = require("../utils/logger");
const error_handler_1 = require("../utils/error.handler");
const fs_extra_1 = __importDefault(require("fs-extra"));
const config_service_1 = require("./config/config.service");
const path_1 = require("../utils/path");
/**
 * TemplateService handles the generation of io.Connect Desktop projects using a modular template system.
 *
 * This service supports two types of templates:
 * - Base templates: Core project structure and essential files (e.g., ioconnect-desktop)
 * - Feature templates: Optional functionality modules that extend the base template
 *
 * The service processes templates by:
 * 1. Copying base template files to the target directory
 * 2. Applying variable substitutions ({{variable}} patterns)
 * 3. Conditionally applying feature templates based on user selection:
 *    - Validates that requested features exist as separate template directories
 *    - Only processes features that are explicitly selected by the user
 *    - Each feature template contains its own template.json with type: "feature"
 *    - Features are independent and can be combined in any combination
 * 4. Merging feature-specific files into the project structure:
 *    - Feature templates overlay their files onto the base project structure
 *    - Files from features are copied to their corresponding paths in the target
 *    - Directory structures are preserved (e.g., modifications/iocd/assets/feature/)
 *    - No conflicts occur since features target different subdirectories
 *    - Variable substitution is applied to feature files just like base template files
 *
 * Key capabilities:
 * - Template discovery and validation from filesystem
 * - Variable interpolation for dynamic content generation
 * - Modular feature system for extensible project setup
 * - Comprehensive error handling and logging
 * - Support for nested directory structures and file copying
 */
class TemplateService {
    logger = logger_1.Logger.getInstance();
    templatesPath;
    constructor() {
        this.templatesPath = (0, node_path_1.join)(__dirname, '../templates');
    }
    /**
     * Get the path to embedded templates
     */
    getTemplatesPath() {
        return this.templatesPath;
    }
    /**
     * List available templates
     */
    getAvailableTemplates() {
        try {
            if (!(0, node_fs_1.existsSync)(this.templatesPath)) {
                this.logger.warn('Templates directory does not exist:', this.templatesPath);
                return [];
            }
            return (0, node_fs_1.readdirSync)(this.templatesPath, { withFileTypes: true })
                .filter(dirent => dirent.isDirectory())
                .map(dirent => dirent.name);
        }
        catch (error) {
            this.logger.error('Failed to list templates:', error);
            throw new error_handler_1.CLIError('Failed to list available templates', {
                code: error_handler_1.ErrorCode.FILE_SYSTEM_ERROR,
                cause: error,
                suggestions: [
                    'Check if the CLI installation is complete',
                    'Try reinstalling the CLI tool'
                ]
            });
        }
    }
    /**
     * Check if a template exists
     */
    templateExists(templateName) {
        const templatePath = (0, node_path_1.join)(this.templatesPath, templateName);
        return (0, node_fs_1.existsSync)(templatePath) && (0, node_fs_1.statSync)(templatePath).isDirectory();
    }
    /**
     * Generate a project from a template
     */
    async generateProject(templateName, options) {
        this.logger.info(`Generating project from template: ${templateName}`);
        this.logger.debug(`Generating project with options:\n`, options);
        // Validate template exists
        if (!this.templateExists(templateName)) {
            throw new error_handler_1.CLIError(`Template '${templateName}' not found`, {
                code: error_handler_1.ErrorCode.NOT_FOUND,
                suggestions: [
                    'Check available templates with: iocd templates list',
                    'Verify the template name is spelled correctly'
                ]
            });
        }
        // Ensure only base templates can be used for project generation
        const templateInfo = this.getTemplateInfo(templateName);
        if (templateInfo.type !== 'base') {
            throw new error_handler_1.CLIError(`Template '${templateName}' is not a base template`, {
                code: error_handler_1.ErrorCode.VALIDATION_ERROR,
                suggestions: [
                    'Use a base template like "ioconnect-desktop"',
                    'Feature templates are applied automatically through the features option'
                ]
            });
        }
        // Check if target directory already exists
        if ((0, node_fs_1.existsSync)(options.targetDirectory)) {
            throw new error_handler_1.CLIError(`Directory '${options.targetDirectory}' already exists`, {
                code: error_handler_1.ErrorCode.FILE_SYSTEM_ERROR,
                suggestions: [
                    'Choose a different project name',
                    'Remove the existing directory',
                    'Use a different target location'
                ]
            });
        }
        const templatePath = (0, node_path_1.join)(this.templatesPath, templateName);
        try {
            // Create target directory
            (0, node_fs_1.mkdirSync)(options.targetDirectory, { recursive: true });
            this.logger.debug(`Created target directory: ${options.targetDirectory}`);
            // Prepare template variables
            const variables = this.prepareTemplateVariables(options);
            // Copy base template files with variable replacement
            await this.copyTemplateFiles(templatePath, options.targetDirectory, variables);
            // add components configuration to package.json 
            await this.updateComponentsConfig(options.targetDirectory, options.initialComponents);
            // Process template application files if selected
            if (options.templateApplications && options.templateApplications.length > 0) {
                await this.processApplicationsTemplates(options.targetDirectory, variables, options.templateApplications);
            }
            this.logger.info(`Project generated successfully at: ${options.targetDirectory}`);
        }
        catch (error) {
            this.logger.error('Failed to generate project:', error);
            throw new error_handler_1.CLIError('Failed to generate project from template', {
                code: error_handler_1.ErrorCode.FILE_SYSTEM_ERROR,
                cause: error,
                suggestions: [
                    'Check write permissions for the target directory',
                    'Ensure sufficient disk space is available',
                    'Try running with --verbose for more details'
                ]
            });
        }
    }
    /** Add a new template application to an already created seed project */
    async addTemplateApplication(appName) {
        this.logger.info(`Adding template application: ${appName}`);
        const config = config_service_1.ConfigManager.config;
        // Implementation goes here
        const variables = this.prepareTemplateVariables({
            productSlug: config.productSlug,
            productName: config.productName,
            company: config.company,
            copyright: config.copyright,
            version: config.version,
            folderName: config.productSlug,
            targetDirectory: path_1.PathUtils.getRootDir(),
            templateApplications: [appName],
            initialComponents: []
        });
        await this.processApplicationsTemplates('./', variables, [appName]);
        this.logger.info(`Template application ${appName} added successfully`);
    }
    /** Adds initialComponents to the iocd.cli.config.json -> components.list object in the target directory */
    updateComponentsConfig(targetDirectory, initialComponents) {
        const configPath = (0, node_path_1.join)(targetDirectory, 'iocd.cli.config.json');
        this.logger.debug(`Updating components configuration in ${configPath} to include:`, initialComponents);
        if (!(0, node_fs_1.existsSync)(configPath)) {
            this.logger.warn(`iocd.cli.config.json not found in target directory, skipping components configuration`);
            return;
        }
        const configContent = (0, node_fs_1.readFileSync)(configPath, 'utf-8');
        const config = JSON.parse(configContent);
        // Ensure the components structure exists
        if (!config.components) {
            config.components = {};
        }
        if (!config.components.list) {
            config.components.list = {};
        }
        // Add initial components, avoiding duplicates
        for (const component of initialComponents) {
            config.components.list[component] = "latest";
        }
        // write back the updated config
        (0, node_fs_1.writeFileSync)(configPath, JSON.stringify(config, null, 4), 'utf-8');
        this.logger.debug(`Updated components configuration in iocd.cli.config.json`);
    }
    /**
     * Prepare template variables for replacement
     */
    prepareTemplateVariables(options) {
        return [
            {
                key: '{{PRODUCT_NAME}}',
                value: options.productName,
                description: 'The product name for the application'
            },
            {
                key: '{{FOLDER_NAME}}',
                value: options.folderName,
                description: 'The folder name for the project'
            },
            {
                key: '{{PRODUCT_SLUG}}',
                value: this.generatePackageName(options.productSlug),
                description: 'NPM-compatible package name'
            },
            {
                key: '{{FEATURES}}',
                value: JSON.stringify(options.templateApplications || []),
                description: 'Selected features array'
            },
            {
                key: '{{YEAR}}',
                value: new Date().getFullYear().toString(),
                description: 'Current year'
            }
        ];
    }
    /**
     * Generate NPM-compatible package name from product name
     */
    generatePackageName(productName) {
        return productName
            .toLowerCase()
            .replace(/[^a-z0-9-]/g, '-')
            .replace(/-+/g, '-')
            .replace(/^-|-$/g, '');
    }
    /**
     * Recursively copy template files with variable replacement
     */
    async copyTemplateFiles(sourcePath, targetPath, variables) {
        const items = (0, node_fs_1.readdirSync)(sourcePath, { withFileTypes: true });
        for (const item of items) {
            const sourceItemPath = (0, node_path_1.join)(sourcePath, item.name);
            const targetItemPath = (0, node_path_1.join)(targetPath, item.name);
            // Skip template.json files - they are metadata for the template system
            if (item.isFile() && item.name === 'template.json') {
                continue;
            }
            if (item.isDirectory()) {
                // Create directory and recurse
                (0, node_fs_1.mkdirSync)(targetItemPath, { recursive: true });
                await this.copyTemplateFiles(sourceItemPath, targetItemPath, variables);
            }
            else if (item.isFile()) {
                // the logic here is to check the extension and only replace known text-based extensions
                // the rest should be copied as-is (e.g. images, binaries), otherwise they may get corrupted
                // the check is a bit complicated because of the .json.merge and .json.merge-<<suffix>> files      
                const ext = (0, node_path_1.extname)(sourceItemPath);
                const allowedExtensions = ['.env', '.json', '.merge'];
                const isAllowedExtension = allowedExtensions.some(e => ext.startsWith(e) || ext === e);
                if (ext && isAllowedExtension) {
                    await this.copyFileWithReplacement(sourceItemPath, targetItemPath, variables);
                }
                else {
                    await this.copyFileNoReplacement(sourceItemPath, targetItemPath);
                }
            }
        }
    }
    /**
     * Copy a single file with variable replacement
     */
    async copyFileWithReplacement(sourcePath, targetPath, variables) {
        const sourceContent = (0, node_fs_1.readFileSync)(sourcePath, 'utf-8');
        // Replace template variables
        let targetContent = sourceContent;
        for (const variable of variables) {
            const regex = new RegExp(this.escapeRegExp(variable.key), 'g');
            targetContent = targetContent.replace(regex, variable.value);
        }
        (0, node_fs_1.writeFileSync)(targetPath, targetContent, 'utf-8');
        this.logger.debug(`Copied and processed: ${(0, node_path_1.relative)(process.cwd(), targetPath)}`);
    }
    copyFileNoReplacement(source, destination) {
        fs_extra_1.default.copyFile(source, destination);
    }
    /**
     * Process feature-specific templates
     */
    async processApplicationsTemplates(targetPath, variables, applications) {
        for (const application of applications) {
            const featureTemplatePath = (0, node_path_1.join)(this.templatesPath, application);
            if (!(0, node_fs_1.existsSync)(featureTemplatePath)) {
                this.logger.warn(`Application template '${application}' not found in templates directory`);
                continue;
            }
            // Verify it's a application template
            const templateInfo = this.getTemplateInfo(application);
            if (templateInfo.type !== 'application') {
                this.logger.warn(`Template '${application}' is not a application template, skipping`);
                continue;
            }
            this.logger.debug(`Processing application template: ${application}`);
            await this.copyTemplateFiles(featureTemplatePath, targetPath, variables);
        }
    }
    /**
     * Escape special regex characters
     */
    escapeRegExp(string) {
        return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }
    /**
     * Get template information
     */
    getTemplateInfo(templateName) {
        const templatePath = (0, node_path_1.join)(this.templatesPath, templateName);
        const templateInfoPath = (0, node_path_1.join)(templatePath, 'template.json');
        if ((0, node_fs_1.existsSync)(templateInfoPath)) {
            try {
                return JSON.parse((0, node_fs_1.readFileSync)(templateInfoPath, 'utf-8'));
            }
            catch (error) {
                this.logger.warn(`Failed to parse template.json for ${templateName}:`, error);
            }
        }
        return {
            name: templateName,
            description: `${templateName} template`,
            version: '1.0.0',
            type: 'base' // Default fallback
        };
    }
    /**
     * Get available application templates with their metadata
     */
    getAvailableApplicationTemplates() {
        const allTemplates = this.getAvailableTemplates();
        const applicationTemplates = [];
        for (const templateName of allTemplates) {
            const templateInfo = this.getTemplateInfo(templateName);
            if (templateInfo.type === 'application') {
                applicationTemplates.push(templateInfo);
            }
        }
        return applicationTemplates;
    }
}
exports.TemplateService = TemplateService;
//# sourceMappingURL=template.service.js.map