import { execArgv } from "node:process";
import path from "pathe";
import { onExit } from "signal-exit";
import { isReplacedSignal } from "./apply.js";
import { DestroyStrategy, destroy, isDestroyedSignal } from "./destroy.js";
import { runStateCommand } from "./state-cli.js";
import { env } from "./env.js";
import { ResourceFQN, ResourceID, ResourceKind, ResourceScope, ResourceSeq, } from "./resource.js";
import { DEFAULT_STAGE, Scope } from "./scope.js";
import { secret } from "./secret.js";
import { cliArgs, parseOption } from "./util/cli-args.js";
import { logger } from "./util/logger.js";
import { ALCHEMY_ROOT } from "./util/root-dir.js";
import { createAndSendEvent } from "./util/telemetry.js";
export const alchemy = _alchemy;
_alchemy.destroy = destroy;
_alchemy.run = run;
_alchemy.secret = secret;
_alchemy.env = env;
/**
 * Implementation of the alchemy function.
 */
async function _alchemy(appName, options) {
    const startedAt = performance.now();
    let firstAnalyticsPromise;
    if (!options?.noTrack) {
        firstAnalyticsPromise = createAndSendEvent({
            event: "alchemy.start",
            duration: performance.now() - startedAt,
        });
    }
    // user may select a specific app to auto-enable read mode for any other app
    const app = parseOption("--app");
    const cliOptions = {
        phase: app && app !== appName
            ? "read"
            : cliArgs.includes("--destroy")
                ? "destroy"
                : cliArgs.includes("--read")
                    ? "read"
                    : "up",
        local: cliArgs.includes("--local") || cliArgs.includes("--dev"),
        watch: cliArgs.includes("--watch") || execArgv.includes("--watch"),
        quiet: cliArgs.includes("--quiet"),
        force: cliArgs.includes("--force"),
        tunnel: cliArgs.includes("--tunnel"),
        // Parse stage argument (--stage my-stage) functionally and inline as a property declaration
        stage: (function parseStage() {
            const i = cliArgs.indexOf("--stage");
            return i !== -1 && i + 1 < cliArgs.length
                ? cliArgs[i + 1]
                : process.env.STAGE;
        })(),
        password: process.env.ALCHEMY_PASSWORD,
        adopt: cliArgs.includes("--adopt"),
        eraseSecrets: cliArgs.includes("--erase-secrets"),
        rootDir: path.resolve(parseOption("--root-dir", ALCHEMY_ROOT)),
        profile: parseOption("--profile"),
    };
    const mergedOptions = {
        ...cliOptions,
        ...options,
    };
    if (mergedOptions.stateStore === undefined &&
        process.env.CI &&
        process.env.ALCHEMY_CI_STATE_STORE_CHECK !== "false") {
        throw new Error(`You are running Alchemy in a CI environment with the default local state store.
This can lead to orphaned infrastructure and is rarely what you want to do.

Instead, you should choose a persistent state store:
1. CloudflareStateStore (https://alchemy.run/concepts/state/#cloudflare-state-store)
2. S3StateStore (https://alchemy.run/providers/aws/s3-state-store/)

You can read more about State and State Stores here: https://alchemy.run/concepts/state/#customizing-state-storage

If this is a mistake, you can disable this check by setting the ALCHEMY_CI_STATE_STORE_CHECK=false.
`);
    }
    if (mergedOptions.eraseSecrets && !mergedOptions.force) {
        throw new Error("--erase-secrets requires --force");
    }
    const isRunningFromCLI = parseOption("--telemetry-ref")?.endsWith("cli");
    if (app && options?.phase) {
        throw new Error(`Cannot hard-code phase to "${options?.phase}" when running with --app ${app}`);
    }
    if (options?.phase === "up" &&
        (cliArgs.includes("--destroy") || cliArgs.includes("--read"))) {
        throw new Error(`Cannot hard-code phase to "up" when running with --destroy or --read`);
    }
    if (options?.phase === "destroy" &&
        (cliArgs.includes("--read") || cliArgs.includes("--dev"))
    // TODO(sam): this is missing running from CLI "up" phase (because that is default)
    ) {
        throw new Error(`Cannot hard-code phase to "destroy" when running with --read or --dev`);
    }
    if (isRunningFromCLI &&
        options?.phase === "destroy" &&
        !cliArgs.includes("--destroy")) {
        throw new Error('The phase was hard-coded to "destroy", but alchemy is running from the CLI without the --destroy option.');
    }
    if (options?.phase === "read" && cliArgs.includes("--destroy")) {
        throw new Error(`Cannot hard-code phase to "read" when running with --destroy`);
    }
    if (isRunningFromCLI &&
        options?.phase === "read" &&
        !cliArgs.includes("--read")) {
        logger.warn('The phase was hard-coded to "read", but alchemy is running from the CLI without the --read option, this is likely a mistake.');
    }
    const phase = mergedOptions?.phase ?? "up";
    const root = new Scope({
        ...mergedOptions,
        parent: null,
        scopeName: appName,
        phase,
        password: mergedOptions?.password ?? process.env.ALCHEMY_PASSWORD,
        noTrack: mergedOptions?.noTrack ?? false,
        isSelected: app === undefined ? undefined : app === appName,
        startedAt,
    });
    onExit((code) => {
        root.cleanup().then(() => {
            code = code === 130 ? 0 : (code ?? 0);
            process.exit(code);
        });
        return true;
    });
    const stageName = mergedOptions?.stage ?? DEFAULT_STAGE;
    const stage = new Scope({
        ...mergedOptions,
        parent: root,
        scopeName: stageName,
        stage: stageName,
    });
    Scope.storage.enterWith(root);
    Scope.storage.enterWith(stage);
    const stateCmd = parseOption("--state-cmd");
    if (stateCmd) {
        const stateArg = parseOption("--state-arg");
        await runStateCommand(stage, stateCmd, stateArg);
        return process.exit(0);
    }
    if (mergedOptions?.phase === "destroy") {
        const err = await destroy(stage).catch((e) => e);
        if (!options?.noTrack) {
            await createAndSendEvent({
                event: err instanceof Error ? "alchemy.error" : "alchemy.success",
                duration: performance.now() - root.startedAt,
            }, err instanceof Error ? err : undefined);
        }
        return process.exit(0);
    }
    if (firstAnalyticsPromise) {
        await firstAnalyticsPromise;
    }
    return root;
}
/**
 * Run a function in a new scope asynchronously.
 * Useful for isolating secret handling with a specific password.
 *
 * @example
 * // Run operations in a scope with its own password
 * await alchemy.run("secure-scope", {
 *   password: process.env.SCOPE_PASSWORD
 * }, async () => {
 *   // Secrets in this scope will use this password
 *   const resource = await Resource("my-resource", {
 *     apiKey: alchemy.secret(process.env.API_KEY)
 *   });
 * });
 */
async function run(...args) {
    const [id, options, fn] = typeof args[1] === "function"
        ? [args[0], undefined, args[1]]
        : args;
    const _scope = new Scope({
        ...options,
        parent: options?.parent,
        scopeName: id,
        noTrack: options?.noTrack ?? false,
    });
    let noop = options?.noop ?? false;
    try {
        if (options?.isResource !== true && _scope.parent) {
            // TODO(sam): this is an awful hack to differentiate between naked scopes and resources
            const seq = _scope.parent.seq();
            const output = {
                [ResourceID]: id,
                [ResourceFQN]: "",
                [ResourceKind]: Scope.KIND,
                [ResourceScope]: _scope,
                [ResourceSeq]: seq,
                [DestroyStrategy]: options?.destroyStrategy ?? "sequential",
            };
            const resource = {
                kind: Scope.KIND,
                id,
                seq,
                data: {},
                fqn: "",
                props: {},
                status: "created",
                output,
            };
            const prev = await _scope.parent.state.get(id);
            if (!prev) {
                await _scope.parent.state.set(id, resource);
            }
            else if (prev.kind !== Scope.KIND) {
                throw new Error(`Tried to create a Scope that conflicts with a Resource (${prev.kind}): ${id}`);
            }
            _scope.parent.resources.set(id, Object.assign(Promise.resolve(resource), output));
        }
        return await _scope.run(async () => fn.bind(_scope)(_scope));
    }
    catch (error) {
        if (!(isDestroyedSignal(error) || isReplacedSignal(error))) {
            _scope.fail();
        }
        if (isDestroyedSignal(error)) {
            noop = noop || error.noop;
        }
        throw error;
    }
    finally {
        await _scope.finalize({
            noop,
        });
    }
}
//# sourceMappingURL=alchemy.js.map