import * as VAPI from "../mod.ts";
import * as VScript from "http://172.16.0.12/bladerunner_sdk/vscript@2.5.2/deno/release/mod.ts";
type HardwareModel = "AT1101" | "AT1130";
type Direction = "read" | "write";
type ManagementNamespaceSuffix<Model extends HardwareModel> =
  Model extends "AT1130" ? "rear" | "front" : "eth1" | "eth3";
type MediaNamespaceSuffix<Model extends HardwareModel> = Model extends "AT1130"
  ? "p0" | "p1"
  :
      | "eth0.0"
      | "eth0.1"
      | "eth0.2"
      | "eth0.3"
      | "eth2.0"
      | "eth2.1"
      | "eth2.2"
      | "eth2.3";
// we permit both namespace designators and port names (i.e., their suffixes)
type ManagementNamespace<Model extends HardwareModel> =
  `ns_${ManagementNamespaceSuffix<Model>}`;
type MediaNamespace<Model extends HardwareModel> =
  `ns_${MediaNamespaceSuffix<Model>}`;
export interface IPAddressConfig {
  address: string;
  prefix: number;
}
export interface Route {
  dst?: IPAddressConfig;
  via: string;
  weight?: number;
}
export interface SyslogServer {
  address_with_port: string;
  keep_alive: boolean | null;
  // in seconds
  rebind_interval: number | null;
  protocol: VAPI.NetworkInterfaces.Protocol | null;
}
type MaybePartial<dir extends Direction, T> = dir extends "write"
  ? Partial<T>
  : T;
export type AT1130PhyConfig<dir extends Direction> = MaybePartial<
  dir,
  {
    port_mode: VAPI.AT1130.NetworkInterfaces.PortMode;
    pma_settings: VAPI.AT1130.NetworkInterfaces.PMASettings;
  }
>;
export interface FullVLANConfig {
  ip_addresses: IPAddressConfig[];
  routes: Route[];
  dhcp: boolean;
  ntpd: boolean;
  reverse_path_filter: null | VAPI.NetworkInterfaces.ReversePathFilter;
}
export type MaybeReset<dir extends Direction, T> = dir extends "write"
  ? "factory-reset" | T
  : T;
export type VLANConfig<dir extends Direction> = dir extends "read"
  ? FullVLANConfig
  : Partial<FullVLANConfig>;
export type NamespaceConfig<dir extends Direction> = MaybePartial<
  dir,
  {
    vlans: MaybeReset<dir, Record<VAPI.Primitives.VlanID, VLANConfig<dir>>>;
    syslog: MaybeReset<dir, Array<SyslogServer>>;
  }
>;
// TODO: add bridge support? (Can be done backwards-compatibly later on)
export type MediaNamespaceConfig<
  Model extends HardwareModel,
  dir extends Direction,
> = NamespaceConfig<dir> &
  (Model extends "AT1130"
    ? MaybePartial<dir, { phy: MaybeReset<dir, AT1130PhyConfig<dir>> }>
    : {});
export type LiteralNetworkConfig<
  Model extends HardwareModel,
  dir extends Direction,
> = MaybeReset<
  dir,
  {
    [key in
      | ManagementNamespace<Model>
      | ManagementNamespaceSuffix<Model>
      | MediaNamespace<Model>
      | MediaNamespaceSuffix<Model>]?: MaybeReset<
      dir,
      key extends MediaNamespace<Model> | MediaNamespaceSuffix<Model>
        ? MediaNamespaceConfig<Model, dir>
        : NamespaceConfig<dir>
    >;
  }
>;
export class NetworkConfig<Model extends HardwareModel, dir extends Direction> {
  constructor(private literal: LiteralNetworkConfig<Model, dir>) {}
  as_literal() {
    return this.literal;
  }
  // NOTE: automatically filters out any factory-resets
  filter_map(
    f: (
      x: MediaNamespaceConfig<Model, dir> | NamespaceConfig<dir>,
    ) => typeof x | undefined,
  ): NetworkConfig<Model, dir> {
    const result = {} as any; // :(
    for (const k in this.literal) {
      if (this.literal[k] === "factory-reset") continue;
      VScript.enforce(typeof this.literal[k] === "object");
      const filtered = f(this.literal[k] as any);
      if (filtered !== undefined) result[k] = filtered as any;
    }
    return new NetworkConfig<Model, dir>(result as NetworkConfig<Model, dir>);
  }
}
export async function do_upload_config(
  vsocket: VScript.VSocket,
  config:
    | NetworkConfig<"AT1101", "write">
    | NetworkConfig<"AT1130", "write">
    | LiteralNetworkConfig<"AT1101", "write">
    | LiteralNetworkConfig<"AT1130", "write">,
): Promise<{ restart_required: boolean }> {
  const cfg_obj =
    config instanceof NetworkConfig ? config.as_literal() : config;
  const protocol = vsocket.protocol === "ws" ? "http" : "https";
  const maybe_login = vsocket.login;
  const rsp = await fetch(`${protocol}://${vsocket.ip}/network_config`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      ...(maybe_login
        ? {
            Authorization: `Basic ${btoa(
              `${maybe_login.user}:${maybe_login.password}`,
            )}`,
          }
        : {}),
    },
    body: JSON.stringify(cfg_obj),
  });
  const code = `Network reconfiguration request sent to ${protocol}://${vsocket.ip} failed with status code`;
  switch (rsp.status) {
    case 200:
    case 201:
      break;
    case 401:
      throw new Error(
        `${code} 401: apparently the login you are currently using does not have net-admin rights; you may have to specify a different login or contact your system administrator`,
      );
    case 503:
      throw new Error(
        `${code} 503: a concurrent configuration request may currently be underway. Please try again later`,
      );
    default:
      throw new Error(
        `${code} ${
          rsp.status
        }. The submitted config file was:\n${JSON.stringify(config, null, 2)}`,
      );
  }
  return { restart_required: rsp.status === 201 };
}
async function read_vlan_config(
  x: VAPI.NetworkInterfaces.CurrentVirtualInterfaceConfiguration,
): Promise<VLANConfig<"read">> {
  return {
    dhcp: await x.dhcp.read(),
    ntpd: await x.ntpd.read(),
    reverse_path_filter: await x.reverse_path_filter.read(),
    routes: await VScript.asyncMap(await x.routes.rows(), async (route) => {
      const result: Route = {
        via: VScript.enforce_nonnull(await route.via.read()),
      };
      const maybe_dst = await route.dst.read();
      const maybe_dst_prefix = await route.dst_prefix.read();
      const maybe_weight = await route.weight.read();
      if (
        !!maybe_dst &&
        maybe_dst_prefix !== undefined &&
        maybe_dst_prefix !== null
      ) {
        result.dst = { address: maybe_dst, prefix: maybe_dst_prefix };
      }
      if (maybe_weight !== null && maybe_weight !== undefined)
        result.weight = maybe_weight;
      return result;
    }),
    ip_addresses: await VScript.asyncMap(
      await x.ip_addresses.rows(),
      async (entry) => {
        const result: IPAddressConfig = {
          address: VScript.enforce_nonnull(await entry.ip_address.read()),
          prefix: VScript.enforce_nonnull(await entry.prefix.read()),
        };
        return result;
      },
    ),
  };
}
async function read_vlans(
  port: VAPI.Any.NetworkInterfaces.Port,
): Promise<Record<VAPI.Primitives.VlanID, VLANConfig<"read">>> {
  const result: Record<VAPI.Primitives.VlanID, VLANConfig<"read">> = {};
  result[0] = await read_vlan_config(port.current_configuration.base);
  await VScript.asyncIter(
    await port.current_configuration.vlans.rows(),
    async (vlan) => {
      const vlan_id = await vlan.vlan_id.read();
      if (!vlan_id) return; // non-null but numerically zero is considered an invalid vlan, too
      result[vlan_id] = await read_vlan_config(vlan.settings);
    },
  );
  return result;
}
async function read_syslog(
  port: VAPI.Any.NetworkInterfaces.Port,
): Promise<SyslogServer[]> {
  const result: SyslogServer[] = [];
  await VScript.asyncIter(
    [...port.current_syslog_configuration.syslog_servers],
    async (entry) => {
      const address_with_port = await entry.address.read();
      if (!address_with_port || address_with_port.trim().length === 0) return;
      result.push({
        address_with_port,
        protocol: await entry.protocol.read(),
        keep_alive: await entry.keep_alive.read(),
        rebind_interval: await entry.rebind_interval.read(),
      });
    },
  );
  return result;
}
async function download_at1130_config(
  network_interfaces: VAPI.AT1130.NetworkInterfaces.All,
): Promise<LiteralNetworkConfig<"AT1130", "read">> {
  const result: LiteralNetworkConfig<"AT1130", "read"> = {};
  await VScript.asyncIter(
    await network_interfaces.ports.rows(),
    async (port) => {
      const ns_name = VScript.enforce_nonnull(
        await port.network_namespace.read(),
      );
      const p: NamespaceConfig<"read"> = {
        vlans: await read_vlans(port),
        syslog: await read_syslog(port),
      };
      if (await port.supports_rtp.read()) {
        // media port
        const m: MediaNamespaceConfig<"AT1130", "read"> = {
          phy: {
            pma_settings: VScript.enforce_nonnull(
              await port.pma_settings.read(),
            ),
            port_mode: VScript.enforce_nonnull(
              await port.port_mode.status.read(),
            ),
          },
          ...p,
        };
        result[ns_name as MediaNamespace<"AT1130">] = m;
      } else {
        result[ns_name as ManagementNamespace<"AT1130">] = p;
      }
    },
  );
  return result;
}
async function download_at1101_config(
  network_interfaces: VAPI.AT1101.NetworkInterfaces.All,
): Promise<LiteralNetworkConfig<"AT1101", "read">> {
  const result: LiteralNetworkConfig<"AT1101", "read"> = {};
  await VScript.asyncIter(
    await network_interfaces.ports.rows(),
    async (port) => {
      const ns_name = VScript.enforce_nonnull(
        await port.network_namespace.read(),
      );
      const p: NamespaceConfig<"read"> = {
        vlans: await read_vlans(port),
        syslog: await read_syslog(port),
      };
      result[
        ns_name as ManagementNamespace<"AT1101"> | MediaNamespace<"AT1101">
      ] = p;
    },
  );
  return result;
}
// NOTE: not sure if there should be http GET support for network configs?
// For now I'm just going to use our websocket interface
export async function assemble_config_record(
  network_interfaces: VAPI.Any.NetworkInterfaces.All,
): Promise<NetworkConfig<"AT1101", "read"> | NetworkConfig<"AT1130", "read">> {
  if (network_interfaces instanceof VAPI.AT1101.NetworkInterfaces.All)
    return new NetworkConfig(await download_at1101_config(network_interfaces));
  else {
    VScript.enforce(
      network_interfaces instanceof VAPI.AT1130.NetworkInterfaces.All,
    );
    return new NetworkConfig(await download_at1130_config(network_interfaces));
  }
}
