import assert from "node:assert";
import fs from "node:fs/promises";
import path from "pathe";
import { Exec } from "../os/index.js";
import { Scope } from "../scope.js";
import { dedent } from "../util/dedent.js";
import { logger } from "../util/logger.js";
import { createCloudflareApi } from "./api.js";
import { Assets } from "./assets.js";
import { DEFAULT_COMPATIBILITY_DATE } from "./compatibility-date.js";
import { unionCompatibilityFlags } from "./compatibility-presets.js";
import { quickTunnel } from "./quick-tunnel.js";
import { extractStringAndSecretBindings, unencryptSecrets, } from "./util/filter-env-bindings.js";
import { computeWorkerDevDomain } from "./worker-subdomain.js";
import { Worker } from "./worker.js";
import { WranglerJson } from "./wrangler.json.js";
export async function Website(id, props) {
    const { name = Scope.current.createPhysicalName(id).toLowerCase(), build: buildProps, assets, dev, script, spa = true, command, commandEnv, memoize, ...workerProps } = props;
    assert(!workerProps.bindings?.ASSETS, "ASSETS binding is reserved for internal use");
    if (command) {
        logger.warnOnce("[website] The `command` prop is deprecated. Use `build.command` instead.");
    }
    if (commandEnv) {
        logger.warnOnce("[website] The `commandEnv` prop is deprecated. Use `build.env` instead.");
    }
    if (memoize) {
        logger.warnOnce("[website] The `memoize` prop is deprecated. Use `build.memoize` instead.");
    }
    const build = (() => {
        if (typeof buildProps === "string") {
            return { command: buildProps, env: commandEnv, memoize };
        }
        if (buildProps) {
            return buildProps;
        }
        if (command) {
            return {
                command,
                env: commandEnv,
                memoize,
            };
        }
        return undefined;
    })();
    const paths = (() => {
        const cwd = props.cwd ?? process.cwd();
        return {
            cwd,
            assets: path.resolve(cwd, typeof assets === "string" ? assets : (assets?.directory ?? "dist")),
            local: path.resolve(cwd, ".alchemy/local"),
            entrypoint: path.resolve(cwd, props.entrypoint ?? ".alchemy/local/worker.js"),
            get wrangler() {
                return {
                    path: path.resolve(cwd, props.wrangler?.path ?? ".alchemy/local/wrangler.jsonc"),
                    main: props.wrangler?.main
                        ? path.resolve(cwd, props.wrangler.main)
                        : this.entrypoint,
                };
            },
        };
    })();
    const secrets = props.wrangler?.secrets ?? !props.wrangler?.path;
    const env = {
        ...process.env,
        ...props.env,
        ...extractStringAndSecretBindings(props.bindings ?? {}, secrets),
    };
    if (props.bindings) {
        for (const [key, value] of Object.entries(props.bindings)) {
            if (typeof value !== "object")
                continue;
            if (value.type === "cloudflare::Worker::DevDomain" ||
                value.type === "cloudflare::Worker::DevUrl") {
                if (Scope.current.local) {
                    const domain = typeof dev === "object" ? dev.domain : undefined;
                    if (!domain) {
                        // we can't know which port the local web server (e.g. vite dev) will use
                        // vite dev # default to 5173 or configured in vite.config.ts
                        // vite dev --port XYZ
                        // next dev # who fucking knows
                        // for now: we require the user tell us the local domain
                        throw new Error("You must set `dev.domain` when running in local development mode and a Worker.DevDomain binding");
                    }
                    env[key] =
                        value.type === "cloudflare::Worker::DevDomain"
                            ? domain
                            : `http://${domain}`;
                }
                else {
                    const api = await createCloudflareApi(props);
                    const domain = await computeWorkerDevDomain(api, name);
                    env[key] =
                        value.type === "cloudflare::Worker::DevDomain"
                            ? domain
                            : `https://${domain}`;
                }
            }
        }
    }
    const worker = {
        ...workerProps,
        name,
        cwd: path.relative(process.cwd(), paths.cwd),
        compatibilityFlags: unionCompatibilityFlags(workerProps.compatibility, workerProps.compatibilityFlags),
        compatibilityDate: workerProps.compatibilityDate ?? DEFAULT_COMPATIBILITY_DATE,
        assets: {
            html_handling: "auto-trailing-slash",
            not_found_handling: spa ? "single-page-application" : "none",
            run_worker_first: false,
            ...(typeof props.assets === "string" ? {} : props.assets),
        },
        entrypoint: path.relative(paths.cwd, paths.entrypoint),
    };
    if (!workerProps.entrypoint) {
        await fs.mkdir(path.dirname(paths.entrypoint), { recursive: true });
        const content = script ??
            dedent `
      export default {
          async fetch(request, env) {
              return new Response("Not Found", { status: 404 });
          },
      };`;
        await fs.writeFile(paths.entrypoint, content);
    }
    await WranglerJson({
        path: path.relative(paths.cwd, paths.wrangler.path),
        worker,
        assets: {
            binding: "ASSETS",
            directory: path.relative(paths.cwd, paths.assets),
        },
        main: path.relative(paths.cwd, paths.wrangler.main),
        secrets,
        transform: {
            wrangler: props.wrangler?.transform,
        },
    });
    const scope = Scope.current;
    if (build?.command && !scope.local) {
        await Exec(`${id}-build`, {
            cwd: path.relative(process.cwd(), paths.cwd),
            command: build.command,
            env: {
                ...env,
                ...(typeof build === "object" ? build.env : {}),
                NODE_ENV: "production",
                ALCHEMY_ROOT: Scope.current.rootDir,
            },
            memoize: typeof build === "object" ? build.memoize : undefined,
        });
    }
    let url;
    const devCommand = typeof dev === "string" ? dev : dev?.command;
    if (devCommand && scope.local) {
        const tunnelEnabled = (typeof dev === "object" && dev.tunnel) || scope.tunnel;
        url = await scope.spawn(name, {
            cmd: devCommand,
            cwd: paths.cwd,
            extract: (line) => {
                const URL_REGEX = /http:\/\/(localhost|0\.0\.0\.0|127\.0\.0\.1|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):\d+\/?/;
                const match = line
                    .replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "")
                    .match(URL_REGEX);
                if (match) {
                    return match[0];
                }
            },
            env: {
                ...unencryptSecrets(env ?? {}),
                ...(typeof dev === "object" ? dev.env : {}),
                FORCE_COLOR: "1",
                ...process.env,
                // NOTE: we must set this to ensure the user does not accidentally set `NODE_ENV=production`
                // which breaks `vite dev` (it won't, for example, re-write `process.env.TSS_APP_BASE` in the `.js` client side bundle)
                NODE_ENV: "development",
                ALCHEMY_ROOT: Scope.current.rootDir,
                ...(tunnelEnabled ? { ALCHEMY_DEV_TUNNEL: "quick" } : {}),
            },
        });
        if (url && tunnelEnabled) {
            const { tunnelUrl } = await quickTunnel(scope, url);
            url = tunnelUrl;
        }
    }
    return (await Worker(id, {
        ...worker,
        bindings: {
            ...worker.bindings,
            ...(!scope.local
                ? {
                    ASSETS: await Assets({
                        path: path.relative(process.cwd(), paths.assets),
                    }),
                }
                : {}),
        },
        dev: worker.dev
            ? {
                url: url,
                port: worker.dev.port,
                remote: worker.dev.remote,
                tunnel: worker.dev.tunnel,
            }
            : url
                ? { url }
                : undefined,
    }));
}
export const spreadBuildProps = (props, defaultCommand) => {
    if (typeof props?.build === "object") {
        return {
            ...props.build,
            command: props.build.command ?? defaultCommand,
        };
    }
    return props?.build ?? defaultCommand;
};
export const spreadDevProps = (props, defaultCommand) => {
    if (typeof props?.dev === "object") {
        return {
            ...props.dev,
            command: props.dev.command ?? defaultCommand,
        };
    }
    return props?.dev ?? defaultCommand;
};
//# sourceMappingURL=website.js.map