import fs from "fs/promises";
import fsSync from "fs";
import path from "path";
import url from "url";
import os from "os";
import { Logger } from "./logger.js";
import { Context } from "./context/context.js";
import { VisibleError } from "./error.js";
import { blue } from "colorette";
import dotenv from "dotenv";
import { load } from "./stacks/build.js";
const DEFAULTS = {
    stage: undefined,
    ssmPrefix: undefined,
};
export const ProjectContext = Context.create("Project");
export function useProject() {
    return ProjectContext.use();
}
const CONFIG_EXTENSIONS = [
    ".config.ts",
    ".config.mts",
    ".config.cts",
    ".config.cjs",
    ".config.mjs",
    ".config.js",
];
export async function initProject(globals) {
    // Logger.debug("initing project");
    const root = globals.root || (await findRoot());
    const out = path.join(root, ".sst");
    await fs.mkdir(out, {
        recursive: true,
    });
    // Logger.debug("made out dir");
    let file;
    const [metafile, sstConfig] = await (async function () {
        for (const ext of CONFIG_EXTENSIONS) {
            file = path.join(root, "sst" + ext);
            if (!fsSync.existsSync(file))
                continue;
            // Logger.debug("found sst config");
            const [metafile, config] = await load(file, true);
            // Logger.debug("loaded sst config");
            return [metafile, config];
        }
        throw new VisibleError("Could not found a configuration file", "Make sure one of the following exists", ...CONFIG_EXTENSIONS.map((x) => `  - sst${x}`));
    })();
    const config = await Promise.resolve(sstConfig.config(globals));
    const stage = process.env.SST_STAGE ||
        globals.stage ||
        config.stage ||
        (await usePersonalStage(out)) ||
        (await promptPersonalStage(out));
    // Set stage to SST_STAGE so that if SST spawned processes are aware
    // of the stage. ie.
    // `sst deploy --stage prod`: `prod` stage passed in via CLI
    // -> spawns `open-next build`
    // -> spawns `sst bind`: `prod` stage read from SST_STAGE
    process.env.SST_STAGE = stage;
    const [version, cdkVersion, constructsVersion] = await (async () => {
        try {
            const packageJson = JSON.parse(await fs
                .readFile(url.fileURLToPath(new URL("./package.json", import.meta.url)))
                .then((x) => x.toString()));
            return [
                packageJson.version,
                packageJson.dependencies["aws-cdk-lib"],
                packageJson.dependencies["constructs"],
            ];
        }
        catch {
            return ["unknown", "unknown"];
        }
    })();
    const project = {
        version,
        cdkVersion,
        constructsVersion,
        config: {
            ...config,
            stage,
            profile: process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY
                ? undefined
                : globals.profile || config.profile,
            region: globals.region || config.region,
            role: globals.role || config.role,
            ssmPrefix: config.ssmPrefix || `/sst/${config.name}/${stage}/`,
            bootstrap: config.bootstrap,
            cdk: config.cdk,
        },
        stacks: sstConfig.stacks,
        metafile,
        paths: {
            config: file,
            root,
            out,
            artifacts: path.join(out, "artifacts"),
        },
    };
    ProjectContext.provide(project);
    // Cleanup old config files
    (async function () {
        const files = await fs.readdir(project.paths.root);
        for (const file of files) {
            if (file.startsWith(".sst.config")) {
                // Do not remove recently generated config files. This allows for multiple
                // SST processes to run concurrently.
                const timeGenerated = (file.match(/\b\d{13}\b/g) ?? []).at(0);
                if (timeGenerated && Date.now() - parseInt(timeGenerated, 10) < 30000) {
                    continue;
                }
                await fs.unlink(path.join(project.paths.root, file));
                Logger.debug(`Removed old config file ${file}`);
            }
        }
    })();
    dotenv.config({
        path: path.join(project.paths.root, `.env.${project.config.stage}`),
        override: true,
    });
    dotenv.config({
        path: path.join(project.paths.root, `.env.${project.config.stage}.local`),
        override: true,
    });
    Logger.debug("Config loaded", project);
}
async function usePersonalStage(out) {
    try {
        const result = await fs.readFile(path.join(out, "stage"));
        return result.toString("utf8").trim();
    }
    catch {
        return;
    }
}
async function promptPersonalStage(out, isRetry) {
    const readline = await import("readline");
    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout,
    });
    const stage = await new Promise((resolve) => {
        const suggested = sanitizeStageName(os.userInfo().username) || "local";
        const instruction = !isRetry
            ? `Please enter a name you’d like to use for your personal stage.`
            : `Please enter a name that starts with a letter, followed by letters, numbers, or hyphens.`;
        rl.question(`${instruction} Or hit enter to use ${blue(suggested)}: `, async (input) => {
            rl.close();
            const result = input === "" ? suggested : input;
            resolve(result);
        });
    });
    // Validate stage name
    if (isValidStageName(stage)) {
        await fs.writeFile(path.join(out, "stage"), stage);
        return stage;
    }
    return await promptPersonalStage(out, true);
}
async function findRoot() {
    async function find(dir) {
        if (dir === "/")
            throw new VisibleError("Could not found a configuration file", "Make sure one of the following exists", ...CONFIG_EXTENSIONS.map((ext) => `  - sst${ext}`));
        for (const ext of CONFIG_EXTENSIONS) {
            const configPath = path.join(dir, `sst${ext}`);
            if (fsSync.existsSync(configPath)) {
                return dir;
            }
        }
        return await find(path.join(dir, ".."));
    }
    const result = await find(process.cwd());
    return result;
}
function sanitizeStageName(stage) {
    return (stage
        .replace(/[^A-Za-z0-9-]/g, "-")
        .replace(/--+/g, "-")
        .replace(/^[^A-Za-z]/, "")
        .replace(/-$/, "") || "local");
}
function isValidStageName(stage) {
    return Boolean(stage.match(/^[A-Za-z][A-Za-z0-9-]*$/));
}
export const exportedForTesting = {
    sanitizeStageName,
    isValidStageName,
};
