/**
 * @beignet/core/flags
 *
 * Provider-neutral feature flag primitives for Beignet applications.
 */

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

/**
 * JSON-compatible value used by object flags and flag metadata.
 */
export type FlagJsonValue =
  | null
  | boolean
  | number
  | string
  | readonly FlagJsonValue[]
  | { readonly [key: string]: FlagJsonValue };

/**
 * Object-valued flags intentionally exclude scalar JSON values so the flag
 * kind remains predictable.
 */
export type FlagObjectValue =
  | readonly FlagJsonValue[]
  | { readonly [key: string]: FlagJsonValue };

/**
 * Value kinds supported by Beignet flags.
 */
export type FlagValue = boolean | string | number | FlagObjectValue;

/**
 * Stable feature flag key.
 */
export type FlagKey = string;

/**
 * Feature flag value kind.
 */
export type FlagValueKind = "boolean" | "string" | "number" | "object";

/**
 * Subject used for provider-neutral flag targeting.
 */
export type FlagSubject = {
  type: string;
  id: string;
};

/**
 * Tenant or account used for provider-neutral flag targeting.
 */
export type FlagTenant = {
  id: string;
  slug?: string;
};

/**
 * Attribute value accepted by the provider-neutral flag context.
 */
export type FlagAttributeValue =
  | null
  | boolean
  | number
  | string
  | readonly FlagAttributeValue[]
  | { readonly [key: string]: FlagAttributeValue };

/**
 * Context supplied to flag evaluations, exposure recording, and tracking.
 */
export type FlagEvaluationContext = {
  /**
   * Stable key used by providers for rollout bucketing and targeting.
   */
  targetingKey: string;
  /**
   * Actor, user, service, tenant, or account being targeted.
   */
  subject?: FlagSubject;
  /**
   * Tenant/account scope, when available.
   */
  tenant?: FlagTenant;
  /**
   * Provider-neutral targeting attributes. Keep private data out unless the
   * selected provider and retention policy are approved for it.
   */
  attributes?: Record<string, FlagAttributeValue>;
  /**
   * Attribute keys that should be treated as private by providers and
   * instrumentation.
   */
  privateAttributes?: readonly string[];
  requestId?: string;
  traceId?: string;
  spanId?: string;
  parentSpanId?: string;
  traceparent?: string;
};

/**
 * Flag definition registered through `defineFlags(...)`.
 */
export type FlagDef<TValue extends FlagValue = FlagValue> = {
  kind: "flag";
  key: FlagKey;
  valueKind: FlagValueKindForValue<TValue>;
  defaultValue: TValue;
  description?: string;
  metadata?: Record<string, unknown>;
};

/**
 * Infer the flag value type from a flag definition.
 */
export type InferFlagValue<TFlag> =
  TFlag extends FlagDef<infer TValue> ? TValue : never;

/**
 * Map a value type to its runtime flag kind.
 */
export type FlagValueKindForValue<TValue extends FlagValue> =
  TValue extends boolean
    ? "boolean"
    : TValue extends string
      ? "string"
      : TValue extends number
        ? "number"
        : "object";

/**
 * Options accepted when declaring a flag.
 */
export type DefineFlagOptions<TValue extends FlagValue> = {
  default: TValue;
  description?: string;
  metadata?: Record<string, unknown>;
};

/**
 * Registry returned by `defineFlags(...)`.
 */
export type FlagRegistry = Record<string, FlagDef>;

/**
 * Reason a flag received its value.
 */
export type FlagEvaluationReason =
  | "static"
  | "targeting_match"
  | "default"
  | "error"
  | "unknown"
  | (string & {});

/**
 * Normalized error summary for failed flag provider evaluations.
 */
export type FlagEvaluationError = {
  message: string;
  code?: string;
};

/**
 * Options accepted by flag evaluations.
 */
export type FlagEvaluationOptions<TValue extends FlagValue = FlagValue> = {
  context?: FlagEvaluationContext;
  /**
   * Override the flag declaration default for this call.
   */
  defaultValue?: TValue;
  /**
   * Explicitly record an exposure after evaluation succeeds. Plain evaluation
   * does not record exposure by default.
   */
  expose?: boolean;
};

/**
 * Normalized flag evaluation details.
 */
export type FlagEvaluationDetails<TValue extends FlagValue = FlagValue> = {
  key: string;
  value: TValue;
  defaultValue: TValue;
  valueKind: FlagValueKindForValue<TValue>;
  reason: FlagEvaluationReason;
  defaulted: boolean;
  variant?: string;
  metadata?: Record<string, unknown>;
  error?: FlagEvaluationError;
};

/**
 * Options accepted by explicit exposure recording.
 */
export type FlagExposureOptions = {
  context?: FlagEvaluationContext;
  value?: FlagValue;
  variant?: string;
  metadata?: Record<string, unknown>;
};

/**
 * Options accepted by tracking calls.
 */
export type FlagTrackOptions = {
  context?: FlagEvaluationContext;
  value?: number;
  metadata?: Record<string, unknown>;
};

/**
 * App-facing feature flag port.
 */
export type FlagsPort = {
  evaluate<TValue extends FlagValue>(
    flag: FlagDef<TValue>,
    options?: FlagEvaluationOptions<TValue>,
  ): Promise<TValue>;
  details<TValue extends FlagValue>(
    flag: FlagDef<TValue>,
    options?: FlagEvaluationOptions<TValue>,
  ): Promise<FlagEvaluationDetails<TValue>>;
  recordExposure<TValue extends FlagValue>(
    flag: FlagDef<TValue>,
    options?: FlagExposureOptions,
  ): Promise<void>;
  track(event: string, options?: FlagTrackOptions): Promise<void>;
};

/**
 * Flag evaluation observation emitted by memory/static adapters.
 */
export type FlagEvaluationObservation<TValue extends FlagValue = FlagValue> = {
  source: "evaluate" | "details";
  flag: FlagDef<TValue>;
  context?: FlagEvaluationContext;
  details: FlagEvaluationDetails<TValue>;
  durationMs: number;
  requestId?: string;
  traceId?: string;
  spanId?: string;
  parentSpanId?: string;
  traceparent?: string;
};

/**
 * Explicit flag exposure observation.
 */
export type FlagExposureObservation<TValue extends FlagValue = FlagValue> = {
  flag: FlagDef<TValue>;
  context?: FlagEvaluationContext;
  value?: FlagValue;
  variant?: string;
  metadata?: Record<string, unknown>;
  requestId?: string;
  traceId?: string;
  spanId?: string;
  parentSpanId?: string;
  traceparent?: string;
};

/**
 * Flag tracking observation.
 */
export type FlagTrackObservation = {
  event: string;
  context?: FlagEvaluationContext;
  value?: number;
  metadata?: Record<string, unknown>;
  requestId?: string;
  traceId?: string;
  spanId?: string;
  parentSpanId?: string;
  traceparent?: string;
};

export type FlagEvaluationObserver = (
  observation: FlagEvaluationObservation,
) => MaybePromise<void>;

export type FlagExposureObserver = (
  observation: FlagExposureObservation,
) => MaybePromise<void>;

export type FlagTrackObserver = (
  observation: FlagTrackObservation,
) => MaybePromise<void>;

/**
 * Options for memory/static flag adapters.
 */
export type CreateFlagsOptions = {
  onEvaluation?: FlagEvaluationObserver;
  onExposure?: FlagExposureObserver;
  onTrack?: FlagTrackObserver;
};

/**
 * Static flag values keyed by flag key.
 */
export type StaticFlagValues = Record<string, FlagValue>;

/**
 * Options for `createStaticFlags(...)`.
 */
export type CreateStaticFlagsOptions = CreateFlagsOptions & {
  values?: StaticFlagValues;
};

/**
 * In-memory flags port with mutation helpers for tests and local development.
 */
export type MemoryFlagsPort = FlagsPort & {
  values: Map<string, FlagValue>;
  set<TValue extends FlagValue>(flag: FlagDef<TValue>, value: TValue): void;
  reset(flag?: FlagDef): void;
};

function createFlag<TValue extends FlagValue>(
  valueKind: FlagValueKind,
  key: string,
  options: DefineFlagOptions<TValue>,
): FlagDef<TValue> {
  const defaultValue = options.default;
  if (!matchesValueKind(defaultValue, valueKind)) {
    throw new Error(
      `Flag "${key}" default value does not match "${valueKind}" flag value kind.`,
    );
  }

  return {
    kind: "flag",
    key,
    valueKind: valueKind as FlagValueKindForValue<TValue>,
    defaultValue,
    description: options.description,
    metadata: options.metadata,
  };
}

function defineBooleanFlag(
  key: string,
  options: DefineFlagOptions<boolean>,
): FlagDef<boolean> {
  return createFlag("boolean", key, options);
}

function defineStringFlag(
  key: string,
  options: DefineFlagOptions<string>,
): FlagDef<string>;
function defineStringFlag<const TValue extends string>(
  key: string,
  options: DefineFlagOptions<TValue>,
): FlagDef<TValue>;
function defineStringFlag<TValue extends string>(
  key: string,
  options: DefineFlagOptions<TValue>,
): FlagDef<TValue> {
  return createFlag("string", key, options);
}

function defineNumberFlag(
  key: string,
  options: DefineFlagOptions<number>,
): FlagDef<number>;
function defineNumberFlag<const TValue extends number>(
  key: string,
  options: DefineFlagOptions<TValue>,
): FlagDef<TValue>;
function defineNumberFlag<TValue extends number>(
  key: string,
  options: DefineFlagOptions<TValue>,
): FlagDef<TValue> {
  return createFlag("number", key, options);
}

function defineObjectFlag<TValue extends FlagObjectValue>(
  key: string,
  options: DefineFlagOptions<TValue>,
): FlagDef<TValue> {
  return createFlag("object", key, options);
}

/**
 * Define one feature flag declaration.
 */
export const defineFlag = {
  boolean: defineBooleanFlag,
  string: defineStringFlag,
  number: defineNumberFlag,
  object: defineObjectFlag,
};

/**
 * Define a typed registry of feature flags.
 */
export function defineFlags<const TRegistry extends FlagRegistry>(
  registry: TRegistry,
): TRegistry {
  return registry;
}

/**
 * Evaluate a flag through any `FlagsPort`.
 */
export function evaluateFlag<TValue extends FlagValue>(
  flags: FlagsPort,
  flag: FlagDef<TValue>,
  options?: FlagEvaluationOptions<TValue>,
): Promise<TValue> {
  return flags.evaluate(flag, options);
}

/**
 * Return detailed flag evaluation information through any `FlagsPort`.
 */
export function getFlagDetails<TValue extends FlagValue>(
  flags: FlagsPort,
  flag: FlagDef<TValue>,
  options?: FlagEvaluationOptions<TValue>,
): Promise<FlagEvaluationDetails<TValue>> {
  return flags.details(flag, options);
}

/**
 * Create static flags backed by fixed values.
 */
export function createStaticFlags(
  options: CreateStaticFlagsOptions = {},
): FlagsPort {
  return createFlagsFromStore(() => options.values ?? {}, options);
}

/**
 * Create mutable in-memory flags for tests, examples, and local development.
 */
export function createMemoryFlags(
  options: CreateStaticFlagsOptions = {},
): MemoryFlagsPort {
  const values = new Map<string, FlagValue>(
    Object.entries(options.values ?? {}),
  );
  const port = createFlagsFromStore(() => Object.fromEntries(values), options);

  return {
    ...port,
    values,
    set(flag, value) {
      if (!matchesValueKind(value, flag.valueKind)) {
        throw new Error(
          `Flag "${flag.key}" value does not match "${flag.valueKind}" flag value kind.`,
        );
      }
      values.set(flag.key, value);
    },
    reset(flag) {
      if (flag) {
        values.delete(flag.key);
        return;
      }
      values.clear();
    },
  };
}

function createFlagsFromStore(
  readValues: () => StaticFlagValues,
  observers: CreateFlagsOptions,
): FlagsPort {
  async function resolve<TValue extends FlagValue>(
    flag: FlagDef<TValue>,
    options: FlagEvaluationOptions<TValue> | undefined,
    source: FlagEvaluationObservation["source"],
  ): Promise<FlagEvaluationDetails<TValue>> {
    const startedAt = Date.now();
    const defaultValue = options?.defaultValue ?? flag.defaultValue;
    const values = readValues();
    const stored = values[flag.key];
    const hasStored = Object.hasOwn(values, flag.key);
    const usedStoredValue =
      hasStored && matchesValueKind(stored, flag.valueKind);
    const value = usedStoredValue
      ? (stored as typeof defaultValue)
      : defaultValue;
    const details: FlagEvaluationDetails<typeof defaultValue> = {
      key: flag.key,
      valueKind: flag.valueKind as FlagValueKindForValue<typeof defaultValue>,
      value,
      defaultValue,
      reason: usedStoredValue ? "static" : "default",
      defaulted: !usedStoredValue,
    } satisfies FlagEvaluationDetails<typeof defaultValue>;

    await observeEvaluation(observers.onEvaluation, {
      source,
      flag,
      context: options?.context,
      details,
      durationMs: Date.now() - startedAt,
      ...correlationFromContext(options?.context),
    });

    if (options?.expose) {
      await flags.recordExposure(flag, {
        context: options.context,
        value: details.value,
        variant: details.variant,
        metadata: details.metadata,
      });
    }

    return details;
  }

  const flags: FlagsPort = {
    async evaluate(flag, options) {
      const details = await resolve(flag, options, "evaluate");
      return details.value;
    },
    async details(flag, options) {
      return resolve(flag, options, "details");
    },
    async recordExposure(flag, options) {
      await observeExposure(observers.onExposure, {
        flag,
        context: options?.context,
        value: options?.value,
        variant: options?.variant,
        metadata: options?.metadata,
        ...correlationFromContext(options?.context),
      });
    },
    async track(event, options) {
      await observeTrack(observers.onTrack, {
        event,
        context: options?.context,
        value: options?.value,
        metadata: options?.metadata,
        ...correlationFromContext(options?.context),
      });
    },
  };

  return flags;
}

function matchesValueKind(value: unknown, kind: FlagValueKind): boolean {
  switch (kind) {
    case "boolean":
      return typeof value === "boolean";
    case "string":
      return typeof value === "string";
    case "number":
      return typeof value === "number" && Number.isFinite(value);
    case "object":
      return typeof value === "object" && value !== null;
  }
}

function correlationFromContext(context: FlagEvaluationContext | undefined) {
  return {
    requestId: context?.requestId,
    traceId: context?.traceId,
    spanId: context?.spanId,
    parentSpanId: context?.parentSpanId,
    traceparent: context?.traceparent,
  };
}

async function observeEvaluation(
  observer: FlagEvaluationObserver | undefined,
  observation: FlagEvaluationObservation,
): Promise<void> {
  try {
    await observer?.(observation);
  } catch {
    // Observers are diagnostic only.
  }
}

async function observeExposure(
  observer: FlagExposureObserver | undefined,
  observation: FlagExposureObservation,
): Promise<void> {
  try {
    await observer?.(observation);
  } catch {
    // Observers are diagnostic only.
  }
}

async function observeTrack(
  observer: FlagTrackObserver | undefined,
  observation: FlagTrackObservation,
): Promise<void> {
  try {
    await observer?.(observation);
  } catch {
    // Observers are diagnostic only.
  }
}
