"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.applyModifications = applyModifications;
const fs_1 = require("fs");
const path_1 = require("path");
const child_process_1 = require("child_process");
const logger_1 = require("../../utils/logger");
const config_service_1 = require("../config/config.service");
const deep_merge_1 = require("../../utils/deep.merge");
const windows_helper_1 = require("./windows.helper");
const modifications_tracker_1 = require("./modifications.tracker");
const isMac = process.platform === "darwin";
// Conditionally import macOS helper only on macOS
let macOSHelper = null;
if (isMac) {
    try {
        macOSHelper = require("./macOS.helper");
    }
    catch (_error) {
        // Will be handled later when needed
    }
}
const logger = logger_1.Logger.getInstance("modifications.service");
const getVars = () => {
    return {
        dev: [
            {
                name: "COMPONENTS_PATH",
                value: isMac ? "%IO_CD_ROOT_DIR%/../../.." : "%IO_CD_ROOT_DIR%/..",
            },
            {
                name: "PRODUCT_VERSION",
                value: config_service_1.ConfigManager.config.version || "1.0.0",
            },
            {
                name: "PRODUCT_NAME",
                value: config_service_1.ConfigManager.config.productName || "io.Connect Desktop",
            },
            {
                name: "PRODUCT_SLUG",
                value: config_service_1.ConfigManager.config.productSlug || "io-connect-desktop",
            },
        ],
        build: [
            { name: "COMPONENTS_PATH", value: "%IO_CD_ROOT_DIR%/components" },
            {
                name: "PRODUCT_VERSION",
                value: config_service_1.ConfigManager.config.version || "1.0.0",
            },
            {
                name: "PRODUCT_NAME",
                value: config_service_1.ConfigManager.config.productName || "io.Connect Desktop",
            },
            {
                name: "PRODUCT_SLUG",
                value: config_service_1.ConfigManager.config.productSlug || "io-connect-desktop",
            },
        ],
    };
};
async function applyModifications(mode) {
    try {
        logger.info(`Resetting previous modifications...`);
        // Reset regular modifications (without basePath)
        modifications_tracker_1.ModificationsTracker.resetAll();
        // On macOS, also reset iocd component modifications that use basePath tracking
        if (process.platform === "darwin") {
            const componentsDir = (0, path_1.join)(process.cwd(), "components");
            const iocdDir = (0, path_1.join)(componentsDir, "iocd");
            const appBundleName = config_service_1.ConfigManager.config.mac?.appBundleName;
            if (appBundleName && (0, fs_1.existsSync)(iocdDir)) {
                const targetAppBundlePath = (0, path_1.join)(iocdDir, appBundleName);
                // Reset modifications tracked relative to the app bundle
                modifications_tracker_1.ModificationsTracker.resetAll(targetAppBundlePath);
            }
        }
        logger.info(`Applying modifications in ${mode} mode...`);
        const componentsDir = (0, path_1.join)(process.cwd(), "components");
        if (!(0, fs_1.existsSync)(componentsDir)) {
            logger.debug("No components directory found - skipping modifications");
            return;
        }
        if (process.platform === "win32") {
            logger.debug("Running on Windows, proceeding with Windows-specific modifications");
            await (0, windows_helper_1.modifyExe)();
            logger.debug("Windows-specific modifications applied successfully!");
        }
        // Define modification directories to process in order using new structure
        const modificationDirs = [
            {
                path: (0, path_1.join)(process.cwd(), "modifications", "base"),
                name: "base modifications",
            },
            {
                path: (0, path_1.join)(process.cwd(), "modifications", mode),
                name: `${mode}-specific modifications`,
            },
        ];
        let totalModificationsApplied = 0;
        // Process each modification directory in sequence
        for (const { path: modificationsDir, name: dirDescription, } of modificationDirs) {
            if (!(0, fs_1.existsSync)(modificationsDir)) {
                logger.debug(`No ${dirDescription} directory found - skipping`);
                continue;
            }
            logger.info(`Processing ${dirDescription} from: ${(0, path_1.relative)(process.cwd(), modificationsDir)}`);
            // Get all component directories in this modifications folder
            const componentDirs = (0, fs_1.readdirSync)(modificationsDir, {
                withFileTypes: true,
            })
                .filter(dirent => dirent.isDirectory())
                .map(dirent => dirent.name);
            if (componentDirs.length === 0) {
                logger.debug(`No component modifications found in ${dirDescription}`);
                continue;
            }
            for (const componentName of componentDirs) {
                const modComponentDir = (0, path_1.join)(modificationsDir, componentName);
                const baseTargetComponentDir = (0, path_1.join)(componentsDir, componentName);
                if (!(0, fs_1.existsSync)(baseTargetComponentDir)) {
                    logger.warn(`Target component directory ${componentName} not found - skipping`);
                    continue;
                }
                // Handle macOS app bundle structure for iocd component
                if (process.platform === "darwin" && componentName === "iocd") {
                    await processIocdComponentOnMacOS(modComponentDir, baseTargetComponentDir, mode);
                }
                else {
                    const targetComponentDir = getActualComponentPath(baseTargetComponentDir, componentName);
                    if (!(0, fs_1.existsSync)(targetComponentDir)) {
                        logger.warn(`Target component path ${(0, path_1.relative)(process.cwd(), targetComponentDir)} not found - skipping`);
                        continue;
                    }
                    logger.info(`Applying ${dirDescription} for component: ${componentName}`);
                    await processModificationDirectory(modComponentDir, targetComponentDir, mode);
                }
                totalModificationsApplied++;
            }
        }
        if (totalModificationsApplied === 0) {
            logger.info("No modifications found to apply");
        }
        else {
            logger.info(`All modifications applied successfully! (${totalModificationsApplied} component modification sets processed)`);
        }
    }
    catch (error) {
        logger.error(`Failed to apply modifications: ${error}`);
        throw error;
    }
}
/**
 * Recursively process a modification directory and apply changes to the target
 * @param modDir - The modifications source directory
 * @param targetDir - The target directory to apply modifications to
 * @param mode - The mode (dev or build)
 * @param basePath - Optional base path for tracking (used for macOS iocd component)
 * @param physicalBasePath - Optional physical base path where files actually exist (for computing relative paths)
 */
async function processModificationDirectory(modDir, targetDir, mode, basePath, physicalBasePath) {
    const items = (0, fs_1.readdirSync)(modDir, { withFileTypes: true });
    logger.debug(`Processing modifications in ${modDir} with targetDir ${targetDir}`);
    for (const item of items) {
        logger.debug(`Processing item ${item.name}`);
        const modItemPath = (0, path_1.join)(modDir, item.name);
        const targetItemPath = (0, path_1.join)(targetDir, item.name);
        if (item.isDirectory()) {
            await processDirectory(modItemPath, targetItemPath, mode, basePath, physicalBasePath);
        }
        else {
            await processFile(modItemPath, targetItemPath, item.name, mode, basePath, physicalBasePath);
        }
    }
}
/**
 * Process a directory modification
 */
async function processDirectory(modDirPath, targetDirPath, mode, basePath, physicalBasePath) {
    logger.debug(`Processing directory ${modDirPath} with targetDir ${targetDirPath}`);
    // Check for .replace marker in the modification directory
    const replaceMarkerPath = (0, path_1.join)(modDirPath, ".replace");
    const hasReplaceMarker = (0, fs_1.existsSync)(replaceMarkerPath);
    // Check for .delete marker in the modification directory
    const deleteMarkerPath = (0, path_1.join)(modDirPath, ".delete");
    const hasDeleteMarker = (0, fs_1.existsSync)(deleteMarkerPath);
    if (hasDeleteMarker) {
        // Delete the entire target directory
        if ((0, fs_1.existsSync)(targetDirPath)) {
            logger.debug(` Deleting directory: ${(0, path_1.relative)(process.cwd(), targetDirPath)}`);
            trackAndRemove(targetDirPath, basePath, physicalBasePath);
            logger.debug(`Deleted directory: ${(0, path_1.relative)(process.cwd(), targetDirPath)}`);
        }
        return;
    }
    if (hasReplaceMarker) {
        // Replace the entire directory
        if ((0, fs_1.existsSync)(targetDirPath)) {
            logger.debug(`Replacing directory: ${(0, path_1.relative)(process.cwd(), targetDirPath)}`);
            trackAndRemove(targetDirPath, basePath, physicalBasePath);
        }
        // Create target directory
        (0, fs_1.mkdirSync)(targetDirPath, { recursive: true });
        // Copy all files except the .replace marker
        const items = (0, fs_1.readdirSync)(modDirPath, { withFileTypes: true });
        for (const item of items) {
            if (item.name === ".replace")
                continue;
            const srcPath = (0, path_1.join)(modDirPath, item.name);
            const destPath = (0, path_1.join)(targetDirPath, item.name);
            if (item.isDirectory()) {
                await copyDirectoryRecursive(srcPath, destPath);
            }
            else {
                ensureDirectoryExists((0, path_1.dirname)(destPath));
                (0, fs_1.copyFileSync)(srcPath, destPath);
                logger.debug(`Copied file: ${(0, path_1.relative)(process.cwd(), destPath)}`);
            }
        }
        logger.debug(`Replaced directory: ${(0, path_1.relative)(process.cwd(), targetDirPath)}`);
    }
    else {
        // Normal directory processing - ensure target exists and recurse
        if (!(0, fs_1.existsSync)(targetDirPath)) {
            (0, fs_1.mkdirSync)(targetDirPath, { recursive: true });
        }
        await processModificationDirectory(modDirPath, targetDirPath, mode, basePath, physicalBasePath);
    }
}
/**
 * Process a file modification
 */
async function processFile(modFilePath, targetFilePath, fileName, mode, basePath, physicalBasePath) {
    if (fileName.endsWith(".gitkeep")) {
        // ignore .gitkeep files
        return;
    }
    // Handle .delete files
    if (fileName.endsWith(".delete")) {
        const targetFile = fileName.slice(0, -7); // Remove '.delete' suffix
        const actualTargetPath = (0, path_1.join)((0, path_1.dirname)(targetFilePath), targetFile);
        logger.debug(`Deleting file: ${(0, path_1.relative)(process.cwd(), actualTargetPath)}`);
        if ((0, fs_1.existsSync)(actualTargetPath)) {
            trackAndRemove(actualTargetPath, basePath, physicalBasePath);
            logger.debug(`Deleted file: ${(0, path_1.relative)(process.cwd(), actualTargetPath)}`);
        }
        else {
            logger.debug(`File not found: ${(0, path_1.relative)(process.cwd(), actualTargetPath)}`);
        }
        return;
    }
    // Handle .json.merge files
    // names can be like system.json.merge or system.json.merge-suffix
    const isJSONMergeFile = fileName.endsWith(".json.merge") || fileName.includes(".json.merge-");
    if (isJSONMergeFile) {
        // Check for platform-specific suffix (darwin or win32)
        const currentPlatform = process.platform;
        const supportedPlatforms = ["darwin", "win32"];
        // Extract suffix after .json.merge-
        const suffixMatch = fileName.match(/\.json\.merge-(.+)$/);
        if (suffixMatch) {
            const suffix = suffixMatch[1];
            // Check if suffix contains a platform identifier
            const platformInSuffix = supportedPlatforms.find(p => suffix.includes(p));
            if (platformInSuffix && platformInSuffix !== currentPlatform) {
                logger.debug(`Skipping ${fileName} - platform-specific for ${platformInSuffix}, current platform is ${currentPlatform}`);
                return; // Skip this file
            }
        }
        // get the base filename without the .merge or .merge-suffix
        const baseFileName = fileName.split(".json.merge")[0];
        const targetFile = baseFileName + ".json"; // Remove '.merge' and ensure .json
        const actualTargetPath = (0, path_1.join)((0, path_1.dirname)(targetFilePath), targetFile);
        modifications_tracker_1.ModificationsTracker.add(actualTargetPath, basePath, physicalBasePath);
        await mergeJsonFile(modFilePath, actualTargetPath);
        expandVarsSync(actualTargetPath, mode);
        return;
    }
    // Handle regular file replacement
    ensureDirectoryExists((0, path_1.dirname)(targetFilePath));
    modifications_tracker_1.ModificationsTracker.add(targetFilePath, basePath, physicalBasePath);
    (0, fs_1.copyFileSync)(modFilePath, targetFilePath);
    if (fileName.endsWith(".json")) {
        // expand vars in the final file only if json
        expandVarsSync(targetFilePath, mode);
    }
    logger.debug(`Copied file: ${(0, path_1.relative)(process.cwd(), targetFilePath)}`);
}
function expandVarsSync(filePath, mode) {
    const vars = getVars()[mode] || [];
    // Read the file content
    let content = (0, fs_1.readFileSync)(filePath, "utf-8");
    // Expand environment variables in ${VAR_NAME} format
    content = content.replace(/\$\{([^}]+)\}/g, (_, varName) => {
        let varsValue = vars.find((v) => v.name === varName)?.value;
        const value = varsValue || process.env[varName] || "";
        // Normalize path separators to forward slashes for JSON safety
        const finalValue = value.replace(/\\/g, "/");
        logger.debug(`Expanded variable ${varName} to ${finalValue}`);
        return finalValue;
    });
    // Write the modified content back to the file
    (0, fs_1.writeFileSync)(filePath, content, "utf-8");
}
/**
 * Merge a JSON modification file with the target JSON file
 */
async function mergeJsonFile(mergePath, targetPath) {
    try {
        logger.debug(`Merging JSON file: ${mergePath} into ${targetPath}`);
        // Read the merge file
        const mergeContent = (0, fs_1.readFileSync)(mergePath, "utf-8");
        const mergeData = JSON.parse(mergeContent);
        let targetData = {};
        // Read existing target file if it exists
        if ((0, fs_1.existsSync)(targetPath)) {
            const targetContent = (0, fs_1.readFileSync)(targetPath, "utf-8");
            targetData = JSON.parse(targetContent);
        }
        // Deep merge the objects
        const mergedData = (0, deep_merge_1.deepMerge)(targetData, mergeData);
        // Write the merged result
        ensureDirectoryExists((0, path_1.dirname)(targetPath));
        (0, fs_1.writeFileSync)(targetPath, JSON.stringify(mergedData, null, 4), "utf-8");
        logger.debug(`Merged JSON file: ${(0, path_1.relative)(process.cwd(), targetPath)}`);
    }
    catch (error) {
        logger.error(`Failed to merge JSON file ${mergePath}: ${error}`);
        throw error;
    }
}
/**
 * Recursively copy a directory
 */
async function copyDirectoryRecursive(srcDir, destDir) {
    (0, fs_1.mkdirSync)(destDir, { recursive: true });
    const items = (0, fs_1.readdirSync)(srcDir, { withFileTypes: true });
    for (const item of items) {
        const srcPath = (0, path_1.join)(srcDir, item.name);
        const destPath = (0, path_1.join)(destDir, item.name);
        if (item.isDirectory()) {
            await copyDirectoryRecursive(srcPath, destPath);
        }
        else {
            (0, fs_1.copyFileSync)(srcPath, destPath);
            logger.debug(`📄 Copied file: ${(0, path_1.relative)(process.cwd(), destPath)}`);
        }
    }
}
/**
 * Ensure a directory exists
 */
function ensureDirectoryExists(dirPath) {
    if (!(0, fs_1.existsSync)(dirPath)) {
        (0, fs_1.mkdirSync)(dirPath, { recursive: true });
    }
}
/**
 * Get the actual component path, handling platform-specific structures
 * On macOS, iocd component is packaged as an app bundle with extra directory levels
 */
function getActualComponentPath(baseComponentPath, componentName) {
    // Handle macOS app bundle structure for iocd component
    if (process.platform === "darwin" && componentName === "iocd") {
        // Check for app bundle structure: iocd/{BUNDLE_NAME}/Contents/
        const items = (0, fs_1.readdirSync)(baseComponentPath, { withFileTypes: true });
        const appBundle = items.find(item => item.isDirectory() && item.name.endsWith(".app"));
        if (appBundle) {
            const appBundlePath = (0, path_1.join)(baseComponentPath, appBundle.name);
            const contentsPath = (0, path_1.join)(appBundlePath, "Contents");
            // Verify the Contents directory exists
            if ((0, fs_1.existsSync)(contentsPath)) {
                logger.debug(`Using macOS app bundle path: ${(0, path_1.relative)(process.cwd(), contentsPath)}`);
                return contentsPath;
            }
        }
    }
    // For other platforms or components, use the base path
    return baseComponentPath;
}
/**
 * Special handling for iocd component on macOS app bundles.
 *
 * This function handles the complex process of applying modifications to macOS .app bundles,
 * which require special handling due to code signing and permission constraints.
 *
 * Strategy (optimized for performance and symlink preservation):
 * 1. Reset any previous modifications tracked with the target bundle as basePath
 * 2. Move the original bundle to temp (fast, no copy overhead)
 * 3. Apply modifications to the temp bundle, tracking individual files with basePath
 * 4. Rename app bundle contents (executable, Info.plist, Helper apps)
 * 5. Move the modified bundle back to the target location
 *
 * This approach:
 * - Tracks individual modified files instead of the whole bundle
 * - Uses move instead of copy for ~500MB bundles (much faster)
 * - Preserves Electron Framework symlinks (avoids codesign failures)
 * - Supports proper incremental restore on subsequent runs
 *
 * @param modComponentDir - Path to the modifications directory for the iocd component
 * @param baseTargetComponentDir - Path to the base target component directory (components/iocd)
 * @param mode - The mode (dev or build)
 * @throws {Error} When macOS helper is not available or app bundle cannot be found
 * @throws {Error} When file operations fail during the modification process
 */
async function processIocdComponentOnMacOS(modComponentDir, baseTargetComponentDir, mode) {
    logger.info("Applying modifications for component: iocd (macOS app bundle)");
    const appBundleName = config_service_1.ConfigManager.config.mac.appBundleName;
    const targetAppBundlePath = (0, path_1.join)(baseTargetComponentDir, appBundleName); // where we expect the app bundle to be at the end
    let currentAppBundlePath = targetAppBundlePath; // where the app bundle currently is
    // Check if macOS helper is available
    if (!macOSHelper) {
        logger.error("macOS helper module is required but not available");
        throw new Error("macOS helper module is required but not available");
    }
    if (!(0, fs_1.existsSync)(targetAppBundlePath)) {
        // APP BUNDLE NOT FOUND, TRY io.Connect Desktop.app as fallback
        const fallbackAppBundlePath = (0, path_1.join)(baseTargetComponentDir, "io.Connect Desktop.app");
        if ((0, fs_1.existsSync)(fallbackAppBundlePath)) {
            logger.debug(`Using fallback app bundle: ${(0, path_1.relative)(process.cwd(), fallbackAppBundlePath)}`);
            currentAppBundlePath = fallbackAppBundlePath;
        }
        else {
            throw new Error(`App bundle path not found: ${currentAppBundlePath}`);
        }
    }
    // Temp directory structure: .iocd-cli/temp/components/iocd/<bundle>.app
    const tempDir = (0, path_1.join)(process.cwd(), ".iocd-cli", "temp", "components", "iocd");
    const tempAppPath = (0, path_1.join)(tempDir, appBundleName);
    try {
        // Create temp directory
        if (!(0, fs_1.existsSync)(tempDir)) {
            (0, fs_1.mkdirSync)(tempDir, { recursive: true });
        }
        // Remove any existing temp app from previous failed runs
        if ((0, fs_1.existsSync)(tempAppPath)) {
            (0, fs_1.rmSync)(tempAppPath, { recursive: true, force: true });
        }
        // Step 1: Move the bundle to temp (fast, preserves symlinks)
        logger.debug(`Moving app bundle to temp location: ${tempAppPath}`);
        (0, child_process_1.execSync)(`mv "${currentAppBundlePath}" "${tempAppPath}"`, {
            stdio: "pipe",
        });
        // Step 2: Apply modifications to the temp bundle
        // Track individual files with targetAppBundlePath as basePath so resetAll() can find them later
        // Pass tempAppPath as physicalBasePath since files are physically there during processing
        const tempContentsPath = (0, path_1.join)(tempAppPath, "Contents");
        await processModificationDirectory(modComponentDir, tempContentsPath, mode, targetAppBundlePath, tempAppPath);
        // Step 3: Rename app bundle contents (executable, Info.plist, Helper apps)
        await macOSHelper.renameAppBundleContents(tempAppPath, appBundleName);
        // Step 4: Move the modified bundle back to target location
        logger.debug(`Moving modified app bundle back: ${targetAppBundlePath}`);
        (0, child_process_1.execSync)(`mv "${tempAppPath}" "${targetAppBundlePath}"`, { stdio: "pipe" });
        // Step 5: Clean up temp directory if empty
        try {
            // Remove the iocd temp dir if it's now empty
            if ((0, fs_1.existsSync)(tempDir) && (0, fs_1.readdirSync)(tempDir).length === 0) {
                (0, fs_1.rmSync)(tempDir, { recursive: true, force: true });
            }
        }
        catch (cleanupError) {
            // Ignore cleanup errors - not critical
            logger.debug(`Temp cleanup skipped: ${cleanupError}`);
        }
        logger.debug("Successfully applied modifications to macOS app bundle");
    }
    catch (error) {
        logger.error(`Failed to process iocd component on macOS: ${error}`);
        // Recovery: Try to move the bundle back if it's still in temp
        if ((0, fs_1.existsSync)(tempAppPath) && !(0, fs_1.existsSync)(targetAppBundlePath)) {
            try {
                logger.warn("Attempting to recover by moving bundle back from temp...");
                (0, child_process_1.execSync)(`mv "${tempAppPath}" "${targetAppBundlePath}"`, {
                    stdio: "pipe",
                });
                logger.info("Recovery successful - bundle restored to original location");
            }
            catch (recoveryError) {
                logger.error(`Recovery failed: ${recoveryError}`);
            }
        }
        throw error;
    }
}
function trackAndRemove(targetPath, basePath, physicalBasePath) {
    modifications_tracker_1.ModificationsTracker.add(targetPath, basePath, physicalBasePath);
    (0, fs_1.rmSync)(targetPath, { recursive: true, force: true });
}
//# sourceMappingURL=modifications.service.js.map