import { $inject, $state, AlephaError, z } from "alepha";
import { type AppEntry, AppEntryProvider, ViteBuildProvider } from "alepha/cli";
import {
  CloudflareAdapter,
  type DetectedResources,
  NamingService,
  PlatformInspector,
  PlatformOrchestrator,
  type PlatformPlanOutput,
  type PlatformStatusOutput,
  platformOptions,
  type ResolvedPlatformConfig,
  resolveTenant,
  VercelAdapter,
} from "alepha/cli/platform-lib";
import { $command, EnvUtils } from "alepha/command";
import { $logger, ConsoleColorProvider } from "alepha/logger";
import { SecretsCommand } from "./SecretsCommand.ts";

export class PlatformCommand {
  protected readonly log = $logger();
  protected readonly options = $state(platformOptions);
  protected readonly orchestrator = $inject(PlatformOrchestrator);
  protected readonly inspector = $inject(PlatformInspector);
  protected readonly naming = $inject(NamingService);
  protected readonly boot = $inject(AppEntryProvider);
  protected readonly viteBuild = $inject(ViteBuildProvider);
  protected readonly color = $inject(ConsoleColorProvider);
  protected readonly envUtils = $inject(EnvUtils);
  protected readonly secretsCommand = $inject(SecretsCommand);

  /**
   * Common flags for env targeting.
   */
  protected readonly envFlags = z.object({
    env: z
      .text({
        aliases: ["e"],
        description: "Target environment",
      })
      .optional(),
    tenant: z
      .text({
        description:
          "Tenant slug (apps with tenancy: optional | required). Names resources <tenant>-<project>-<env> and serves <tenant>.<domain>.",
      })
      .optional(),
    verbose: z
      .boolean()
      .meta({ aliases: ["v"] })
      .describe("Verbose output")
      .optional(),
    json: z.boolean().describe("Output as JSON").optional(),
  });

  // -----------------------------------------------------------------------
  // alepha p plan
  // -----------------------------------------------------------------------

  protected readonly plan = $command({
    name: "plan",
    description: "Show project topology and resource names",
    flags: this.envFlags,
    handler: async ({ flags, root }) => {
      const config = await this.inspector.resolveConfig(root);
      const env = flags.env ?? config.defaultEnv;
      const envConfig = config.environments[env];
      const adapterName = envConfig?.adapter ?? "cloudflare";

      const apps = await this.resolveApps(
        root,
        config,
        this.isServerless(adapterName),
      );
      const tenant = resolveTenant(config.tenancy, flags.tenant);
      const namingCtx = this.naming.forContext(config.project, env, tenant);

      // --- Data collection ---

      const hasDB = apps.some((a) => a.resources.hasDatabase);
      const hasBucket = apps.some((a) => a.resources.hasBucket);
      const envVars = await this.envUtils.parseEnv(root, [`.env.${env}`]);

      const resources: Array<{ label: string; value: string }> = [];

      const deployLabel = adapterName === "vercel" ? "Project" : "Worker";
      resources.push({ label: deployLabel, value: namingCtx.worker() });

      if (adapterName === "cloudflare") {
        if (hasDB) {
          const dbUrl = envVars.DATABASE_URL ?? process.env.DATABASE_URL;
          if (dbUrl?.startsWith("postgres:")) {
            resources.push({
              label: "Hyperdrive",
              value: namingCtx.hyperdrive(),
            });
          } else {
            resources.push({ label: "D1", value: namingCtx.d1() });
          }
        }

        if (hasBucket) {
          resources.push({ label: "R2", value: namingCtx.r2() });
        }

        for (const app of apps) {
          if (app.resources.hasKV) {
            resources.push({ label: "KV", value: namingCtx.kv() });
          }
          if (app.resources.hasQueue) {
            resources.push({ label: "Queue", value: namingCtx.queue() });
          }
        }
      }

      const excludedKeys =
        adapterName === "vercel"
          ? VercelAdapter.EXCLUDED_SECRET_KEYS
          : CloudflareAdapter.EXCLUDED_SECRET_KEYS;
      const secretCount = Object.entries(envVars).filter(
        ([key, value]) =>
          value && !excludedKeys.has(key) && !key.startsWith("VITE_"),
      ).length;

      // --- JSON output ---

      if (flags.json) {
        const environments: Record<
          string,
          { adapter: string; domain?: string }
        > = {};
        for (const [key, val] of Object.entries(config.environments)) {
          environments[key] = {
            adapter: val.adapter,
            ...(val.domain ? { domain: val.domain } : {}),
          };
        }

        const output: PlatformPlanOutput = {
          project: config.project,
          env,
          mode: "standalone",
          apps: apps.map((a) => ({
            name: a.name,
            path: a.path,
            resources: a.resources,
          })),
          environments,
          resources,
          secretCount,
        };

        process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
        return;
      }

      // --- Tree output ---

      const c = this.color;

      process.stdout.write(
        `\n\u{1F4E6} ${c.set("WHITE_BOLD", config.project)} ${c.set("GREY_DARK", "\u2014")} ${c.set("CYAN", env)}\n\n`,
      );

      process.stdout.write(`   ${c.set("GREY_LIGHT", "Mode:")} standalone\n`);

      process.stdout.write(`\n   ${c.set("GREY_LIGHT", "Environments:")}\n`);
      const envKeys = Object.keys(config.environments);
      for (const [i, envKey] of envKeys.entries()) {
        const envConfig = config.environments[envKey];
        const prefix =
          i === envKeys.length - 1
            ? "\u2514\u2500\u2500"
            : "\u251C\u2500\u2500";
        const domain = envConfig.domain
          ? `     ${c.set("GREY_DARK", envConfig.domain)}`
          : "";
        const marker = envKey === env ? `  ${c.set("GREEN", "\u25C0")}` : "";
        process.stdout.write(
          `   ${c.set("GREY_DARK", prefix)} ${c.set("CYAN", envKey.padEnd(10))} ${c.set("GREY_LIGHT", envConfig.adapter)}${domain}${marker}\n`,
        );
      }

      process.stdout.write(`\n   ${c.set("GREY_LIGHT", "Resources:")}\n`);

      if (secretCount > 0) {
        resources.push({
          label: "Secrets",
          value: `${secretCount} from .env.${env}`,
        });
      }

      for (const [i, res] of resources.entries()) {
        const isLast = i === resources.length - 1;
        const branch = isLast ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
        process.stdout.write(
          `   ${c.set("GREY_DARK", branch)} ${c.set("GREY_LIGHT", res.label.padEnd(11))} ${c.set("CYAN", res.value)}\n`,
        );
      }

      process.stdout.write("\n");
    },
  });

  // -----------------------------------------------------------------------
  // alepha p up
  // -----------------------------------------------------------------------

  protected readonly up = $command({
    name: "up",
    mode: "production",
    description: "Build, migrate, and deploy",
    flags: z.object({
      ...this.envFlags.properties,
      prebuilt: z
        .boolean()
        .describe(
          "Pre-built mode. Skips the Vite bundle steps; only regenerates the target deploy config (wrangler.jsonc) so it reflects current bindings and per-tenant overrides. Use when `dist/` is already produced upstream (e.g. inside Alepha Rocket).",
        )
        .optional(),
    }),
    handler: async ({ flags, root, run }) => {
      process.env.NODE_ENV = "production";

      const config = await this.inspector.resolveConfig(root);
      const env = flags.env ?? config.defaultEnv;
      const adapter = config.environments[env]?.adapter ?? "cloudflare";
      const apps = await this.resolveApps(
        root,
        config,
        this.isServerless(adapter),
        { prebuilt: flags.prebuilt },
      );

      const result = await this.orchestrator.up({
        root,
        env,
        entry: apps[0].entry,
        resources: apps[0].resources,

        run,
        prebuilt: flags.prebuilt,
        tenant: flags.tenant,
      });

      if (flags.json) {
        process.stdout.write(
          `${JSON.stringify(
            {
              status: "succeeded",
              project: config.project,
              env,
              urls: result.urls,
              domain: result.domain,
            },
            null,
            2,
          )}\n`,
        );
      } else {
        this.orchestrator.printUpSummary(result);
      }
    },
  });

  // -----------------------------------------------------------------------
  // alepha p down
  // -----------------------------------------------------------------------

  protected readonly down = $command({
    name: "down",
    description: "Tear down an environment",
    flags: z.object({
      ...this.envFlags.properties,
      yes: z
        .boolean()
        .meta({ aliases: ["y"] })
        .describe(
          "Skip the interactive confirmation. Required for non-interactive callers (CI, Alepha Rocket). The caller is responsible for not invoking this accidentally — there's no second chance.",
        )
        .optional(),
    }),
    handler: async ({ flags, root, run, ask }) => {
      if (!flags.env) {
        throw new AlephaError(
          "--env is required for teardown. This command deletes resources.",
        );
      }

      const config = await this.inspector.resolveConfig(root);
      const adapter = config.environments[flags.env]?.adapter ?? "cloudflare";
      const apps = await this.resolveApps(
        root,
        config,
        this.isServerless(adapter),
      );

      const completed = await this.orchestrator.down({
        root,
        env: flags.env,
        entry: apps[0].entry,
        resources: apps[0].resources,

        run,
        tenant: flags.tenant,
        confirm: async (prompt) => {
          if (flags.yes) {
            return flags.env as string;
          }
          ask.intro("Confirm teardown");
          const value = await ask(prompt);
          ask.outro("");
          return value;
        },
      });

      if (flags.json) {
        process.stdout.write(
          `${JSON.stringify(
            {
              status: completed ? "succeeded" : "aborted",
              project: config.project,
              env: flags.env,
            },
            null,
            2,
          )}\n`,
        );
      }
    },
  });

  // -----------------------------------------------------------------------
  // alepha p status
  // -----------------------------------------------------------------------

  protected readonly status = $command({
    name: "status",
    aliases: ["s"],
    description: "Show deployed state",
    flags: this.envFlags,
    handler: async ({ flags, root, run }) => {
      const config = await this.inspector.resolveConfig(root);
      const env = flags.env ?? config.defaultEnv;
      const adapter = config.environments[env]?.adapter ?? "cloudflare";
      const apps = await this.resolveApps(
        root,
        config,
        this.isServerless(adapter),
      );

      const { state } = await this.orchestrator.status({
        root,
        env,
        entry: apps[0].entry,
        resources: apps[0].resources,

        run,
        tenant: flags.tenant,
      });

      // --- JSON output ---

      if (flags.json) {
        run.end();

        const output: PlatformStatusOutput = {
          project: config.project,
          env,
          adapter: config.environments[env].adapter,
          ...state,
        };

        process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
        return;
      }

      // --- Tree output ---

      run.end();

      const c = this.color;

      process.stdout.write(
        `\n\u{1F4E6} ${c.set("WHITE_BOLD", config.project)} ${c.set("GREY_DARK", "\u2014")} ${c.set("CYAN", env)} ${c.set("GREY_DARK", `(${config.environments[env].adapter})`)}\n\n`,
      );

      const hasDB = state.databases.length > 0;
      const hasBuckets = state.buckets.length > 0;

      process.stdout.write(`   ${c.set("GREY_LIGHT", "Workers:")}\n`);
      for (const [i, w] of state.workers.entries()) {
        const isLast = i === state.workers.length - 1;
        const branch = isLast ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
        if (w.exists) {
          const versionShort = w.version?.slice(0, 8) ?? "unknown";
          const tag = w.tag ? ` ${c.set("GREY_DARK", `(${w.tag})`)}` : "";
          const date = w.createdAt
            ? ` ${c.set("GREY_DARK", "\u2014")} ${c.set("GREY_DARK", new Date(w.createdAt).toLocaleString())}`
            : "";
          process.stdout.write(
            `   ${c.set("GREY_DARK", branch)} ${c.set("CYAN", w.name)}  ${c.set("GREEN", "\u2713")} ${c.set("GREY_LIGHT", versionShort)}${tag}${date}\n`,
          );
        } else {
          process.stdout.write(
            `   ${c.set("GREY_DARK", branch)} ${c.set("CYAN", w.name)}  ${c.set("RED", "\u2717")} ${c.set("RED", "not deployed")}\n`,
          );
        }
      }

      if (hasDB) {
        const envVars = await this.envUtils.parseEnv(root, [`.env.${env}`]);
        const dbUrl = envVars.DATABASE_URL ?? process.env.DATABASE_URL;
        const dbLabel = dbUrl?.startsWith("postgres:")
          ? "Hyperdrive:"
          : "Database:";
        process.stdout.write(`\n   ${c.set("GREY_LIGHT", dbLabel)}\n`);
        for (const [i, db] of state.databases.entries()) {
          const isLast = i === state.databases.length - 1;
          const branch = isLast ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
          if (db.exists) {
            const id = db.id
              ? ` ${c.set("GREY_LIGHT", db.id.slice(0, 8))}`
              : "";
            const detail = db.detail
              ? ` ${c.set("GREY_DARK", "\u2014")} ${c.set("GREY_DARK", db.detail.slice(0, 40))}`
              : "";
            process.stdout.write(
              `   ${c.set("GREY_DARK", branch)} ${c.set("CYAN", db.name)}  ${c.set("GREEN", "\u2713")}${id}${detail}\n`,
            );
          } else {
            process.stdout.write(
              `   ${c.set("GREY_DARK", branch)} ${c.set("CYAN", db.name)}  ${c.set("RED", "\u2717")} ${c.set("RED", "not provisioned")}\n`,
            );
          }
        }
      }

      if (hasBuckets) {
        process.stdout.write(`\n   ${c.set("GREY_LIGHT", "Buckets:")}\n`);
        for (const [i, b] of state.buckets.entries()) {
          const isLast = i === state.buckets.length - 1;
          const branch = isLast ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
          if (b.exists) {
            process.stdout.write(
              `   ${c.set("GREY_DARK", branch)} ${c.set("CYAN", b.name)}  ${c.set("GREEN", "\u2713")}\n`,
            );
          } else {
            process.stdout.write(
              `   ${c.set("GREY_DARK", branch)} ${c.set("CYAN", b.name)}  ${c.set("RED", "\u2717")} ${c.set("RED", "not provisioned")}\n`,
            );
          }
        }
      }

      if (state.kvNamespaces.length > 0) {
        process.stdout.write(`\n   ${c.set("GREY_LIGHT", "KV:")}\n`);
        for (const [i, kv] of state.kvNamespaces.entries()) {
          const isLast = i === state.kvNamespaces.length - 1;
          const branch = isLast ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
          if (kv.exists) {
            const id = kv.id
              ? ` ${c.set("GREY_LIGHT", kv.id.slice(0, 8))}`
              : "";
            process.stdout.write(
              `   ${c.set("GREY_DARK", branch)} ${c.set("CYAN", kv.name)}  ${c.set("GREEN", "\u2713")}${id}\n`,
            );
          } else {
            process.stdout.write(
              `   ${c.set("GREY_DARK", branch)} ${c.set("CYAN", kv.name)}  ${c.set("RED", "\u2717")} ${c.set("RED", "not provisioned")}\n`,
            );
          }
        }
      }

      if (state.queues.length > 0) {
        process.stdout.write(`\n   ${c.set("GREY_LIGHT", "Queues:")}\n`);
        for (const [i, q] of state.queues.entries()) {
          const isLast = i === state.queues.length - 1;
          const branch = isLast ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
          if (q.exists) {
            const id = q.id ? ` ${c.set("GREY_LIGHT", q.id.slice(0, 8))}` : "";
            process.stdout.write(
              `   ${c.set("GREY_DARK", branch)} ${c.set("CYAN", q.name)}  ${c.set("GREEN", "\u2713")}${id}\n`,
            );
          } else {
            process.stdout.write(
              `   ${c.set("GREY_DARK", branch)} ${c.set("CYAN", q.name)}  ${c.set("RED", "\u2717")} ${c.set("RED", "not provisioned")}\n`,
            );
          }
        }
      }

      if (state.secrets.length > 0) {
        process.stdout.write(`\n   ${c.set("GREY_LIGHT", "Secrets:")}\n`);
        for (const [i, s] of state.secrets.entries()) {
          const isLast = i === state.secrets.length - 1;
          const branch = isLast ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
          const icon = s.deployed
            ? c.set("GREEN", "\u2713")
            : c.set("RED", "\u2717");
          process.stdout.write(
            `   ${c.set("GREY_DARK", branch)} ${c.set("CYAN", s.name)}  ${icon}\n`,
          );
        }
      }

      process.stdout.write("\n");
    },
  });

  // -----------------------------------------------------------------------
  // Granular commands
  // -----------------------------------------------------------------------

  protected readonly build = $command({
    name: "build",
    mode: "production",
    description: "Build all apps locally",
    flags: this.envFlags,
    handler: async ({ flags, root, run }) => {
      process.env.NODE_ENV = "production";
      const config = await this.inspector.resolveConfig(root);
      const env = flags.env ?? config.defaultEnv;
      const envConfig = config.environments[env];
      const adapter = this.orchestrator.resolveAdapter(envConfig.adapter);
      const apps = await this.resolveApps(
        root,
        config,
        this.isServerless(envConfig.adapter),
      );
      const tenant = resolveTenant(config.tenancy, flags.tenant);
      const namingCtx = this.naming.forContext(config.project, env, tenant);

      const ctx = {
        project: config.project,
        env,
        envConfig,
        entry: apps[0].entry,
        resources: apps[0].resources,

        root,
        naming: namingCtx,
        tenant,
      };

      await adapter.build(ctx, run);
    },
  });

  protected readonly deploy = $command({
    name: "deploy",
    description: "Deploy apps to cloud",
    flags: this.envFlags,
    handler: async ({ flags, root, run }) => {
      const config = await this.inspector.resolveConfig(root);
      const env = flags.env ?? config.defaultEnv;
      const envConfig = config.environments[env];
      const adapter = this.orchestrator.resolveAdapter(envConfig.adapter);
      const apps = await this.resolveApps(
        root,
        config,
        this.isServerless(envConfig.adapter),
      );
      const tenant = resolveTenant(config.tenancy, flags.tenant);
      const namingCtx = this.naming.forContext(config.project, env, tenant);

      const ctx = {
        project: config.project,
        env,
        envConfig,
        entry: apps[0].entry,
        resources: apps[0].resources,

        root,
        naming: namingCtx,
        tenant,
      };

      await adapter.authenticate(ctx, run);
      await adapter.deploy(ctx, run);
    },
  });

  protected readonly migrate = $command({
    name: "migrate",
    description: "Run database migrations",
    flags: this.envFlags,
    handler: async ({ flags, root, run }) => {
      const config = await this.inspector.resolveConfig(root);
      const env = flags.env ?? config.defaultEnv;
      const envConfig = config.environments[env];
      const adapter = this.orchestrator.resolveAdapter(envConfig.adapter);
      const apps = await this.resolveApps(
        root,
        config,
        this.isServerless(envConfig.adapter),
      );
      const tenant = resolveTenant(config.tenancy, flags.tenant);
      const namingCtx = this.naming.forContext(config.project, env, tenant);

      const ctx = {
        project: config.project,
        env,
        envConfig,
        entry: apps[0].entry,
        resources: apps[0].resources,

        root,
        naming: namingCtx,
        tenant,
      };

      await adapter.authenticate(ctx, run);
      await adapter.migrate(ctx, run);

      if (flags.json) {
        process.stdout.write(
          `${JSON.stringify(
            { status: "succeeded", project: config.project, env },
            null,
            2,
          )}\n`,
        );
      }
    },
  });

  protected readonly dbExport = $command({
    name: "export",
    description:
      "Export the deployed database to a local snapshot (remote → local dev DB).",
    flags: z.object({
      ...this.envFlags.properties,
      output: z
        .text({
          description:
            "Destination SQLite file. Defaults to the dev DB path (node_modules/.alepha/sqlite.db).",
        })
        .optional(),
      keepSql: z
        .boolean()
        .describe("Keep the intermediate .sql dump file.")
        .optional(),
    }),
    handler: async ({ flags, root, run }) => {
      const config = await this.inspector.resolveConfig(root);
      const env = flags.env ?? config.defaultEnv;
      const envConfig = config.environments[env];
      const adapter = this.orchestrator.resolveAdapter(envConfig.adapter);
      const app = await this.resolveApp(
        root,
        config,
        this.isServerless(envConfig.adapter),
      );
      const tenant = resolveTenant(config.tenancy, flags.tenant);
      const namingCtx = this.naming.forContext(config.project, env, tenant);

      const ctx = {
        project: config.project,
        env,
        envConfig,
        entry: app.entry,
        resources: app.resources,

        root,
        naming: namingCtx,
        tenant,
      };

      await adapter.authenticate(ctx, run);
      await adapter.exportDb(ctx, run, {
        output: flags.output,
        keepSql: flags.keepSql,
      });
    },
  });

  /**
   * `db` subgroup — operations against the *deployed* database (export,
   * migrate). They live under `platform` (not core `alepha db`) because
   * they need the env config, tenancy, adapter, and resource naming.
   */
  protected readonly db = $command({
    name: "db",
    description: "Deployed-database operations (export, migrate).",
    children: [this.dbExport, this.migrate],
    handler: async ({ help, root }) => {
      await this.inspector.resolveConfig(root);
      help();
    },
  });

  // -----------------------------------------------------------------------
  // Parent command
  // -----------------------------------------------------------------------

  public readonly platform = $command({
    name: "platform",
    aliases: ["p"],
    description: "Cloud deployment orchestrator",
    children: [
      this.plan,
      this.up,
      this.down,
      this.status,
      this.build,
      this.deploy,
      this.db,
      this.secretsCommand.secrets,
    ],
    handler: async ({ help, root }) => {
      await this.inspector.resolveConfig(root);
      help();
    },
  });

  // -----------------------------------------------------------------------
  // Helpers
  // -----------------------------------------------------------------------

  /**
   * Resolve app definitions.
   *
   * For standalone: returns a single app from the root.
   * For monorepo: resolves each app path, introspects for resources.
   *
   * NOTE: Resource detection (hasDatabase, hasBucket, etc.) requires
   * ViteBuildProvider.init() per app. This is expensive -- only done
   * for up/down/status, not for plan.
   */
  protected async resolveApp(
    root: string,
    _config: ResolvedPlatformConfig,
    isServerless: boolean,
    options: { prebuilt?: boolean } = {},
  ): Promise<{ entry: AppEntry; resources: DetectedResources }> {
    // Prebuilt + manifest fast-path: read `dist/manifest.json` produced
    // by the original `alepha build` instead of re-booting the workspace
    // via Vite. Lets external orchestrators (Alepha Rocket) avoid the
    // workspace's runtime `npm install` — the app source is never
    // imported here, so missing deps (react-dom, etc.) don't matter.
    if (options.prebuilt) {
      const manifest = await this.readManifest(root);
      if (manifest) {
        return {
          entry: { root, server: "" },
          resources: manifest.resources,
        };
      }
      // No manifest — fall through to introspection. Useful for older
      // artifacts that pre-date the manifest emission.
    }

    const entry = await this.boot.getAppEntry(root);
    if (isServerless) {
      process.env.ALEPHA_SERVERLESS = "true";
    }
    const appAlepha = await this.viteBuild.init({ entry });
    delete process.env.ALEPHA_SERVERLESS;
    const resources = this.detectResources(appAlepha);

    return { entry, resources };
  }

  /**
   * @deprecated single-app projects; use `resolveApp` directly.
   * Kept temporarily so existing commands can be migrated one at a time.
   */
  protected async resolveApps(
    root: string,
    config: ResolvedPlatformConfig,
    isServerless: boolean,
    options: { prebuilt?: boolean } = {},
  ): Promise<
    Array<{
      name: string;
      path: string;
      entry: AppEntry;
      resources: DetectedResources;
    }>
  > {
    const { entry, resources } = await this.resolveApp(
      root,
      config,
      isServerless,
      options,
    );
    return [{ name: config.project, path: "", entry, resources }];
  }

  protected isServerless(adapter: string): boolean {
    return adapter === "vercel" || adapter === "cloudflare";
  }

  /**
   * Read `dist/manifest.json` if present. Returns `null` when the file
   * doesn't exist or isn't parseable — caller falls back to the
   * Vite-introspection path.
   */
  protected async readManifest(root: string): Promise<{
    version: number;
    project: string;
    resources: DetectedResources;
  } | null> {
    try {
      const fs = await import("node:fs/promises");
      const path = await import("node:path");
      const raw = await fs.readFile(
        path.join(root, "dist", "manifest.json"),
        "utf-8",
      );
      return JSON.parse(raw);
    } catch {
      return null;
    }
  }

  protected detectResources(alepha: any): DetectedResources {
    let hasDatabase = false;
    let hasBucket = false;
    let hasKV = false;
    let hasQueue = false;
    let hasCron = false;

    try {
      const repo = alepha.inject("RepositoryProvider");
      hasDatabase = repo.getRepositories().length > 0;
    } catch {}

    try {
      const buckets = alepha.primitives("$bucket");
      hasBucket = buckets.length > 0;
    } catch {}

    try {
      // Provision KV only when the user actually wants it: a `$cache` declared
      // *without* an explicit `provider` falls back to the runtime default
      // (CloudflareKVProvider on workerd). Any explicit choice — `"memory"`,
      // `DatabaseCacheProvider`, a Redis provider, or a custom one — opts out
      // of the platform default and therefore should not trigger KV
      // provisioning. See platform.ts hasKV docs and `$cache.options.provider`.
      hasKV =
        alepha
          .primitives("cache")
          .filter((it: any) => it.options?.provider == null).length > 0;
    } catch {}

    try {
      hasQueue = alepha.primitives("queue").length > 0;
    } catch {}

    try {
      const cron = alepha.inject("CronProvider");
      hasCron = cron.getCronJobs().length > 0;
    } catch {}

    return { hasDatabase, hasBucket, hasKV, hasQueue, hasCron };
  }
}
