import { Arbiter } from '../src/core/Arbiter.js';

function parseArgs(argv) {
  const args = new Map();
  for (let i = 2; i < argv.length; i++) {
    const value = argv[i];
    if (!value.startsWith('--')) continue;
    const [key, inline] = value.slice(2).split('=');
    if (inline !== undefined) {
      args.set(key, inline);
      continue;
    }
    const next = argv[i + 1];
    if (next && !next.startsWith('--')) {
      args.set(key, next);
      i++;
    } else {
      args.set(key, true);
    }
  }
  return args;
}

function parseSizes(value) {
  if (!value) return null;
  return value.split(',').map(item => Number(item.trim())).filter(Number.isFinite);
}

function createRng(seed) {
  let state = seed >>> 0;
  return () => {
    state = (1664525 * state + 1013904223) >>> 0;
    return state / 0x100000000;
  };
}

function randInt(rng, max) {
  return Math.floor(rng() * max);
}

function buildUsers(arbiter, count, prefix) {
  for (let i = 0; i < count; i++) {
    arbiter.addNode(`${prefix}:${i}`, prefix);
  }
}

function buildFeatureFlagScenario(arbiter, size) {
  const flagCount = Math.max(1, Math.floor(size / 10));
  buildUsers(arbiter, size, 'user');
  buildUsers(arbiter, size, 'resource');
  buildUsers(arbiter, flagCount, 'flag');
  for (let i = 0; i < size; i++) {
    const flagId = i % flagCount;
    arbiter.addRelation(`user:${i}`, 'has_flag', `flag:${flagId}`);
    if (i % 2 === 0) {
      arbiter.addRelation(`resource:${i}`, 'feature_flag', `flag:${flagId}`);
    }
  }
  arbiter.setRelationConfig('has_flag', { type: 'direct' });
  arbiter.setRelationConfig('feature_flag', { type: 'direct' });
  arbiter.setRelationConfig('can_feature', {
    type: 'tuple_to_userset',
    tuplesetRelation: 'feature_flag',
    tuplesetDirection: 'out',
    computedRelation: 'has_flag'
  });
}

function buildQueries(size, rng, samples) {
  const queries = [];
  const half = Math.floor(samples / 2);
  for (let i = 0; i < half; i++) {
    const userId = randInt(rng, Math.floor(size / 2)) * 2;
    queries.push({
      userKey: `user:${userId}`,
      objectKey: `resource:${userId}`,
      flagId: userId % Math.max(1, Math.floor(size / 10)),
      expected: true
    });
  }
  for (let i = 0; i < samples - half; i++) {
    const userId = randInt(rng, Math.floor(size / 2)) * 2 + 1;
    queries.push({
      userKey: `user:${userId}`,
      objectKey: `resource:${userId}`,
      flagId: userId % Math.max(1, Math.floor(size / 10)),
      expected: false
    });
  }
  return queries;
}

function measure(label, iterations, fn) {
  const start = process.hrtime.bigint();
  for (let i = 0; i < iterations; i++) {
    fn();
  }
  const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6;
  return { label, elapsedMs, qps: elapsedMs > 0 ? Math.round((iterations / elapsedMs) * 1000) : 0 };
}

function measureMedian(label, iterations, fn, warmups, runs) {
  for (let w = 0; w < warmups; w++) {
    for (let i = 0; i < iterations; i++) fn();
  }

  const samples = [];
  for (let r = 0; r < runs; r++) {
    const result = measure(label, iterations, fn);
    samples.push(result.qps);
  }

  samples.sort((a, b) => a - b);
  const mid = Math.floor(samples.length / 2);
  const median = samples.length % 2 === 0
    ? Math.round(((samples[mid - 1] + samples[mid]) / 2) * 100) / 100
    : samples[mid];
  return { label, qps: median, elapsedMs: null };
}

const args = parseArgs(process.argv);
const config = {
  sizes: parseSizes(args.get('sizes')) || [5000, 10000, 20000, 40000],
  samples: Number(args.get('samples') || 2000),
  seed: Number(args.get('seed') || 42),
  maxSeconds: Number(args.get('max-seconds') || 60),
  includeMeta: !args.has('no-meta'),
  collectValues: !args.has('no-values'),
  relationGraphTraversal: args.has('relation-graph'),
  relationGraphMinDegree: Number(args.get('relation-graph-min-degree') || 200),
  relationCsr: args.has('relation-csr'),
  relationCsrMinDegree: Number(args.get('relation-csr-min-degree') || 200),
  relationCsrDeltaThreshold: Number(args.get('relation-csr-delta-threshold') || 1000),
  warmupRuns: Number(args.get('warmup-runs') || 2),
  medianRuns: Number(args.get('median-runs') || 5),
  verify: args.has('verify')
};

const rng = createRng(config.seed);
const startTime = Date.now();

console.log('feature_flag_micro_bench');
console.log('size,samples,meta,values,relation_graph,relation_csr,metric,elapsed_ms,qps');

for (const size of config.sizes) {
  if ((Date.now() - startTime) / 1000 > config.maxSeconds) break;
  const arbiter = new Arbiter({
    useRelationGraphTraversal: config.relationGraphTraversal,
    relationGraphMinDegree: config.relationGraphMinDegree,
    useRelationCsrIndex: config.relationCsr,
    relationCsrMinDegree: config.relationCsrMinDegree,
    relationCsrDeltaThreshold: config.relationCsrDeltaThreshold
  });
  buildFeatureFlagScenario(arbiter, size);
  const queries = buildQueries(size, rng, config.samples);

  const visited = new Set([{ userKey: 'seed', relation: 'can_feature', objectKey: 'seed' }]);
  const baseOptions = { fastPath: true, includeMeta: config.includeMeta, collectValues: config.collectValues };

  if (config.verify) {
    for (const q of queries) {
      const result = arbiter.check(q.userKey, 'can_feature', q.objectKey, { fastPath: false });
      const predicted = result && result.possibility > 0;
      if (predicted !== q.expected) {
        throw new Error(`Verification failed size ${size}: ${q.userKey} -> ${q.objectKey} expected=${q.expected}`);
      }
    }
  }

  const directFast = measureMedian('direct_fast', queries.length, () => {
    const q = queries[randInt(rng, queries.length)];
    arbiter.authChecker.check(q.userKey, 'has_flag', `flag:${q.flagId}`, baseOptions);
  }, config.warmupRuns, config.medianRuns);

  const directNested = measureMedian('direct_nested', queries.length, () => {
    const q = queries[randInt(rng, queries.length)];
    arbiter.authChecker.check(q.userKey, 'has_flag', `flag:${q.flagId}`, {
      ...baseOptions,
      _visited: visited,
      _currentRelation: 'can_feature'
    });
  }, config.warmupRuns, config.medianRuns);

  const tuplesetFetch = measureMedian('tupleset_fetch', queries.length, () => {
    const q = queries[randInt(rng, queries.length)];
    const objectId = arbiter.nodeIdByKey.get(q.objectKey);
    arbiter.relationManager.getRelationsFromSrc(objectId, 'feature_flag');
  }, config.warmupRuns, config.medianRuns);

  const tuplesetInner = measureMedian('tupleset_inner', queries.length, () => {
    const q = queries[randInt(rng, queries.length)];
    const objectId = arbiter.nodeIdByKey.get(q.objectKey);
    const tuples = arbiter.relationManager.getRelationsFromSrc(objectId, 'feature_flag');
    if (!tuples.length) return;
    const flagKey = arbiter.keyByNodeId.get(tuples[0].dst);
    if (!flagKey) return;
    arbiter.authChecker.check(q.userKey, 'has_flag', flagKey, {
      ...baseOptions,
      _visited: visited,
      _currentRelation: 'can_feature'
    });
  }, config.warmupRuns, config.medianRuns);

  const tupleToUserset = measureMedian('tuple_to_userset', queries.length, () => {
    const q = queries[randInt(rng, queries.length)];
    arbiter.check(q.userKey, 'can_feature', q.objectKey, baseOptions);
  }, config.warmupRuns, config.medianRuns);

  const mode = config.relationGraphTraversal ? 'on' : 'off';
  const csrMode = config.relationCsr ? 'on' : 'off';
  for (const metric of [directFast, directNested, tuplesetFetch, tuplesetInner, tupleToUserset]) {
    console.log([
      size,
      queries.length,
      config.includeMeta ? 'on' : 'off',
      config.collectValues ? 'on' : 'off',
      mode,
      csrMode,
      metric.label,
      metric.elapsedMs === null ? 'median' : Math.round(metric.elapsedMs),
      metric.qps
    ].join(','));
  }
}
