import path from "node:path";
import { parseManifestUseCases } from "./_manifest-ast.js";
import {
  manifestPathForFeature,
  featureRootForFile,
} from "./_manifest-source.js";

/**
 * Every manifest-declared use case must be bound through `wireUseCase(...)` in
 * the feature's `bind-production.ts` and `bind-dev-seed.ts`. `wireUseCase` is
 * the canonical helper that wraps factories with `withSpan` + `withCapture`
 * (+ `withAudit` for mutating use cases) and attaches the corresponding
 * brands. Skipping it produces an unbranded binding that `assertFeatureConformance`
 * will reject at boot — this rule shifts that check from ~3s (boot) to <1s
 * (lint), so drift is caught while typing.
 *
 * Lint-time complement to:
 *   - the TypeScript brand check (`ProductionUseCase<I,O>` slot type) — compile time
 *   - the boot-time assertion (`assertFeatureConformance`) — runtime
 *   - the smoke test (`bind-production.smoke.test.ts`) — CI
 */
const BINDER_BASENAMES = new Set(["bind-production.ts", "bind-dev-seed.ts"]);

/** @type {import("eslint").Rule.RuleModule} */
export default {
  meta: {
    type: "problem",
    docs: {
      description:
        "Every manifest use case must be bound through wireUseCase(...) in the feature's bind-production.ts / bind-dev-seed.ts.",
    },
    schema: [
      {
        type: "object",
        properties: { repoRoot: { type: "string" } },
        additionalProperties: false,
      },
    ],
    messages: {
      missing:
        'Use case "{{useCase}}" is declared in {{feature}}.manifest.ts but not wired through wireUseCase({ name: "{{useCase}}", ... }) in this binder. Add the wireUseCase call or remove the manifest entry.',
    },
  },
  create(context) {
    const opts = context.options[0] ?? {};
    const repoRoot = opts.repoRoot ?? context.cwd ?? process.cwd();
    const filename = context.filename;
    if (!BINDER_BASENAMES.has(path.basename(filename))) return {};

    const featureRoot = featureRootForFile(filename, repoRoot);
    if (!featureRoot) return {};
    const manifest = parseManifestUseCases(manifestPathForFeature(featureRoot));
    if (!manifest) return {};
    const declaredNames = Object.keys(manifest);
    if (declaredNames.length === 0) return {};
    const featureName = path.basename(featureRoot);

    const wired = new Set();
    return {
      CallExpression(node) {
        if (
          node.callee.type !== "Identifier" ||
          node.callee.name !== "wireUseCase"
        ) {
          return;
        }
        const arg = node.arguments[0];
        if (!arg || arg.type !== "ObjectExpression") return;
        const nameProp = arg.properties.find(
          (p) =>
            p.type === "Property" &&
            p.key.type === "Identifier" &&
            p.key.name === "name",
        );
        if (
          nameProp &&
          nameProp.value.type === "Literal" &&
          typeof nameProp.value.value === "string"
        ) {
          wired.add(nameProp.value.value);
        }
      },
      "Program:exit"(programNode) {
        for (const useCase of declaredNames) {
          if (!wired.has(useCase)) {
            context.report({
              node: programNode,
              messageId: "missing",
              data: { useCase, feature: featureName },
            });
          }
        }
      },
    };
  },
};
