import type { Container } from "inversify";
import type { FeatureManifest, UseCaseManifest } from "./define-feature";
import {
  isInstrumented,
  isCaptured,
  isAudited,
  isAnalyzed,
  isConsentChecked,
  isRateLimited,
} from "./brand-runtime";
import { ConformanceError } from "./conformance-error";
import type { BindContext } from "../di/bind-context";

function requireBrand(ok: boolean, label: string, message: string): void {
  if (!ok) throw new ConformanceError(`${label}: ${message}`);
}

function checkUseCaseBrands(
  bound: unknown,
  label: string,
  useCase: UseCaseManifest,
  requiresConsent: boolean,
): void {
  requireBrand(
    isInstrumented(bound),
    label,
    "missing __instrumented brand — was withSpan applied at bind time?",
  );
  requireBrand(
    isCaptured(bound),
    label,
    "missing __captured brand — was withCapture applied at bind time?",
  );
  if (useCase.mutates && useCase.audits.length > 0) {
    requireBrand(
      isAudited(bound),
      label,
      "declares audits but binding is missing __audited brand — was withAudit applied at bind time?",
    );
  }
  if ((useCase.analyticsEvents?.length ?? 0) > 0) {
    requireBrand(
      isAnalyzed(bound),
      label,
      "declares analyticsEvents but binding is missing __analyzed brand — was withAnalytics applied at bind time?",
    );
  }
  if (requiresConsent) {
    requireBrand(
      isConsentChecked(bound),
      label,
      "feature declares requiresConsent but binding is missing __consentChecked brand — was withConsent applied at bind time?",
    );
  }
  if ((useCase.rateLimit?.length ?? 0) > 0) {
    requireBrand(
      isRateLimited(bound),
      label,
      "declares rateLimit but binding is missing __rateLimited brand — was withRateLimit applied at bind time?",
    );
  }
}

/**
 * Runtime check that every manifest-declared use case is bound through the
 * brand-attaching wrappers (`withSpan` → `__instrumented`,
 * `withCapture` → `__captured`, `withAudit` → `__audited` when required).
 *
 * Called at the tail of each feature's `bindProductionX(ctx)` so:
 *   - `pnpm dev` refuses to boot on drift (synchronous throw)
 *   - The check runs once per feature, not per request
 *   - Future apps that wire `bindProductionX` inherit the check for free
 *
 * The `symbols` map is declared inline by the feature's binder; the manifest
 * holds the contract, the binder holds the container symbols. This keeps
 * the manifest free of DI-coupling while letting each feature declare its
 * own wiring keys.
 */
export function assertFeatureConformance(
  container: Container,
  manifest: FeatureManifest,
  symbols: Record<string, symbol>,
  _ctx: BindContext,
): void {
  void _ctx; // future: also check ctx.bus / ctx.auditLog presence vs requiredCores
  const requiresConsent = (manifest.requiresConsent?.length ?? 0) > 0;
  for (const [name, useCase] of Object.entries(manifest.useCases)) {
    const sym = symbols[name];
    if (!sym) {
      throw new ConformanceError(
        `${manifest.name}.${name}: no symbol provided in symbols map`,
      );
    }
    let bound: unknown;
    try {
      bound = container.get(sym);
    } catch (cause) {
      throw new ConformanceError(
        `${manifest.name}.${name}: container could not resolve symbol (${String(cause)})`,
      );
    }
    checkUseCaseBrands(
      bound,
      `${manifest.name}.${name}`,
      useCase,
      requiresConsent,
    );
  }
}
