/* eslint-disable @typescript-eslint/no-unused-vars */
/**
 * A/B Testing — Deterministic bucketing and assignment strategies.
 * FNV-1a hashing for sticky assignments, Thompson sampling, UCB, epsilon-greedy.
 */

import type { AllocationStrategy, ExperimentVariant, VariantDoc } from './types.js';

// ─────────────────────────────────────────────────────────────────────────────
// FNV-1a Hash (consistent with feature flags module)
// ─────────────────────────────────────────────────────────────────────────────

export function fnv1a(str: string): number {
  let hash = 0x811c9dc5;
  for (let i = 0; i < str.length; i++) {
    hash ^= str.charCodeAt(i);
    hash = (hash * 0x01000193) >>> 0;
  }
  return hash;
}

/**
 * Deterministic variant assignment using FNV-1a hash.
 * Same user+experiment always gets same variant (sticky assignment).
 */
export function assignVariant(
  experimentId: string,
  userId: string,
  variants: Array<{ key: string; weight: number }>
): string {
  const hash = fnv1a(`${experimentId}:${userId}`);
  const bucket = hash % 100;
  let cumulative = 0;
  for (const v of variants) {
    cumulative += v.weight;
    if (bucket < cumulative) return v.key;
  }
  return variants[variants.length - 1].key;
}

/**
 * Check if user is in experiment bucket (traffic percentage filter).
 */
export function isInExperimentBucket(
  experimentId: string,
  userId: string,
  trafficPercent: number
): boolean {
  const hash = fnv1a(`${experimentId}:bucket:${userId}`);
  const bucket = hash % 100;
  return bucket < trafficPercent;
}

// ─────────────────────────────────────────────────────────────────────────────
// Assignment Strategies
// ─────────────────────────────────────────────────────────────────────────────

export interface StrategyContext {
  variants: VariantDoc[];
  controlVariant: VariantDoc;
  totalParticipants: number;
  explorationRate?: number; // For epsilon-greedy
}

/**
 * Random assignment based on current allocation percentages.
 */
export function randomAssignment(ctx: StrategyContext): string {
  const { variants } = ctx;
  const hash = Math.random() * 100;
  let cumulative = 0;
  for (const v of variants) {
    cumulative += v.currentAllocationPercent;
    if (hash < cumulative) return v.id;
  }
  return variants[variants.length - 1].id;
}

/**
 * Thompson Sampling — Bayesian multi-armed bandit.
 * Samples from Beta distribution for each variant, picks highest.
 * Optimizes for reward while exploring uncertain variants.
 */
export function thompsonSampling(ctx: StrategyContext): string {
  const { variants, controlVariant } = ctx;

  let bestVariantId = variants[0].id;
  let bestSample = -Infinity;

  for (const variant of variants) {
    const sample = sampleFromPosterior(variant, controlVariant);
    if (sample > bestSample) {
      bestSample = sample;
      bestVariantId = variant.id;
    }
  }

  return bestVariantId;
}

/**
 * Sample from variant's posterior distribution (Beta for conversion, Normal for continuous).
 */
function sampleFromPosterior(variant: VariantDoc, controlVariant: VariantDoc): number {
  // Use conversion rate if available, otherwise primary metric value
  const rate = variant.stats.conversionRate ?? variant.stats.primaryMetricValue;
  const participants = variant.stats.participants || 1;

  // For conversion metrics, use Beta distribution
  if (variant.stats.betaAlpha !== undefined && variant.stats.betaBeta !== undefined) {
    return sampleBeta(variant.stats.betaAlpha, variant.stats.betaBeta);
  }

  // For continuous metrics, use Normal approximation
  const stdDev = variant.stats.primaryMetricStdDev ?? 0.1;
  return sampleNormal(rate, stdDev / Math.sqrt(participants));
}

/**
 * Epsilon-Greedy — explore random with probability ε, otherwise exploit best.
 */
export function epsilonGreedy(ctx: StrategyContext): string {
  const { variants, explorationRate = 0.1 } = ctx;
  const epsilon = explorationRate;

  // Explore: random assignment
  if (Math.random() < epsilon) {
    const randomIndex = Math.floor(Math.random() * variants.length);
    return variants[randomIndex].id;
  }

  // Exploit: best performing variant
  return getBestVariant(variants);
}

/**
 * Upper Confidence Bound (UCB1) — pick variant with highest upper bound.
 * Balances exploration (high uncertainty) with exploitation (high reward).
 */
export function ucbAssignment(ctx: StrategyContext): string {
  const { variants, totalParticipants } = ctx;

  let bestVariantId = variants[0].id;
  let bestUcb = -Infinity;

  for (const variant of variants) {
    const rate = variant.stats.conversionRate ?? variant.stats.primaryMetricValue;
    const n = variant.stats.participants || 1;
    const totalN = Math.max(totalParticipants, 1);

    // UCB1 formula: mean + sqrt(2 * ln(total) / n)
    const explorationBonus = Math.sqrt((2 * Math.log(totalN)) / n);
    const ucb = rate + explorationBonus;

    if (ucb > bestUcb) {
      bestUcb = ucb;
      bestVariantId = variant.id;
    }
  }

  return bestVariantId;
}

/**
 * Get best performing variant by conversion rate or primary metric.
 */
function getBestVariant(variants: VariantDoc[]): string {
  let bestId = variants[0].id;
  let bestRate = -Infinity;

  for (const v of variants) {
    const rate = v.stats.conversionRate ?? v.stats.primaryMetricValue;
    if (rate > bestRate) {
      bestRate = rate;
      bestId = v.id;
    }
  }

  return bestId;
}

// ─────────────────────────────────────────────────────────────────────────────
// Distribution Sampling (for Thompson Sampling)
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Sample from Beta distribution using approximation.
 * Uses the fact that Beta(α,β) ~ Gamma(α) / (Gamma(α) + Gamma(β))
 */
function sampleBeta(alpha: number, beta: number): number {
  const x = sampleGamma(alpha, 1);
  const y = sampleGamma(beta, 1);
  return x / (x + y);
}

/**
 * Sample from Normal distribution (Box-Muller transform).
 */
function sampleNormal(mean: number, stdDev: number): number {
  const u1 = Math.random();
  const u2 = Math.random();
  const z0 = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
  return mean + z0 * stdDev;
}

/**
 * Sample from Gamma distribution (Marsaglia-Tsang method).
 */
function sampleGamma(shape: number, scale: number): number {
  if (shape < 1) {
    return sampleGamma(1 + shape, scale) * Math.pow(Math.random(), 1 / shape);
  }

  const d = shape - 1 / 3;
  const c = 1 / Math.sqrt(9 * d);

  while (true) {
    const x = sampleStandardNormal();
    let v = 1 + c * x;
    if (v <= 0) continue;

    v = v * v * v;
    const u = Math.random();

    if (u < 1 - 0.0331 * x * x * x * x) {
      return d * v * scale;
    }

    if (Math.log(u) < 0.5 * x * x + d * (1 - v + Math.log(v))) {
      return d * v * scale;
    }
  }
}

function sampleStandardNormal(): number {
  return sampleNormal(0, 1);
}

// ─────────────────────────────────────────────────────────────────────────────
// Strategy Router
// ─────────────────────────────────────────────────────────────────────────────

export function assignByStrategy(strategy: AllocationStrategy, ctx: StrategyContext): string {
  switch (strategy) {
    case 'random':
      return randomAssignment(ctx);
    case 'thompson':
      return thompsonSampling(ctx);
    case 'epsilon_greedy':
      return epsilonGreedy(ctx);
    case 'ucb':
      return ucbAssignment(ctx);
    default:
      return randomAssignment(ctx);
  }
}
