import type { StandardSchemaV1 } from "@standard-schema/spec";
import {
  type EventPublishOptions,
  prepareEventPayloadForTransport,
} from "../events/index.js";
import { markEventPayloadParsed } from "../events/payload-state.js";
import {
  type ProviderInstrumentationEventInput,
  resolveProviderInstrumentationPort,
} from "../providers/instrumentation.js";
import {
  createChildTraceContext,
  resolveTracingPort,
  runWithTracing,
  type TraceContextInput,
} from "../tracing/index.js";

/**
 * Any Standard Schema compatible validator.
 */
export type StandardSchema = StandardSchemaV1<unknown, unknown>;

/**
 * Infer the parsed output type from a Standard Schema.
 */
export type InferOutput<T extends StandardSchemaV1> =
  StandardSchemaV1.InferOutput<T>;

/**
 * Infer the input type accepted by a Standard Schema.
 */
export type InferInput<T extends StandardSchemaV1> =
  StandardSchemaV1.InferInput<T>;

type SchemaInput<T> = T extends StandardSchemaV1 ? InferInput<T> : never;
type SchemaOutput<T> = T extends StandardSchemaV1 ? InferOutput<T> : never;

/**
 * Boundary phase that failed use-case schema validation.
 */
export type UseCaseValidationPhase = "input" | "output";

/**
 * Error thrown when a use case input or output fails schema validation.
 */
export class UseCaseValidationError extends Error {
  readonly name = "UseCaseValidationError";
  readonly useCaseName: string;
  readonly phase: UseCaseValidationPhase;
  readonly issues: readonly StandardSchemaV1.Issue[];

  constructor(args: {
    useCaseName: string;
    phase: UseCaseValidationPhase;
    issues: readonly StandardSchemaV1.Issue[];
  }) {
    super(
      `Use case "${args.useCaseName}" ${args.phase} validation failed: ${formatIssues(args.issues)}`,
    );
    this.useCaseName = args.useCaseName;
    this.phase = args.phase;
    this.issues = args.issues;
  }
}

/**
 * Error thrown when a use case tries to emit an event it did not declare with
 * `.emits(...)`.
 */
export class UseCaseEventDeclarationError extends Error {
  readonly name = "UseCaseEventDeclarationError";
  readonly useCaseName: string;
  readonly eventName: string;
  readonly declaredEventNames: readonly string[];

  constructor(args: {
    useCaseName: string;
    eventName: string;
    declaredEventNames: readonly string[];
  }) {
    const declared =
      args.declaredEventNames.length > 0
        ? args.declaredEventNames.map((name) => `"${name}"`).join(", ")
        : "none";

    super(
      `Use case "${args.useCaseName}" cannot emit undeclared event "${args.eventName}". Declare it with .emits([...]). Declared events: ${declared}.`,
    );
    this.useCaseName = args.useCaseName;
    this.eventName = args.eventName;
    this.declaredEventNames = args.declaredEventNames;
  }
}

function formatPath(path: StandardSchemaV1.Issue["path"]): string {
  if (!path?.length) return "";
  return path
    .map((segment) =>
      typeof segment === "object" && segment !== null && "key" in segment
        ? String(segment.key)
        : String(segment),
    )
    .join(".");
}

function formatIssues(issues: readonly StandardSchemaV1.Issue[]): string {
  return issues
    .map((issue) => {
      const path = formatPath(issue.path);
      return path ? `${path}: ${issue.message}` : issue.message;
    })
    .join("; ");
}

/**
 * Error thrown when a use-case event helper fails to validate an event payload
 * before recording or publishing it.
 */
export class UseCaseEventValidationError extends Error {
  readonly name = "UseCaseEventValidationError";
  readonly useCaseName: string;
  readonly eventName: string;
  readonly issues: readonly StandardSchemaV1.Issue[];

  constructor(args: {
    useCaseName: string;
    eventName: string;
    issues: readonly StandardSchemaV1.Issue[];
  }) {
    super(
      `Use case "${args.useCaseName}" event "${args.eventName}" payload validation failed: ${formatIssues(args.issues)}`,
    );
    this.useCaseName = args.useCaseName;
    this.eventName = args.eventName;
    this.issues = args.issues;
  }
}

async function parseSchema<TSchema extends StandardSchemaV1>(
  schema: TSchema,
  value: unknown,
  useCaseName: string,
  phase: UseCaseValidationPhase,
): Promise<InferOutput<TSchema>> {
  const result = await schema["~standard"].validate(value);
  if (result.issues?.length) {
    throw new UseCaseValidationError({
      useCaseName,
      phase,
      issues: result.issues,
    });
  }
  if ("value" in result) {
    return result.value as InferOutput<TSchema>;
  }
  throw new Error("Invalid Standard Schema result: missing value");
}

/**
 * Minimal domain event definition accepted by use-case event helpers.
 *
 * This structurally matches events from `@beignet/core/events` and compatible
 * app-owned definitions.
 */
export interface DomainEventLike {
  /**
   * Stable event name.
   */
  name: string;
  /**
   * Standard Schema payload validator.
   */
  payload: StandardSchema;
}

/**
 * Infer the output payload type from a use-case event definition.
 */
export type InferUseCaseEventPayload<E extends DomainEventLike> =
  E["payload"] extends StandardSchemaV1<unknown, infer Output> ? Output : never;

/**
 * Minimal recorder shape accepted by use-case event helpers.
 */
export interface UseCaseEventRecorderTarget {
  /**
   * Record a domain event payload.
   */
  record<E extends DomainEventLike>(
    event: E,
    payload: InferUseCaseEventPayload<E>,
    options?: EventPublishOptions,
  ): Promise<void> | void;
}

/**
 * Minimal event-bus shape accepted by use-case event helpers.
 */
export interface UseCaseEventBusTarget {
  /**
   * Publish a domain event payload.
   */
  publish<E extends DomainEventLike>(
    event: E,
    payload: InferUseCaseEventPayload<E>,
    options?: EventPublishOptions,
  ): Promise<void> | void;
}

/**
 * Event helper scoped to the events declared by a use case.
 */
export interface UseCaseEventHelpers<Emits extends readonly DomainEventLike[]> {
  /**
   * The exact event definitions declared with `.emits(...)`.
   */
  readonly declared: Emits;

  /**
   * Return whether an event is declared by this use case.
   */
  isDeclared(event: DomainEventLike): boolean;

  /**
   * Throw if an event is not declared by this use case.
   */
  assertDeclared(event: DomainEventLike): void;

  /**
   * Validate and record a declared event into a transaction-scoped recorder.
   */
  record<E extends Emits[number]>(
    recorder: UseCaseEventRecorderTarget,
    event: E,
    payload: InferUseCaseEventPayload<E>,
  ): Promise<void>;

  /**
   * Validate and publish a declared event directly through an event bus.
   */
  publish<E extends Emits[number]>(
    eventBus: UseCaseEventBusTarget,
    event: E,
    payload: InferUseCaseEventPayload<E>,
  ): Promise<void>;
}

async function parseEventPayload<E extends DomainEventLike>(
  event: E,
  payload: unknown,
  useCaseName: string,
): Promise<InferUseCaseEventPayload<E>> {
  const result = await event.payload["~standard"].validate(payload);

  if (result.issues?.length) {
    throw new UseCaseEventValidationError({
      useCaseName,
      eventName: event.name,
      issues: result.issues,
    });
  }

  if ("value" in result) {
    return result.value as InferUseCaseEventPayload<E>;
  }

  throw new Error("Invalid Standard Schema result: missing value");
}

function createUseCaseEventHelpers<Emits extends readonly DomainEventLike[]>(
  useCaseName: string,
  declared: Emits,
): UseCaseEventHelpers<Emits> {
  const declaredEventNames = declared.map((event) => event.name);
  const declaredEventByName = new Map<string, DomainEventLike>();

  for (const event of declared) {
    if (declaredEventByName.has(event.name)) {
      throw new Error(
        `Use case "${useCaseName}" declares duplicate event "${event.name}". Event names must be unique within .emits([...]).`,
      );
    }

    declaredEventByName.set(event.name, event);
  }

  function resolveDeclared(event: DomainEventLike): DomainEventLike {
    const declaredEvent = declaredEventByName.get(event.name);
    if (declaredEvent) return declaredEvent;

    throw new UseCaseEventDeclarationError({
      useCaseName,
      eventName: event.name,
      declaredEventNames,
    });
  }

  function assertDeclared(event: DomainEventLike): void {
    resolveDeclared(event);
  }

  return {
    declared,
    isDeclared(event) {
      return declaredEventByName.has(event.name);
    },
    assertDeclared,
    async record(recorder, event, payload) {
      const declaredEvent = resolveDeclared(event) as typeof event;
      const parsed = await parseEventPayload(
        declaredEvent,
        payload,
        useCaseName,
      );
      const prepared = await prepareEventPayloadForTransport(
        declaredEvent,
        parsed,
        markEventPayloadParsed(declaredEvent, parsed),
      );
      await recorder.record(
        declaredEvent,
        prepared.payload,
        prepared.publishOptions,
      );
    },
    async publish(eventBus, event, payload) {
      const declaredEvent = resolveDeclared(event) as typeof event;
      const parsed = await parseEventPayload(
        declaredEvent,
        payload,
        useCaseName,
      );
      const prepared = await prepareEventPayloadForTransport(
        declaredEvent,
        parsed,
        markEventPayloadParsed(declaredEvent, parsed),
      );
      await eventBus.publish(
        declaredEvent,
        prepared.payload,
        prepared.publishOptions,
      );
    },
  };
}

/**
 * Use case kind - distinguishes commands (write/side-effect) from queries (read-only)
 */
export type UseCaseKind = "command" | "query";

/**
 * Symbol key for the trusted run path attached to finalized use cases.
 *
 * The server route binder calls this method instead of `run` when the route's
 * input was already validated by the exact same schema object at the HTTP
 * boundary. It behaves like `run` but skips the input parse; output
 * validation, instrumentation, events, and `onRun` are unchanged.
 *
 * The key uses `Symbol.for(...)` so the binder and the application builder
 * agree on the key even across separately bundled copies of the package.
 */
export const USE_CASE_TRUSTED_RUN: unique symbol = Symbol.for(
  "beignet.useCase.trustedRun",
);

const USE_CASE_OUTPUT_VALIDATED = Symbol.for("beignet.useCase.outputValidated");

/**
 * Finalized use case definition.
 *
 * Use cases validate their input before `run(...)` executes and validate their
 * output before returning, unless validation is disabled on the builder.
 */
export interface UseCaseDef<
  Ctx,
  Name extends string,
  Kind extends UseCaseKind,
  InputSchema extends StandardSchemaV1,
  OutputSchema extends StandardSchemaV1,
  Emits extends readonly DomainEventLike[] = readonly [],
> {
  /**
   * Stable use-case name, usually namespaced by feature.
   */
  name: Name;
  /**
   * Whether this use case is a command or query.
   */
  kind: Kind;
  /** Input schema, suitable for reuse in HTTP contracts and forms. */
  inputSchema: InputSchema;
  /** Output schema, suitable for reuse in HTTP contracts and clients. */
  outputSchema: OutputSchema;
  /**
   * Domain events this use case is allowed to record or publish through the
   * scoped `events` helper.
   */
  emits: Emits;
  /**
   * Execute the use case with application context and typed input.
   */
  run: (args: {
    ctx: Ctx;
    input: InferInput<InputSchema>;
  }) => Promise<InferOutput<OutputSchema>>;
}

/**
 * Event passed to the `onRun` hook for instrumentation.
 */
export interface UseCaseRunEvent<Ctx> {
  /**
   * Use-case name.
   */
  name: string;
  /**
   * Use-case kind.
   */
  kind: UseCaseKind;
  /**
   * Execution phase being observed.
   */
  phase: "start" | "end" | "error";
  /**
   * Elapsed time for end/error events.
   */
  durationMs?: number;
  /**
   * Error captured for error events.
   */
  error?: unknown;
  /**
   * Application context used for the run.
   */
  ctx: Ctx;
}

/**
 * Options for `createUseCase(...)`.
 */
export interface CreateUseCaseOptions<Ctx> {
  /**
   * Optional app-owned observer called on use case start, end, and error.
   *
   * Observers run in addition to the built-in instrumentation.
   */
  onRun?: (event: UseCaseRunEvent<Ctx>) => void | Promise<void>;

  /**
   * Built-in use-case instrumentation.
   *
   * By default every run records `usecase` lifecycle events (plus `error`
   * events for failed runs) into the provider instrumentation port resolved
   * from `ctx.ports` (`ports.instrumentation`, then `ports.devtools`). When no
   * port is installed, runs stay silent. Pass `false` to opt out.
   *
   * @default true
   */
  instrumentation?: boolean;

  /**
   * Enable or disable schema validation for use case boundaries.
   *
   * Defaults to validating both input and output. Pass `false` to opt out, or
   * configure phases independently with `{ input: boolean, output: boolean }`.
   */
  validate?: boolean | { input?: boolean; output?: boolean };
}

function notifyUseCaseRunObserver<Ctx>(
  observer: CreateUseCaseOptions<Ctx>["onRun"],
  event: UseCaseRunEvent<Ctx>,
): void {
  try {
    const result = observer?.(event);
    if (result) void result.catch(() => {});
  } catch {
    // Observers are best-effort instrumentation and cannot change execution.
  }
}

type ValidationOptions = {
  input: boolean;
  output: boolean;
};

function normalizeValidationOptions(
  validate: CreateUseCaseOptions<unknown>["validate"],
): ValidationOptions {
  if (validate === false) {
    return { input: false, output: false };
  }
  if (typeof validate === "object" && validate !== null) {
    return {
      input: validate.input ?? true,
      output: validate.output ?? true,
    };
  }
  return { input: true, output: true };
}

type UseCaseRunInstrumentation = {
  end(durationMs: number): void;
  error(durationMs: number, error: unknown): void;
};

function getInstrumentationRequestId(ctx: unknown): string | undefined {
  if (!ctx || typeof ctx !== "object") return undefined;
  const requestId = (ctx as { requestId?: unknown }).requestId;
  return typeof requestId === "string" ? requestId : undefined;
}

function getInstrumentationTrace(ctx: unknown): TraceContextInput | undefined {
  if (!ctx || typeof ctx !== "object") return undefined;
  const context = ctx as TraceContextInput;
  if (!context.traceId && !context.spanId && !context.traceparent) {
    return undefined;
  }
  return {
    traceId: context.traceId,
    spanId: context.spanId,
    parentSpanId: context.parentSpanId,
    traceparent: context.traceparent,
    tracestate: context.tracestate,
  };
}

function getRunErrorMessage(error: unknown): string {
  if (error instanceof Error) return error.message;
  if (typeof error === "string") return error;
  return "Unknown error";
}

/**
 * Start built-in instrumentation for one use-case run.
 *
 * The instrumentation port is resolved from `ctx.ports` per run so use cases
 * stay decoupled from any specific sink. Runs without a resolved port stay
 * silent.
 */
function startUseCaseRunInstrumentation(args: {
  ctx: unknown;
  name: string;
  kind: UseCaseKind;
}): UseCaseRunInstrumentation | undefined {
  const ports =
    args.ctx && typeof args.ctx === "object"
      ? (args.ctx as { ports?: unknown }).ports
      : undefined;
  const port = resolveProviderInstrumentationPort(
    ports as Parameters<typeof resolveProviderInstrumentationPort>[0],
  );
  if (!port) return undefined;

  const useCasesEnabled = port.isWatcherEnabled?.("useCases") ?? true;
  const errorsEnabled = port.isWatcherEnabled?.("errors") ?? true;
  if (!useCasesEnabled && !errorsEnabled) return undefined;

  const requestId = getInstrumentationRequestId(args.ctx);
  const trace =
    resolveTracingPort(ports)?.current() ??
    createChildTraceContext(getInstrumentationTrace(args.ctx) ?? {});

  const record = (event: ProviderInstrumentationEventInput) => {
    try {
      port.record(event);
    } catch {
      // Instrumentation sinks must never affect use-case behavior.
    }
  };

  const recordPhase = (
    phase: "start" | "end" | "error",
    durationMs?: number,
    error?: unknown,
  ) => {
    if (useCasesEnabled) {
      record({
        type: "usecase",
        requestId,
        traceId: trace.traceId,
        spanId: trace.spanId,
        parentSpanId: trace.parentSpanId,
        traceparent: trace.traceparent,
        tracestate: trace.tracestate,
        name: args.name,
        kind: args.kind,
        phase,
        durationMs,
        error: phase === "error" ? getRunErrorMessage(error) : undefined,
      });
    }

    if (phase === "error" && errorsEnabled) {
      record({
        type: "error",
        requestId,
        traceId: trace.traceId,
        spanId: trace.spanId,
        parentSpanId: trace.parentSpanId,
        traceparent: trace.traceparent,
        tracestate: trace.tracestate,
        message: getRunErrorMessage(error),
        stack: error instanceof Error ? error.stack : undefined,
        useCaseName: args.name,
        owner: "route",
      });
    }
  };

  recordPhase("start");

  return {
    end: (durationMs) => recordPhase("end", durationMs),
    error: (durationMs, error) => recordPhase("error", durationMs, error),
  };
}

/**
 * Internal configuration for the use case builder
 */
interface UseCaseBuilderConfig<
  Name extends string,
  Kind extends UseCaseKind,
  InputSchema extends StandardSchemaV1 | undefined,
  OutputSchema extends StandardSchemaV1 | undefined,
  Emits extends readonly DomainEventLike[],
> {
  name: Name;
  kind: Kind;
  input?: InputSchema;
  output?: OutputSchema;
  emits: Emits;
}

/**
 * Fluent builder for creating use cases
 */
class UseCaseBuilder<
  Ctx,
  Name extends string,
  Kind extends UseCaseKind,
  InputSchema extends StandardSchemaV1 | undefined,
  OutputSchema extends StandardSchemaV1 | undefined,
  Emits extends readonly DomainEventLike[] = readonly [],
> {
  constructor(
    private readonly config: UseCaseBuilderConfig<
      Name,
      Kind,
      InputSchema,
      OutputSchema,
      Emits
    >,
    private readonly onRun?: (
      event: UseCaseRunEvent<Ctx>,
    ) => void | Promise<void>,
    private readonly validation: ValidationOptions = {
      input: true,
      output: true,
    },
    private readonly instrumented: boolean = true,
  ) {}

  /**
   * Define the input schema for this use case
   */
  input<I extends StandardSchemaV1>(
    schema: I,
  ): UseCaseBuilder<Ctx, Name, Kind, I, OutputSchema, Emits> {
    return new UseCaseBuilder<Ctx, Name, Kind, I, OutputSchema, Emits>(
      {
        ...this.config,
        input: schema,
      },
      this.onRun,
      this.validation,
      this.instrumented,
    );
  }

  /**
   * Define the output schema for this use case
   */
  output<O extends StandardSchemaV1>(
    schema: O,
  ): UseCaseBuilder<Ctx, Name, Kind, InputSchema, O, Emits> {
    return new UseCaseBuilder<Ctx, Name, Kind, InputSchema, O, Emits>(
      {
        ...this.config,
        output: schema,
      },
      this.onRun,
      this.validation,
      this.instrumented,
    );
  }

  /**
   * Define the domain events that this use case may emit.
   */
  emits<E extends readonly DomainEventLike[]>(
    events: E,
  ): UseCaseBuilder<Ctx, Name, Kind, InputSchema, OutputSchema, E> {
    return new UseCaseBuilder<Ctx, Name, Kind, InputSchema, OutputSchema, E>(
      {
        ...this.config,
        emits: events,
      },
      this.onRun,
      this.validation,
      this.instrumented,
    );
  }

  /**
   * Define the run function and finalize the use case definition
   */
  run(
    fn: InputSchema extends StandardSchemaV1
      ? OutputSchema extends StandardSchemaV1
        ? (args: {
            ctx: Ctx;
            input: SchemaOutput<InputSchema>;
            events: UseCaseEventHelpers<Emits>;
          }) => Promise<SchemaOutput<OutputSchema>> | SchemaOutput<OutputSchema>
        : never
      : never,
  ): InputSchema extends StandardSchemaV1
    ? OutputSchema extends StandardSchemaV1
      ? UseCaseDef<Ctx, Name, Kind, InputSchema, OutputSchema, Emits>
      : never
    : never {
    if (!this.config.input) {
      throw new Error(`Use case "${this.config.name}" is missing input schema`);
    }
    if (!this.config.output) {
      throw new Error(
        `Use case "${this.config.name}" is missing output schema`,
      );
    }

    const useCaseName = this.config.name;
    const useCaseKind = this.config.kind;
    const onRun = this.onRun;
    const instrumented = this.instrumented;
    const inputSchema = this.config.input as Extract<
      InputSchema,
      StandardSchemaV1
    >;
    const outputSchema = this.config.output as Extract<
      OutputSchema,
      StandardSchemaV1
    >;
    const validation = this.validation;
    const eventHelpers = createUseCaseEventHelpers(
      useCaseName,
      this.config.emits,
    );

    const execute = async (
      args: InputSchema extends StandardSchemaV1
        ? OutputSchema extends StandardSchemaV1
          ? {
              ctx: Ctx;
              input: SchemaInput<InputSchema>;
            }
          : never
        : never,
      parseInput: boolean,
    ) => {
      const traceAttributes = {
        "beignet.use_case.name": useCaseName,
        "beignet.use_case.kind": useCaseKind,
      } as const;

      return await runWithTracing(
        args.ctx,
        {
          name: `beignet.use_case ${useCaseName}`,
          type: "useCase",
          kind: "internal",
          attributes: traceAttributes,
          metricAttributes: traceAttributes,
        },
        async () => {
          const startedAt = Date.now();
          const instrumentation = instrumented
            ? startUseCaseRunInstrumentation({
                ctx: args.ctx,
                name: useCaseName,
                kind: useCaseKind,
              })
            : undefined;
          notifyUseCaseRunObserver(onRun, {
            name: useCaseName,
            kind: useCaseKind,
            phase: "start",
            ctx: args.ctx,
          });

          try {
            const parsedInput =
              parseInput && validation.input
                ? await parseSchema(
                    inputSchema,
                    args.input,
                    useCaseName,
                    "input",
                  )
                : (args.input as SchemaOutput<InputSchema>);

            const rawResult = await fn({
              ctx: args.ctx,
              input: parsedInput,
              events: eventHelpers,
            });

            const result = validation.output
              ? await parseSchema(
                  outputSchema,
                  rawResult,
                  useCaseName,
                  "output",
                )
              : (rawResult as SchemaOutput<OutputSchema>);

            const durationMs = Date.now() - startedAt;
            instrumentation?.end(durationMs);
            notifyUseCaseRunObserver(onRun, {
              name: useCaseName,
              kind: useCaseKind,
              phase: "end",
              durationMs,
              ctx: args.ctx,
            });

            return result;
          } catch (err) {
            const durationMs = Date.now() - startedAt;
            instrumentation?.error(durationMs, err);
            notifyUseCaseRunObserver(onRun, {
              name: useCaseName,
              kind: useCaseKind,
              phase: "error",
              durationMs,
              error: err,
              ctx: args.ctx,
            });
            throw err;
          }
        },
      );
    };

    type RunArgs = Parameters<typeof execute>[0];

    // Type assertion required to satisfy the conditional return type.
    // The runtime checks above ensure input/output schemas are set.
    // The conditional types ensure type safety at compile time - run() returns
    // UseCaseDef only when both InputSchema and OutputSchema are StandardSchemaV1.
    const def = {
      name: this.config.name,
      kind: this.config.kind,
      inputSchema: this.config.input,
      outputSchema: this.config.output,
      emits: this.config.emits,
      run: (args: RunArgs) => execute(args, true),
    };

    // The trusted run path skips only the input parse. It is non-enumerable so
    // serialization and object spreads keep treating use cases as plain data.
    Object.defineProperty(def, USE_CASE_TRUSTED_RUN, {
      value: (args: RunArgs) => execute(args, false),
      enumerable: false,
    });
    Object.defineProperty(def, USE_CASE_OUTPUT_VALIDATED, {
      value: validation.output,
      enumerable: false,
    });

    return def as unknown as InputSchema extends StandardSchemaV1
      ? OutputSchema extends StandardSchemaV1
        ? UseCaseDef<Ctx, Name, Kind, InputSchema, OutputSchema, Emits>
        : never
      : never;
  }
}

/**
 * Root builder returned by createUseCase.
 */
export interface UseCaseBuilderRoot<Ctx> {
  /**
   * Create a command use case (write/side-effect path)
   */
  command<Name extends string>(
    name: Name,
  ): UseCaseBuilder<Ctx, Name, "command", undefined, undefined, readonly []>;

  /**
   * Create a query use case (read-only path)
   */
  query<Name extends string>(
    name: Name,
  ): UseCaseBuilder<Ctx, Name, "query", undefined, undefined, readonly []>;
}

/**
 * Infer the application context type from a finalized use case.
 */
export type UseCaseContext<TUseCase> = TUseCase extends {
  run: (args: {
    ctx: infer Ctx;
    input: infer _Input;
  }) => Promise<infer _Output>;
}
  ? Ctx
  : never;

/**
 * Infer the public input type accepted by a finalized use case.
 */
export type UseCaseInput<TUseCase> = TUseCase extends {
  run: (args: {
    ctx: infer _Ctx;
    input: infer Input;
  }) => Promise<infer _Output>;
}
  ? Input
  : never;

/**
 * Infer the public output type returned by a finalized use case.
 */
export type UseCaseOutput<TUseCase> = TUseCase extends {
  run: (args: {
    ctx: infer _Ctx;
    input: infer _Input;
  }) => Promise<infer Output>;
}
  ? Output
  : never;

type MaybePromise<T> = T | Promise<T>;

/**
 * Small test harness for running use cases with typed inputs.
 */
export interface UseCaseTester<Ctx> {
  /**
   * Create a fresh test context.
   */
  ctx(): Promise<Ctx>;

  /**
   * Run a use case with a typed input and either a fresh or explicit context.
   */
  run<Input, Output>(
    useCase: {
      run(args: { ctx: Ctx; input: Input }): Promise<Output>;
    },
    input: Input,
    options?: { ctx?: Ctx },
  ): Promise<Output>;
}

/**
 * Create a small test harness for use cases.
 *
 * Pass a context factory when tests mutate ports or state. Pass a fixed context
 * for simple, immutable tests.
 */
export function createUseCaseTester<Ctx>(
  createContext: Ctx | (() => MaybePromise<Ctx>),
): UseCaseTester<Ctx> {
  const ctx = async () =>
    typeof createContext === "function"
      ? await (createContext as () => MaybePromise<Ctx>)()
      : createContext;

  return {
    ctx,
    async run<Input, Output>(
      useCase: {
        run(args: { ctx: Ctx; input: Input }): Promise<Output>;
      },
      input: Input,
      options?: { ctx?: Ctx },
    ): Promise<Output> {
      return useCase.run({
        ctx: options?.ctx ?? (await ctx()),
        input,
      });
    },
  };
}

/** Empty emits array used as default. */
const EMPTY_EMITS = [] as const;

/**
 * Create a use case builder with a specific context type.
 *
 * Create this once in app code, usually in `lib/use-case.ts`, then import that
 * configured builder from feature use-case modules.
 *
 * @example
 * ```ts
 * export const useCase = createUseCase<AppContext>();
 *
 * export const createTodo = useCase
 *   .command("todos.create")
 *   .input(CreateTodoInput)
 *   .output(CreateTodoOutput)
 *   .run(async ({ ctx, input }) => ctx.ports.todos.create(input));
 * ```
 *
 * @param options - Optional instrumentation and validation configuration.
 * @returns A root builder for command and query use cases.
 */
export function createUseCase<Ctx>(
  options?: CreateUseCaseOptions<Ctx>,
): UseCaseBuilderRoot<Ctx> {
  const onRun = options?.onRun;
  const validation = normalizeValidationOptions(options?.validate);
  const instrumented = options?.instrumentation !== false;
  return {
    command<Name extends string>(name: Name) {
      return new UseCaseBuilder<
        Ctx,
        Name,
        "command",
        undefined,
        undefined,
        readonly []
      >(
        {
          name,
          kind: "command",
          emits: EMPTY_EMITS,
        },
        onRun,
        validation,
        instrumented,
      );
    },
    query<Name extends string>(name: Name) {
      return new UseCaseBuilder<
        Ctx,
        Name,
        "query",
        undefined,
        undefined,
        readonly []
      >(
        {
          name,
          kind: "query",
          emits: EMPTY_EMITS,
        },
        onRun,
        validation,
        instrumented,
      );
    },
  };
}
