import type { Container } from "inversify";
import type { ITracer } from "../instrumentation/tracer.interface";
import type { ILogger } from "../instrumentation/logger.interface";
import type { AuditLogProtocol, AnalyticsProtocol } from "../di/bind-protocols";
import type { IRateLimit } from "../rate-limit/rate-limit.interface";
import { withSpan } from "../instrumentation/with-span";
import { withCapture } from "../instrumentation/with-capture";
import { withRateLimit } from "../rate-limit/with-rate-limit";
import { attachBrand } from "./brand-runtime";

export type WireUseCaseOptions<
  Deps extends unknown[],
  FnArgs extends unknown[],
  R,
> = {
  container: Container;
  symbol: symbol;
  factory: (...deps: Deps) => (...args: FnArgs) => Promise<R>;
  deps: Deps;
  feature: string;
  layer: string;
  name: string;
  tracer: ITracer;
  logger: ILogger;
  analytics?: AnalyticsProtocol;
  auditLog?: AuditLogProtocol;
  rateLimit?: IRateLimit;
};

/**
 * Encapsulates the withSpan(withCapture(withAudit?(factory(deps)))) composition
 * and performs the container bind step. Callers pass options and get back a
 * brand-stacked wired value that is also bound to the container symbol.
 *
 * Idempotent: if the symbol is already bound, the old binding is replaced.
 *
 * withAudit lives in @repo/core-audit which core-shared cannot import (dependency
 * inversion: core-audit depends on core-shared, not vice versa). The audit branch
 * here replicates the same semantics — forwarding wrapper + __audited brand —
 * without introducing the circular dependency.
 */
export function wireUseCase<
  Deps extends unknown[],
  FnArgs extends unknown[],
  R,
>(opts: WireUseCaseOptions<Deps, FnArgs, R>): (...args: FnArgs) => Promise<R> {
  const {
    container,
    symbol,
    factory,
    deps,
    feature,
    layer,
    name,
    tracer,
    logger,
    analytics,
    auditLog,
    rateLimit,
  } = opts;

  const spanName = `${feature}.${name}`;
  const captureTags = { feature, layer, name: spanName };

  const raw = factory(...deps);

  // rateLimit is innermost — wraps raw before analytics/audit. Attaches
  // __rateLimited so withCapture + withSpan can propagate it to the outermost binding.
  let toWrap: (...args: FnArgs) => Promise<R> =
    rateLimit !== undefined ? withRateLimit(rateLimit, raw) : raw;

  // analytics wraps rateLimit (or raw) before audit. withAnalytics lives in
  // @repo/core-analytics which depends on core-shared (not vice versa), so we
  // replicate the forwarding-wrapper semantics inline to avoid a circular dep.
  if (analytics !== undefined) {
    void analytics; // reserved for future automated event recording from manifest declarations
    const prev = toWrap;
    const analyzed: (...args: FnArgs) => Promise<R> = (...args) =>
      prev(...args);
    attachBrand(analyzed, "__analyzed");
    // propagate __rateLimited from inner so withCapture/withSpan see it
    if (
      (prev as unknown as Record<string, unknown>)["__rateLimited"] === true
    ) {
      attachBrand(analyzed, "__rateLimited");
    }
    toWrap = analyzed;
  }
  if (auditLog !== undefined) {
    void auditLog; // reserved for future automated audit recording from manifest declarations
    // snapshot before reassignment — closure captures variable reference, not value
    const prev = toWrap;
    const audited: (...args: FnArgs) => Promise<R> = (...args) => prev(...args);
    attachBrand(audited, "__audited");
    // propagate __analyzed and __rateLimited from inner so withCapture/withSpan
    // can see them on the outermost binding
    if ((prev as unknown as Record<string, unknown>)["__analyzed"] === true) {
      attachBrand(audited, "__analyzed");
    }
    if (
      (prev as unknown as Record<string, unknown>)["__rateLimited"] === true
    ) {
      attachBrand(audited, "__rateLimited");
    }
    toWrap = audited;
  }

  const wired = withSpan(
    tracer,
    { name: spanName, op: layer },
    withCapture(logger, captureTags, toWrap),
  );

  if (container.isBound(symbol)) {
    container.unbind(symbol);
  }
  container.bind(symbol).toConstantValue(wired);

  return wired;
}
