package com.ruoyi.cc.utils;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
 * Resolve paths under recording_path with containment checks.
 */
public final class RecordingPathUtils {

    public enum ResolveStatus {
        OK,
        FORBIDDEN,
        NOT_FOUND,
        BAD_REQUEST
    }

    public static final class FileResolveResult {
        private final ResolveStatus status;
        private final Path path;

        private FileResolveResult(ResolveStatus status, Path path) {
            this.status = status;
            this.path = path;
        }

        public static FileResolveResult ok(Path path) {
            return new FileResolveResult(ResolveStatus.OK, path);
        }

        public static FileResolveResult of(ResolveStatus status) {
            return new FileResolveResult(status, null);
        }

        public ResolveStatus getStatus() {
            return status;
        }

        public Path getPath() {
            return path;
        }
    }

    private RecordingPathUtils() {
    }

    /**
     * Resolve {@code recordingPath/rootId} and ensure it stays under recording root.
     *
     * @return normalized directory path, or null if rootId/path is unsafe
     */
    public static Path resolveRootDir(String recordingPath, String rootId) {
        if (!FsEslNameUtils.isSafeName(rootId)) {
            return null;
        }
        Path base = Paths.get(recordingPath).toAbsolutePath().normalize();
        Path dir = base.resolve(rootId).normalize();
        if (!dir.startsWith(base)) {
            return null;
        }
        return dir;
    }

    /**
     * Resolve a file under {@code recordingPath/rootId/fileName}.
     *
     * @return normalized file path, or null if any segment escapes the recording root
     */
    public static Path resolveRootFile(String recordingPath, String rootId, String fileName) {
        Path dir = resolveRootDir(recordingPath, rootId);
        if (dir == null || fileName == null || fileName.isEmpty()) {
            return null;
        }
        if (fileName.indexOf('/') >= 0 || fileName.indexOf('\\') >= 0 || fileName.indexOf('\0') >= 0) {
            return null;
        }
        if (fileName.contains("..")) {
            return null;
        }
        Path file = dir.resolve(fileName).normalize();
        if (!file.startsWith(dir)) {
            return null;
        }
        return file;
    }

    /**
     * Resolve a download path under recording_path.
     * Compatible with relative names and absolute paths that stay under the recording root.
     * Applies normalize + lexical containment, then toRealPath (symlink-aware) containment.
     */
    public static FileResolveResult resolveReadableRecordingFile(String recordingPath, String filename) {
        if (filename == null || filename.isEmpty() || filename.indexOf('\0') >= 0) {
            return FileResolveResult.of(ResolveStatus.BAD_REQUEST);
        }
        try {
            Path base = Paths.get(recordingPath).toAbsolutePath().normalize();
            Path baseReal = Files.exists(base) ? base.toRealPath() : base;

            Path input = Paths.get(filename);
            Path candidate = input.isAbsolute()
                    ? input.toAbsolutePath().normalize()
                    : base.resolve(filename).normalize();

            // Lexical containment (blocks ../ before touching FS)
            if (!candidate.startsWith(base) && !candidate.startsWith(baseReal)) {
                return FileResolveResult.of(ResolveStatus.FORBIDDEN);
            }

            if (!Files.exists(candidate)) {
                return FileResolveResult.of(ResolveStatus.NOT_FOUND);
            }

            // Reject symlink escape: real path must remain under recording root
            Path realFile = candidate.toRealPath();
            if (!realFile.startsWith(baseReal)) {
                return FileResolveResult.of(ResolveStatus.FORBIDDEN);
            }
            if (!Files.isRegularFile(realFile) || !Files.isReadable(realFile)) {
                return FileResolveResult.of(ResolveStatus.NOT_FOUND);
            }
            return FileResolveResult.ok(realFile);
        } catch (IOException e) {
            return FileResolveResult.of(ResolveStatus.BAD_REQUEST);
        }
    }
}
