/* eslint-disable @typescript-eslint/no-unused-vars */
/**
 * Intelligent A/B Testing — Bayesian Statistics Engine.
 * Beta-Binomial for conversions, Normal for continuous metrics.
 * Probability calculations, credible intervals, early stopping rules.
 */

import type { VariantDoc, MetricType, ExperimentResult, ExperimentDoc } from './types.js';

// ─────────────────────────────────────────────────────────────────────────────
// Beta Distribution (for conversion metrics)
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Beta distribution parameters.
 * Beta(α, β) where α = successes + 1, β = failures + 1
 */
export interface BetaParams {
  alpha: number;
  beta: number;
}

/**
 * Compute Beta parameters from variant stats.
 * Uses uninformative prior Beta(1, 1) = Uniform(0, 1)
 */
export function betaFromVariant(variant: VariantDoc): BetaParams {
  const conversions = variant.stats.conversions ?? 0;
  const participants = variant.stats.participants || 1;
  const failures = participants - conversions;

  // Posterior: Beta(conversions + 1, failures + 1)
  return {
    alpha: conversions + 1,
    beta: failures + 1,
  };
}

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

/**
 * Beta PDF (probability density function).
 * Used for analytical probability calculations.
 */
export function betaPdf(x: number, alpha: number, beta: number): number {
  if (x <= 0 || x >= 1) return 0;
  const B = (gamma(alpha) * gamma(beta)) / gamma(alpha + beta);
  return (Math.pow(x, alpha - 1) * Math.pow(1 - x, beta - 1)) / B;
}

/**
 * Beta CDF (cumulative distribution function).
 * Uses regularized incomplete beta function approximation.
 */
export function betaCdf(x: number, alpha: number, beta: number): number {
  if (x <= 0) return 0;
  if (x >= 1) return 1;

  // Use incomplete beta approximation (numerical integration)
  const steps = 100;
  const dx = x / steps;
  let sum = 0;
  for (let i = 0; i < steps; i++) {
    const xi = i * dx + dx / 2;
    sum += betaPdf(xi, alpha, beta) * dx;
  }
  return sum;
}

// ─────────────────────────────────────────────────────────────────────────────
// Normal Distribution (for continuous metrics)
// ─────────────────────────────────────────────────────────────────────────────

export interface NormalParams {
  mean: number;
  stdDev: number;
}

/**
 * Compute Normal parameters from variant stats.
 * Uses conjugate prior (Normal-Inverse-Gamma).
 */
export function normalFromVariant(variant: VariantDoc): NormalParams {
  const mean = variant.stats.primaryMetricValue ?? 0;
  const n = variant.stats.participants || 1;
  const sampleStd = variant.stats.primaryMetricStdDev ?? 1;

  // Posterior standard deviation (reduced uncertainty with more samples)
  const posteriorStdDev = sampleStd / Math.sqrt(n);

  return { mean, stdDev: posteriorStdDev };
}

/**
 * Sample from Normal distribution (Box-Muller transform).
 */
export 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;
}

/**
 * Normal PDF.
 */
export function normalPdf(x: number, mean: number, stdDev: number): number {
  const z = (x - mean) / stdDev;
  return (1 / (stdDev * Math.sqrt(2 * Math.PI))) * Math.exp(-0.5 * z * z);
}

/**
 * Normal CDF (approximation using error function).
 */
export function normalCdf(x: number, mean: number, stdDev: number): number {
  const z = (x - mean) / stdDev;
  return 0.5 * (1 + erf(z / Math.sqrt(2)));
}

// ─────────────────────────────────────────────────────────────────────────────
// Gamma Distribution (for count/duration metrics)
// ─────────────────────────────────────────────────────────────────────────────

export interface GammaParams {
  shape: number;
  scale: number;
}

/**
 * Compute Gamma parameters from variant stats.
 * Uses method of moments estimation.
 */
export function gammaFromVariant(variant: VariantDoc): GammaParams {
  const mean = variant.stats.primaryMetricValue ?? 1;
  const variance = Math.pow(variant.stats.primaryMetricStdDev ?? 1, 2);

  // Method of moments: shape = mean² / variance, scale = variance / mean
  const shape = (mean * mean) / Math.max(variance, 0.001);
  const scale = variance / Math.max(mean, 0.001);

  return { shape, scale };
}

/**
 * Sample from Gamma distribution (Marsaglia-Tsang method).
 */
export 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);
}

// ─────────────────────────────────────────────────────────────────────────────
// Mathematical Utilities
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Error function approximation (Abramowitz & Stegun).
 */
function erf(x: number): number {
  const sign = x < 0 ? -1 : 1;
  x = Math.abs(x);

  const a1 = 0.254829592;
  const a2 = -0.284496736;
  const a3 = 1.421413741;
  const a4 = -1.453152027;
  const a5 = 1.061405429;
  const p = 0.3275911;

  const t = 1 / (1 + p * x);
  const y = 1 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);

  return sign * y;
}

/**
 * Gamma function approximation (Lanczos approximation).
 * Only valid for positive numbers.
 */
function gamma(z: number): number {
  if (z < 0.5) {
    return Math.PI / (Math.sin(Math.PI * z) * gamma(1 - z));
  }

  z -= 1;
  const g = 7;
  const C = [
    0.99999999999980993, 676.5203681218851, -1259.1392167224028, 771.32342877765313,
    -176.61502916214059, 12.507343278686905, -0.13857109526572012, 9.9843695780195716e-6,
    1.5056327351493116e-7,
  ];

  let x = C[0];
  for (let i = 1; i < g + 2; i++) {
    x += C[i] / (z + i);
  }

  const t = z + g + 0.5;
  return Math.sqrt(2 * Math.PI) * Math.pow(t, z + 0.5) * Math.exp(-t) * x;
}

// ─────────────────────────────────────────────────────────────────────────────
// Probability Calculations
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Calculate probability that variant beats control using Monte Carlo simulation.
 * P(variant > control) via sampling from posterior distributions.
 */
export function probabilityVariantBeatsControl(
  variant: VariantDoc,
  control: VariantDoc,
  metricType: MetricType,
  samples = 10000
): number {
  let wins = 0;

  for (let i = 0; i < samples; i++) {
    const variantSample = sampleFromPosterior(variant, metricType);
    const controlSample = sampleFromPosterior(control, metricType);
    if (variantSample > controlSample) wins++;
  }

  return wins / samples;
}

/**
 * Calculate probability that variant beats all other variants.
 */
export function probabilityVariantBeatsAll(
  variant: VariantDoc,
  allVariants: VariantDoc[],
  metricType: MetricType,
  samples = 10000
): number {
  const others = allVariants.filter(v => v.id !== variant.id);
  if (others.length === 0) return 1;

  let wins = 0;

  for (let i = 0; i < samples; i++) {
    const variantSample = sampleFromPosterior(variant, metricType);
    const otherSamples = others.map(v => sampleFromPosterior(v, metricType));
    const maxOther = Math.max(...otherSamples);
    if (variantSample > maxOther) wins++;
  }

  return wins / samples;
}

/**
 * Calculate expected loss if we choose this variant.
 * E[loss] = E[max(0, best_other - this)]
 */
export function expectedLossIfChosen(
  variant: VariantDoc,
  allVariants: VariantDoc[],
  metricType: MetricType,
  samples = 10000
): number {
  const others = allVariants.filter(v => v.id !== variant.id);
  if (others.length === 0) return 0;

  let totalLoss = 0;

  for (let i = 0; i < samples; i++) {
    const variantSample = sampleFromPosterior(variant, metricType);
    const otherSamples = others.map(v => sampleFromPosterior(v, metricType));
    const bestOther = Math.max(...otherSamples);
    const loss = Math.max(0, bestOther - variantSample);
    totalLoss += loss;
  }

  return totalLoss / samples;
}

/**
 * Sample from appropriate posterior distribution based on metric type.
 */
function sampleFromPosterior(variant: VariantDoc, metricType: MetricType): number {
  switch (metricType) {
    case 'conversion': {
      const beta = betaFromVariant(variant);
      return sampleBeta(beta.alpha, beta.beta);
    }
    case 'count':
    case 'duration': {
      const gamma = gammaFromVariant(variant);
      return sampleGamma(gamma.shape, gamma.scale);
    }
    case 'revenue':
    case 'custom':
    default: {
      const normal = normalFromVariant(variant);
      return sampleNormal(normal.mean, normal.stdDev);
    }
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Credible Intervals
// ─────────────────────────────────────────────────────────────────────────────

export interface CredibleInterval {
  lower: number; // 2.5th percentile
  mean: number;
  upper: number; // 97.5th percentile
}

/**
 * Calculate 95% credible interval for variant's metric.
 */
export function calculateCredibleInterval(
  variant: VariantDoc,
  metricType: MetricType,
  samples = 10000
): CredibleInterval {
  const samples_array: number[] = [];

  for (let i = 0; i < samples; i++) {
    samples_array.push(sampleFromPosterior(variant, metricType));
  }

  samples_array.sort((a, b) => a - b);

  const lowerIndex = Math.floor(samples * 0.025);
  const upperIndex = Math.floor(samples * 0.975);

  return {
    lower: samples_array[lowerIndex],
    mean: samples_array.reduce((a, b) => a + b, 0) / samples,
    upper: samples_array[upperIndex],
  };
}

/**
 * Analytical credible interval for Beta distribution.
 * Uses inverse Beta CDF (approximation via binary search).
 */
export function betaCredibleInterval(alpha: number, beta: number): CredibleInterval {
  // Binary search for percentiles
  const lower = betaQuantile(alpha, beta, 0.025);
  const upper = betaQuantile(alpha, beta, 0.975);
  const mean = alpha / (alpha + beta);

  return { lower, mean, upper };
}

/**
 * Find quantile of Beta distribution via binary search.
 */
function betaQuantile(alpha: number, beta: number, p: number): number {
  let low = 0;
  let high = 1;
  let mid = 0.5;

  for (let iter = 0; iter < 50; iter++) {
    mid = (low + high) / 2;
    const cdf = betaCdf(mid, alpha, beta);
    if (cdf < p) {
      low = mid;
    } else {
      high = mid;
    }
  }

  return mid;
}

// ─────────────────────────────────────────────────────────────────────────────
// Early Stopping Rules
// ─────────────────────────────────────────────────────────────────────────────

export interface EarlyStoppingResult {
  shouldStop: boolean;
  reason?: string;
  winnerVariantId?: string;
  confidence: number;
}

/**
 * Check if experiment should stop early based on statistical criteria.
 */
export function checkEarlyStopping(
  experiment: ExperimentDoc,
  variants: VariantDoc[],
  daysRunning: number
): EarlyStoppingResult {
  // Minimum sample size guardrail
  const minSamples = experiment.guardrails.minSampleSizePerVariant;
  const allHaveMinSamples = variants.every(v => v.stats.participants >= minSamples);

  if (!allHaveMinSamples) {
    return { shouldStop: false, confidence: 0 };
  }

  const controlVariant = variants.find(v => v.isControl);
  if (!controlVariant || variants.length < 2) {
    return { shouldStop: false, confidence: 0 };
  }

  // Calculate probabilities for each variant
  const results = variants.map(variant => ({
    variant,
    probBeatsControl: variant.isControl
      ? 0
      : probabilityVariantBeatsControl(variant, controlVariant, experiment.primaryMetric.type),
  }));

  // Find variant with highest probability
  const nonControlResults = results.filter(r => !r.variant.isControl);
  const bestResult = nonControlResults.reduce(
    (best, current) => (current.probBeatsControl > best.probBeatsControl ? current : best),
    nonControlResults[0]
  );

  const threshold = experiment.guardrails.winnerThreshold / 100;

  // Winner found: variant has > 95% probability of beating control
  if (bestResult && bestResult.probBeatsControl >= threshold) {
    return {
      shouldStop: true,
      reason: 'Winner found: variant has > 95% probability of beating control',
      winnerVariantId: bestResult.variant.id,
      confidence: bestResult.probBeatsControl,
    };
  }

  // No winner clear: control has > 95% probability of beating all variants
  // (Skip this check if max duration is reached - duration takes precedence)
  if (daysRunning < experiment.guardrails.maxDurationDays) {
    const controlProbBeatsAll = probabilityVariantBeatsAll(
      controlVariant,
      variants,
      experiment.primaryMetric.type
    );
    if (controlProbBeatsAll >= threshold) {
      return {
        shouldStop: true,
        reason: 'No winner: control outperforms all variants with > 95% confidence',
        confidence: controlProbBeatsAll,
      };
    }
  }

  // Time bound: max duration reached
  if (daysRunning >= experiment.guardrails.maxDurationDays) {
    return {
      shouldStop: true,
      reason: 'Max duration reached',
      confidence: bestResult?.probBeatsControl ?? 0,
    };
  }

  return {
    shouldStop: false,
    confidence: bestResult?.probBeatsControl ?? 0,
  };
}

// ─────────────────────────────────────────────────────────────────────────────
// Experiment Results
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Generate comprehensive experiment results.
 */
export function generateExperimentResult(
  experiment: ExperimentDoc,
  variants: VariantDoc[],
  daysRunning: number
): ExperimentResult {
  const controlVariant = variants.find(v => v.isControl);
  const earlyStop = checkEarlyStopping(experiment, variants, daysRunning);

  const variantResults = variants.map(variant => {
    const credibleInterval = calculateCredibleInterval(variant, experiment.primaryMetric.type);
    const probBeatsControl = controlVariant
      ? probabilityVariantBeatsControl(variant, controlVariant, experiment.primaryMetric.type)
      : 0.5;

    // Expected lift relative to control
    let expectedLift = 0;
    if (controlVariant && controlVariant.stats.primaryMetricValue > 0) {
      expectedLift =
        ((variant.stats.primaryMetricValue - controlVariant.stats.primaryMetricValue) /
          controlVariant.stats.primaryMetricValue) *
        100;
    }

    return {
      variantId: variant.id,
      variantName: variant.name,
      isControl: variant.isControl,
      participants: variant.stats.participants,
      primaryMetricValue: variant.stats.primaryMetricValue,
      probabilityBeatsControl: probBeatsControl,
      expectedLiftPercent: expectedLift,
      credibleInterval,
    };
  });

  const winnerResult = earlyStop.winnerVariantId
    ? variantResults.find(vr => vr.variantId === earlyStop.winnerVariantId)
    : undefined;

  // Determine recommended action
  let recommendedAction: ExperimentResult['statisticalSummary']['recommendedAction'] = 'continue';
  if (earlyStop.shouldStop) {
    if (earlyStop.winnerVariantId) {
      recommendedAction = 'ship';
    } else if (earlyStop.reason?.includes('control')) {
      recommendedAction = 'rollback';
    } else {
      recommendedAction = 'stop';
    }
  }

  // Calculate probability that any variant beats control
  const probAnyBeatsControl = Math.max(...variantResults.map(vr => vr.probabilityBeatsControl));

  // Calculate expected loss if we ship the winner
  const expectedLoss = earlyStop.winnerVariantId
    ? expectedLossIfChosen(
        variants.find(v => v.id === earlyStop.winnerVariantId)!,
        variants,
        experiment.primaryMetric.type
      )
    : 0;

  return {
    experimentId: experiment.id,
    status: earlyStop.shouldStop
      ? earlyStop.winnerVariantId
        ? 'winner_found'
        : 'no_winner'
      : 'in_progress',
    totalParticipants: experiment.totalParticipants,
    totalEvents: experiment.totalEvents,
    daysRunning,
    winnerVariantId: earlyStop.winnerVariantId,
    winnerProbability: earlyStop.confidence,
    variantResults,
    statisticalSummary: {
      probabilityAnyBeatsControl: probAnyBeatsControl,
      expectedLossIfShipped: expectedLoss,
      recommendedAction,
    },
    earlyStopped: earlyStop.shouldStop,
    stopReason: earlyStop.reason,
    generatedAt: new Date().toISOString(),
  };
}

// ─────────────────────────────────────────────────────────────────────────────
// Statistical Tests
// ─────────────────────────────────────────────────────────────────────────────

/**
 * A/A test: check that two identical variants produce similar results.
 * Used for validating the statistical engine.
 */
export function validateAA(
  n: number,
  trueRate: number,
  samples = 1000
): { passRate: number; bias: number } {
  let passes = 0;
  let totalError = 0;

  for (let i = 0; i < samples; i++) {
    // Simulate two "identical" variants
    const aSuccesses = simulateBinomial(n, trueRate);
    const bSuccesses = simulateBinomial(n, trueRate);

    const aRate = aSuccesses / n;
    const bRate = bSuccesses / n;

    // Check if confidence intervals overlap (simplified)
    const aStd = Math.sqrt((aRate * (1 - aRate)) / n);
    const bStd = Math.sqrt((bRate * (1 - bRate)) / n);

    const diff = Math.abs(aRate - bRate);
    const pooledStd = Math.sqrt(aStd * aStd + bStd * bStd);

    // Pass if difference is within 2 standard deviations (95% roughly)
    if (diff < 2 * pooledStd) passes++;
    totalError += diff;
  }

  return {
    passRate: passes / samples,
    bias: totalError / samples,
  };
}

/**
 * Simulate binomial distribution (inverse transform sampling).
 */
function simulateBinomial(n: number, p: number): number {
  let successes = 0;
  for (let i = 0; i < n; i++) {
    if (Math.random() < p) successes++;
  }
  return successes;
}

/**
 * Power analysis: calculate required sample size for given effect size.
 * Uses standard two-proportion z-test formula.
 */
export function calculateSampleSize(
  baselineRate: number,
  minDetectableEffect: number, // relative change, e.g., 0.05 for 5%
  alpha = 0.05,
  power = 0.8
): number {
  // Handle edge cases
  if (baselineRate <= 0 || baselineRate >= 1) return 100;
  if (minDetectableEffect <= 0) return 100;

  const zAlpha = 1.96; // ~95% confidence
  const zBeta = 0.84; // ~80% power

  const p1 = baselineRate;
  const p2 = Math.min(baselineRate * (1 + minDetectableEffect), 0.99);

  // If effect is too small, return large but bounded sample size
  const delta = Math.abs(p2 - p1);
  if (delta < 0.001) return 100000;

  const pAvg = (p1 + p2) / 2;

  // Standard two-proportion sample size formula
  const term1 = zAlpha * Math.sqrt(2 * pAvg * (1 - pAvg));
  const term2 = zBeta * Math.sqrt(p1 * (1 - p1) + p2 * (1 - p2));
  const numerator = term1 + term2;

  const n = Math.ceil(Math.pow(numerator / delta, 2));

  // Return bounded value
  return Math.max(100, Math.min(n, 100000));
}
