/**
 * Daemon Binary Resolver
 *
 * Resolves the path to the ghostflow-daemon binary based on platform and architecture.
 * Supports both development (local binary) and production (npm binary packages) modes.
 *
 * Uses @effect/platform stable modules (FileSystem, Path) for file operations.
 */
import { FileSystem, Path } from "@effect/platform";
import { Array as Arr, Data, Effect, Option } from "effect";
import * as os from "node:os";
import { fileURLToPath } from "node:url";
// ============================================================================
// Errors
// ============================================================================
/**
 * Error during daemon path resolution.
 * Used when the daemon binary cannot be located or validated.
 */
export class ResolverError extends Data.TaggedError("ResolverError") {
}
// ============================================================================
// Path Resolution Helpers
// ============================================================================
/**
 * Find the first path that exists from a list of candidates.
 * Uses Effect.findFirst for functional short-circuit evaluation.
 */
const findExistingPath = (paths, fs) => Effect.findFirst(paths, (p) => fs.exists(p));
/**
 * Resolve a path to its normalized absolute form.
 * Relative paths are resolved against the provided base directory.
 */
const resolveToAbsolute = (inputPath, basePath, pathService) => {
    const absolutePath = pathService.isAbsolute(inputPath)
        ? inputPath
        : pathService.join(basePath, inputPath);
    return pathService.normalize(absolutePath);
};
// ============================================================================
// Platform Detection
// ============================================================================
/** Map of supported platform-architecture combinations to package names */
const PLATFORM_PACKAGE_MAP = new Map([
    ["darwin-arm64", "@ghostflow/daemon-darwin-arm64"],
    ["darwin-x64", "@ghostflow/daemon-darwin-x64"]
]);
/** Path to the assembled app bundle in Application Support */
const APP_BUNDLE_PATH = "Library/Application Support/ghostflow/Ghostflow.app/Contents/MacOS/ghostflow-daemon";
/**
 * Get the binary package name for the current platform.
 * Returns Option.none() for unsupported platforms.
 */
const getBinaryPackageName = () => {
    const platformKey = `${os.platform()}-${os.arch()}`;
    return Option.fromNullable(PLATFORM_PACKAGE_MAP.get(platformKey));
};
// ============================================================================
// Path Resolution
// ============================================================================
/**
 * Try to resolve binary from the platform-specific npm package.
 */
const resolveFromNpmPackage = () => Effect.gen(function* () {
    const fs = yield* FileSystem.FileSystem;
    const pathService = yield* Path.Path;
    const packageName = yield* Option.match(getBinaryPackageName(), {
        onSome: Effect.succeed,
        onNone: () => Effect.fail(new ResolverError({ message: `Unsupported platform: ${os.platform()}-${os.arch()}` }))
    });
    // Try to find the package in node_modules
    // This file is at packages/cli/dist/resolver.js
    const __filename = fileURLToPath(import.meta.url);
    const __dirname = pathService.dirname(__filename);
    // Check various possible locations
    const searchPaths = [
        // Relative to this package (monorepo sibling)
        pathService.join(__dirname, "..", "..", "..", "node_modules", packageName, "bin", "ghostflow-daemon"),
        // Relative to project root
        pathService.join(__dirname, "..", "..", "node_modules", packageName, "bin", "ghostflow-daemon"),
        // Global install
        pathService.join(process.env.npm_config_prefix ?? "/usr/local", "lib", "node_modules", packageName, "bin", "ghostflow-daemon")
    ];
    const found = yield* findExistingPath(searchPaths, fs);
    return yield* Option.match(found, {
        onSome: Effect.succeed,
        onNone: () => Effect.fail(new ResolverError({
            message: `Binary package ${packageName} not found. Install it with: npm install ${packageName}`
        }))
    });
});
/**
 * Try to resolve binary from local development build.
 */
const resolveFromLocalBuild = () => Effect.gen(function* () {
    const fs = yield* FileSystem.FileSystem;
    const pathService = yield* Path.Path;
    const __filename = fileURLToPath(import.meta.url);
    const __dirname = pathService.dirname(__filename);
    // Common development locations
    const searchPaths = [
        // Sibling daemon package (monorepo structure: packages/cli -> packages/daemon)
        pathService.join(__dirname, "..", "..", "daemon", ".build", "release", "ghostflow-daemon"),
        pathService.join(__dirname, "..", "..", "daemon", ".build", "debug", "ghostflow-daemon"),
        pathService.join(__dirname, "..", "..", "daemon", ".build", "arm64-apple-macosx", "release", "ghostflow-daemon"),
        pathService.join(__dirname, "..", "..", "daemon", ".build", "arm64-apple-macosx", "debug", "ghostflow-daemon"),
        pathService.join(__dirname, "..", "..", "daemon", ".build", "x86_64-apple-macosx", "release", "ghostflow-daemon"),
        pathService.join(__dirname, "..", "..", "daemon", ".build", "x86_64-apple-macosx", "debug", "ghostflow-daemon"),
        // Monorepo root .build directory (Swift)
        pathService.join(__dirname, "..", "..", "..", ".build", "release", "ghostflow-daemon"),
        pathService.join(__dirname, "..", "..", "..", ".build", "debug", "ghostflow-daemon"),
        // Relative to CLI package
        pathService.join(__dirname, "..", "..", ".build", "release", "ghostflow-daemon"),
        pathService.join(__dirname, "..", "..", ".build", "debug", "ghostflow-daemon"),
        // Project root
        pathService.join(process.cwd(), ".build", "release", "ghostflow-daemon"),
        pathService.join(process.cwd(), ".build", "debug", "ghostflow-daemon")
    ];
    const found = yield* findExistingPath(searchPaths, fs);
    return yield* Option.match(found, {
        onSome: Effect.succeed,
        onNone: () => Effect.fail(new ResolverError({ message: "Local daemon binary not found. Build with: swift build -c release" }))
    });
});
/**
 * Try to resolve binary from PATH.
 */
/** PATH environment variable separator: colon on Unix, semicolon on Windows */
const PATH_SEPARATOR = os.platform() === "win32" ? ";" : ":";
const resolveFromPath = () => Effect.gen(function* () {
    const fsService = yield* FileSystem.FileSystem;
    const pathService = yield* Path.Path;
    const pathEnvOption = Option.fromNullable(process.env.PATH);
    const pathDirs = Option.match(pathEnvOption, {
        onNone: () => [],
        onSome: (pathEnv) => pathEnv.split(PATH_SEPARATOR)
    });
    const searchPaths = Arr.map(pathDirs, (dir) => pathService.join(dir, "ghostflow-daemon"));
    const found = yield* findExistingPath(searchPaths, fsService);
    return yield* Option.match(found, {
        onSome: Effect.succeed,
        onNone: () => Effect.fail(new ResolverError({ message: "ghostflow-daemon not found in PATH" }))
    });
});
// ============================================================================
// App Bundle Resolution
// ============================================================================
/**
 * Try to resolve binary from the assembled app bundle in Application Support.
 * This is the preferred location after postinstall assembles the bundle.
 */
const resolveFromAppBundle = () => Effect.gen(function* () {
    const fs = yield* FileSystem.FileSystem;
    const pathService = yield* Path.Path;
    const appBundlePath = pathService.join(os.homedir(), APP_BUNDLE_PATH);
    const exists = yield* fs.exists(appBundlePath);
    return yield* exists
        ? Effect.succeed(appBundlePath)
        : Effect.fail(new ResolverError({ message: "App bundle not found in Application Support" }));
});
// ============================================================================
// Public API
// ============================================================================
/**
 * Resolve the path to the ghostflow-daemon binary.
 *
 * Resolution order:
 * 1. Assembled app bundle in ~/Library/Application Support/ghostflow/Ghostflow.app
 * 2. Platform-specific npm binary package (@ghostflow/daemon-darwin-arm64, etc.)
 * 3. Local development build (.build/release or .build/debug)
 * 4. System PATH
 *
 * @returns Effect that resolves to the binary path or fails with ResolverError
 */
export const resolveDaemonPath = () => resolveFromAppBundle().pipe(Effect.orElse(() => resolveFromNpmPackage()), Effect.orElse(() => resolveFromLocalBuild()), Effect.orElse(() => resolveFromPath()));
// ============================================================================
// Security Validation
// ============================================================================
/**
 * Allowed absolute path prefixes for daemon binary.
 * This prevents arbitrary binary execution via --daemon-path.
 * All paths must be absolute to prevent path injection attacks.
 */
const getAllowedPathPrefixes = (pathService, cwd) => {
    const homeDir = os.homedir();
    return [
        // System paths
        "/usr/local/bin/",
        "/opt/homebrew/bin/",
        // User paths
        pathService.join(homeDir, ".local", "bin") + "/",
        // App bundle in Application Support
        pathService.join(homeDir, "Library", "Application Support", "ghostflow", "Ghostflow.app") + "/",
        // Development paths (resolved to absolute from cwd)
        pathService.join(cwd, ".build", "release") + "/",
        pathService.join(cwd, ".build", "debug") + "/",
        // Node modules (resolved to absolute from cwd)
        pathService.join(cwd, "node_modules", "@ghostflow") + "/",
        pathService.join(cwd, "node_modules", ".pnpm") + "/"
    ];
};
/**
 * Check if the normalized path starts with an allowed prefix.
 * All prefixes are now absolute, so we only use startsWith.
 */
const matchesPrefix = (normalizedPath, prefix) => normalizedPath.startsWith(prefix);
/**
 * Check if the path is in an allowed location for security.
 * Resolves the path to absolute before checking against allowed prefixes.
 */
const isAllowedPath = (daemonPath, pathService, cwd) => {
    const normalizedPath = resolveToAbsolute(daemonPath, cwd, pathService);
    const allowedPrefixes = getAllowedPathPrefixes(pathService, cwd);
    return Arr.some(allowedPrefixes, (prefix) => matchesPrefix(normalizedPath, prefix));
};
/** Unix executable bit for user */
const EXECUTABLE_BIT = 0o100;
/** Check if original path contains traversal attempts (before normalization) */
const hasPathTraversal = (path) => path.includes("..");
/** Check if file mode indicates executable */
const isExecutableMode = (mode) => (mode & EXECUTABLE_BIT) !== 0;
/**
 * Validate a user-provided daemon path for security and existence.
 *
 * Security measures:
 * - Path traversal attempts (..) are rejected on the ORIGINAL path (before normalization)
 * - Path must resolve to an allowed location (all checks use absolute paths)
 * - Binary must exist and be executable
 */
export const validateDaemonPath = (daemonPath) => Effect.gen(function* () {
    const fsService = yield* FileSystem.FileSystem;
    const pathService = yield* Path.Path;
    const cwd = process.cwd();
    // SECURITY: Check for path traversal on ORIGINAL path before normalization
    // This prevents attacks like "/usr/local/bin/../../../tmp/evil"
    yield* hasPathTraversal(daemonPath)
        ? Effect.fail(new ResolverError({ message: "Path traversal (..) not allowed in daemon path" }))
        : Effect.void;
    // Resolve to absolute and normalize
    const normalizedPath = resolveToAbsolute(daemonPath, cwd, pathService);
    // Security: Validate path is in an allowed location
    yield* isAllowedPath(normalizedPath, pathService, cwd)
        ? Effect.void
        : Effect.fail(new ResolverError({
            message: "Daemon path must be in an allowed location (node_modules, .build, or system PATH). " +
                "Use 'ghostflow start' without --daemon-path for auto-detection."
        }));
    // Check existence
    const exists = yield* fsService.exists(normalizedPath);
    yield* exists
        ? Effect.void
        : Effect.fail(new ResolverError({ message: `Daemon binary not found: ${normalizedPath}` }));
    // Check if executable using stat to get file mode
    const fileInfo = yield* fsService.stat(normalizedPath);
    yield* isExecutableMode(fileInfo.mode)
        ? Effect.void
        : Effect.fail(new ResolverError({ message: `Daemon binary is not executable: ${normalizedPath}` }));
    return normalizedPath;
});
//# sourceMappingURL=resolver.js.map