/**
 * Sync the shipped XLL directory to a stable user-local path.
 *
 * The npm package ships xll/ containing:
 *   xll/ShortcutXL.xll     — the add-in DLL
 *   xll/modules/            — shortcut_xl.py and user UDF modules
 *   xll/python/             — Python 3.13 embeddable distribution (stdlib, DLLs, .pyd)
 *
 * On startup, if the package version has changed (or no sync has happened yet),
 * we sync xll/ to ~/.shortcut/agent/xll/ so the Windows registry key always
 * points to a stable path that survives npm re-installs.
 *
 * A version stamp (.sync-version) in the stable directory tracks the last
 * synced version. If it matches the current package version, sync is skipped
 * entirely — no filesystem walking, no stat comparisons.
 *
 * Note: files removed from the shipped directory are NOT deleted from the
 * stable directory. This is intentional — pip-installed packages live in
 * python/Lib/site-packages/ and must survive re-syncs.
 */
import { createHash } from 'crypto';
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, unlinkSync, utimesSync, writeFileSync } from 'fs';
import { join } from 'path';
import { getPackageDir, getStableXllDir, VERSION } from '../config.js';
import { hasVersionedPythonRuntimeFileDrift } from './python-abi.js';
/** Strip the NTFS Zone.Identifier alternate data stream (Mark of the Web). */
function stripMotw(filePath) {
    try {
        unlinkSync(`${filePath}:Zone.Identifier`);
    }
    catch {
        // No MOTW or not on NTFS — nothing to do
    }
}
/**
 * Returns true if `src` and `dest` have identical size and mtime.
 * Avoids reading file contents — stat-only comparison is ~100x faster
 * than MD5 hashing multi-MB DLLs on every startup.
 */
function filesMatch(src, dest) {
    if (!existsSync(dest))
        return false;
    const srcStat = statSync(src);
    const destStat = statSync(dest);
    return srcStat.size === destStat.size && srcStat.mtimeMs === destStat.mtimeMs;
}
/** Returns true if two files have identical content (SHA-256). */
function filesContentMatch(src, dest) {
    const hash = (p) => createHash('sha256').update(readFileSync(p)).digest('hex');
    return hash(src) === hash(dest);
}
/**
 * Recursively copy `src` directory to `dest`, only overwriting files
 * whose content has changed. Creates directories as needed.
 */
function syncDir(src, dest) {
    if (!existsSync(src))
        return { updated: 0, staleLocked: false };
    mkdirSync(dest, { recursive: true });
    let updated = 0;
    let staleLocked = false;
    for (const entry of readdirSync(src)) {
        const srcPath = join(src, entry);
        const destPath = join(dest, entry);
        const stat = statSync(srcPath);
        if (stat.isDirectory()) {
            const sub = syncDir(srcPath, destPath);
            updated += sub.updated;
            staleLocked = staleLocked || sub.staleLocked;
        }
        else if (stat.isFile()) {
            if (!filesMatch(srcPath, destPath)) {
                try {
                    copyFileSync(srcPath, destPath);
                    // Preserve mtime so the stat-based filesMatch() short-circuits next startup.
                    const srcMtime = statSync(srcPath).mtime;
                    utimesSync(destPath, srcMtime, srcMtime);
                    // Strip Mark of the Web so Excel doesn't show "file format mismatch" dialogs.
                    // npm downloads carry a Zone.Identifier ADS that Windows propagates through copies.
                    stripMotw(destPath);
                    updated++;
                }
                catch (err) {
                    // On Windows, Excel may have the XLL/DLL locked (EBUSY/EPERM).
                    const code = err.code;
                    if (code === 'EBUSY' || code === 'EPERM') {
                        // The stat-based check flagged a difference (usually just mtime after
                        // npm reinstall). Check actual content before alarming the user.
                        if (!filesContentMatch(srcPath, destPath)) {
                            staleLocked = true;
                        }
                        // If content matches, silently skip — no update needed.
                    }
                    else {
                        throw err;
                    }
                }
            }
        }
    }
    return { updated, staleLocked };
}
const SYNC_VERSION_FILE = '.sync-version';
const PIP_VERSION_FILE = '.pip-version';
export function hasPythonAbiDrift(shippedDir, stableDir) {
    return hasVersionedPythonRuntimeFileDrift(join(shippedDir, 'python'), join(stableDir, 'python'));
}
function resetStablePythonRuntime(stableDir) {
    try {
        rmSync(join(stableDir, 'python'), { recursive: true, force: true });
        try {
            unlinkSync(join(stableDir, PIP_VERSION_FILE));
        }
        catch {
            // No pip stamp yet.
        }
        return true;
    }
    catch (err) {
        console.warn('Warning: ShortcutXL needs to replace its embedded Python runtime, but Excel has files locked. ' +
            'Please close Excel and restart the Shortcut agent to apply it.');
        console.warn(`Warning: Python runtime reset failed: ${err}`);
        return false;
    }
}
export function stablePythonRuntimeNeedsReset() {
    return hasPythonAbiDrift(join(getPackageDir(), 'xll'), getStableXllDir());
}
/**
 * Sync shipped XLL files (including embedded Python distribution) to
 * the stable user-local directory.
 *
 * Compares the package version against a stamp in the stable directory.
 * If they match, sync is skipped entirely. On version mismatch (package update)
 * or first install (no stamp), a full sync runs. Pip-installed packages
 * in python/Lib/site-packages/ are preserved because syncDir only copies
 * from source — it never deletes files missing from source.
 *
 * Callers should check `xllReady` — if false on first install, the agent
 * cannot proceed with registration.
 */
export function syncXll() {
    const shippedDir = join(getPackageDir(), 'xll');
    const shippedXll = join(shippedDir, 'ShortcutXL.xll');
    const stableDir = getStableXllDir();
    const stableXll = join(stableDir, 'ShortcutXL.xll');
    const versionFile = join(stableDir, SYNC_VERSION_FILE);
    // Ensure stableDir exists.
    mkdirSync(stableDir, { recursive: true });
    const pythonRuntimeNeedsReset = hasPythonAbiDrift(shippedDir, stableDir);
    // Check version stamp — skip sync if already up to date.
    try {
        if (existsSync(versionFile)) {
            const stamped = readFileSync(versionFile, 'utf-8').trim();
            if (stamped === VERSION) {
                const xllReady = existsSync(stableXll);
                const shippedXllReady = existsSync(shippedXll);
                if (!pythonRuntimeNeedsReset &&
                    (!shippedXllReady || (xllReady && filesContentMatch(shippedXll, stableXll)))) {
                    return { updated: 0, stableDir, xllReady, skipped: true, staleLocked: false };
                }
                // A previous update may have stamped this version while Excel still
                // held ShortcutXL.xll open. Fall through and repair the stable copy.
            }
        }
    }
    catch {
        // Corrupt or unreadable stamp — proceed with sync.
    }
    let totalUpdated = 0;
    let hasStaleLockedFiles = false;
    let syncFailed = false;
    if (pythonRuntimeNeedsReset && !resetStablePythonRuntime(stableDir)) {
        hasStaleLockedFiles = true;
        syncFailed = true;
    }
    if (existsSync(shippedDir)) {
        try {
            const { updated, staleLocked } = syncDir(shippedDir, stableDir);
            totalUpdated += updated;
            hasStaleLockedFiles = hasStaleLockedFiles || staleLocked;
            if (staleLocked) {
                console.warn('Warning: A ShortcutXL update is available but Excel has files locked. ' +
                    'Please close Excel and restart the Shortcut agent to apply it.');
            }
        }
        catch (err) {
            syncFailed = true;
            console.warn(`Warning: XLL sync failed: ${err}`);
        }
    }
    // Write version stamp only after all changed files are updated. If Excel kept
    // a stale XLL locked, leave the old stamp so startup retries after Excel exits.
    if (!hasStaleLockedFiles && !syncFailed) {
        try {
            writeFileSync(versionFile, VERSION, 'utf-8');
        }
        catch {
            // Non-fatal; sync will just run again next time.
        }
    }
    const xllReady = existsSync(stableXll);
    return {
        updated: totalUpdated,
        stableDir,
        xllReady,
        skipped: false,
        staleLocked: hasStaleLockedFiles
    };
}
//# sourceMappingURL=sync-xll.js.map