/**
 * Flag evaluation engine — pure functions, no I/O.
 *
 * Evaluates a flag against an EvaluationContext and returns the resolved
 * variation value + the reason it was chosen. Supports:
 *
 * - Schedule checking (enableAt / disableAt / gradual rollout)
 * - Prerequisite dependencies
 * - Individual user targeting
 * - Targeting rules with attribute matching (AND within a rule)
 * - Segment matching
 * - Percentage rollout (deterministic via FNV-1a)
 * - OS version range filtering
 * - Platform / region filtering
 * - Fallback to default variation
 */

import type {
  FeatureFlagDoc,
  FlagVariation,
  TargetingClause,
  SegmentDoc,
  EvaluationContext,
  EvaluationResult,
} from './types.js';

// ── FNV-1a hash ─────────────────────────────────────────────────────────────

/**
 * FNV-1a 32-bit hash → 0–99 bucket.
 * Deterministic: same (userId, flagKey) always produces the same bucket.
 */
export function hashUserFlag(userId: string, flagKey: string): number {
  const input = `${userId}:${flagKey}`;
  let hash = 0x811c9dc5; // FNV offset basis
  for (let i = 0; i < input.length; i++) {
    hash ^= input.charCodeAt(i);
    hash = Math.imul(hash, 0x01000193); // FNV prime
  }
  return (hash >>> 0) % 100; // 0-99 bucket
}

// ── Semver comparison ───────────────────────────────────────────────────────

/**
 * Compare two dot-separated version strings.
 * Returns -1 if a < b, 0 if equal, 1 if a > b.
 */
export function compareVersions(a: string, b: string): number {
  const pa = a.split('.').map(Number);
  const pb = b.split('.').map(Number);
  const len = Math.max(pa.length, pb.length);
  for (let i = 0; i < len; i++) {
    const na = pa[i] ?? 0;
    const nb = pb[i] ?? 0;
    if (na < nb) return -1;
    if (na > nb) return 1;
  }
  return 0;
}

// ── Clause evaluation ───────────────────────────────────────────────────────

function getContextAttribute(
  ctx: EvaluationContext,
  attribute: string
): string | number | boolean | undefined {
  switch (attribute) {
    case 'userId':
      return ctx.userId;
    case 'platform':
      return ctx.platform;
    case 'region':
      return ctx.region;
    case 'osVersion':
      return ctx.osVersion;
    case 'appVersion':
      return ctx.appVersion;
    case 'email':
      return ctx.email;
    default:
      return ctx.custom?.[attribute];
  }
}

function evaluateClause(clause: TargetingClause, ctx: EvaluationContext): boolean {
  const actual = getContextAttribute(ctx, clause.attribute);
  if (actual === undefined) return false;

  const strActual = String(actual);

  switch (clause.operator) {
    case 'eq':
      return clause.values.some(v => String(v) === strActual);
    case 'neq':
      return clause.values.every(v => String(v) !== strActual);
    case 'contains':
      return clause.values.some(v => strActual.includes(String(v)));
    case 'not_contains':
      return clause.values.every(v => !strActual.includes(String(v)));
    case 'starts_with':
      return clause.values.some(v => strActual.startsWith(String(v)));
    case 'ends_with':
      return clause.values.some(v => strActual.endsWith(String(v)));
    case 'gt':
      return clause.values.some(v => Number(actual) > Number(v));
    case 'gte':
      return clause.values.some(v => Number(actual) >= Number(v));
    case 'lt':
      return clause.values.some(v => Number(actual) < Number(v));
    case 'lte':
      return clause.values.some(v => Number(actual) <= Number(v));
    case 'in':
      return clause.values.map(String).includes(strActual);
    case 'not_in':
      return !clause.values.map(String).includes(strActual);
    case 'semver_gt':
      return clause.values.some(v => compareVersions(strActual, String(v)) > 0);
    case 'semver_gte':
      return clause.values.some(v => compareVersions(strActual, String(v)) >= 0);
    case 'semver_lt':
      return clause.values.some(v => compareVersions(strActual, String(v)) < 0);
    case 'semver_lte':
      return clause.values.some(v => compareVersions(strActual, String(v)) <= 0);
    case 'semver_eq':
      return clause.values.some(v => compareVersions(strActual, String(v)) === 0);
    case 'regex':
      try {
        return clause.values.some(v => new RegExp(String(v)).test(strActual));
      } catch {
        return false;
      }
    default:
      return false;
  }
}

// ── Helpers ─────────────────────────────────────────────────────────────────

function getVariation(flag: FeatureFlagDoc, key: string): FlagVariation | undefined {
  return flag.variations.find(v => v.key === key);
}

function offResult(flag: FeatureFlagDoc, reason: EvaluationResult['reason']): EvaluationResult {
  const variation = getVariation(flag, flag.offVariation);
  return {
    key: flag.key,
    value: variation?.value ?? false,
    variationKey: flag.offVariation,
    reason,
  };
}

function variationResult(
  flag: FeatureFlagDoc,
  variationKey: string,
  reason: EvaluationResult['reason']
): EvaluationResult {
  const variation = getVariation(flag, variationKey);
  return {
    key: flag.key,
    value: variation?.value ?? true,
    variationKey,
    reason,
  };
}

// ── Schedule checking ───────────────────────────────────────────────────────

function isScheduleActive(flag: FeatureFlagDoc, now: string): boolean | null {
  if (!flag.schedule) return null; // no schedule — use flag.enabled

  if (flag.schedule.enableAt && now < flag.schedule.enableAt) return false;
  if (flag.schedule.disableAt && now >= flag.schedule.disableAt) return false;

  return true;
}

/**
 * Calculate the effective rollout percentage considering gradual rollout schedule.
 */
export function getEffectivePercentage(flag: FeatureFlagDoc, now: string): number {
  const gr = flag.schedule?.gradualRollout;
  if (!gr) return flag.percentage;

  if (now < gr.startAt) return gr.startPercentage;
  if (now >= gr.endAt) return gr.endPercentage;

  // Linear interpolation between start and end
  const totalMs = new Date(gr.endAt).getTime() - new Date(gr.startAt).getTime();
  const elapsedMs = new Date(now).getTime() - new Date(gr.startAt).getTime();
  const progress = Math.min(1, Math.max(0, elapsedMs / totalMs));
  return Math.round(gr.startPercentage + (gr.endPercentage - gr.startPercentage) * progress);
}

// ── OS version filtering ────────────────────────────────────────────────────

function matchesOsVersion(flag: FeatureFlagDoc, ctx: EvaluationContext): boolean {
  if (!flag.osVersions || flag.osVersions.length === 0) return true;
  if (!ctx.platform || !ctx.osVersion) return true; // no context to filter on

  const rule = flag.osVersions.find(r => r.platform === ctx.platform);
  if (!rule) return false; // osVersions defined but no rule for this platform

  if (rule.minVersion && compareVersions(ctx.osVersion, rule.minVersion) < 0) return false;
  if (rule.maxVersion && compareVersions(ctx.osVersion, rule.maxVersion) > 0) return false;
  return true;
}

// ── Platform / region filtering ─────────────────────────────────────────────

function matchesPlatform(flag: FeatureFlagDoc, ctx: EvaluationContext): boolean {
  if (flag.platforms.length === 0) return true;
  if (!ctx.platform) return true;
  return flag.platforms.includes(ctx.platform);
}

function matchesRegion(flag: FeatureFlagDoc, ctx: EvaluationContext): boolean {
  if (!flag.regions || flag.regions.length === 0) return true;
  if (!ctx.region) return true;
  return flag.regions.includes(ctx.region);
}

// ── Segment matching ────────────────────────────────────────────────────────

function userMatchesSegment(segment: SegmentDoc, ctx: EvaluationContext): boolean {
  if (!ctx.userId) return false;

  // Explicit exclusion takes priority
  if (segment.excludedUsers.includes(ctx.userId)) return false;

  // Explicit inclusion
  if (segment.includedUsers.includes(ctx.userId)) return true;

  // Rule-based matching: all clauses must match (AND)
  if (segment.rules.length === 0) return false;
  return segment.rules.every(clause => evaluateClause(clause, ctx));
}

// ── Main evaluation function ────────────────────────────────────────────────

export interface EvaluateFlagOptions {
  flag: FeatureFlagDoc;
  ctx: EvaluationContext;
  allFlags: FeatureFlagDoc[];
  segments: SegmentDoc[];
  now?: string;
  /** Guard against circular prerequisites — tracks visited flag keys. */
  _visited?: Set<string>;
}

export function evaluateFlag(opts: EvaluateFlagOptions): EvaluationResult {
  const { flag, ctx, allFlags, segments, _visited = new Set() } = opts;
  const now = opts.now ?? new Date().toISOString();

  // Prevent circular prerequisites
  if (_visited.has(flag.key)) {
    return offResult(flag, 'error');
  }
  _visited.add(flag.key);

  // 1. Check schedule
  const scheduleActive = isScheduleActive(flag, now);
  if (scheduleActive === false) {
    return offResult(flag, 'schedule_inactive');
  }

  // 2. Check enabled
  if (!flag.enabled) {
    return offResult(flag, 'off');
  }

  // 3. Check archived
  if (flag.archived) {
    return offResult(flag, 'off');
  }

  // 4. Check platform / region / OS version
  if (!matchesPlatform(flag, ctx) || !matchesRegion(flag, ctx) || !matchesOsVersion(flag, ctx)) {
    return offResult(flag, 'off');
  }

  // 5. Check prerequisites
  for (const prereq of flag.prerequisites) {
    const prereqFlag = allFlags.find(f => f.key === prereq.flagKey);
    if (!prereqFlag) {
      return offResult(flag, 'prerequisite_failed');
    }
    const prereqResult = evaluateFlag({
      flag: prereqFlag,
      ctx,
      allFlags,
      segments,
      now,
      _visited: new Set(_visited),
    });
    if (prereqResult.reason === 'error') {
      return offResult(flag, 'error');
    }
    if (prereqResult.variationKey !== prereq.variationKey) {
      return offResult(flag, 'prerequisite_failed');
    }
  }

  // 6. Individual user targeting
  if (ctx.userId && flag.individualTargets[ctx.userId]) {
    const targetVariation = flag.individualTargets[ctx.userId];
    return variationResult(flag, targetVariation, 'individual_target');
  }

  // 7. Targeting rules (evaluated in order, first match wins)
  for (const rule of flag.targetingRules) {
    const allClausesMatch = rule.clauses.every(clause => evaluateClause(clause, ctx));
    if (allClausesMatch) {
      // Apply rule's rollout percentage
      if (rule.rolloutPercentage >= 100) {
        return variationResult(flag, rule.variationKey, 'rule_match');
      }
      if (rule.rolloutPercentage <= 0) continue;
      if (
        ctx.userId &&
        hashUserFlag(ctx.userId, `${flag.key}:${rule.id}`) < rule.rolloutPercentage
      ) {
        return variationResult(flag, rule.variationKey, 'rule_match');
      }
    }
  }

  // 8. Segment matching
  if (flag.segments.length > 0 && ctx.userId) {
    for (const segKey of flag.segments) {
      const segment = segments.find(s => s.key === segKey);
      if (segment && userMatchesSegment(segment, ctx)) {
        return variationResult(flag, flag.defaultVariation, 'segment_match');
      }
    }
  }

  // 9. Percentage rollout
  const effectivePercentage = getEffectivePercentage(flag, now);
  if (effectivePercentage >= 100) {
    return variationResult(flag, flag.defaultVariation, 'percentage_rollout');
  }
  if (effectivePercentage <= 0) {
    return offResult(flag, 'off');
  }
  if (ctx.userId) {
    const bucket = hashUserFlag(ctx.userId, flag.key);
    if (bucket < effectivePercentage) {
      return variationResult(flag, flag.defaultVariation, 'percentage_rollout');
    }
    return offResult(flag, 'off');
  }

  // 10. No userId with partial rollout — can't hash, return off
  return offResult(flag, 'off');
}

/**
 * Evaluate all flags for a given context. Returns a map of flagKey → EvaluationResult.
 */
export function evaluateAllFlags(
  flags: FeatureFlagDoc[],
  ctx: EvaluationContext,
  segments: SegmentDoc[],
  now?: string
): Record<string, EvaluationResult> {
  const results: Record<string, EvaluationResult> = {};
  const activeFlags = flags.filter(f => !f.archived);

  for (const flag of activeFlags) {
    results[flag.key] = evaluateFlag({
      flag,
      ctx,
      allFlags: activeFlags,
      segments,
      now,
    });
  }

  return results;
}
