import { existsSync } from "node:fs";
import { join } from "node:path";
import type { PlopTypes } from "@turbo/gen";
import { assertAnchors } from "./lib/anchor-validate.js";
import {
  assertOptionalPackageNotPresent,
  addToTranspilePackages,
  splicePluginRulesAt,
  splicePluginImportsAt,
  addBoundariesEntry,
  emitTemplateTree,
} from "./lib/core-package-utils.js";
import { registerFeatureInReleasePlease } from "./lib/release-please-utils.js";

/**
 * Turbo generator: `feature`
 *
 * Scaffolds a feature package under `packages/<name>/`
 * matching the shape of the existing `navigation` reference feature.
 *
 * Scope (intentionally limited):
 *   - Single entity, single use case (`getX`)
 *   - Skips Payload CMS collection/global templates (integrations/cms/**)
 *   - Skips UI query helpers (ui/query.ts) — emits an empty barrel
 *   - Skips faker-driven factories — emits empty stubs
 *   - Skips multi-entity / multi-use-case
 *   - Skips aggregator wiring (core-api/root.ts, core-cms/**, apps/web-next/server/bind-production.ts)
 *
 * After running, the developer must hand-wire the new feature into:
 *   - apps/web-next/src/server/bind-production.ts  (bindAll dispatcher)
 *   - packages/core-api/src/root.ts                (mount on app router)
 *   - packages/core-cms/...                        (when CMS templates are added later)
 *
 * The generator prints these manual steps when it finishes.
 */
export default function generator(plop: PlopTypes.NodePlopAPI): void {
  // Handlebars helper used by templates that branch on prompt values
  // (e.g. `gen job`'s inputShape void/typed split).
  plop.setHelper("eq", (a: unknown, b: unknown) => a === b);

  plop.setGenerator("feature", {
    description: "Scaffold a feature package (single entity / single use case)",
    prompts: [
      {
        type: "input",
        name: "name",
        message:
          "Feature package name (kebab-case, becomes @repo/<name> and packages/<name>/):",
        validate: (input: string) => {
          if (!input) return "Required";
          if (!/^[a-z][a-z0-9-]*$/.test(input)) {
            return "Must be kebab-case (lowercase letters, digits, hyphens; must start with a letter)";
          }
          return true;
        },
      },
      {
        type: "input",
        name: "entity",
        message:
          "Entity name (PascalCase singular, e.g. 'Widget' for a getWidget use case):",
        validate: (input: string) => {
          if (!input) return "Required";
          if (!/^[A-Z][A-Za-z0-9]*$/.test(input)) {
            return "Must be PascalCase (e.g. Widget, BlogPost)";
          }
          return true;
        },
      },
      {
        type: "input",
        name: "entityPlural",
        message:
          "Entity plural slug (kebab-case, used for Payload collection slug; e.g. 'widgets'):",
        validate: (input: string) => {
          if (!input) return "Required";
          if (!/^[a-z][a-z0-9-]*$/.test(input)) {
            return "Must be kebab-case";
          }
          return true;
        },
      },
    ],
    actions: [
      // Top-level package files
      {
        type: "add",
        path: "packages/{{kebabCase name}}/package.json",
        templateFile: "templates/feature/package.json.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/tsconfig.json",
        templateFile: "templates/feature/tsconfig.json.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/vitest.config.ts",
        templateFile: "templates/feature/vitest.config.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/stryker.config.json",
        templateFile: "templates/feature/stryker.config.json.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/eslint.config.js",
        templateFile: "templates/feature/eslint.config.js.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/turbo.json",
        templateFile: "templates/feature/turbo.json.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/AGENTS.md",
        templateFile: "templates/feature/AGENTS.md.hbs",
      },

      // Public surface
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/index.ts",
        templateFile: "templates/feature/src/index.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/feature.manifest.ts",
        templateFile: "templates/feature/src/feature.manifest.ts.hbs",
      },

      // Entities — models + errors
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/entities/models/{{kebabCase entity}}.ts",
        templateFile: "templates/feature/src/entities/models/entity.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/entities/models/{{kebabCase entity}}.test.ts",
        templateFile:
          "templates/feature/src/entities/models/entity.test.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/entities/errors/common.ts",
        templateFile: "templates/feature/src/entities/errors/common.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/entities/errors/{{kebabCase entity}}.ts",
        templateFile: "templates/feature/src/entities/errors/entity.ts.hbs",
      },

      // Application layer
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/application/repositories/{{kebabCase entity}}.repository.interface.ts",
        templateFile:
          "templates/feature/src/application/repositories/entity.repository.interface.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/application/use-cases/get-{{kebabCase entity}}.use-case.ts",
        templateFile:
          "templates/feature/src/application/use-cases/get-entity.use-case.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/application/use-cases/get-{{kebabCase entity}}.use-case.test.ts",
        templateFile:
          "templates/feature/src/application/use-cases/get-entity.use-case.test.ts.hbs",
      },

      // Infrastructure
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/infrastructure/repositories/{{kebabCase entity}}.repository.ts",
        templateFile:
          "templates/feature/src/infrastructure/repositories/entity.repository.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/infrastructure/repositories/{{kebabCase entity}}.repository.test.ts",
        templateFile:
          "templates/feature/src/infrastructure/repositories/entity.repository.test.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/infrastructure/repositories/{{kebabCase entity}}.repository.mock.ts",
        templateFile:
          "templates/feature/src/infrastructure/repositories/entity.repository.mock.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/infrastructure/repositories/{{kebabCase entity}}.repository.mock.test.ts",
        templateFile:
          "templates/feature/src/infrastructure/repositories/entity.repository.mock.test.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/infrastructure/repositories/{{kebabCase entity}}.repository.span.test.ts",
        templateFile:
          "templates/feature/src/infrastructure/repositories/entity.repository.span.test.ts.hbs",
      },

      // Interface adapters
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/interface-adapters/controllers/get-{{kebabCase entity}}.controller.ts",
        templateFile:
          "templates/feature/src/interface-adapters/controllers/get-entity.controller.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/interface-adapters/controllers/get-{{kebabCase entity}}.controller.test.ts",
        templateFile:
          "templates/feature/src/interface-adapters/controllers/get-entity.controller.test.ts.hbs",
      },

      // DI
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/di/symbols.ts",
        templateFile: "templates/feature/src/di/symbols.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/di/module.ts",
        templateFile: "templates/feature/src/di/module.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/di/container.ts",
        templateFile: "templates/feature/src/di/container.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/di/container.test.ts",
        templateFile: "templates/feature/src/di/container.test.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/di/bind-production.ts",
        templateFile: "templates/feature/src/di/bind-production.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/di/bind-dev-seed.ts",
        templateFile: "templates/feature/src/di/bind-dev-seed.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/di/bind-dev-seed.test.ts",
        templateFile: "templates/feature/src/di/bind-dev-seed.test.ts.hbs",
      },

      // Integrations: api
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/integrations/api/procedures.ts",
        templateFile:
          "templates/feature/src/integrations/api/procedures.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/integrations/api/router.ts",
        templateFile: "templates/feature/src/integrations/api/router.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/integrations/api/router.test.ts",
        templateFile:
          "templates/feature/src/integrations/api/router.test.ts.hbs",
      },

      // Seeds + factories + contracts (minimal stubs that still typecheck/test)
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/__seeds__/dev.ts",
        templateFile: "templates/feature/src/__seeds__/dev.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/__factories__/index.ts",
        templateFile: "templates/feature/src/__factories__/index.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/__factories__/{{kebabCase entity}}.factory.ts",
        templateFile:
          "templates/feature/src/__factories__/entity.factory.ts.hbs",
      },
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/__contracts__/{{kebabCase entity}}-repository.contract.ts",
        templateFile:
          "templates/feature/src/__contracts__/entity-repository.contract.ts.hbs",
      },

      // ui — empty barrel (./ui subpath kept reserved)
      {
        type: "add",
        path: "packages/{{kebabCase name}}/src/ui/index.ts",
        templateFile: "templates/feature/src/ui/index.ts.hbs",
      },

      // CHANGELOG (release-please will append future versions; v0.1.0 seed)
      {
        type: "add",
        path: "packages/{{kebabCase name}}/CHANGELOG.md",
        templateFile: "templates/feature/CHANGELOG.md.hbs",
      },

      // Register the new feature in release-please tracking (ADR-021).
      // Adds an entry to .release-please-manifest.json and a per-package
      // block to release-please-config.json. Idempotent + sorted so reruns
      // are stable. Throws if either file is missing.
      function registerInReleasePlease(
        answers: Record<string, unknown>,
      ): string {
        const a = answers as { name: string };
        const repoRoot = process.cwd();
        const { manifestChanged, configChanged } =
          registerFeatureInReleasePlease(repoRoot, a.name);
        if (manifestChanged || configChanged) {
          return `Registered @repo/${a.name} in release-please tracking (manifest + config)`;
        }
        return `@repo/${a.name} already tracked by release-please (no changes)`;
      },

      // Final manual-wiring instructions printed to the user
      function printNextSteps(answers: Record<string, unknown>): string {
        const a = answers as {
          name: string;
          entity: string;
          entityPlural: string;
        };
        const kebab = a.name;
        const constSym = a.name.toUpperCase().replace(/-/g, "_");
        const pkg = `@repo/${kebab}`;
        return [
          "",
          "─────────────────────────────────────────────────────────────",
          `Feature package ${pkg} scaffolded.`,
          "",
          "Next steps (manual aggregator wiring — not generated):",
          "",
          `  1. pnpm install   # link the new workspace package`,
          "",
          `  2. apps/web-next/src/server/bind-production.ts:`,
          `     - import { bindProduction${cap(a.name)} } from "${pkg}/di/bind-production";`,
          `     - import { bindDevSeed${cap(a.name)} } from "${pkg}/di/bind-dev-seed";`,
          `     - call bindProduction${cap(a.name)}(ctx) (production branch, ctx: BindProductionContext)`,
          `     - call await bindDevSeed${cap(a.name)}(ctx) (dev-seed branch, ctx: BindContext)`,
          "",
          `  3. packages/core-api/src/root.ts:`,
          `     - import { ${camel(a.name)}Router } from "${pkg}/api";`,
          `     - mount it on the app router (e.g. ${camel(a.name)}: ${camel(a.name)}Router)`,
          "",
          `  4. packages/core-api/package.json:`,
          `     - add "${pkg}": "workspace:*" to dependencies`,
          "",
          `  5. apps/web-next/package.json:`,
          `     - add "${pkg}": "workspace:*" to dependencies`,
          "",
          `  6. (Later) Add Payload CMS collection/global at:`,
          `     packages/${kebab}/src/integrations/cms/collections/${a.entityPlural}.ts`,
          `     and register in packages/core-cms/...`,
          "",
          `  7. Verify: pnpm --filter ${pkg} lint typecheck test`,
          "",
          `Reference symbols exported by this package:`,
          `  - ${constSym}_SYMBOLS (from ${pkg}/* internal)`,
          `  - bindProduction${cap(a.name)}, bindDevSeed${cap(a.name)}`,
          `  - ${camel(a.name)}Router (tRPC)`,
          "─────────────────────────────────────────────────────────────",
          "",
        ].join("\n");
      },
    ],
  });

  /**
   * Turbo generator: `event`
   *
   * Mode `publish` scaffolds an event contract under
   * `packages/<feature>/src/events/<event-kebab>.event.ts` (+ test) and
   * re-exports it from the feature's public surface at the `// <gen:events>`
   * anchor. Mode `consume` (Task 40) scaffolds a handler in the consumer
   * feature.
   */
  plop.setGenerator("event", {
    description: "Scaffold an event contract (publish) or handler (consume)",
    prompts: [
      {
        type: "list",
        name: "mode",
        message: "Mode:",
        choices: ["publish", "consume"],
      },
      {
        type: "input",
        name: "feature",
        message:
          "Owning feature (kebab-case; for publish: contract package; for consume: consumer package):",
        validate(input: string) {
          if (!/^[a-z][a-z0-9-]*$/.test(input)) return "Must be kebab-case";
          if (!existsSync(join(process.cwd(), "packages", input, "src"))) {
            return `packages/${input}/src does not exist`;
          }
          return true;
        },
      },
      {
        type: "input",
        name: "event",
        message: "Event slug (dotted-kebab past tense, e.g. 'user.signed-up'):",
        validate(input: string) {
          if (!/^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/.test(input)) {
            return "Must be dotted-kebab past tense (e.g. user.signed-up)";
          }
          return true;
        },
      },
      {
        type: "input",
        name: "publisher",
        message:
          "Publisher feature (kebab-case; required for consume, ignored for publish):",
        // Always shown — `--args` cannot bypass conditional prompts (Plop limitation).
        // Publish mode accepts any value (it's ignored by publishActions).
        validate(input: string, answers: { mode: string; event: string }) {
          if (answers.mode === "publish") return true;
          if (!/^[a-z][a-z0-9-]*$/.test(input)) return "Must be kebab-case";
          const eventKebab = answers.event.replace(/\./g, "-");
          const path = join(
            process.cwd(),
            "packages",
            input,
            "src",
            "events",
            `${eventKebab}.event.ts`,
          );
          if (!existsSync(path)) {
            return `Publisher contract not found: ${path}`;
          }
          return true;
        },
      },
    ],
    actions(answers) {
      const a = answers as {
        mode: "publish" | "consume";
        feature: string;
        event: string;
        publisher?: string;
      };
      if (a.mode === "publish") return publishActions(a);
      if (!a.publisher) {
        throw new Error("consume mode requires a publisher feature");
      }
      return consumeActions(a as Required<typeof a>);
    },
  });

  /**
   * Turbo generator: `realtime`
   *
   * Mode `channel` scaffolds a realtime channel descriptor under
   * `packages/<feature>/src/realtime/<slug>.channel.ts` (+ test) and
   * re-exports it from the feature's public surface at the
   * `// <gen:realtime-channels>` anchor.
   * Mode `handler` scaffolds an inbound handler + DI binding at the
   * `// <gen:realtime-handlers>` anchor.
   */
  plop.setGenerator("realtime", {
    description: "Scaffold a realtime channel descriptor or inbound handler",
    prompts: [
      {
        type: "input",
        name: "mode",
        message: "Mode: channel | handler",
        validate(input: string) {
          if (!["channel", "handler"].includes(input))
            return "Must be 'channel' or 'handler'";
          return true;
        },
      },
      {
        type: "input",
        name: "feature",
        message: "Feature (kebab-case, must exist):",
        validate(input: string) {
          if (!/^[a-z][a-z0-9-]*$/.test(input)) return "Must be kebab-case";
          if (!existsSync(join(process.cwd(), "packages", input, "src"))) {
            return `packages/${input}/src does not exist`;
          }
          return true;
        },
      },
      {
        type: "input",
        name: "channelSlug",
        message: "Channel slug (kebab-case, e.g. 'presence-ping'):",
        validate(input: string) {
          if (!/^[a-z][a-z0-9-]+$/.test(input)) return "Must be kebab-case";
          return true;
        },
      },
      {
        type: "input",
        name: "scope",
        message:
          "Scope (channel mode only): public | authenticated | role:NAME | user-scoped (ignored for handler mode):",
        validate(input: string, answers: { mode: string }) {
          if (answers.mode === "handler") return true;
          if (
            input === "public" ||
            input === "authenticated" ||
            input === "user-scoped"
          )
            return true;
          if (/^role:[a-z][a-z0-9_-]*$/.test(input)) return true;
          return "Must be public | authenticated | role:NAME | user-scoped";
        },
      },
    ],
    actions(answers) {
      const a = answers as {
        mode: string;
        feature: string;
        channelSlug: string;
        scope: string;
      };
      if (a.mode === "channel") return realtimeChannelActions(a);
      if (a.mode === "handler") return realtimeHandlerActions(a);
      throw new Error(`Unknown mode: ${a.mode}`);
    },
  });

  /**
   * Turbo generator: `core-package`
   *
   * Scaffolds an optional core package back into a slimmed template. Each
   * name maps to a verbatim snapshot of the package captured at generator-add
   * time. Phases 3-6 each register one entry in CORE_PACKAGE_GENERATORS.
   */
  const REALTIME_RULE_IMPORTS = `import noDirectSocketIO from "./rules/no-direct-socket-io.js";
import noRealtimeHandlerReexport from "./rules/no-realtime-handler-reexport.js";`;

  const REALTIME_RULE_BLOCK = `  {
    files: ["**/*.{ts,tsx,mjs,cjs,js}"],
    plugins: {
      "repo-rules": {
        rules: {
          "no-direct-socket-io": noDirectSocketIO,
          "no-realtime-handler-reexport": noRealtimeHandlerReexport,
        },
      },
    },
    rules: {
      "repo-rules/no-direct-socket-io": "error",
      "repo-rules/no-realtime-handler-reexport": "error",
    },
  },`;

  const EVENTS_RULE_BLOCK = `  {
    files: ["**/*.{ts,tsx,mjs,cjs,js}"],
    rules: {
      "no-restricted-syntax": [
        "error",
        {
          selector:
            "ExportNamedDeclaration[source.value=/\\/events\\/handlers\\//]",
          message:
            "Event handlers (events/handlers/*.handler.ts) must not be re-exported. Wire them only inside the consumer feature's bind-production / bind-dev-seed (Rule E1).",
        },
        {
          selector:
            "ExportAllDeclaration[source.value=/\\/events\\/handlers\\//]",
          message:
            "Event handlers (events/handlers/*.handler.ts) must not be re-exported. Wire them only inside the consumer feature's bind-production / bind-dev-seed (Rule E1).",
        },
        {
          selector:
            "MemberExpression[object.type='MemberExpression'][object.object.type='Identifier'][object.object.name='payload'][object.property.type='Identifier'][object.property.name='jobs']",
          message:
            "Direct \`payload.jobs.*\` access is not allowed here. Use IJobQueue (from @repo/core-shared/jobs) instead. Allowed only in **/integrations/cms/jobs/** and **/core-shared/src/jobs/**.",
        },
      ],
    },
  },
  // J — \`payload.jobs.*\` is allowed only in the integration layer.
  // In these paths, no-restricted-syntax is narrowed to keep E1 active but
  // drop the payload.jobs check.
  // Note: "**/core-shared/src/jobs/**" does not match from within a package-local
  // ESLint run because ESLint resolves globs relative to the config file location.
  // The pattern is kept for documentation; in practice, the PayloadJobQueue class
  // uses \`this.payload.jobs.*\` which the selector already ignores (it only catches
  // bare \`payload.jobs.*\`). Any new file added there that does use bare \`payload.jobs.*\`
  // would need this allowlist to be expressed as "**/jobs/payload-*" or similar.
  {
    files: [
      "**/integrations/cms/jobs/**",
      "**/core-shared/src/jobs/**",
    ],
    rules: {
      "no-restricted-syntax": [
        "error",
        {
          selector:
            "ExportNamedDeclaration[source.value=/\\/events\\/handlers\\//]",
          message:
            "Event handlers (events/handlers/*.handler.ts) must not be re-exported. Wire them only inside the consumer feature's bind-production / bind-dev-seed (Rule E1).",
        },
        {
          selector:
            "ExportAllDeclaration[source.value=/\\/events\\/handlers\\//]",
          message:
            "Event handlers (events/handlers/*.handler.ts) must not be re-exported. Wire them only inside the consumer feature's bind-production / bind-dev-seed (Rule E1).",
        },
      ],
    },
  },`;

  const CORE_PACKAGE_GENERATORS: Record<string, () => PlopTypes.ActionType[]> =
    {
      realtime: () => [
        () => {
          assertOptionalPackageNotPresent("core-realtime");
          return "Guard passed — packages/core-realtime does not exist yet.";
        },
        ...emitTemplateTree("core-package/realtime", "packages/core-realtime"),
        ...emitTemplateTree(
          "core-package/realtime/docs/library-decisions",
          "docs/library-decisions",
          { force: true },
        ),
        ...emitTemplateTree(
          "core-package/realtime-eslint-rules",
          "packages/core-eslint/rules",
        ),
        () => {
          addToTranspilePackages(
            "apps/web-next/next.config.mjs",
            "@repo/core-realtime",
          );
          return "Added @repo/core-realtime to transpilePackages.";
        },
        () => {
          addBoundariesEntry(
            "packages/core-eslint/base.js",
            "packages/core-realtime",
            { mode: "folder" },
          );
          return "Added core-realtime boundaries entry.";
        },
        () => {
          splicePluginImportsAt(
            "packages/core-eslint/base.js",
            "realtime-rules-imports",
            REALTIME_RULE_IMPORTS,
          );
          return "Added realtime rule imports to base.js.";
        },
        () => {
          splicePluginRulesAt(
            "packages/core-eslint/base.js",
            "realtime-rules",
            REALTIME_RULE_BLOCK,
          );
          return "Added realtime rule block to base.js.";
        },
        printRealtimeNextSteps,
      ],
      events: () => [
        () => {
          assertOptionalPackageNotPresent("core-events");
          return "Guard passed — packages/core-events does not exist yet.";
        },
        ...emitTemplateTree("core-package/events", "packages/core-events"),
        ...emitTemplateTree(
          "core-package/events/docs/library-decisions",
          "docs/library-decisions",
          { force: true },
        ),
        () => {
          addToTranspilePackages(
            "apps/web-next/next.config.mjs",
            "@repo/core-events",
          );
          return "Added @repo/core-events to transpilePackages.";
        },
        () => {
          splicePluginRulesAt(
            "packages/core-eslint/base.js",
            "events-rules",
            EVENTS_RULE_BLOCK,
          );
          return "Added events rule block to base.js.";
        },
        printEventsNextSteps,
      ],
      trpc: () => [
        () => {
          assertOptionalPackageNotPresent("core-trpc");
          return "Guard passed — packages/core-trpc does not exist yet.";
        },
        ...emitTemplateTree("core-package/trpc", "packages/core-trpc"),
        ...emitTemplateTree(
          "core-package/trpc/docs/library-decisions",
          "docs/library-decisions",
          { force: true },
        ),
        () => {
          addToTranspilePackages(
            "apps/web-next/next.config.mjs",
            "@repo/core-trpc",
          );
          return "Added @repo/core-trpc to transpilePackages.";
        },
        printTrpcNextSteps,
      ],
      ui: () => [
        () => {
          assertOptionalPackageNotPresent("core-ui");
          return "Guard passed — packages/core-ui does not exist yet.";
        },
        ...emitTemplateTree("core-package/ui", "packages/core-ui"),
        ...emitTemplateTree(
          "core-package/ui/docs/library-decisions",
          "docs/library-decisions",
          { force: true },
        ),
        () => {
          addToTranspilePackages(
            "apps/web-next/next.config.mjs",
            "@repo/core-ui",
          );
          return "Added @repo/core-ui to transpilePackages.";
        },
        printUiNextSteps,
      ],
      audit: () => [
        () => {
          assertOptionalPackageNotPresent("core-audit");
          return "Guard passed — packages/core-audit does not exist yet.";
        },
        ...emitTemplateTree("core-package/audit", "packages/core-audit"),
        ...emitTemplateTree(
          "core-package/audit/docs/library-decisions",
          "docs/library-decisions",
          { force: true },
        ),
        () => {
          addToTranspilePackages(
            "apps/web-next/next.config.mjs",
            "@repo/core-audit",
          );
          return "Added @repo/core-audit to transpilePackages.";
        },
        printAuditNextSteps,
      ],
      analytics: () => [
        () => {
          assertOptionalPackageNotPresent("core-analytics");
          return "Guard passed — packages/core-analytics does not exist yet.";
        },
        ...emitTemplateTree(
          "core-package/analytics",
          "packages/core-analytics",
        ),
        () => {
          addToTranspilePackages(
            "apps/web-next/next.config.mjs",
            "@repo/core-analytics",
          );
          return "Added @repo/core-analytics to transpilePackages.";
        },
        printAnalyticsNextSteps,
      ],
      consent: () => [
        () => {
          assertOptionalPackageNotPresent("core-consent");
          return "Guard passed — packages/core-consent does not exist yet.";
        },
        ...emitTemplateTree("core-package/consent", "packages/core-consent"),
        () => {
          addToTranspilePackages(
            "apps/web-next/next.config.mjs",
            "@repo/core-consent",
          );
          return "Added @repo/core-consent to transpilePackages.";
        },
        printConsentNextSteps,
      ],
      dsr: () => [
        () => {
          assertOptionalPackageNotPresent("core-dsr");
          return "Guard passed — packages/core-dsr does not exist yet.";
        },
        ...emitTemplateTree("core-package/dsr", "packages/core-dsr"),
        () => {
          addToTranspilePackages(
            "apps/web-next/next.config.mjs",
            "@repo/core-dsr",
          );
          return "Added @repo/core-dsr to transpilePackages.";
        },
        printDsrNextSteps,
      ],
    };

  plop.setGenerator("core-package", {
    description:
      "Scaffold an optional core package (realtime, events, trpc, ui, audit, analytics, consent, dsr)",
    prompts: [
      {
        type: "list",
        name: "name",
        message: "Which optional core package?",
        choices: [
          "analytics",
          "audit",
          "consent",
          "dsr",
          "events",
          "realtime",
          "trpc",
          "ui",
        ],
      },
    ],
    actions: (answers) => {
      const a = answers as { name: string };
      const handler = CORE_PACKAGE_GENERATORS[a.name];
      if (!handler) {
        throw new Error(`No generator for core-package '${a.name}'`);
      }
      return handler();
    },
  });

  /**
   * Turbo generator: `job`
   *
   * Scaffolds a background job inside an existing feature: factory + test +
   * Payload TaskConfig, plus the per-feature DI binding (span+capture wrap)
   * and re-export from `integrations/cms/index.ts`.
   */
  plop.setGenerator("job", {
    description: "Scaffold a background job in an existing feature",
    prompts: [
      {
        type: "input",
        name: "feature",
        message: "Feature (kebab-case, must exist):",
        validate(input: string) {
          if (!/^[a-z][a-z0-9-]*$/.test(input)) return "Must be kebab-case";
          if (!existsSync(join(process.cwd(), "packages", input, "src"))) {
            return `packages/${input}/src does not exist`;
          }
          return true;
        },
      },
      {
        type: "input",
        name: "job",
        message: "Job slug (verb-noun kebab, e.g. 'send-welcome-email'):",
        validate(input: string) {
          if (!/^[a-z][a-z0-9-]+$/.test(input)) return "Must be kebab-case";
          return true;
        },
      },
      {
        type: "list",
        name: "inputShape",
        message: "Input shape:",
        choices: ["void", "typed"],
        default: "void",
      },
    ],
    actions(answers) {
      const a = answers as {
        feature: string;
        job: string;
        inputShape: "void" | "typed";
      };
      return jobActions(a);
    },
  });

  /**
   * Turbo generator: `core-ui-component`
   *
   * Scaffolds a core-ui atomic-design component (atom / molecule / organism)
   * using the established 4-file pattern: `<name>.tsx`, `<name>.stories.tsx`,
   * `<name>.test.tsx`, `index.ts`. Splices the new export into the matching
   * tier barrel via the `// <gen:<tier>s>` anchor.
   *
   * Refuses to run unless `packages/core-ui/` exists (see
   * `pnpm turbo gen core-package ui`).
   */
  plop.setGenerator("core-ui-component", {
    description:
      "Scaffold a core-ui atomic-design component (atom / molecule / organism)",
    prompts: [
      {
        type: "list",
        name: "tier",
        message: "Tier:",
        choices: ["atom", "molecule", "organism"],
      },
      {
        type: "input",
        name: "name",
        message: "Component name (PascalCase, e.g. Spinner):",
        validate: (input: string) => {
          if (!input) return "Required";
          if (!/^[A-Z][A-Za-z0-9]*$/.test(input)) {
            return "Must be PascalCase (e.g. Spinner, IconButton)";
          }
          return true;
        },
      },
    ],
    actions: (answers) => {
      const a = answers as {
        tier: "atom" | "molecule" | "organism";
        name: string;
      };
      return coreUiComponentActions(a);
    },
  });
}

function jobActions(a: {
  feature: string;
  job: string;
  inputShape: "void" | "typed";
}): PlopTypes.ActionType[] {
  const symbolFile = `packages/${a.feature}/src/di/symbols.ts`;
  const bindProdFile = `packages/${a.feature}/src/di/bind-production.ts`;
  const bindDevFile = `packages/${a.feature}/src/di/bind-dev-seed.ts`;
  const cmsIndexFile = `packages/${a.feature}/src/integrations/cms/index.ts`;
  return [
    () => {
      assertAnchors(process.cwd(), symbolFile, ["// <gen:job-symbols>"]);
      assertAnchors(process.cwd(), bindProdFile, ["// <gen:jobs>"]);
      assertAnchors(process.cwd(), bindDevFile, ["// <gen:jobs>"]);
      assertAnchors(process.cwd(), cmsIndexFile, ["// <gen:job-tasks>"]);
      return "All required anchors present";
    },
    {
      type: "add",
      path: `packages/${a.feature}/src/jobs/${a.job}.job.ts`,
      templateFile: "templates/job/job.ts.hbs",
      data: a,
    },
    {
      type: "add",
      path: `packages/${a.feature}/src/jobs/${a.job}.job.test.ts`,
      templateFile: "templates/job/job.test.ts.hbs",
      data: a,
    },
    {
      type: "add",
      path: `packages/${a.feature}/src/integrations/cms/jobs/${a.job}.task.ts`,
      templateFile: "templates/job/task.ts.hbs",
      data: a,
    },
    {
      type: "modify",
      path: cmsIndexFile,
      pattern: /\/\/ <gen:job-tasks>/,
      template: `// <gen:job-tasks>\nexport { ${camel(a.job)}Task } from "./jobs/${a.job}.task";`,
    },
    {
      type: "modify",
      path: symbolFile,
      pattern: /\/\/ <gen:job-symbols>/,
      template: `// <gen:job-symbols>\n  I${pascalCase(a.job)}Job: Symbol.for("@repo/${a.feature}/${camel(a.job)}Job"),`,
    },
    {
      type: "modify",
      path: bindProdFile,
      pattern: /\/\/ <gen:jobs>/,
      template: jobBindBlock(a),
    },
    {
      type: "modify",
      path: bindDevFile,
      pattern: /\/\/ <gen:jobs>/,
      template: jobBindBlock(a),
    },
    () => printJobNextSteps(a),
  ];
}

function jobBindBlock(a: { feature: string; job: string }): string {
  const factoryFn = `${camel(a.job)}Job`;
  const symbol = `${constantCase(a.feature)}_SYMBOLS.I${pascalCase(a.job)}Job`;
  const containerVar = `${camel(a.feature)}Container`;
  return `// <gen:jobs>
  const wrapped${pascalCase(a.job)} = withSpan(
    tracer,
    { name: "${a.feature}.${camel(a.job)}", op: "job" },
    withCapture(
      logger,
      {
        feature: "${a.feature}",
        layer: "job",
        name: "${a.feature}.${camel(a.job)}",
      },
      ${factoryFn}(),
    ),
  );
  if (${containerVar}.isBound(${symbol})) {
    ${containerVar}.unbind(${symbol});
  }
  ${containerVar}.bind(${symbol}).toConstantValue(wrapped${pascalCase(a.job)});`;
}

function printJobNextSteps(a: { feature: string; job: string }): string {
  return [
    "",
    "─────────────────────────────────────────────────────────────",
    `Job ${a.feature}.${a.job} scaffolded.`,
    "",
    "Next steps (manual):",
    `  1. Fill in the job body in packages/${a.feature}/src/jobs/${a.job}.job.ts`,
    `  2. Add Payload field config to inputSchema in ${a.job}.task.ts (matches your Zod schema).`,
    `  3. Add the import for ${camel(a.job)}Job at the top of bind-production.ts and bind-dev-seed.ts.`,
    `  4. (Optional) Add a cron schedule in core-cms's buildConfig if this job runs periodically.`,
    `  5. Verify: pnpm --filter @repo/${a.feature} lint typecheck test`,
    "─────────────────────────────────────────────────────────────",
    "",
  ].join("\n");
}

function publishActions(a: {
  feature: string;
  event: string;
}): PlopTypes.ActionType[] {
  const eventKebab = a.event.replace(/\./g, "-");
  const indexPath = `packages/${a.feature}/src/index.ts`;
  return [
    () => {
      assertAnchors(process.cwd(), indexPath, ["// <gen:events>"]);
      return `Anchors verified in ${indexPath}`;
    },
    {
      type: "add",
      path: `packages/${a.feature}/src/events/${eventKebab}.event.ts`,
      templateFile: "templates/event/publish/event.ts.hbs",
      data: { event: a.event, feature: a.feature, eventKebab },
    },
    {
      type: "add",
      path: `packages/${a.feature}/src/events/${eventKebab}.event.test.ts`,
      templateFile: "templates/event/publish/event.test.ts.hbs",
      data: { event: a.event, feature: a.feature, eventKebab },
    },
    {
      type: "modify",
      path: indexPath,
      pattern: /\/\/ <gen:events>/,
      template: `// <gen:events>\nexport {\n  {{camelCase event}}Event,\n  {{camelCase event}}EventSchema,\n  type {{pascalCase event}}Event,\n} from "./events/{{kebabCase event}}.event";`,
      data: { event: a.event },
    },
    () => printPublishNextSteps(a),
  ];
}

function consumeActions(a: {
  feature: string;
  event: string;
  publisher: string;
}): PlopTypes.ActionType[] {
  const eventKebab = a.event.replace(/\./g, "-");
  const handlerName = `on${pascalCase(a.publisher)}${pascalCase(a.event)}`;
  const symbolFile = `packages/${a.feature}/src/di/symbols.ts`;
  const bindProdFile = `packages/${a.feature}/src/di/bind-production.ts`;
  const bindDevFile = `packages/${a.feature}/src/di/bind-dev-seed.ts`;
  const cmsIndexFile = `packages/${a.feature}/src/integrations/cms/index.ts`;
  return [
    () => {
      assertAnchors(process.cwd(), symbolFile, [
        "// <gen:event-handler-symbols>",
      ]);
      assertAnchors(process.cwd(), bindProdFile, ["// <gen:event-handlers>"]);
      assertAnchors(process.cwd(), bindDevFile, ["// <gen:event-handlers>"]);
      assertAnchors(process.cwd(), cmsIndexFile, ["// <gen:job-tasks>"]);
      return "All required anchors present";
    },
    {
      type: "add",
      path: `packages/${a.feature}/src/events/handlers/on-${a.publisher}-${eventKebab}.handler.ts`,
      templateFile: "templates/event/consume/handler.ts.hbs",
      data: a,
    },
    {
      type: "add",
      path: `packages/${a.feature}/src/events/handlers/on-${a.publisher}-${eventKebab}.handler.test.ts`,
      templateFile: "templates/event/consume/handler.test.ts.hbs",
      data: a,
    },
    {
      type: "add",
      path: `packages/${a.feature}/src/integrations/cms/jobs/__events-${a.publisher}-${eventKebab}.task.ts`,
      templateFile: "templates/event/consume/event-task.ts.hbs",
      data: a,
    },
    {
      type: "modify",
      path: symbolFile,
      pattern: /\/\/ <gen:event-handler-symbols>/,
      template: `// <gen:event-handler-symbols>\n  IOn${pascalCase(a.publisher)}${pascalCase(a.event)}Handler: Symbol.for("@repo/${a.feature}/${handlerName}"),`,
    },
    {
      type: "modify",
      path: bindProdFile,
      pattern: /\/\/ <gen:event-handlers>/,
      template: handlerBindBlock(a),
    },
    {
      type: "modify",
      path: bindDevFile,
      pattern: /\/\/ <gen:event-handlers>/,
      template: handlerBindBlock(a),
    },
    {
      type: "modify",
      path: cmsIndexFile,
      pattern: /\/\/ <gen:job-tasks>/,
      template: `// <gen:job-tasks>\nexport { on${pascalCase(a.publisher)}${pascalCase(a.event)}EventTask } from "./jobs/__events-${a.publisher}-${eventKebab}.task";`,
    },
    () => printConsumeNextSteps(a),
  ];
}

function handlerBindBlock(a: {
  feature: string;
  event: string;
  publisher: string;
}): string {
  const handlerFn = `on${pascalCase(a.publisher)}${pascalCase(a.event)}Handler`;
  const eventConst = `${camel(a.event.replace(/\./g, "-"))}Event`;
  const handlerSymbol = `${constantCase(a.feature)}_SYMBOLS.IOn${pascalCase(a.publisher)}${pascalCase(a.event)}Handler`;
  const wrappedVar = `wrapped${pascalCase(a.publisher)}${pascalCase(a.event)}`;
  const containerVar = `${camel(a.feature)}Container`;
  return `// <gen:event-handlers>
  // ${handlerFn} subscription — generated, edit the handler file (not this block) for behavior.
  const ${wrappedVar} = withSpan(
    tracer,
    { name: "${a.feature}.${handlerFn}", op: "event-handler" },
    withCapture(
      logger,
      {
        feature: "${a.feature}",
        layer: "event-handler",
        name: "${a.feature}.${handlerFn}",
      },
      ${handlerFn}(),
    ),
  );
  if (${containerVar}.isBound(${handlerSymbol})) {
    ${containerVar}.unbind(${handlerSymbol});
  }
  ${containerVar}.bind(${handlerSymbol}).toConstantValue(${wrappedVar});
  bus.subscribe(${eventConst}, "${a.feature}", ${wrappedVar});`;
}

function printConsumeNextSteps(a: {
  feature: string;
  event: string;
  publisher: string;
}): string {
  const eventKebab = a.event.replace(/\./g, "-");
  const eventConst = `${camel(a.event.replace(/\./g, "-"))}Event`;
  return [
    "",
    "─────────────────────────────────────────────────────────────",
    `Handler on-${a.publisher}-${eventKebab} scaffolded in ${a.feature}.`,
    "",
    "Next steps (manual):",
    `  1. Implement the handler body in packages/${a.feature}/src/events/handlers/on-${a.publisher}-${eventKebab}.handler.ts`,
    `  2. Add the import at the top of bind-production.ts AND bind-dev-seed.ts:`,
    `       import { ${eventConst} } from "@repo/${a.publisher}";`,
    `       import { ${camel(`on-${a.publisher}-${a.event.replace(/\./g, "-")}-handler`)} } from "../events/handlers/on-${a.publisher}-${eventKebab}.handler";`,
    `  3. If your handler needs deps, extend the factory signature and pass them in the generated bind block.`,
    `  4. Add "@repo/${a.publisher}": "workspace:*" to packages/${a.feature}/package.json dependencies (if not already present).`,
    `  5. Verify: pnpm --filter @repo/${a.feature} lint typecheck test`,
    "─────────────────────────────────────────────────────────────",
    "",
  ].join("\n");
}

function printPublishNextSteps(a: { feature: string; event: string }): string {
  const eventKebab = a.event.replace(/\./g, "-");
  return [
    "",
    "─────────────────────────────────────────────────────────────",
    `Event ${a.feature}.${a.event} contract scaffolded.`,
    "",
    "Next steps (manual):",
    `  1. Fill in the schema in packages/${a.feature}/src/events/${eventKebab}.event.ts`,
    `  2. Pick a use case in packages/${a.feature}/src/application/use-cases/ that should publish.`,
    `     - Add 'bus: IEventBus' to the factory signature.`,
    `     - Call 'await bus.publish(${camel(a.event.replace(/\./g, "-"))}Event, payload)' after success.`,
    `     - Update the use case's DI binding to inject the bus.`,
    `  3. Verify: pnpm --filter @repo/${a.feature} lint typecheck test`,
    "─────────────────────────────────────────────────────────────",
    "",
  ].join("\n");
}

function realtimeChannelActions(a: {
  feature: string;
  channelSlug: string;
  scope: string;
}): PlopTypes.ActionType[] {
  const indexPath = `packages/${a.feature}/src/index.ts`;
  const scopeLiteral = renderScopeLiteral(a.scope);
  return [
    () => {
      assertAnchors(process.cwd(), indexPath, ["// <gen:realtime-channels>"]);
      return `Anchors verified in ${indexPath}`;
    },
    {
      type: "add",
      path: `packages/${a.feature}/src/realtime/${a.channelSlug}.channel.ts`,
      templateFile: "templates/realtime/channel/channel.ts.hbs",
      data: { ...a, scopeLiteral },
    },
    {
      type: "add",
      path: `packages/${a.feature}/src/realtime/${a.channelSlug}.channel.test.ts`,
      templateFile: "templates/realtime/channel/channel.test.ts.hbs",
      data: { ...a, scopeLiteral },
    },
    {
      type: "modify",
      path: indexPath,
      pattern: /\/\/ <gen:realtime-channels>/,
      template: `// <gen:realtime-channels>\nexport {\n  {{camelCase channelSlug}}Channel,\n  {{camelCase channelSlug}}Schema,\n  type {{pascalCase channelSlug}}Payload,\n} from "./realtime/{{kebabCase channelSlug}}.channel";`,
      data: a,
    },
    () => printChannelNextSteps(a),
  ];
}

function renderScopeLiteral(scope: string): string {
  if (scope === "public") return `"public"`;
  if (scope === "authenticated") return `"authenticated"`;
  if (scope.startsWith("role:"))
    return `{ role: "${scope.slice("role:".length)}" }`;
  if (scope === "user-scoped")
    return `{ userScoped: true, template: "TODO_TEMPLATE" }`;
  throw new Error(`unknown scope: ${scope}`);
}

function printChannelNextSteps(a: {
  feature: string;
  channelSlug: string;
}): string {
  return [
    "─────────────────────────────────────────────────────────────",
    `Realtime channel ${a.feature}.${a.channelSlug} scaffolded.`,
    "",
    "Next steps (manual):",
    `  1. Fill in the Zod schema in packages/${a.feature}/src/realtime/${a.channelSlug}.channel.ts`,
    `  2. To broadcast: inject 'realtime: IRealtimeBroadcaster' into a use case factory and call`,
    `       realtime.broadcast(${a.channelSlug}Channel, payload)`,
    `  3. To receive client-emitted messages on this channel: pnpm turbo gen realtime`,
    `     and pick "handler" mode with the same channel slug.`,
    `  4. Verify: pnpm --filter @repo/${a.feature} lint typecheck test`,
    "─────────────────────────────────────────────────────────────",
  ].join("\n");
}

function realtimeHandlerActions(a: {
  feature: string;
  channelSlug: string;
}): PlopTypes.ActionType[] {
  const symbolFile = `packages/${a.feature}/src/di/symbols.ts`;
  const bindProdFile = `packages/${a.feature}/src/di/bind-production.ts`;
  const bindDevFile = `packages/${a.feature}/src/di/bind-dev-seed.ts`;
  return [
    () => {
      assertAnchors(process.cwd(), symbolFile, [
        "// <gen:realtime-handler-symbols>",
      ]);
      assertAnchors(process.cwd(), bindProdFile, [
        "// <gen:realtime-handlers>",
      ]);
      assertAnchors(process.cwd(), bindDevFile, ["// <gen:realtime-handlers>"]);
      return "All required anchors present";
    },
    {
      type: "add",
      path: `packages/${a.feature}/src/realtime/handlers/on-${a.channelSlug}.handler.ts`,
      templateFile: "templates/realtime/handler/handler.ts.hbs",
      data: a,
    },
    {
      type: "add",
      path: `packages/${a.feature}/src/realtime/handlers/on-${a.channelSlug}.handler.test.ts`,
      templateFile: "templates/realtime/handler/handler.test.ts.hbs",
      data: a,
    },
    {
      type: "modify",
      path: symbolFile,
      pattern: /\/\/ <gen:realtime-handler-symbols>/,
      template: `// <gen:realtime-handler-symbols>\n  IOn${pascalCase(a.channelSlug)}Handler: Symbol.for("@repo/${a.feature}/on${pascalCase(a.channelSlug)}Handler"),`,
    },
    {
      type: "modify",
      path: bindProdFile,
      pattern: /\/\/ <gen:realtime-handlers>/,
      template: realtimeHandlerBindBlock(a),
    },
    {
      type: "modify",
      path: bindDevFile,
      pattern: /\/\/ <gen:realtime-handlers>/,
      template: realtimeHandlerBindBlock(a),
    },
    () => printHandlerNextSteps(a),
  ];
}

function realtimeHandlerBindBlock(a: {
  feature: string;
  channelSlug: string;
}): string {
  const handlerFn = `on${pascalCase(a.channelSlug)}Handler`;
  const channelConst = `${camel(a.channelSlug)}Channel`;
  return `// <gen:realtime-handlers>
  // ${handlerFn} — generated by gen realtime handler. Edit the handler file (not this block).
  const wrapped${pascalCase(a.channelSlug)} = withSpan(
    tracer,
    { name: "${a.feature}.${handlerFn}", op: "realtime-handler" },
    withCapture(
      logger,
      { feature: "${a.feature}", layer: "realtime-handler", name: "${a.feature}.${handlerFn}" },
      ${handlerFn}(),
    ),
  );
  realtimeRegistry.register({ descriptor: ${channelConst}, handler: wrapped${pascalCase(a.channelSlug)} });`;
}

function printHandlerNextSteps(a: {
  feature: string;
  channelSlug: string;
}): string {
  return [
    "─────────────────────────────────────────────────────────────",
    `Realtime handler on-${a.channelSlug} scaffolded in ${a.feature}.`,
    "",
    "Next steps (manual):",
    `  1. Implement the handler body in packages/${a.feature}/src/realtime/handlers/on-${a.channelSlug}.handler.ts`,
    `  2. Add the imports to bind-production.ts AND bind-dev-seed.ts:`,
    `       import { ${camel(a.channelSlug)}Channel } from "../realtime/${a.channelSlug}.channel";`,
    `       import { on${pascalCase(a.channelSlug)}Handler } from "../realtime/handlers/on-${a.channelSlug}.handler";`,
    `  3. If the handler needs deps, extend the factory signature and pass them in the bind block.`,
    `  4. Verify: pnpm --filter @repo/${a.feature} lint typecheck test`,
    "─────────────────────────────────────────────────────────────",
  ].join("\n");
}

function printTrpcNextSteps(): string {
  return [
    "─────────────────────────────────────────────────────────────",
    "@repo/core-trpc scaffolded.",
    "",
    "Next steps (manual wiring — not generated):",
    "",
    "  1. pnpm install   # link the new workspace package",
    "",
    "  2. apps/web-next/src/app/providers.tsx:",
    '     - import { NextTrpcProvider } from "@repo/core-trpc/next";',
    '     - wrap children: <NextTrpcProvider trpcUrl="/api/trpc">{children}</NextTrpcProvider>',
    "",
    "  3. apps/web-next/src/app/api/trpc/[trpc]/route.ts:",
    '     - import { fetchRequestHandler } from "@trpc/server/adapters/fetch";',
    '     - import { appRouter } from "@repo/core-api";',
    "     - export GET and POST handlers using fetchRequestHandler",
    "",
    "  4. apps/web-tanstack/src/routes/__root.tsx:",
    '     - import { TanstackTrpcProvider } from "@repo/core-trpc/tanstack";',
    '     - wrap <Outlet /> with <TanstackTrpcProvider trpcUrl="http://localhost:3000/api/trpc">',
    "",
    "  5. Add @repo/core-trpc to apps/web-next/package.json and apps/web-tanstack/package.json dependencies",
    "",
    "  6. pnpm typecheck && pnpm lint && pnpm test",
    "─────────────────────────────────────────────────────────────",
  ].join("\n");
}

function printUiNextSteps(): string {
  return [
    "─────────────────────────────────────────────────────────────",
    "@repo/core-ui scaffolded.",
    "",
    "Next steps (manual wiring — not generated):",
    "",
    "  1. pnpm install   # link the new workspace package",
    "",
    "  2. apps/web-next/package.json:",
    '     - add "@repo/core-ui": "workspace:*" to dependencies',
    "",
    "  3. apps/storybook/.storybook/main.ts:",
    '     - add "../../../packages/core-ui/src/**/*.stories.@(ts|tsx)" to the stories array',
    "",
    "  4. apps/storybook/.storybook/preview.ts:",
    '     - add: import "@repo/core-ui/styles/globals.css";',
    "",
    "  5. apps/storybook/package.json:",
    '     - add "@repo/core-ui": "workspace:*" to dependencies',
    "",
    "  6. apps/web-tanstack/package.json:",
    '     - add "@repo/core-ui": "workspace:*" to dependencies',
    "     - when TanStack Start gains a real vite.config.ts, add @repo/core-ui",
    "       to optimizeDeps.include",
    "",
    "  7. pnpm typecheck && pnpm lint && pnpm test",
    "─────────────────────────────────────────────────────────────",
  ].join("\n");
}

function printEventsNextSteps(): string {
  return [
    "─────────────────────────────────────────────────────────────",
    "@repo/core-events scaffolded.",
    "",
    "Next steps (manual wiring — not generated):",
    "",
    "  1. pnpm install   # link the new workspace package",
    "",
    "  2. apps/web-next/src/server/bind-production.ts:",
    '     - import { InMemoryEventBus, PayloadJobsEventBus, type IEventBus } from "@repo/core-events";',
    "     - construct bus in resolveEventsAndJobsProduction + resolveEventsAndJobsDevSeed",
    "     - add bus to BindProductionContext generic arg and ctx object",
    "     - update BindAllDeps to include bus field",
    "",
    "  3. Add @repo/core-events to each feature package.json that publishes or subscribes events",
    "",
    "  4. pnpm typecheck && pnpm lint && pnpm test",
    "─────────────────────────────────────────────────────────────",
  ].join("\n");
}

function printRealtimeNextSteps(): string {
  return [
    "─────────────────────────────────────────────────────────────",
    "@repo/core-realtime scaffolded.",
    "",
    "Next steps (manual wiring — not generated):",
    "",
    "  1. pnpm install   # link the new workspace package",
    "",
    "  2. apps/web-next/server.ts:",
    '     - import { Server as IOServer } from "socket.io";',
    '     - import { RealtimeHandlerRegistry, SocketIORealtimeBroadcaster, SocketIORealtimeServer } from "@repo/core-realtime";',
    "     - create IOServer on the http server, construct broadcaster + registry",
    "     - call bindAll({ realtime: broadcaster, realtimeRegistry: registry })",
    "     - start SocketIORealtimeServer",
    "",
    "  3. apps/web-next/src/server/bind-production.ts:",
    '     - import { IRealtimeBroadcaster, IRealtimeHandlerRegistry, InMemoryRealtimeBroadcaster, RealtimeHandlerRegistry } from "@repo/core-realtime";',
    "     - add realtime + realtimeRegistry to BindAllDeps",
    "     - update BindProductionContext generic args",
    "     - add maybeRegisterRealtimePing + bindRealtimeBridge functions",
    "",
    "  4. Add @repo/core-realtime to each feature package.json that uses realtime",
    "",
    "  5. pnpm typecheck && pnpm lint && pnpm test",
    "─────────────────────────────────────────────────────────────",
  ].join("\n");
}

function printAuditNextSteps(): string {
  return [
    "─────────────────────────────────────────────────────────────",
    "@repo/core-audit scaffolded into packages/core-audit/.",
    "",
    "Manual wiring required (compliance-critical):",
    "",
    "1. Set AUDIT_PSEUDONYM_SALT env var (production REQUIRED):",
    '     export AUDIT_PSEUDONYM_SALT="$(openssl rand -hex 32)"',
    "   Add to your deployment secrets manager.",
    "",
    "2. Mount the audit-logs Payload collection in packages/core-cms/src/payload.config.ts:",
    '     import { auditLogsCollection } from "@repo/core-audit/collection";',
    "     // collections: [..., auditLogsCollection],",
    "",
    "3. Mount the admin tRPC router in packages/core-api/src/root.ts:",
    '     import { createAuditRouter } from "@repo/core-audit/api";',
    '     // const { auditLog } = bindAudit(container, { payloadConfig, sinks: ["payload", "stdout"] });',
    "     // routers: { ..., audit: createAuditRouter(auditLog) },",
    "",
    "4. Bind audit in apps/web-next/src/server/bind-production.ts:",
    '     const { bindAudit } = await import("@repo/core-audit/di");',
    "     const { auditLog } = bindAudit(sharedContainer, {",
    "       payloadConfig: resolvedConfig,",
    '       sinks: ["payload", "stdout"],',
    "     });",
    "",
    "5. Install user-collection hooks (recommended for DPA compliance):",
    "   In packages/auth/src/di/bind-production.ts, gate on ctx.auditLog:",
    "     if (ctx.auditLog) {",
    "       const { createAuditErasureHook, createAuditAfterReadHook } =",
    '         await import("@repo/core-audit/hooks");',
    "       // wire onto users collection — see docs/guides/audit-and-compliance.md",
    "     }",
    "",
    "6. Set up a log shipper (Vector / Fluent Bit) to forward stdout JSON to",
    "   your aggregator. See docs/guides/audit-and-compliance.md for configs.",
    "",
    "7. Verify:",
    "     pnpm install",
    "     pnpm lint && pnpm typecheck && pnpm test",
    "     pnpm turbo boundaries",
    "",
    "See docs/guides/audit-and-compliance.md for the full guide.",
    "─────────────────────────────────────────────────────────────",
  ].join("\n");
}

function printAnalyticsNextSteps(): string {
  return [
    "─────────────────────────────────────────────────────────────",
    "@repo/core-analytics scaffolded into packages/core-analytics/.",
    "",
    "Next steps:",
    "",
    "  1. pnpm install   # link the new workspace package",
    "",
    "  2. Implement IAnalytics + NoopAnalytics in packages/core-analytics/src/",
    "     (see story 01-scaffold-core-analytics-package bullet 2)",
    "",
    "  3. Add @repo/core-analytics to feature package.json files that need analytics",
    "",
    "  4. Wire IAnalytics into apps/web-next/src/server/bind-production.ts:",
    '     - import { NoopAnalytics, type IAnalytics } from "@repo/core-analytics";',
    "     - add analytics field to BindAllDeps and BindProductionContext",
    "     - pass NoopAnalytics (or a vendor adapter) into each feature binder",
    "",
    "  5. pnpm typecheck && pnpm lint && pnpm test",
    "─────────────────────────────────────────────────────────────",
  ].join("\n");
}

function printConsentNextSteps(): string {
  return [
    "─────────────────────────────────────────────────────────────",
    "@repo/core-consent scaffolded into packages/core-consent/.",
    "",
    "Next steps:",
    "",
    "  1. pnpm install   # link the new workspace package",
    "",
    "  2. Implement consent-types.ts, consent.interface.ts, and withConsent",
    "     in packages/core-consent/src/ (see story 03-core-consent-foundation)",
    "",
    "  3. Add @repo/core-consent to feature package.json files that need consent",
    "",
    "  4. Wire IConsent into apps/web-next/src/server/bind-production.ts:",
    '     - import { type IConsent } from "@repo/core-consent";',
    "     - add consent field to BindAllDeps and BindProductionContext",
    "     - pass the consent instance into each feature binder",
    "",
    "  5. pnpm typecheck && pnpm lint && pnpm test",
    "─────────────────────────────────────────────────────────────",
  ].join("\n");
}

function printDsrNextSteps(): string {
  return [
    "─────────────────────────────────────────────────────────────",
    "@repo/core-dsr scaffolded into packages/core-dsr/.",
    "",
    "Next steps:",
    "",
    "  1. pnpm install   # link the new workspace package",
    "",
    "  2. Implement DSR interfaces and types in packages/core-dsr/src/",
    "     (see story 06-core-dsr)",
    "",
    "  3. Add @repo/core-dsr to feature package.json files that need DSR",
    "",
    "  4. pnpm typecheck && pnpm lint && pnpm test",
    "─────────────────────────────────────────────────────────────",
  ].join("\n");
}

function coreUiComponentActions(a: {
  tier: "atom" | "molecule" | "organism";
  name: string;
}): PlopTypes.ActionType[] {
  const tierPlural = `${a.tier}s` as const;
  const tierTitle = tierPlural[0]!.toUpperCase() + tierPlural.slice(1);
  const tierBarrel = `packages/core-ui/src/${tierPlural}/index.ts`;
  const componentDir = `packages/core-ui/src/${tierPlural}/{{kebabCase name}}`;

  return [
    // 1. Guard: core-ui must be scaffolded
    () => {
      const pkgRoot = join(process.cwd(), "packages", "core-ui");
      if (!existsSync(pkgRoot)) {
        throw new Error(
          `packages/core-ui/ does not exist. Run \`pnpm turbo gen core-package ui\` first.`,
        );
      }
      return "Guard passed — packages/core-ui exists.";
    },

    // 2. Guard: tier barrel must have its anchor
    () => {
      assertAnchors(process.cwd(), tierBarrel, [`// <gen:${tierPlural}>`]);
      return `Anchor // <gen:${tierPlural}> present in ${tierBarrel}.`;
    },

    // 3. Emit the 4 component files
    {
      type: "add",
      path: `${componentDir}/{{kebabCase name}}.tsx`,
      templateFile: "templates/core-ui-component/component.tsx.hbs",
      data: { name: a.name },
    },
    {
      type: "add",
      path: `${componentDir}/{{kebabCase name}}.stories.tsx`,
      templateFile: "templates/core-ui-component/component.stories.tsx.hbs",
      data: { name: a.name, tierTitle },
    },
    {
      type: "add",
      path: `${componentDir}/{{kebabCase name}}.test.tsx`,
      templateFile: "templates/core-ui-component/component.test.tsx.hbs",
      data: { name: a.name },
    },
    {
      type: "add",
      path: `${componentDir}/index.ts`,
      templateFile: "templates/core-ui-component/index.ts.hbs",
      data: { name: a.name },
    },

    // 4. Splice export into tier barrel.
    //    The `template` string is itself Handlebars-rendered by plop, so
    //    {{pascalCase name}} and {{kebabCase name}} are resolved at apply-
    //    time. This matches the pattern used by the `realtime` and `event`
    //    generators (no kebabCase JS helper exists in config.ts).
    {
      type: "modify",
      path: tierBarrel,
      pattern: new RegExp(`// <gen:${tierPlural}>`),
      template: `// <gen:${tierPlural}>\nexport { {{pascalCase name}}, type {{pascalCase name}}Props } from "./{{kebabCase name}}/index";`,
    },

    // 5. Print next-steps
    () => printCoreUiComponentNextSteps(a),
  ];
}

function printCoreUiComponentNextSteps(a: {
  tier: "atom" | "molecule" | "organism";
  name: string;
}): string {
  const tierPlural = `${a.tier}s`;
  const kebab = a.name
    .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
    .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
    .toLowerCase();
  const pascal = a.name;
  return [
    "─────────────────────────────────────────────────────────────",
    `${pascal} scaffolded into packages/core-ui/src/${tierPlural}/${kebab}/.`,
    "",
    "Next steps (manual):",
    `  1. Implement the component body in ${kebab}.tsx (currently a div`,
    `     passthrough — change the element/type to fit, add variants/sizes`,
    `     if needed; see button.tsx for the canonical rich pattern).`,
    `  2. Flesh out the Default story in ${kebab}.stories.tsx; add variant`,
    `     stories if your component takes a variant prop.`,
    `  3. Tighten the tests in ${kebab}.test.tsx beyond the smoke-test`,
    `     baseline.`,
    "",
    "Verify:",
    "  pnpm --filter @repo/core-ui lint typecheck test",
    "  pnpm dev --filter @repo/storybook   # view in Storybook",
    "─────────────────────────────────────────────────────────────",
  ].join("\n");
}

// Local helpers used inside the printNextSteps action — Plop's helpers aren't
// available outside template strings, so we replicate the bits we need.
function cap(input: string): string {
  return input
    .split(/[-_\s]+/)
    .filter(Boolean)
    .map((s) => s[0]!.toUpperCase() + s.slice(1))
    .join("");
}
function camel(input: string): string {
  const p = cap(input);
  return p ? p[0]!.toLowerCase() + p.slice(1) : "";
}
function pascalCase(input: string): string {
  // Same as `cap` but also splits on dots, so "user.signed-up" → "UserSignedUp".
  return input
    .split(/[-_.\s]+/)
    .filter(Boolean)
    .map((s) => s[0]!.toUpperCase() + s.slice(1))
    .join("");
}
function constantCase(input: string): string {
  return input.toUpperCase().replace(/[-.\s]/g, "_");
}
