import { with_suppressed_tls_checks } from "./node/polyfills.js";
import * as VAPI from "../artefacts/vapi/index.js";
import {
  asyncIter,
  asyncFind,
  asyncMap,
  asyncZip,
  ContainerType,
  Duration,
  enforce,
  enforce_nonnull,
  IReadableKeyword,
  KWLName,
  Logger,
  MaybeMissing,
  MissingData,
  path_index,
  path_strip_trailing_index,
  pause,
  Referenceable,
  INamedTableRow,
  Reflection,
  StronglyTypedNamedTable,
  StronglyTypedSubtree,
  Timestamp,
  unreachable,
  VSocket,
  VSettings,
  WorkQueue,
} from "vscript";

export async function scrub(
  vm: VAPI.VM.Any,
  opts?: {
    log?: (msg: string) => void;
    reset_keywords?: boolean;
    kwl_whitelist?: RegExp[];
  },
) {
  const is_whitelisted = (kwl: KWLName<"full">) =>
    !!(opts?.kwl_whitelist ?? []).find((re) => !!re.test(kwl));
  const wq = new WorkQueue({ num_workers: 8 });
  wq.push(async () => {
    if (!vm.p_t_p_flows) return;
    await asyncIter(await vm.p_t_p_flows.agents.rows(), async (agent) => {
      if (!is_whitelisted(agent.raw.kwl)) await agent.hosting_port.command.write(null);
    });
  });
  wq.push(async () => {
    if (!vm.r_t_p_receiver) return;
    await asyncIter(await vm.r_t_p_receiver.sessions.rows(), async (session) => {
      if (!is_whitelisted(session.raw.kwl)) await session.active.command.write(false);
    });
  });
  wq.push(async () => {
    if (!vm.r_t_p_transmitter) return;
    await asyncIter(await vm.r_t_p_transmitter.sessions.rows(), async (session) => {
      if (!is_whitelisted(session.raw.kwl)) {
        await session.active.command.write(false);
        await session.reserve_resources.command.write(false);
      }
    });
  });
  wq.push(async () => {
    if (!vm.sample_rate_converter) return;
    await asyncIter(await vm.sample_rate_converter.instances.rows(), async (inst) => {
      if (!is_whitelisted(inst.raw.kwl)) await inst.active.command.write(false);
    });
  });
  wq.push(async () => {
    if (!vm.i_o_module) return;
    await asyncIter(await vm.i_o_module.merger.rows(), async (merger) => {
      if (!is_whitelisted(merger.raw.kwl)) await merger.substream_2_s_i.write(false);
    });
  });
  await wq.drain();
  await Reflection.subtree_iter({
    backing_store: vm.raw,
    include: "allocated",
    on_named_table: async (table) => {
      if (is_whitelisted(table.kwl)) return "recurse";
      if (opts?.log) {
        const n = (await table.allocated_indices()).length;
        if (n !== 0)
          opts.log(`Deleting ${n} row${n > 1 ? "s" : ""} of ${table.kwl}@${vm.raw.ip}...`);
      }
      await table.delete_all();
      return "do-not-recurse";
    },
  });
  if (opts?.reset_keywords ?? false) {
    const wq = new WorkQueue({ num_workers: 512 });
    await Reflection.subtree_iter({
      backing_store: vm.raw,
      include: "allocated",
      on_subtree: async (st) => {
        if ("network_interfaces" == st.kwl) return "do-not-recurse";
        if (is_whitelisted(st.kwl)) return "recurse";
        if (st.description.container_type === ContainerType.None) {
          opts?.log?.(`Resetting everything within ${st.kwl}...`);
          await Reflection.reset_subtree_to_defaults(vm.raw, st.kwl, st.description, {
            wq,
            drain_wq: false,
          });
          return "do-not-recurse";
        }
        return "recurse";
      },
    });
    await wq.drain();
  }
}

export function deduplicateCustom<T>(ts: T[], stringifier: (t: T) => string): T[] {
  const identifiers: Set<string> = new Set();
  const result: T[] = [];
  for (const t of ts) {
    const identifier = stringifier(t);
    if (identifiers.has(identifier)) {
      continue;
    }
    identifiers.add(identifier);
    result.push(t);
  }
  return result;
}

export function deduplicate<T extends StronglyTypedSubtree<VSocket>>(ts: T[]): T[] {
  return deduplicateCustom(ts, (t: T) => `${t.raw.backing_store.ip}:${t.raw.kwl}`);
}

export class ValueWithRange {
  constructor(readonly min:number, readonly max:number, readonly unit?:string) {}

  toString() {
    return this.unit ? `[${this.min}…${this.max} ${this.unit}]` :  `[${this.min}…${this.max}]`;
  }

  toError() {
    return new ValueWithError((this.max + this.min) * 0.5, this.max - this.min, this.unit);
  }

  extend(v:number) {
    return new ValueWithRange(Math.min(this.min, v), Math.max(this.max, v), this.unit);
  }
}

export class ValueWithError {
  constructor(readonly value:number, readonly error:number, readonly unit?:string) {}

  toString() {
    return this.unit ? `${this.value} ±${this.error} ${this.unit}]` :  `${this.value} ±${this.error}`;
  }

  toRange() {
    return new ValueWithRange(this.value - this.error, this.value + this.error, this.unit);
  }
}

export interface Stats<T extends number> {
  min: T;
  q5: T;
  median: T;
  q95: T;
  max: T;
  mean: T;
  stddev: T;
}

export function stats<T extends number>(xs: T[]): Stats<T> | null {
  if (xs.length < 5) return null;
  const sorted = [...xs].sort((a, b) => a - b);
  const min = enforce_nonnull(sorted[0]);
  const q5 = enforce_nonnull(sorted[Math.round(sorted.length / 20)]);
  // not precise but good enough for our purposes
  const median = enforce_nonnull(sorted[Math.round(sorted.length / 2)]);
  const q95 = enforce_nonnull(sorted[Math.max(0, Math.round(sorted.length * 0.95) - 1)]);
  const max = enforce_nonnull(sorted[sorted.length - 1]);
  let sum = 0;
  let sqsum = 0;
  for (const x of sorted) {
    sum += x;
    sqsum += x * x;
  }
  const mean = (sum / sorted.length) as T;
  const stddev = Math.sqrt(sqsum / sorted.length - mean * mean) as T;
  return { min, q5, median, q95, max, mean, stddev };
}

export function histogram(pars: {
  data: Duration[] | number[];
  bincount: number;
}): Array<[x: Duration, y: number]> | Array<[x: number, y: number]> {
  if (pars.data.length === 0)
    throw new Error(`Unable to generate histogram: supplied data array is empty`);
  if (pars.bincount <= 1 || Math.round(pars.bincount) !== pars.bincount || isNaN(pars.bincount))
    throw new Error(`Invalid bincount parameter: should be an integer > 1`);

  const worker = (data: number[]): Array<[x: number, y: number]> => {
    const result = new Array(pars.bincount).fill(0);
    const [min, max] = data.reduce(
      (prev, v) => [Math.min(prev[0], v), Math.max(prev[1], v)],
      [Infinity, -Infinity],
    );
    const w = (max - min) / (pars.bincount - 1);
    const inverse_w = w === 0 ? 0 : 1 / w;
    for (const x of data) {
      const i = Math.round(inverse_w * (x - min));
      enforce(i >= 0 && i < result.length);
      result[i] += 1;
      enforce(!isNaN(result[i]));
    }
    return result.map((y, i) => {
      enforce(!isNaN(y));
      return [Math.min(max, Math.max(min, min + w * i)), y];
    });
  };
  if (pars.data[0] instanceof Duration) {
    return worker(pars.data.map((d) => (d as Duration).s())).map(([x, y]) => [
      new Duration(x, "s"),
      y,
    ]);
  } else {
    return worker(pars.data as number[]);
  }
}

export function shorttime(d = new Date()) {
  return `${d.getHours()}:${d.getMinutes().toString().padStart(2, "0")}:${d
    .getSeconds()
    .toString()
    .padStart(2, "0")}:${d.getMilliseconds().toString().padStart(3, "0")}`;
}

export function range(start: number, stopExclusive: number, step?: number): number[] {
  const result: number[] = [];
  for (let x = start; x < stopExclusive; x += step ?? 1) {
    result.push(x);
  }
  return result;
}

export function* enumerate<T>(xs: Iterable<T>): Iterable<[i: number, x: T]> {
  let i = 0;
  for (const x of xs) {
    yield [i++, x];
  }
}

// FIXME: this is a (temporary?) kludge meant to work around AT1101.x | AT1130.x typing
// difficulties; should find a better solution
export function video_ref(
  essence: VAPI.AT1101.Video.Essence | VAPI.AT1130.Video.Essence | null,
  switch_time?: Timestamp,
): { source: any /* boo hoo */; switch_time: Timestamp | null } {
  const result: VAPI.AT1101.Video.TimedSource | VAPI.AT1130.Video.TimedSource = {
    source: essence,
    switch_time: switch_time ?? null,
  };
  return result;
}

export function audio_ref(
  essence: VAPI.AT1101.Audio.Essence | VAPI.AT1130.Audio.Essence | null,
  switch_time?: Timestamp,
): { source: any /* boo hoo */; switch_time: Timestamp | null } {
  const result: VAPI.AT1101.Audio.TimedSource | VAPI.AT1130.Audio.TimedSource = {
    source: essence,
    switch_time: switch_time ?? null,
  };
  return result;
}

export function time_ref(
  t_src: VAPI.AT1101.Time.Source | VAPI.AT1130.Time.Source,
): any /* boo hoo */ {
  return t_src as any;
}

type Accumulator<T, Acc> = (pars: { acc: undefined | Acc; value: T }) => Acc;

interface StreakParameters<T, Acc> {
  // minimum required streak length
  n: number;
  // should return true iff value is to be considered part of the current streak
  test: (x: { acc: Acc; prev: T; value: T }) => boolean;
  timeout: Duration;
  poll_interval?: Duration;
}

export function streak<T, Acc = T>(
  kw: IReadableKeyword<T>,
  pars: StreakParameters<T, Acc>,
): Promise<T>;

export function streak<T, Acc>(
  kw: IReadableKeyword<T>,
  pars: StreakParameters<T, Acc>,
  maybe_accumulator: Accumulator<T, Acc>,
): Promise<Acc>;

export async function streak<T, Acc = T>(
  kw: IReadableKeyword<T>,
  pars: StreakParameters<T, Acc>,
  maybe_accumulator?: Accumulator<T, Acc>,
) {
  const accumulator = (maybe_accumulator ??
    ((x: { acc: undefined | Acc; value: T }) => x.value)) as Accumulator<T, Acc>; // FIXME: get rid of typecast (how?)
  let streaklength = 0;
  let acc: MaybeMissing<Acc> = MissingData;
  let prev: MaybeMissing<T> = MissingData;
  let done = false;
  if (pars.poll_interval) {
    (async () => {
      const interval = enforce_nonnull(pars.poll_interval);
      while (!done) {
        await pause(interval);
        await kw.read();
      }
    })();
  }
  await kw.wait_until(
    (value: T) => {
      if (prev === MissingData) {
        streaklength = 1;
        acc = accumulator({ acc: undefined, value });
      } else {
        enforce(acc !== MissingData);
        if (pars.test({ acc, prev, value })) {
          streaklength++;
          acc = accumulator({ acc, value });
        } else {
          streaklength = 1;
          acc = accumulator({ acc: undefined, value });
        }
      }
      prev = value;
      return streaklength >= pars.n;
    },
    { timeout: pars.timeout },
  );
  done = true;
  enforce(acc !== MissingData);
  return acc;
}

export function fc_diff(fc_from: number, fc_to: number) {
  let diff = fc_to - fc_from;
  if (diff > Math.pow(2, 31)) {
    diff -= Math.pow(2, 32);
  } else if (diff < -Math.pow(2, 31)) {
    diff += Math.pow(2, 32);
  }
  return diff;
}

export function counts_to_duration(counts: number, vm: VAPI.VM.Any): Duration {
  return new Duration(3.2 * (vm instanceof VAPI.AT1101.Root ? 2 : 1), "ns").times(counts);
}

// conctinually calculates variance over a moving window
// use via .push()
export class Variance {
  private dataset: number[];
  private arraySize = 5;
  private resultValid = false;
  private index = 0;

  private variance: number;

  constructor(size_dataset?: number) {
    if (size_dataset && size_dataset > 0) this.arraySize = size_dataset;
    this.dataset = new Array<number>(this.arraySize);
    this.variance = 0;
    this.dataset.fill(0);
  }
  push(val: number) {
    this.dataset[this.index] = val;
    this.index += 1;
    if (this.index >= this.arraySize) this.resultValid = true;
    this.index = this.index % this.arraySize;
    this.calculate();
  }
  mean(): number {
    let mean = 0;
    for (const x of this.dataset) mean += x;
    mean /= this.arraySize;
    return mean;
  }
  value(): number {
    return this.variance;
  }
  size(): number {
    return this.arraySize;
  }
  valid(): boolean {
    return this.resultValid;
  }
  calculate(): number {
    let nominator = 0;
    const mean = this.mean();
    for (const x of this.dataset) {
      nominator += Math.pow(x - mean, 2);
    }
    this.variance = nominator / this.arraySize;
    return this.variance;
  }
}

export function all_pairs<T, S>(xs: T[], ys: S[]): Array<[T, S]> {
  const result: Array<[T, S]> = [];
  for (const x of xs) {
    for (const y of ys) {
      result.push([x, y]);
    }
  }
  return result;
}

export async function gather<T>(
  kw: IReadableKeyword<T>,
  pars: {
    min_count: number;
    // only respond to incoming data if this is left unspecified
    update_interval?: Duration;
    timeout: Duration;
    equal: (a: T, b: T) => boolean;
    filter?: (x: T) => boolean;
  },
): Promise<Array<{ value: T; count: number }>> /* in ascending order, by frequency */ {
  // FIXME: keywords should expose their description member;
  // until then we'll need to specify 'equal'
  const values: T[] = [];
  let done = false;
  if (pars.update_interval)
    (async () => {
      while (!done) {
        await kw.read();
        await pause(enforce_nonnull(pars.update_interval));
      }
    })();
  try {
    await kw.wait_until(
      (payload) => {
        if (pars.filter && !pars.filter(payload)) return false;
        values.push(payload);
        return values.length >= pars.min_count;
      },
      { timeout: pars.timeout },
    );
  } finally {
    done = true;
  }
  const with_counts: Array<{ value: T; count: number }> = [];
  for (const v of values) {
    const maybe_el = with_counts.find(({ value }) => pars.equal(value, v));
    if (maybe_el) maybe_el.count += 1;
    else with_counts.push({ value: v, count: 1 });
  }
  with_counts.sort((a, b) => b.count - a.count);
  return with_counts;
}

export function pretty_join(things: string[]): string {
  switch (things.length) {
    case 0:
      return "";
    case 1:
      return things[0];
    default:
      return `${things.slice(0, things.length - 1).join(", ")} and ${things[things.length - 1]}`;
  }
}

export async function has_reconfigurable_ioboard(vm: VAPI.VM.Any): Promise<boolean> {
  if (!vm.i_o_module) return false;
  const board = await vm.system.io_board.info.type.read();
  if (!board) return false;
  switch (board) {
    case "IO_BNC_10_10":
    case "IO_BNC_18_2":
    case "IO_BNC_11_11":
    case "IO_BNC_11_11_GD32":
    case "IO_BNC_16_16":
    case "IO_BNC_16_16_GD32":
    case "IO_BNC_2_18":
      return false;
    case "IO_BNC_2_2_16bidi":
    case "IO_BNC_16bidi":
    case "IO_BNC_16bidi_GD32":
    case "IO_MSC_v1":
    case "IO_MSC_v2":
    case "IO_MSC_v2_GD32":
      return true;
  }
  unreachable(`Unknown IO board variant '${board}'`);
}

export function hosting_vm(st: StronglyTypedSubtree<VSocket>): VAPI.VM.Any {
  return VAPI.VM.adopt(st.raw.backing_store);
}

export async function delete_row(st: StronglyTypedSubtree<VSocket>): Promise<void> {
  const vsocket = st.raw.backing_store;
  if (
    st.raw.description.container_type === ContainerType.None &&
    st.raw.description.parent.container_type === ContainerType.Table &&
    st.raw.description.parent.named_tables
  ) {
    await vsocket.table_delete_row({
      table_kwl: path_strip_trailing_index(st.raw.kwl),
      index: enforce_nonnull(path_index(st.raw.kwl)),
    });
  } else {
    throw new Error(
      `Unable to delete ${st.raw.kwl}@${vsocket.identify()}: this is no named table row`,
    );
  }
}

export type ResourceCount = { kind: "max" } | { kind: "at-most" | "exactly"; n: number };

export async function find_vm_and_count(
  vms: VAPI.VM.Any[],
  maxcount: (vm: VAPI.VM.Any) => Promise<number>,
  required_count: ResourceCount,
  labels: { resource: string; specs: string },
): Promise<[VAPI.VM.Any | null, number]> {
  const [maybe_vm, count] = await (async () => {
    const maxcounts = await asyncMap(vms, async (vm) => [vm, await maxcount(vm)] as const);
    if (required_count.kind === "max") {
      let best: [vm: VAPI.VM.Any, count: number] | undefined;
      for (const [vm, count] of maxcounts) {
        if (count > (best?.[1] ?? -1)) {
          best = [vm, count];
        }
      }
      return best ?? [null, 0];
    } else {
      let best: [vm: VAPI.VM.Any, count: number] | undefined;
      for (const [vm, count] of maxcounts) {
        if (count >= required_count.n) return [vm, required_count.n];
        if (!best || count > best[1]) best = [vm, count];
      }
      if (best && required_count.kind === "at-most") return best;
    }
    return [null, 0];
  })();
  if (maybe_vm === null && required_count.kind === "exactly" && required_count.n > 0)
    throw new Error(
      `Unable to set up ${required_count.n} ${labels.resource}${
        required_count.n > 1 ? "s" : ""
      } with the specified parameters (${labels.specs}) on ${vms
        .map((vm) => vm.raw.identify())
        .join(", ")}`,
    );
  return [maybe_vm, count];
}

type RowTypeOf<Table> =
  Table extends StronglyTypedNamedTable<VSocket | VSettings, string, infer R> ? R : never;

export async function find_by_rowname<
  Id extends string,
  RowType extends Referenceable<Id> & INamedTableRow & StronglyTypedSubtree<VSocket | VSettings>,
  Table extends StronglyTypedNamedTable<VSocket | VSettings, Id, RowType>,
>(r: Table, name: string): Promise<RowTypeOf<Table> | null> {
  const row = await asyncFind(await r.rows(), async (rw: any) => {
    return (await rw.row_name()) === name;
  });
  return (row as RowTypeOf<Table>) ?? null;
}
export async function ensure_nmos_settings(
  vm: VAPI.VM.Any,
  params?: {
    registry?: URL[];
    enable?: boolean;
    log?: Logger;
  },
) {
  params = {
    log: (msg, level) => {
      console.log(`[${level}]: ` + msg);
    },
    ...params,
  };
  params?.log?.(
    `Pushing NMOS Config to ${vm.raw.identify()}; Registry: ${params.registry ?? "N/A"}, Enable: ${
      params.enable
    }`,
    "Debug",
  );
  if (params?.enable) await vm.system.nmos.enable.write(params.enable);
  await asyncZip(params.registry ?? [], [...vm.system.nmos.is_04], async (url, entry) => {
    await entry.registry_address.write(url.host);
    await entry.protocol.write(url.protocol.startsWith("https") ? "https" : "http");
  });

  await vm.system.nmos.write_config.write("Click");
  const is_active = await with_suppressed_tls_checks(async () => {
    return await fetch(
      `${vm.raw.protocol.startsWith("wss") ? "https" : "http"}://${vm.raw.ip}/x-nmos`,
    );
  });
  if (is_active.status > 200 && params.enable) {
    params.log?.(`${vm.raw.identify()} needs to reboot to activate NMOS`, "Debug");
    await vm.raw.reboot();
  }
  const result = await with_suppressed_tls_checks(async () => {
    return await fetch(
      `${vm.raw.protocol.startsWith("wss") ? "https" : "http"}://${vm.raw.ip}/x-nmos`,
    );
  });
  return !params.enable || result.status == 200;
}
