"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ModificationsTracker = void 0;
const fs_extra_1 = __importDefault(require("fs-extra"));
const path_1 = __importDefault(require("path"));
const crypto_1 = require("crypto");
const logger_1 = require("../../utils/logger");
const path_2 = require("../../utils/path");
const error_handler_1 = require("../../utils/error.handler");
const fs_helper_1 = require("../../utils/fs.helper");
const logger = logger_1.Logger.getInstance("modifications.tracker");
/**
 * Convert a path to POSIX format (forward slashes) for cross-platform storage.
 * This ensures stored paths are consistent regardless of the platform.
 */
function toPosixPath(p) {
    return p.replace(/\\/g, "/");
}
/**
 * Tracks and resets the local changes (modifications/* -> components/*) during dev or build runs.
 * The paths in each record are relative to the project root.
 *
 * Note: To avoid tracking the temporary MacOS app bundle modifications, the tracker skips if originalPath is outside components directory.
 */
class ModificationsTracker {
    static bakDirPathRelative = path_1.default.join(".iocd-cli", "backup");
    static get recordsFilePath() {
        return path_1.default.resolve(this.bakDirPathRelative, "records.json");
    }
    static get bakDirPath() {
        return path_1.default.resolve(this.bakDirPathRelative);
    }
    /**
     * Add a file or directory to the modification tracker.
     * If basePath is provided, paths are stored relative to basePath instead of process.cwd().
     * This is used for macOS app bundle modifications where files are tracked relative to the bundle path.
     *
     * @param srcPath - Absolute path to the file/directory being modified
     * @param basePath - Optional base path for storing in records (used for resetAll() matching)
     * @param physicalBasePath - Optional base path where the file physically exists (for computing relative paths).
     *                           If not provided, defaults to basePath. This is used when files are in a temp location
     *                           but will be moved to basePath later (e.g., macOS iocd bundle processing).
     */
    static add(srcPath, basePath, physicalBasePath) {
        // Skip tracking if outside components directory AND no basePath provided
        if (!basePath && !this.isComponentsSubPath(srcPath)) {
            logger.debug(`Skipped tracking modification outside components directory: ${srcPath}`);
            return;
        }
        // Use physicalBasePath for computing relative path (where file actually is)
        // Use basePath for storing in records (where file will be after processing)
        const relativeToPhysical = physicalBasePath || basePath || process.cwd();
        const srcPathRelative = toPosixPath(path_1.default.relative(relativeToPhysical, srcPath));
        const record = {
            srcPathRelative,
            bakPathRelative: fs_extra_1.default.existsSync(srcPath)
                ? path_1.default.join(this.bakDirPathRelative, (0, crypto_1.randomBytes)(8).toString("hex"))
                : null,
            basePath: basePath
                ? toPosixPath(path_1.default.relative(process.cwd(), basePath))
                : undefined,
        };
        const records = this.readRecords();
        if (records.some(e => e.srcPathRelative === record.srcPathRelative &&
            e.basePath === record.basePath)) {
            return; // Skip backup if the path is already tracked.
        }
        records.push(record);
        try {
            if (record.bakPathRelative) {
                (0, fs_helper_1.copyBundleSafe)(srcPath, path_1.default.resolve(record.bakPathRelative));
            }
            fs_extra_1.default.writeFileSync(this.recordsFilePath, JSON.stringify(records, null, 2), "utf-8");
        }
        catch (e) {
            logger.warn(`Failed to make a record for ${srcPath}: `, e);
            return;
        }
        logger.debug(`Recorded the modification on ${srcPath}${basePath ? ` (relative to ${basePath})` : ""}`);
    }
    /**
     * Reset all tracked modifications, restoring files to their original state.
     * If basePath is provided, only resets records that match that basePath.
     *
     * @param basePath - Optional base path to filter which records to reset (used for macOS iocd component)
     */
    static resetAll(basePath) {
        let records = this.readRecords(true);
        // Migrate old-style .app bundle records to new individual file tracking
        records = this.migrateOldAppBundleRecords(records);
        // Filter records if basePath is provided
        const recordsToReset = basePath
            ? records.filter(r => r.basePath === toPosixPath(path_1.default.relative(process.cwd(), basePath)))
            : records.filter(r => !r.basePath); // Only reset records without basePath when no basePath provided
        for (const record of [...recordsToReset]) {
            try {
                this.reset(record);
                // Remove the processed record from the full list
                const idx = records.findIndex(r => r === record);
                if (idx !== -1) {
                    records.splice(idx, 1);
                }
                fs_extra_1.default.writeFileSync(this.recordsFilePath, JSON.stringify(records, null, 2), "utf-8");
                if (record.bakPathRelative) {
                    fs_extra_1.default.removeSync(path_1.default.resolve(record.bakPathRelative));
                }
            }
            catch (e) {
                const suggestions = [
                    `Verify that the records.json contains valid JSON array: ${this.recordsFilePath}`,
                ];
                const resolveBase = record.basePath
                    ? path_1.default.resolve(record.basePath)
                    : process.cwd();
                const srcPath = path_1.default.resolve(resolveBase, record.srcPathRelative);
                if (fs_extra_1.default.existsSync(srcPath)) {
                    suggestions.push(`Verify that the source path is not locked: ${srcPath}`);
                }
                if (record.bakPathRelative) {
                    const bakPath = path_1.default.resolve(`${record.bakPathRelative}`);
                    suggestions.push(`Verify that the backup path exists: ${bakPath}`);
                }
                suggestions.push("Retry running the command. If error persists, run 'iocd setup' to install fresh components again.");
                throw new error_handler_1.CLIError(`Unexpected error when resetting modification on ${record.srcPathRelative}: ${e}`, { suggestions });
            }
        }
    }
    /**
     * Migrate old-style whole .app bundle records to the new individual file tracking approach.
     * This detects records pointing to .app bundles, restores them, and removes them from tracking.
     *
     * @param records - Current modification records
     * @returns Updated records array with old .app bundle records removed
     */
    static migrateOldAppBundleRecords(records) {
        const oldAppBundleRecords = records.filter(r => !r.basePath && r.srcPathRelative.endsWith(".app"));
        if (oldAppBundleRecords.length === 0) {
            return records;
        }
        logger.info(`Migrating ${oldAppBundleRecords.length} old .app bundle record(s) to new tracking format...`);
        for (const record of oldAppBundleRecords) {
            try {
                // Restore the old .app bundle using the old reset logic
                this.reset(record);
                // Clean up the backup
                if (record.bakPathRelative) {
                    fs_extra_1.default.removeSync(path_1.default.resolve(record.bakPathRelative));
                }
                // Remove from records
                const idx = records.findIndex(r => r === record);
                if (idx !== -1) {
                    records.splice(idx, 1);
                }
                logger.debug(`Migrated old .app bundle record: ${record.srcPathRelative}`);
            }
            catch (e) {
                logger.warn(`Failed to migrate old .app bundle record ${record.srcPathRelative}: ${e}`);
                // Continue with other records even if one fails
            }
        }
        // Save the updated records
        fs_extra_1.default.writeFileSync(this.recordsFilePath, JSON.stringify(records, null, 2), "utf-8");
        return records;
    }
    static clearAll() {
        fs_extra_1.default.emptyDirSync(this.bakDirPath);
        logger.info(`Cleared backup directory: ${this.bakDirPath}`);
    }
    static reset(record) {
        // Resolve srcPath using basePath if present, otherwise use process.cwd()
        const resolveBase = record.basePath
            ? path_1.default.resolve(record.basePath)
            : process.cwd();
        const srcPath = path_1.default.resolve(resolveBase, record.srcPathRelative);
        const bakPath = record.bakPathRelative
            ? path_1.default.resolve(record.bakPathRelative)
            : null;
        if (bakPath && !fs_extra_1.default.existsSync(bakPath)) {
            throw new Error(`Backup item does not exist at ${bakPath}`);
        }
        // The original operation can be an addition, a deletion or a replacement, remove it first.
        fs_extra_1.default.removeSync(srcPath);
        if (bakPath) {
            // Restore only if backup path is not null (i.e. the original operation was not an addition).
            (0, fs_helper_1.copyBundleSafe)(bakPath, srcPath);
        }
        logger.debug(`Reset the modification on ${srcPath}`);
    }
    static readRecords(shouldLog = false) {
        let records = [];
        fs_extra_1.default.ensureDirSync(this.bakDirPath);
        try {
            let content;
            if (fs_extra_1.default.existsSync(this.recordsFilePath)) {
                content = fs_extra_1.default.readFileSync(this.recordsFilePath, "utf-8");
            }
            !!shouldLog && logger.debug("Parsing records file: ", content);
            records = JSON.parse(content || "[]");
        }
        catch (e) {
            logger.warn(`Failed to read records: `, e);
        }
        return records;
    }
    static isComponentsSubPath(target) {
        const rel = path_1.default.relative(path_2.PathUtils.getComponentsDir(), target);
        return !!rel && !rel.startsWith("..") && !path_1.default.isAbsolute(rel);
    }
}
exports.ModificationsTracker = ModificationsTracker;
//# sourceMappingURL=modifications.tracker.js.map