import {
  Duration,
  Level,
  VAPIHelpers,
  Watcher,
  asyncFilterMap,
  asyncIter,
  asyncMap,
  asyncZip,
  enforce,
  enforce_nonnull,
  path_index,
  poll_until,
  same,
} from "vscript";
import {
  AT1101,
  AT1130,
  NetworkInterfaces,
  RTPReceiver,
  VM,
} from "../artefacts/vapi/index.js";
import { RTPInterfaces } from "./rtp_receiver.js";
import {
  DstAddressFn,
  force_activate_hosting_session,
  setup_video_transmitters,
} from "./rtp_transmitter.js";
import { split_sdp } from "./sdp.js";
import { delete_row, fc_diff, hosting_vm } from "./utils.js";

export async function findVirtualIfc(
  vm: VM.Any,
  criterion: (
    vifc: NetworkInterfaces.VirtualInterface,
    port: AT1101.NetworkInterfaces.Port | AT1130.NetworkInterfaces.Port,
  ) => Promise<boolean>,
): Promise<undefined | NetworkInterfaces.VirtualInterface> {
  for (const port of await vm.network_interfaces.ports.rows()) {
    for (const vifc of await port.virtual_interfaces.rows()) {
      if (await criterion(vifc, port)) return vifc;
    }
  }
  return undefined;
}

export async function getVirtualIfc(
  vm: VM.Any,
  criterion: (
    vifc: NetworkInterfaces.VirtualInterface,
    port: AT1101.NetworkInterfaces.Port | AT1130.NetworkInterfaces.Port,
  ) => Promise<boolean>,
): Promise<NetworkInterfaces.VirtualInterface> {
  const result = await findVirtualIfc(vm, criterion);
  if (!result) {
    throw new Error(
      `Unable to find virtual interface @ ${vm.raw.identify()} that satisfies the given criterion ${criterion}`,
    );
  }
  return result;
}

export async function getHostingPort(vifc: NetworkInterfaces.VirtualInterface) {
  const vm = VM.adopt(vifc.raw.backing_store);
  return enforce_nonnull(
    await (async () => {
      for (const port of await vm.network_interfaces.ports.rows()) {
        for (const otherIfc of await port.virtual_interfaces.rows()) {
          if (same(otherIfc, vifc)) {
            return port;
          }
        }
      }
      return null;
    })(),
  );
}

export interface LinkInfo {
  desc: string;
  ifcName: string;
  mgmtAddress: string;
}

export async function getLinkInfo(
  portOrIfc:
    | AT1101.NetworkInterfaces.Port
    | AT1130.NetworkInterfaces.Port
    | NetworkInterfaces.VirtualInterface,
): Promise<LinkInfo> {
  const port =
    portOrIfc instanceof NetworkInterfaces.VirtualInterface
      ? await getHostingPort(portOrIfc)
      : portOrIfc;
  const [mgmtAddress, ifcName, systemDescription] = enforce_nonnull(
    await (async () => {
      return (
        (await poll_until(
          async () => {
            for (const neighbor of await port.lldp_neighbors.rows()) {
              const ifcName1 = await neighbor.interface_name.read();
              const mgmtAddress1 = await neighbor.mgmt_addr_v4.read();
              if (mgmtAddress1 && ifcName1) {
                return {
                  satisfied: true,
                  result: [
                    mgmtAddress1,
                    ifcName1,
                    await neighbor.system_description.read(),
                  ],
                };
              }
            }
            return { satisfied: false };
          },
          {
            timeout: new Duration(1, "min"),
            pollInterval: new Duration(500, "ms"),
          },
        )) ?? null
      );
    })(),
    `LLDP neighbor information for port or interface '${await portOrIfc.brief.read()}'`,
  );

  if (!enforce_nonnull(systemDescription?.toLowerCase()).includes("arista")) {
    throw new Error(
      `Port ${await port.brief.read()} @ ${port.raw.backing_store.identify()} does not seem to be connected to an Arista switch`,
    );
  }

  return {
    desc: enforce_nonnull(systemDescription),
    ifcName: enforce_nonnull(ifcName).replace(/Ethernet(.*)/, "Et $1"),
    mgmtAddress: enforce_nonnull(mgmtAddress),
  };
}

export function splitIPv4Address(
  maybe_addr: string | null,
): null | { address: string; port?: number } {
  if (maybe_addr === null) {
    return null;
  }
  const parts = maybe_addr.split(":");
  enforce(
    parts.length <= 2 && parts.length >= 1,
    `Unable to interpret ${maybe_addr} as an IPv4 address`,
  );
  const address = enforce_nonnull(parts[0]);
  const bytes = address.split(".").map((s) => parseInt(s, 10));
  enforce(
    bytes.length === 4 && bytes.every((b) => !isNaN(b) && b >= 0 && b <= 255),
    `Unable to interpret ${maybe_addr} as an IPv4 address`,
  );
  if (parts.length === 1) {
    return { address };
  } else {
    const port = parseInt(enforce_nonnull(parts[1]), 10);
    enforce(
      !isNaN(port),
      `${maybe_addr} does not appear to specify a valid port`,
    );
    return { address, port };
  }
}

export interface InterfaceSelectorArgs {
  ipaddr: string;
  brief: string;
  vlan_id: null | number;
}

export type InterfaceSelector = (
  x: InterfaceSelectorArgs,
) => Promise<null | "primary" | "secondary">;

export function default_interface_selector(pars: {
  vm: VM.Any;
  permit_vlan_tags: boolean;
  swap_interfaces: boolean;
}): InterfaceSelector {
  const [primary_re, secondary_re] =
    pars.vm instanceof AT1101.Root ? [/^P1/, /^P2/] : [/^P0/, /^P1/];
  return async (x: {
    ipaddr: string;
    brief: string;
    vlan_id: null | number;
  }): Promise<null | "primary" | "secondary"> => {
    if (x.vlan_id !== null && !pars.permit_vlan_tags) return null;
    if (x.brief.match(primary_re)) return "primary";
    if (x.brief.match(secondary_re)) return "secondary";
    return null;
  };
}

// TODO: rename to collect_rtp_interfaces once the swap_interfaces overload has been deprecated
async function do_collect_rtp_interfaces(
  vm: VM.Any,
  pars?: {
    interface_selector?: InterfaceSelector;
  },
) {
  const results: RTPInterfaces = {
    primary: [],
    secondary: [],
  };
  const interface_selector: InterfaceSelector =
    pars?.interface_selector ??
    default_interface_selector({
      vm,
      swap_interfaces: false,
      permit_vlan_tags: true,
    });
  for (const port of await vm.network_interfaces.ports.rows()) {
    const brief = await port.brief.read();
    for (const virtIfc of await port.virtual_interfaces.rows()) {
      const vlan_id = await virtIfc.vlan_id.read();
      for (const row of await virtIfc.ip_addresses.rows()) {
        const ipaddr = await row.ip_address.read();
        if (!ipaddr) continue;
        const verdict = await interface_selector({ brief, vlan_id, ipaddr });
        if (!verdict) continue;
        switch (verdict) {
          case "primary":
            results.primary.push(virtIfc);
            break;
          case "secondary":
            results.secondary.push(virtIfc);
            break;
        }
      }
    }
  }
  return results;
}

export function collect_rtp_interfaces(
  vm: VM.Any,
  pars?: { interface_selector?: InterfaceSelector },
): Promise<RTPInterfaces>;

export async function collect_rtp_interfaces(
  vm: VM.Any,
  pars?:
    | {
        interface_selector?: InterfaceSelector;
      }
    | { swap_interfaces?: boolean },
) {
  if (!!pars && "swap_interfaces" in pars)
    return await do_collect_rtp_interfaces(vm, {
      interface_selector: default_interface_selector({
        vm,
        permit_vlan_tags: false, // we used to require untagged ifcs on legacy branch
        swap_interfaces: pars?.swap_interfaces ?? false,
      }),
    });
  return await do_collect_rtp_interfaces(
    vm,
    pars as
      | {
          interface_selector?: InterfaceSelector;
        }
      | undefined,
  );
}

// TODO: currently collects num_samples measurements, then returns a preprocessed set of
// relative packet delays; there should also be a streaming mode that returns raw samples
// as they come in

// NOTE: currently expects sender and receiver to be in sync to measure transmission delays.
// Moreover, those delays' accuracy is limited by the 90kHz mediaclock's coarse granularity;
// might want to use 2022-6 instead?
export async function measure_packet_delay_variation(pars: {
  preexisting_receivers: "bulldoze" | "preserve"; // FIXME: use
  preexisting_transmitters: "bulldoze" | "preserve";
  transmitting_ifc: NetworkInterfaces.VirtualInterface;
  receiving_ifc: NetworkInterfaces.VirtualInterface;
  num_anc_streams: number;
  // FIXME: use preexisting transmitters/receivers if unused, or if some suitable
  // parameter is set to 'bulldoze'
  num_samples: number;
  // TODO: make anc frame rate configurable?
  error_feedback: (msg: string, level: Level) => void;
  progress?: (ratio_done: number) => void;
  address_schema: DstAddressFn;
}): Promise<{
  relative_packet_delays: Duration[];
  transmission_delays: Array<[value: Duration, error: Duration]>;
}> {
  const vm_tx = hosting_vm(pars.transmitting_ifc);
  if (!vm_tx.r_t_p_transmitter)
    throw new Error(
      `The machine (${vm_tx.raw.ip}) hosting the specified transmitting interface ${pars.transmitting_ifc.raw.kwl} has no RTPTransmitter component; please reboot into some AVP variant and try again`,
    );
  const vm_rx = hosting_vm(pars.receiving_ifc);
  if (!vm_rx.r_t_p_receiver)
    throw new Error(
      `The machine (${vm_rx.raw.ip}) hosting the specified receiving interface ${pars.receiving_ifc.raw.kwl} has no RTPReceiver component; please reboot into some AVP variant and try again`,
    );
  const ptp_clock_states = [
    await vm_tx.p_t_p_clock.state.read(),
    await vm_rx.p_t_p_clock.state.read(),
  ];
  if (
    ptp_clock_states.some(
      (state) => state === "FreeRun" || state === "Uncalibrated",
    )
  ) {
    pars.error_feedback(
      "One or several of your machines are currently free-running or uncalibrated; the absolute latency figures returned by this measurement will likely be of little value (PDV histograms should be fine though)",
      "Warning",
    );
  }

  const anc_receivers = await (async () => {
    const rtp = enforce_nonnull(vm_rx.r_t_p_receiver);
    const result = [await rtp.anc_burst_receivers.create_row()];
    while (result.length < pars.num_anc_streams)
      result.push(await rtp.anc_burst_receivers.create_row());
    return result;
  })();

  const anc_transmitters = await setup_video_transmitters({
    count: { kind: "at-most", n: pars.num_anc_streams },
    preexisting_transmitters: pars.preexisting_transmitters,
    log: (msg) => pars.error_feedback(msg, "Info"),
    port_indices: (_) => [0], // FIXME
    transport_format: { variant: "ST2110_20", value: { add_st2110_40: true } },
    standard: "HD1080p50",
    vms: [vm_tx],
    address_schema: pars.address_schema,
  });

  const stream_indices = new Set<number>();
  const rx_session = await vm_rx.r_t_p_receiver.sessions.create_row();
  await rx_session.interfaces.command.write({
    primary: pars.receiving_ifc,
    secondary: null,
  });
  const sdps: string[] = [];
  await asyncZip(anc_receivers, anc_transmitters, async (rx, tx) => {
    const tx_session = enforce_nonnull(
      await tx.generic.hosting_session.status.read(),
    );
    await tx_session.interfaces.command.write({
      primary: pars.transmitting_ifc,
      secondary: null,
    });
    await rx.generic.hosting_session.command.write(rx_session);
    sdps.push(await force_activate_hosting_session(tx));
  });
  const full_sdp = (() => {
    let result = split_sdp(sdps[0]).header.trim();
    for (const sdp of sdps) {
      for (const md of split_sdp(sdp).media_descriptions) {
        if (md.rtpmap.type !== "smpte291/90000") continue;
        result += "\n" + md.text.trim();
      }
    }
    return result;
  })();
  await rx_session.set_sdp("A", full_sdp);
  await rx_session.active.command.write(true);
  await rx_session.used_tracks.wait_until((t) => t.egress === "A");
  await asyncIter(anc_receivers, async (rx) => {
    for (const row of await rx.generic.packet_streams.sdp_a.read()) {
      if (
        row.subflow_index ===
        0 /* change to 4 if we're going back from ancburst to video receivers */
      ) {
        const stream = enforce_nonnull(row.stream);
        stream_indices.add(enforce_nonnull(path_index(stream.raw.kwl)));
      }
    }
  });
  await vm_rx.r_t_p_receiver.diagnostics.mpacket_tracer.active.command.write(
    true,
  );
  const transmission_delays: Array<[value: Duration, error: Duration]> = [];
  const latency_watchers = await asyncMap(
    [...stream_indices],
    async (stream_idx) => {
      return await enforce_nonnull(vm_rx.r_t_p_receiver)
        .packet_streams.row(stream_idx)
        .media_clock.offset.watch((o) => {
          if (!!o) transmission_delays.push([o.value.times(-1), o.error]);
        });
    },
  );
  const do_measure_deltas = new Promise((resolve: (x: number[]) => void) => {
    enforce(!!vm_rx.r_t_p_receiver);
    const prev_values = new Map<number, RTPReceiver.MPacketDebugInfo>();
    const deltas: number[] = [];
    let w: Watcher | undefined;
    vm_rx.r_t_p_receiver.diagnostics.mpacket_tracer.prev_mpacket
      .watch((entry) => {
        if (!entry || !stream_indices.has(entry.packet_stream_index)) return;
        const maybe_prev = prev_values.get(entry.packet_stream_index);
        if (maybe_prev) {
          deltas.push(fc_diff(maybe_prev.frc, entry.frc));
        }
        prev_values.set(entry.packet_stream_index, entry);
        if (deltas.length % 100 === 0)
          pars.progress?.(deltas.length / pars.num_samples);
        if (deltas.length >= pars.num_samples) {
          enforce_nonnull(w).unwatch();
          resolve(deltas);
        }
      })
      .then((w2) => (w = w2));
  });
  const deltas = await do_measure_deltas;
  latency_watchers.forEach((w) => w.unwatch());
  await rx_session.active.command.write(false);
  await rx_session.delete();
  await asyncIter(anc_receivers, async (rcv) => {
    await rcv.delete();
  });
  await asyncIter(anc_transmitters, async (tx) => {
    const session = enforce_nonnull(
      await tx.generic.hosting_session.status.read(),
    );
    await session.active.command.write(false);
    await delete_row(session);
    await tx.delete();
  });
  const multiplier = vm_rx instanceof AT1101.Root ? 6.4 : 3.2;
  const min = deltas.reduce((prev, delta) => Math.min(prev, delta), Infinity);
  return {
    relative_packet_delays: deltas.map(
      (x) => new Duration((x - min) * multiplier, "ns"),
    ),
    transmission_delays,
  };
}

export interface InterfaceSpecifier {
  port_index: 0 | 1;
  vlan_id?: null | number;
}

export type InterfacesSpecifier =
  // Option A: port indices, prioritizes "adress-ful" interfaces, and vlan-less ones over the rest.
  // One or two indices can be specified, mapping to { primary: indices[0], secondary: null }
  // and { primary: indices[0], secondary: indices[1] }, respectively
  | Readonly<(0 | 1)[]>
  // second option: explicit specification of port indices and vlan IDs per primary/secondary path
  | Readonly<[InterfaceSpecifier | null, InterfaceSpecifier | null]>;

export async function resolve_interfaces(
  vm: VM.Any,
  spec: InterfacesSpecifier,
): Promise<{
  primary: NetworkInterfaces.VirtualInterface | null;
  secondary: NetworkInterfaces.VirtualInterface | null;
}> {
  const lift = (
    x: number | null | InterfaceSpecifier,
  ): null | InterfaceSpecifier => {
    if (x === null) return null;
    if (typeof x === "number") {
      enforce(x === 0 || x === 1);
      return { port_index: x };
    }
    return x;
  };
  const specs = [lift(spec?.[0] ?? null), lift(spec?.[1] ?? null)];
  const ifcs = await asyncMap(specs, async (spec) => {
    if (!spec) return null;
    const port = vm.network_interfaces.ports.row(spec.port_index);
    const candidates = await asyncFilterMap(
      await port.virtual_interfaces.rows(),
      async (ifc) => {
        for (const row of await ifc.ip_addresses.rows()) {
          const maybe_addr = await row.ip_address.read();
          if (!maybe_addr || maybe_addr.startsWith("fe80")) continue;
          const vlan_id = await ifc.vlan_id.read();
          if (spec.vlan_id !== undefined && spec.vlan_id !== vlan_id) continue;
          return [ifc, vlan_id] as const;
        }
        return undefined;
      },
    );
    if (candidates.length === 0)
      throw new Error(
        `Unable to find a valid virtual interface on ${vm.raw.ip}, port ${
          spec.port_index
        }${spec.vlan_id === undefined ? "" : ` with VLAN ID ${spec.vlan_id}`}`,
      );
    if (spec.vlan_id === undefined) {
      // prefer, but don't require vlan-less interfaces
      candidates.sort(
        (a, b) => (a[1] === null ? 0 : 1) - (b[1] === null ? 0 : 1),
      );
    }
    return candidates[0][0];
  });
  return { primary: ifcs[0], secondary: ifcs[1] };
}

// TODO: remove legacy variant after some deprecation period
export type BackwardsCompatibleInterfaceSpec =
  | {
      port_indices: (thing_index: number) => Readonly<(0 | 1)[]>;
    }
  | {
      interfaces: (thing_index: number) => Readonly<InterfacesSpecifier>;
    };

// TODO: remove legacy variant after some deprecation period
export async function resolve_interfaces_backwards_compatibly(
  x: BackwardsCompatibleInterfaceSpec,
  vm: VM.Any,
  thing_index: number,
) {
  if ("port_indices" in x) {
    // KLUDGE: there's no general error channel for nonfatal messages
    console.warn(
      "port_indices are marked deprecated; please use `interfaces` instead",
    );
    return await resolve_interfaces(vm, x.port_indices(thing_index));
  } else {
    return await resolve_interfaces(vm, x.interfaces(thing_index));
  }
}

export function extract_port_indices(
  x: BackwardsCompatibleInterfaceSpec,
  thing_index: number,
): Readonly<number[]> {
  return "port_indices" in x
    ? x.port_indices(thing_index)
    : x
        .interfaces(thing_index)
        .reduce((acc: number[], x: number | InterfaceSpecifier | null) => {
          if (x === null) return acc;
          if (typeof x === "number") return [...acc, x];
          return [...acc, x.port_index];
        }, []);
}

export interface IPRoute {
  dst?: string;
  dst_prefix?: number;
  via: string;
  weight?: number;
}

export interface IPConfig {
  dhcp?: boolean;
  ntpd?: boolean;
  reverse_path_filter?: NetworkInterfaces.ReversePathFilter | null;
  ip_addresses?: Array<[address: string, prefix: number]>;
  routes?: Array<IPRoute>;
}

// @deprecated please use network_interfaces.upload_config
export async function reconfigure_ip_addresses(
  port: AT1130.NetworkInterfaces.Port | AT1101.NetworkInterfaces.Port,
  config: {
    base?: IPConfig;
    vlans?: Record<number, IPConfig>;
  },
): Promise<{ reboot_required: boolean }> {
  const read_ip_config = async (
    kw_ip_config: NetworkInterfaces.CurrentVirtualInterfaceConfiguration,
  ): Promise<IPConfig> => {
    return {
      dhcp: await kw_ip_config.dhcp.read(),
      ntpd: await kw_ip_config.ntpd.read(),
      reverse_path_filter: await kw_ip_config.reverse_path_filter.read(),
      ip_addresses: await asyncMap(
        await kw_ip_config.ip_addresses.rows(),
        async (row) => [
          enforce_nonnull(await row.ip_address.read()),
          enforce_nonnull(await row.prefix.read()),
        ],
      ),
      routes: await asyncMap(await kw_ip_config.routes.rows(), async (row) => {
        const [dst, dst_prefix, via, weight] = await Promise.all([
          row.dst.read(),
          row.dst_prefix.read(),
          row.via.read(),
          row.weight.read(),
        ]);
        const result: IPRoute = { via: enforce_nonnull(via) };
        if (dst && dst.length > 0) result.dst = dst;
        if (dst_prefix !== null) result.dst_prefix = dst_prefix;
        if (weight !== null) result.weight = weight;
        return result;
      }),
    };
  };
  const existing_vlan_configs: Record<number, IPConfig> = {};
  await asyncIter(
    await port.current_configuration.vlans.rows(),
    async (row) => {
      const vlan_id = await row.vlan_id.read();
      if (vlan_id === null) return;
      existing_vlan_configs[vlan_id] = await read_ip_config(row.settings);
    },
  );

  const needs_update = (desired: IPConfig | undefined, cur: IPConfig) => {
    if (!desired) return true;
    if (desired.dhcp !== undefined && desired.dhcp !== cur.dhcp) return true;
    if (desired.ntpd !== undefined && desired.ntpd !== cur.ntpd) return true;
    if (
      desired.reverse_path_filter !== undefined &&
      desired.reverse_path_filter !== cur.reverse_path_filter
    )
      return true;
    if (desired.ip_addresses) {
      const to_s = (x: [addr: string, prefix: number]) => `${x[0]}/${x[1]}`;
      const desired_normalized = [...desired.ip_addresses].map(to_s);
      const cur_normalized = [...(cur.ip_addresses ?? [])].map(to_s);
      if (desired_normalized.length !== cur_normalized.length) return true;
      for (let i = 0; i < desired_normalized.length; ++i) {
        if (desired_normalized[i] !== cur_normalized[i]) return true;
      }
    }
    if (desired.routes) {
      const cur_routes = cur.routes ?? [];
      if (cur_routes.length !== desired.routes.length) return true;
      const cur_per_via = new Map<string, IPRoute>();
      for (const route of cur_routes) {
        cur_per_via.set(route.via, route);
      }
      for (const route of desired.routes) {
        const maybe_cur = cur_per_via.get(route.via);
        if (!maybe_cur) return true;
        if (route.dst && route.dst !== maybe_cur.dst) return true;
        if (route.dst_prefix && route.dst_prefix !== maybe_cur.dst_prefix)
          return true;
        if (route.weight && route.weight !== maybe_cur.weight) return true;
      }
    }
    return false;
  };

  const update_config = async (
    src: IPConfig,
    dst: NetworkInterfaces.DesiredVirtualInterfaceConfiguration,
  ) => {
    if (src.dhcp !== undefined) await dst.dhcp.write(src.dhcp);
    if (src.ntpd !== undefined) await dst.ntpd.write(src.ntpd);
    if (src.reverse_path_filter !== undefined)
      await dst.reverse_path_filter.write(src.reverse_path_filter);
    if (src.ip_addresses) {
      await VAPIHelpers.legacy_table_resize({
        table: dst.ip_addresses,
        add_row: dst.add_ip_address,
        delete_row: (i) => dst.ip_addresses.row(i).delete_ip_address,
        n: src.ip_addresses.length,
      });
      await asyncZip(
        await dst.ip_addresses.rows(),
        src.ip_addresses,
        async (row, [address, prefix]) => {
          await Promise.all([
            row.ip_address.write(address),
            row.prefix.write(prefix),
          ]);
        },
      );
    }
    if (src.routes) {
      await VAPIHelpers.legacy_table_resize({
        table: dst.routes,
        add_row: dst.add_route,
        delete_row: (i) => dst.routes.row(i).delete_route,
        n: src.routes.length,
      });
      await asyncZip(
        await dst.routes.rows(),
        src.routes,
        async (row, route) => {
          await Promise.all([
            row.dst.write(route.dst ?? null),
            row.dst_prefix.write(route.dst_prefix ?? null),
            row.via.write(route.via),
            row.weight.write(route.weight ?? null),
          ]);
        },
      );
    }
  };

  let save_required = false;
  if (
    config.base &&
    needs_update(
      config.base,
      await read_ip_config(port.current_configuration.base),
    )
  ) {
    save_required = true;
    await update_config(config.base, port.desired_configuration.base);
  }

  const vlans_need_update = await (async () => {
    const n_keys = config.vlans ? Object.keys(config.vlans).length : 0;
    if (
      (await port.desired_configuration.vlans.allocated_indices()).length !==
      n_keys
    )
      return true;
    for (const vlan_id in existing_vlan_configs) {
      if (
        !config.vlans ||
        needs_update(config.vlans[vlan_id], existing_vlan_configs[vlan_id])
      )
        return true;
    }
    return false;
  })();

  if (vlans_need_update) {
    const desired_configs = config.vlans ?? {};
    console.log("#desired_configs:", Object.keys(desired_configs).length);
    for (const k in desired_configs) {
      const existing = existing_vlan_configs[k];
      if (existing) {
        desired_configs[k] = {
          dhcp: desired_configs[k].dhcp ?? existing.dhcp,
          ip_addresses:
            desired_configs[k].ip_addresses ?? existing.ip_addresses,
          ntpd: desired_configs[k].ntpd ?? existing.ntpd,
          reverse_path_filter:
            desired_configs[k].reverse_path_filter ??
            existing.reverse_path_filter,
          routes: desired_configs[k].routes ?? existing.routes,
        };
      }
    }
    await VAPIHelpers.legacy_table_resize({
      table: port.desired_configuration.vlans,
      add_row: port.desired_configuration.add_vlan,
      delete_row: (i) => port.desired_configuration.vlans.row(i).delete_vlan,
      n: Object.keys(desired_configs).length,
    });
    await asyncZip(
      Object.keys(desired_configs),
      await port.desired_configuration.vlans.rows(),
      async (k, row) => {
        const vlan_id = parseInt(k);
        await row.vlan_id.write(vlan_id);
        await update_config(desired_configs[vlan_id], row.settings);
      },
    );
    save_required = true;
  }

  if (save_required) {
    let did_save = false;
    const device_name = await port.device_name.read();
    const watchers = await port.raw.backing_store.watch_error_channels(
      (msg) => {
        console.log(msg);
        if (
          msg.content_md.toLowerCase().startsWith("wrote config") &&
          msg.content_md.includes(`ip_${device_name}.sh`)
        )
          did_save = true;
      },
    );
    await port.save_config.write("Click", {
      retry_until: { criterion: "custom", validator: async () => did_save },
      retry_interval: new Duration(1, "s"),
    });
    watchers.forEach((w) => w.unwatch());
  }
  return { reboot_required: save_required };
}
