import './bru.ts';
import { randomIntBetween, randomString } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
import * as immer from 'https://unpkg.com/immer@10.1.3/dist/immer.mjs';
import { check } from 'k6';
import http from 'k6/http';

// Cardinality Variables Start
const countUniqueErrorCodes = 200;
const countUniqueClients = 200;
const appVersionsPerClient = 100;

// Cardinality Variables End
//
export const options = {
  scenarios: {
    example_scenario: {
      executor: 'constant-vus', // Keeps a constant number of VUs
      vus: 10, // 10 virtual users
      duration: '1m', // Run for 10 minutes
    },
  },
};

const otelEndpointUrl = __ENV.OTEL_ENDPOINT || 'http://localhost:4318/v1/traces';
console.log(
  `Endpoint: ${otelEndpointUrl}. (Overwrite using the OTEL_ENDPOINT environment variable)`,
);

const HIVE_ORGANIZATION_ACCESS_TOKEN = __ENV.HIVE_ORGANIZATION_ACCESS_TOKEN;
if (!HIVE_ORGANIZATION_ACCESS_TOKEN) {
  throw new Error('Environment variable HIVE_ORGANIZATION_ACCESS_TOKEN is missing.');
}

const HIVE_TARGET_REF = __ENV.HIVE_TARGET_REF; //'debug/hive/dev';
if (!HIVE_TARGET_REF) {
  throw new Error('Environment variable HIVE_TARGET_REF is missing.');
}

// A helper to generate a random 16-byte trace/span ID in hex
function randomId(bytes: number = 32): string {
  let traceId = '';
  for (let i = 0; i < bytes; i++) {
    // generate random nibble (0–15)
    const nibble = Math.floor(Math.random() * 16);
    traceId += nibble.toString(16);
  }

  // ensure not all zero (very unlikely)
  if (/^0+$/.test(traceId)) {
    return randomId(bytes);
  }

  return traceId;
}

function toTimeUnixNano(date = new Date()) {
  const milliseconds = date.getTime(); // ms since epoch
  const nanoseconds = BigInt(milliseconds) * 1_000_000n; // ns = ms × 1_000_000
  return nanoseconds;
}

function getRandomIndex(length: number) {
  return Math.floor(Math.random() * length);
}

function randomArrayItem<T>(arr: Array<T>) {
  return arr[getRandomIndex(arr.length)];
}

const clientNames = new Array(countUniqueClients)
  .fill(null)
  .map(() => randomString(randomIntBetween(5, 30)));

const appVersions = new Map<string, Array<string>>();

for (const name of clientNames) {
  const versions = new Array<string>();
  for (let i = 0; i <= appVersionsPerClient; i++) {
    versions.push(randomString(20));
  }
  appVersions.set(name, versions);
}

function generateRandomClient() {
  const name = randomArrayItem(clientNames);
  const version = randomArrayItem(appVersions.get(name)!);

  return {
    name,
    version,
  };
}

const errorCodes = new Array(countUniqueErrorCodes)
  .fill(null)
  .map(() => randomString(randomIntBetween(3, 30)));

function getRandomErrorCodes() {
  if (randomIntBetween(0, 10) > 3) {
    return [];
  }

  if (randomIntBetween(0, 10) > 3) {
    return new Array(randomIntBetween(1, 10)).fill(null).map(() => randomArrayItem(errorCodes));
  }

  return [randomArrayItem(errorCodes)];
}

// graphql query document size
// operation name length
//

const references: Array<Reference> = [
  open('./../../scripts/seed-traces/sample-introspection.json'),
  open('./../../scripts/seed-traces/sample-my-profile.json'),
  open('./../../scripts/seed-traces/sample-products-overview.json'),
  open('./../../scripts/seed-traces/sample-user-review.json'),
  open('./../../scripts/seed-traces/sample-user-review-error-missing-variables.json'),
  open('./../../scripts/seed-traces/sample-user-review-not-found.json'),
].map(res => JSON.parse(res));

function mutate(currentTime: Date, reference: Reference) {
  const newTraceId = randomId();
  const newSpanIds = new Map<string, string>();

  function getNewSpanId(spanId: string) {
    let newSpanId = newSpanIds.get(spanId);
    if (!newSpanId) {
      newSpanId = randomId(16);
      newSpanIds.set(spanId, newSpanId);
    }

    return newSpanId;
  }

  let rootTrace:
    | Reference[number]['resourceSpans'][number]['scopeSpans'][number]['spans'][number]
    | null = null;

  for (const payload of reference) {
    for (const resourceSpan of payload.resourceSpans) {
      for (const scopeSpan of resourceSpan.scopeSpans) {
        for (const span of scopeSpan.spans) {
          if (span.parentSpanId === undefined) {
            rootTrace = span;
            const client = generateRandomClient();

            rootTrace.attributes.push(
              {
                key: 'hive.client.name',
                value: { stringValue: client.name },
              },
              {
                key: 'hive.client.version',
                value: { stringValue: client.version },
              },
              // TODO: actually calculate this based on the operation.
              {
                key: 'hive.graphql.operation.hash',
                value: { stringValue: randomString(20) },
              },
            );

            const errors = getRandomErrorCodes();

            if (errors.length) {
              rootTrace.attributes.push(
                {
                  key: 'hive.graphql.error.codes',
                  value: {
                    arrayValue: {
                      values: errors.map(code => ({ stringValue: code })),
                    },
                  },
                },
                {
                  key: 'hive.graphql.error.count',
                  value: { intValue: errors.length },
                },
              );
            }
            break;
          }
        }
      }
    }
  }

  if (!rootTrace) {
    throw new Error('Parent Span must always be the first span in the file.');
  }

  const startTime = BigInt(rootTrace.startTimeUnixNano);
  const currentTimeB = toTimeUnixNano(currentTime);

  for (const payload of reference) {
    for (const resourceSpans of payload.resourceSpans) {
      for (const scopeSpan of resourceSpans.scopeSpans) {
        for (const span of scopeSpan.spans) {
          if (span.parentSpanId) {
            span.parentSpanId = getNewSpanId(span.parentSpanId);
          }

          span.spanId = getNewSpanId(span.spanId);
          span.traceId = newTraceId;

          const spanStartTime = BigInt(span.startTimeUnixNano);
          const spanEndTime = BigInt(span.endTimeUnixNano);
          const spanDuration = spanEndTime - spanStartTime;
          const spanOffset = spanStartTime - startTime;
          const newStartTime = currentTimeB + spanOffset;
          span.startTimeUnixNano = newStartTime.toString();
          span.endTimeUnixNano = (newStartTime + spanDuration).toString();

          if (span.events.length) {
            for (const event of span.events) {
              const spanStartTime = BigInt(event.timeUnixNano);
              const spanOffset = spanStartTime - startTime;
              const newStartTime = currentTimeB + spanOffset;
              event.timeUnixNano = newStartTime.toString();
            }
          }
        }
      }
    }
  }
}

function createTrace(date: Date, reference: Reference) {
  return immer.produce(reference, draft => mutate(date, draft));
}

const data = new Array(50).fill(null).flatMap(() => {
  const reference = randomArrayItem(references);
  const tracePayloads = createTrace(new Date(), reference);
  return tracePayloads.flatMap(payload => payload.resourceSpans);
});

export default function () {
  const response = http.post(otelEndpointUrl, JSON.stringify({ resourceSpans: data }), {
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${HIVE_ORGANIZATION_ACCESS_TOKEN}`,
      'x-hive-target-ref': HIVE_TARGET_REF,
    },
  });

  check(response, {
    'is status 200': r => r.status === 200,
  });
}
