import path from "node:path";
import * as fsPromises from "node:fs/promises";
import { parsePlan } from "@poe-code/pipeline";
import { planConfigScope, readMergedDocumentReadonly, resolveScope } from "@poe-code/poe-code-config";
import { hasOwnErrorCode } from "./error-codes.js";
import { readPlanMetadata, splitFrontmatter } from "./format.js";
function createDefaultFs() {
    return {
        readFile: fsPromises.readFile,
        writeFile: fsPromises.writeFile,
        readdir: fsPromises.readdir,
        realpath: fsPromises.realpath,
        stat: async (filePath) => {
            const stat = await fsPromises.stat(filePath);
            return {
                isFile: () => stat.isFile(),
                isDirectory: () => stat.isDirectory(),
                mtimeMs: stat.mtimeMs
            };
        },
        lstat: async (filePath) => {
            const stat = await fsPromises.lstat(filePath);
            return { isSymbolicLink: () => stat.isSymbolicLink() };
        },
        mkdir: async (directoryPath, mkdirOptions) => {
            await fsPromises.mkdir(directoryPath, mkdirOptions);
        },
        rename: fsPromises.rename,
        unlink: fsPromises.unlink
    };
}
function isNotFound(error) {
    return hasOwnErrorCode(error, "ENOENT");
}
function getOwnValue(record, key) {
    return record !== undefined && Object.prototype.hasOwnProperty.call(record, key)
        ? record[key]
        : undefined;
}
function resolveAbsoluteDirectory(dir, cwd, homeDir) {
    if (dir.startsWith("~/")) {
        return path.join(homeDir, dir.slice(2));
    }
    return path.isAbsolute(dir) ? dir : path.resolve(cwd, dir);
}
function isSupportedPlanFile(name) {
    const lowerName = name.toLowerCase();
    return lowerName.endsWith(".md") || lowerName.endsWith(".yaml") || lowerName.endsWith(".yml");
}
function isYamlPlanFile(name) {
    const lowerName = name.toLowerCase();
    return lowerName.endsWith(".yaml") || lowerName.endsWith(".yml");
}
function getPlanTypeLabel(kind) {
    switch (kind) {
        case "plan":
            return "Plan";
        case "pipeline":
            return "Pipeline";
        case "experiment":
            return "Experiment";
        case "ralph":
            return "Ralph";
        case "superintendent":
            return "Superintendent";
        case "superintendent-base":
            return "Superintendent Base";
    }
}
function getPlanRunner(kind) {
    switch (kind) {
        case "pipeline":
        case "experiment":
        case "ralph":
        case "superintendent":
            return kind;
        default:
            return undefined;
    }
}
async function resolveSharedPlanDirectory(options) {
    const envValue = getOwnValue(options.variables, "POE_PLAN_DIRECTORY")?.trim();
    if (envValue) {
        return envValue;
    }
    const document = await readMergedDocumentReadonly(options.fs, options.configPath, options.projectConfigPath);
    return resolveScope(planConfigScope.schema, document.plan, options.variables).plan_directory;
}
function toPlanKind(value, filePath) {
    if (value === "plan" ||
        value === "pipeline" ||
        value === "experiment" ||
        value === "ralph" ||
        value === "superintendent" ||
        value === "superintendent-base") {
        return value;
    }
    throw new Error(`${filePath}: unsupported frontmatter kind ${JSON.stringify(value)}`);
}
function classifyPlanKind(content, filePath) {
    if (isYamlPlanFile(filePath)) {
        parsePlan(content);
        return "pipeline";
    }
    const { data } = splitFrontmatter(content, filePath);
    if (data === undefined) {
        return "plan";
    }
    if (data.kind === undefined) {
        throw new Error(`${filePath}: missing required frontmatter kind`);
    }
    return toPlanKind(data.kind, filePath);
}
async function discoverSharedPlans(options) {
    const displayDir = await resolveSharedPlanDirectory(options);
    const absoluteDir = resolveAbsoluteDirectory(displayDir, options.cwd, options.homeDir);
    const canonicalDir = await options.fs.realpath(absoluteDir).catch((error) => {
        if (isNotFound(error)) {
            return undefined;
        }
        throw error;
    });
    if (canonicalDir === undefined) {
        return [];
    }
    if (canonicalDir !== path.resolve(absoluteDir)) {
        throw new Error(`Plan directory must not be a symbolic link: ${displayDir}`);
    }
    let entries;
    try {
        entries = await options.fs.readdir(absoluteDir);
    }
    catch (error) {
        if (isNotFound(error)) {
            return [];
        }
        throw error;
    }
    const plans = [];
    for (const name of entries) {
        if (!isSupportedPlanFile(name)) {
            continue;
        }
        const absolutePath = path.join(absoluteDir, name);
        const canonicalPath = await options.fs.realpath(absolutePath).catch((error) => {
            if (isNotFound(error)) {
                return undefined;
            }
            throw error;
        });
        if (canonicalPath === undefined) {
            continue;
        }
        if (canonicalPath !== path.resolve(absolutePath)) {
            throw new Error(`Plan file must not be a symbolic link: ${path.join(displayDir, name)}`);
        }
        const stat = await options.fs.stat(absolutePath).catch((error) => {
            if (isNotFound(error)) {
                return undefined;
            }
            throw error;
        });
        if (stat === undefined) {
            continue;
        }
        if (!stat.isFile()) {
            continue;
        }
        const displayPath = path.join(displayDir, name);
        const content = await options.fs.readFile(absolutePath, "utf8");
        const kind = classifyPlanKind(content, displayPath);
        if (options.kind && kind !== options.kind) {
            continue;
        }
        const metadata = await readPlanMetadata({
            kind,
            absolutePath,
            path: displayPath,
            fs: options.fs,
            content
        });
        plans.push({
            path: displayPath,
            absolutePath,
            kind,
            typeLabel: getPlanTypeLabel(kind),
            runner: getPlanRunner(kind),
            format: metadata.format,
            title: metadata.title,
            detail: metadata.detail,
            updatedAt: stat.mtimeMs
        });
    }
    return plans;
}
export async function discoverAllPlans(options) {
    const fs = options.fs ?? createDefaultFs();
    const results = await discoverSharedPlans({ ...options, fs });
    return results.sort((left, right) => {
        if (right.updatedAt !== left.updatedAt) {
            return right.updatedAt - left.updatedAt;
        }
        return left.path.localeCompare(right.path);
    });
}
