/* eslint-disable @typescript-eslint/no-unused-vars */
/**
 * Intelligent A/B Testing — Repository layer.
 * Cosmos DB CRUD for experiments, variants, assignments, events, metrics.
 */

import { getRegisteredContainer } from '@bytelyst/cosmos';
import crypto from 'node:crypto';
import type {
  ExperimentDoc,
  VariantDoc,
  ExperimentAssignmentDoc,
  ExperimentEventDoc,
  ExperimentMetricDoc,
  ExperimentSuggestion,
  CreateExperimentInput,
  UpdateExperimentInput,
  TargetingConfig,
} from './types.js';
import {
  assignVariant,
  assignByStrategy,
  isInExperimentBucket,
  type StrategyContext,
} from './bucketing.js';
import type { TargetingContext } from './targeting.js';
import { matchesTargeting } from './targeting.js';

// ─────────────────────────────────────────────────────────────────────────────
// Container Access
// ─────────────────────────────────────────────────────────────────────────────

function getExperimentContainer() {
  return getRegisteredContainer('experiments');
}

function getVariantContainer() {
  return getRegisteredContainer('ab_testing_variants');
}

function getAssignmentContainer() {
  return getRegisteredContainer('experiment_assignments');
}

function getEventContainer() {
  return getRegisteredContainer('ab_testing_events');
}

function getMetricContainer() {
  return getRegisteredContainer('ab_testing_metrics');
}

function getSuggestionsContainer() {
  return getRegisteredContainer('experiment_suggestions');
}

// ─────────────────────────────────────────────────────────────────────────────
// Experiment CRUD
// ─────────────────────────────────────────────────────────────────────────────

export async function listExperiments(productId: string): Promise<ExperimentDoc[]> {
  const { resources } = await getExperimentContainer()
    .items.query<ExperimentDoc>({
      query: 'SELECT * FROM c WHERE c.productId = @pid ORDER BY c.createdAt DESC',
      parameters: [{ name: '@pid', value: productId }],
    })
    .fetchAll();
  return resources;
}

export async function listRunningExperiments(productId: string): Promise<ExperimentDoc[]> {
  const { resources } = await getExperimentContainer()
    .items.query<ExperimentDoc>({
      query: 'SELECT * FROM c WHERE c.productId = @pid AND c.status = @status',
      parameters: [
        { name: '@pid', value: productId },
        { name: '@status', value: 'running' },
      ],
    })
    .fetchAll();
  return resources;
}

export async function getExperiment(id: string): Promise<ExperimentDoc | null> {
  try {
    const { resource } = await getExperimentContainer().item(id, id).read<ExperimentDoc>();
    return resource ?? null;
  } catch {
    return null;
  }
}

export async function createExperiment(
  productId: string,
  input: CreateExperimentInput
): Promise<ExperimentDoc> {
  const now = new Date().toISOString();
  const experimentId = `exp_${crypto.randomUUID()}`;

  // Create variants
  const variantIds: string[] = [];
  let controlVariantId = '';
  const numVariants = input.variants.length;
  const baseAllocation = Math.floor(100 / numVariants);
  const remainder = 100 - baseAllocation * numVariants;

  for (let i = 0; i < input.variants.length; i++) {
    const v = input.variants[i];
    const variantId = `var_${crypto.randomUUID()}`;
    variantIds.push(variantId);

    if (v.isControl) {
      controlVariantId = variantId;
    }

    const variantDoc: VariantDoc = {
      id: variantId,
      experimentId,
      name: v.name,
      description: v.description ?? '',
      isControl: v.isControl,
      flagConfig: v.flagConfig,
      currentAllocationPercent: baseAllocation + (i < remainder ? 1 : 0),
      stats: {
        participants: 0,
        events: 0,
        primaryMetricValue: 0,
      },
      createdAt: now,
      updatedAt: now,
    };

    await getVariantContainer().items.create(variantDoc);
  }

  // Default to first variant as control if none specified
  if (!controlVariantId) {
    controlVariantId = variantIds[0];
  }

  const experiment: ExperimentDoc = {
    id: experimentId,
    productId,
    name: input.name,
    description: input.description ?? '',
    hypothesis: input.hypothesis,
    status: 'draft',
    controlVariantId,
    variantIds,
    allocationStrategy: input.allocationStrategy,
    targetPercent: input.targetPercent,
    targeting: input.targeting,
    primaryMetric: input.primaryMetric,
    secondaryMetrics: input.secondaryMetrics,
    guardrails: input.guardrails,
    startAt: input.startAt,
    totalParticipants: 0,
    totalEvents: 0,
    createdAt: now,
    updatedAt: now,
  };

  await getExperimentContainer().items.create(experiment);
  return experiment;
}

export async function updateExperiment(
  id: string,
  updates: UpdateExperimentInput
): Promise<ExperimentDoc | null> {
  const existing = await getExperiment(id);
  if (!existing) return null;

  const now = new Date().toISOString();
  const updated: ExperimentDoc = {
    ...existing,
    ...updates,
    updatedAt: now,
  };

  if (updates.status === 'running' && !existing.startedAt) {
    updated.startedAt = now;
  }
  if ((updates.status === 'completed' || updates.status === 'stopped') && !existing.completedAt) {
    updated.completedAt = now;
    // Set TTL for 2 years
    updated.ttl = 2 * 365 * 86400;
  }

  await getExperimentContainer().item(id, id).replace(updated);
  return updated;
}

export async function deleteExperiment(id: string): Promise<boolean> {
  try {
    // Delete variants first
    const { resources: variants } = await getVariantContainer()
      .items.query({
        query: 'SELECT * FROM c WHERE c.experimentId = @eid',
        parameters: [{ name: '@eid', value: id }],
      })
      .fetchAll();

    for (const v of variants) {
      await getVariantContainer().item(v.id, id).delete();
    }

    await getExperimentContainer().item(id, id).delete();
    return true;
  } catch {
    return false;
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// Variant Operations
// ─────────────────────────────────────────────────────────────────────────────

export async function getVariant(id: string, experimentId: string): Promise<VariantDoc | null> {
  try {
    const { resource } = await getVariantContainer().item(id, experimentId).read<VariantDoc>();
    return resource ?? null;
  } catch {
    return null;
  }
}

export async function listVariants(experimentId: string): Promise<VariantDoc[]> {
  const { resources } = await getVariantContainer()
    .items.query<VariantDoc>({
      query: 'SELECT * FROM c WHERE c.experimentId = @eid',
      parameters: [{ name: '@eid', value: experimentId }],
    })
    .fetchAll();
  return resources;
}

export async function updateVariantStats(
  variantId: string,
  experimentId: string,
  stats: Partial<VariantDoc['stats']>
): Promise<void> {
  const variant = await getVariant(variantId, experimentId);
  if (!variant) return;

  const updated: VariantDoc = {
    ...variant,
    stats: { ...variant.stats, ...stats },
    updatedAt: new Date().toISOString(),
  };

  await getVariantContainer().item(variantId, experimentId).replace(updated);
}

export async function updateVariantAllocation(
  variantId: string,
  experimentId: string,
  newAllocation: number
): Promise<void> {
  const variant = await getVariant(variantId, experimentId);
  if (!variant) return;

  const updated: VariantDoc = {
    ...variant,
    currentAllocationPercent: newAllocation,
    updatedAt: new Date().toISOString(),
  };

  await getVariantContainer().item(variantId, experimentId).replace(updated);
}

export async function updateVariantBayesianResults(
  variantId: string,
  experimentId: string,
  results: VariantDoc['bayesianResults']
): Promise<void> {
  const variant = await getVariant(variantId, experimentId);
  if (!variant) return;

  const updated: VariantDoc = {
    ...variant,
    bayesianResults: results,
    updatedAt: new Date().toISOString(),
  };

  await getVariantContainer().item(variantId, experimentId).replace(updated);
}

// ─────────────────────────────────────────────────────────────────────────────
// Assignment Operations
// ─────────────────────────────────────────────────────────────────────────────

export interface AssignmentResult {
  assignment: ExperimentAssignmentDoc;
  variant: VariantDoc;
  isNew: boolean;
}

export async function getAssignment(
  experimentId: string,
  userId: string
): Promise<ExperimentAssignmentDoc | null> {
  // Query by userId partition key
  const { resources } = await getAssignmentContainer()
    .items.query<ExperimentAssignmentDoc>({
      query: 'SELECT * FROM c WHERE c.experimentId = @eid AND c.userId = @uid',
      parameters: [
        { name: '@eid', value: experimentId },
        { name: '@uid', value: userId },
      ],
    })
    .fetchAll();
  return resources[0] ?? null;
}

export async function getOrCreateAssignment(
  experiment: ExperimentDoc,
  userId: string,
  targetingContext: TargetingContext
): Promise<AssignmentResult | null> {
  // Check targeting first
  if (!matchesTargeting(targetingContext, experiment.targeting)) {
    return null;
  }

  // Check traffic percentage
  if (!isInExperimentBucket(experiment.id, userId, experiment.targetPercent)) {
    return null;
  }

  // Check for existing assignment
  const existing = await getAssignment(experiment.id, userId);
  if (existing) {
    const variant = await getVariant(existing.variantId, experiment.id);
    if (!variant) return null;
    return { assignment: existing, variant, isNew: false };
  }

  // Get all variants
  const variants = await listVariants(experiment.id);
  if (variants.length === 0) return null;

  const controlVariant = variants.find(v => v.isControl) ?? variants[0];

  // Assign variant based on strategy
  let assignedVariantId: string;

  if (experiment.allocationStrategy === 'random') {
    // Use FNV-1a bucketing for random strategy
    assignedVariantId = assignVariant(
      experiment.id,
      userId,
      variants.map(v => ({ key: v.id, weight: v.currentAllocationPercent }))
    );
  } else {
    // Use adaptive strategies
    const ctx: StrategyContext = {
      variants,
      controlVariant,
      totalParticipants: experiment.totalParticipants,
      explorationRate: 0.1, // Default epsilon for epsilon-greedy
    };
    assignedVariantId = assignByStrategy(experiment.allocationStrategy, ctx);
  }

  const assignedVariant = variants.find(v => v.id === assignedVariantId) ?? variants[0];

  // Create assignment record
  const now = new Date().toISOString();
  const assignment: ExperimentAssignmentDoc = {
    id: `ea_${crypto.randomUUID()}`,
    userId,
    experimentId: experiment.id,
    variantId: assignedVariant.id,
    assignedAt: now,
    assignmentContext: {
      platform: targetingContext.platform ?? 'unknown',
      appVersion: targetingContext.appVersion ?? 'unknown',
      osVersion: targetingContext.osVersion ?? 'unknown',
      deviceModel: targetingContext.deviceModel,
      region: targetingContext.region,
    },
    eventCount: 0,
  };

  await getAssignmentContainer().items.create(assignment);

  // Update participant count
  await updateVariantStats(assignedVariant.id, experiment.id, {
    participants: assignedVariant.stats.participants + 1,
  });

  // Update experiment total
  const expUpdate: Partial<ExperimentDoc> = {
    totalParticipants: experiment.totalParticipants + 1,
    updatedAt: now,
  };
  await getExperimentContainer()
    .item(experiment.id, experiment.id)
    .patch({
      operations: [
        { op: 'incr', path: '/totalParticipants', value: 1 },
        { op: 'set', path: '/updatedAt', value: now },
      ],
    });

  return { assignment, variant: assignedVariant, isNew: true };
}

// ─────────────────────────────────────────────────────────────────────────────
// Event Tracking
// ─────────────────────────────────────────────────────────────────────────────

export async function trackEvent(
  experimentId: string,
  userId: string,
  assignmentId: string,
  variantId: string,
  metricName: string,
  metricType: ExperimentEventDoc['metricType'],
  value: number,
  converted: boolean,
  platform: string,
  appVersion: string,
  eventMetadata?: Record<string, unknown>
): Promise<void> {
  const now = new Date().toISOString();
  const event: ExperimentEventDoc = {
    id: `ee_${crypto.randomUUID()}`,
    experimentId,
    timestamp: now,
    userId,
    variantId,
    assignmentId,
    metricName,
    metricType,
    value,
    converted,
    eventMetadata,
    platform,
    appVersion,
    ttl: 90 * 86400, // 90 days
  };

  await getEventContainer().items.create(event);

  // Update assignment event count
  await getAssignmentContainer()
    .item(assignmentId, userId)
    .patch({
      operations: [
        { op: 'incr', path: '/eventCount', value: 1 },
        { op: 'set', path: '/lastEventAt', value: now },
      ],
    });

  // Update experiment total events
  await getExperimentContainer()
    .item(experimentId, experimentId)
    .patch({
      operations: [
        { op: 'incr', path: '/totalEvents', value: 1 },
        { op: 'set', path: '/updatedAt', value: now },
      ],
    });
}

// ─────────────────────────────────────────────────────────────────────────────
// Metrics Aggregation
// ─────────────────────────────────────────────────────────────────────────────

export async function getOrCreateMetric(
  experimentId: string,
  metricName: string,
  variantId: string
): Promise<ExperimentMetricDoc> {
  const id = `em_${experimentId}:${metricName}:${variantId}`;
  const existing = await getMetricContainer().item(id, experimentId).read<ExperimentMetricDoc>();
  if (existing.resource) return existing.resource;

  const now = new Date().toISOString();
  const metric: ExperimentMetricDoc = {
    id,
    experimentId,
    metricName,
    variantId,
    count: 0,
    sum: 0,
    mean: 0,
    stdDev: 0,
    min: 0,
    max: 0,
    conversions: 0,
    conversionRate: 0,
    updatedAt: now,
  };

  await getMetricContainer().items.create(metric);
  return metric;
}

export async function updateMetricAggregation(
  experimentId: string,
  metricName: string,
  variantId: string,
  value: number,
  converted: boolean
): Promise<void> {
  const metric = await getOrCreateMetric(experimentId, metricName, variantId);

  const newCount = metric.count + 1;
  const newSum = metric.sum + value;
  const newMean = newSum / newCount;

  // Welford's online algorithm for variance
  const delta = value - metric.mean;
  const delta2 = value - newMean;
  const newVariance =
    (metric.count * metric.stdDev * metric.stdDev + delta * delta2) / Math.max(1, newCount);
  const newStdDev = Math.sqrt(newVariance);

  const updated: ExperimentMetricDoc = {
    ...metric,
    count: newCount,
    sum: newSum,
    mean: newMean,
    stdDev: newStdDev,
    min: metric.count === 0 ? value : Math.min(metric.min, value),
    max: metric.count === 0 ? value : Math.max(metric.max, value),
    conversions: metric.conversions + (converted ? 1 : 0),
    conversionRate: (metric.conversions + (converted ? 1 : 0)) / newCount,
    updatedAt: new Date().toISOString(),
  };

  await getMetricContainer().item(metric.id, experimentId).replace(updated);
}

// ─────────────────────────────────────────────────────────────────────────────
// AI Suggestions
// ─────────────────────────────────────────────────────────────────────────────

export async function listSuggestions(productId: string): Promise<ExperimentSuggestion[]> {
  const { resources } = await getSuggestionsContainer()
    .items.query<ExperimentSuggestion>({
      query: 'SELECT * FROM c WHERE c.productId = @pid ORDER BY c.priority DESC, c.createdAt DESC',
      parameters: [{ name: '@pid', value: productId }],
    })
    .fetchAll();
  return resources;
}

export async function createSuggestion(
  productId: string,
  suggestion: Omit<ExperimentSuggestion, 'id' | 'createdAt'>
): Promise<ExperimentSuggestion> {
  const doc: ExperimentSuggestion = {
    ...suggestion,
    id: `es_${crypto.randomUUID()}`,
    createdAt: new Date().toISOString(),
  };
  await getSuggestionsContainer().items.create({ ...doc, productId });
  return doc;
}
