package com.correx.infrastructure.tools.filesystem

import java.nio.file.Files
import java.nio.file.Path

/**
 * Symlink-safe path containment for the filesystem tools' jail. Mirrors the
 * materializer's discipline (resolve, then enforce containment) but adds
 * [Path.toRealPath] resolution so a symlink living inside an allowed directory
 * cannot smuggle access to a target outside it. Fails CLOSED: an empty
 * allow-list contains nothing.
 */
object PathJail {

    /**
     * Resolve [path] to a symlink-resolved absolute path. If the target does not
     * exist yet (new-file writes), resolve the nearest existing ancestor via
     * [Path.toRealPath] and re-append the not-yet-created segments — so a
     * symlinked ancestor is still followed to its real location before the
     * containment check.
     */
    fun realize(path: Path): Path {
        val absolute = path.toAbsolutePath().normalize()
        var existing: Path? = absolute
        val missing = ArrayDeque<Path>()
        while (existing != null && !Files.exists(existing)) {
            existing.fileName?.let { missing.addFirst(it) }
            existing = existing.parent
        }
        val anchor = existing ?: return absolute
        val real = runCatching { anchor.toRealPath() }.getOrDefault(anchor)
        return missing.fold(real) { acc, segment -> acc.resolve(segment) }.normalize()
    }

    /**
     * True iff [path] resolves inside one of [allowedRoots]. Empty roots ⇒ false
     * (fail closed). Both the candidate and each root are symlink-resolved before
     * comparison.
     */
    fun isContained(path: Path, allowedRoots: Set<Path>): Boolean {
        if (allowedRoots.isEmpty()) return false
        val real = realize(path)
        return allowedRoots.any { real.startsWith(realize(it)) }
    }
}
