import * as fs from "fs";
import * as path from "path";
import { findPackageRoot } from "./importPaths.js";
// Confirm `target` (already realpath-ed) lives strictly inside the
// realpath-ed project root for `target`. The trailing `+ path.sep` on the
// prefix prevents a sibling directory sharing a string prefix from passing.
// The project root is the nearest package.json walking up from `target`.
// If no package.json is found above `target`, refuse (target is loose in
// the filesystem with no project containing it).
function checkInsideProject(target) {
    if (target === "/") {
        return {
            success: false,
            message: `Refusing to delete root dir`,
        };
    }
    let projectRoot;
    try {
        projectRoot = fs.realpathSync(findPackageRoot(path.dirname(target)));
    }
    catch (err) {
        return {
            success: false,
            message: `Refusing to delete '${target}': no project root found above it (${err instanceof Error ? err.message : String(err)}).`,
        };
    }
    if (target === projectRoot || !target.startsWith(projectRoot + path.sep)) {
        return {
            success: false,
            message: `Refusing to delete '${target}': outside project root '${projectRoot}'.`,
        };
    }
    return null;
}
function safeDelete(targetPath, kind, dryRun) {
    if (!fs.existsSync(targetPath)) {
        return { success: false, message: `Path does not exist: '${targetPath}'.` };
    }
    const resolved = fs.realpathSync(path.resolve(targetPath));
    const stat = fs.statSync(resolved);
    const isFile = stat.isFile();
    const isDir = stat.isDirectory();
    if (kind === "file" && !isFile) {
        return { success: false, message: `Not a file: '${resolved}'.` };
    }
    if (kind === "directory" && !isDir) {
        return { success: false, message: `Not a directory: '${resolved}'.` };
    }
    const containment = checkInsideProject(resolved);
    if (containment)
        return containment;
    if (dryRun) {
        return { success: true, message: `[DRY RUN]: would have deleted ${resolved}` };
    }
    // Best-effort: callers often use this in `finally` blocks where a
    // throw would mask the real error or turn a clean exit into a crash.
    // `force: true` swallows ENOENT/permission errors that race against
    // the real delete; if anything else goes wrong, return the message
    // instead of propagating.
    try {
        if (kind === "file")
            fs.unlinkSync(resolved);
        else
            fs.rmSync(resolved, { recursive: true, force: true });
        return { success: true };
    }
    catch (err) {
        return {
            success: false,
            message: `Failed to delete '${resolved}': ${err instanceof Error ? err.message : String(err)}`,
        };
    }
}
export function safeDeleteFile(targetPath, dryRun = true) {
    return safeDelete(targetPath, "file", dryRun);
}
export function safeDeleteDirectory(targetPath, dryRun = true) {
    return safeDelete(targetPath, "directory", dryRun);
}
export function escape(str) {
    return (str
        //.replace(/\\/g, "\\\\")
        .replace(/"/g, '\\"')
        .replace(/`/g, "\\`")
        .replace(/\$/g, "\\$"));
}
export function deepCopy(obj) {
    return JSON.parse(JSON.stringify(obj));
}
export function zip(arr1, arr2) {
    const length = Math.min(arr1.length, arr2.length);
    const result = [];
    for (let i = 0; i < length; i++) {
        result.push([arr1[i], arr2[i]]);
    }
    return result;
}
export function uniq(arr) {
    return Array.from(new Set(arr));
}
/**
 * Like `uniq`, but de-duplicates by a derived key. The first occurrence
 * of each key is kept; later duplicates are dropped. Useful for de-duping
 * structured values where identity-based Set comparison wouldn't work
 * (e.g. compare-by-JSON.stringify).
 */
export function uniqBy(arr, keyFn) {
    const seen = new Set();
    const result = [];
    for (const item of arr) {
        const key = keyFn(item);
        if (seen.has(key))
            continue;
        seen.add(key);
        result.push(item);
    }
    return result;
}
/**
 * Simple object check.
 */
export function isObject(item) {
    return item && typeof item === "object" && !Array.isArray(item);
}
/**
 * Deep merge two objects.
 */
export function mergeDeep(_objA, _objB) {
    const objA = structuredClone(_objA);
    const objB = structuredClone(_objB);
    if (isObject(objA) && isObject(objB)) {
        for (const key in objB) {
            // console.log("key", key);
            if (isObject(objB[key])) {
                // console.log("is object!");
                if (!objA[key]) {
                    // console.log("initializing", key);
                    Object.assign(objA, { [key]: {} });
                }
                else if (!isObject(objA[key])) {
                    // console.log("overwriting non-object key", key, "with object");
                    Object.assign(objA, { [key]: {} });
                }
                objA[key] = mergeDeep(objA[key], objB[key]);
            }
            else {
                // console.log("setting", key, "to", objB[key]);
                Object.assign(objA, { [key]: objB[key] });
            }
        }
    }
    return objA;
}
export function round(num, decimals = 2) {
    const factor = 10 ** decimals;
    return Math.round(num * factor) / factor;
}
