/**
 * Tools to help generate wireguard configs for common situations.
 *
 * @since 1.0.0
 */
import * as Array from "effect/Array";
import * as Effect from "effect/Effect";
import * as Function from "effect/Function";
import * as Match from "effect/Match";
import * as Option from "effect/Option";
import * as Predicate from "effect/Predicate";
import * as Record from "effect/Record";
import * as Schema from "effect/Schema";
import * as Tuple from "effect/Tuple";
import * as assert from "node:assert";
import * as InternetSchemas from "./InternetSchemas.js";
import * as WireguardConfig from "./WireguardConfig.js";
import * as WireguardErrors from "./WireguardErrors.js";
import * as WireguardKey from "./WireguardKey.js";
/** @internal */
export const ipForNode = /*#__PURE__*/Function.pipe(/*#__PURE__*/Match.type(), /*#__PURE__*/Match.when(/*#__PURE__*/Schema.is(InternetSchemas.IPv4), ({
  ip
}) => ip), /*#__PURE__*/Match.when(/*#__PURE__*/Schema.is(InternetSchemas.IPv6), ({
  ip
}) => ip), /*#__PURE__*/Match.whenOr(/*#__PURE__*/Schema.is(InternetSchemas.IPv4SetupData), /*#__PURE__*/Schema.is(InternetSchemas.IPv6SetupData), /*#__PURE__*/Schema.is(InternetSchemas.HostnameIPv4SetupData), /*#__PURE__*/Schema.is(InternetSchemas.HostnameIPv6SetupData), ([_, {
  ip
}]) => ip), Match.exhaustive);
/** @internal */
export const nodesByIp = nodes => Function.pipe(nodes, Array.map(node => Tuple.make(ipForNode(node), node)), Record.fromEntries);
/** @internal */
export const makeEmptyContext = nodes => Record.map(nodesByIp(nodes), Function.constUndefined);
/**
 * Generates private+public keys for all nodes in the network.
 *
 * @since 1.0.0
 * @category Key Transformers
 */
export const generateKeys = nodesLayer => ({
  nodes: nodesLayer.nodes,
  wireguardNetworkCidr: nodesLayer.wireguardNetworkCidr,
  keys: Function.pipe(nodesLayer.nodes, makeEmptyContext, Record.map(WireguardKey.generateKeyPair))
});
/**
 * Generates preshare keys for all nodes in the network.
 *
 * @since 1.0.0
 * @category Key Transformers
 */
export const addPreshareKeys = keysLayer => ({
  nodes: keysLayer.nodes,
  wireguardNetworkCidr: keysLayer.wireguardNetworkCidr,
  keys: Record.map(keysLayer.keys, (keys, node) => node === ipForNode(keysLayer.nodes[0]) ? keys : {
    ...keys,
    preshareKey: WireguardKey.generatePreshareKey()
  })
});
/**
 * Generates connections in a star pattern for all nodes in the network.
 *
 * @since 1.0.0
 * @category Connection Transformers
 */
export const generateStarConnections = keysLayer => {
  const allNodeIps = Array.map(keysLayer.nodes, ipForNode);
  const connections = Function.pipe(keysLayer.nodes, makeEmptyContext, Record.map((_, currentNode) => {
    const neighbors = Array.filter(allNodeIps, ip => ip !== currentNode);
    assert.ok(Array.isNonEmptyReadonlyArray(neighbors), "There must be at least one neighbor");
    return neighbors;
  }));
  return {
    connections,
    nodes: keysLayer.nodes,
    keys: keysLayer.keys,
    wireguardNetworkCidr: keysLayer.wireguardNetworkCidr
  };
};
/**
 * Generates connections in a hub and spoke pattern for all nodes in the
 * network.
 *
 * @since 1.0.0
 * @category Connection Transformers
 */
export const generateHubAndSpokeConnections = keysLayer => ({
  nodes: keysLayer.nodes,
  keys: keysLayer.keys,
  wireguardNetworkCidr: keysLayer.wireguardNetworkCidr,
  connections: Function.pipe(keysLayer.nodes, makeEmptyContext, Record.map((_, node) => {
    if (node === ipForNode(keysLayer.nodes[0])) {
      return InternetSchemas.tail(keysLayer.nodes);
    } else {
      return Array.of(keysLayer.nodes[0]);
    }
  }), Record.map(Array.map(ipForNode)))
});
/**
 * Adds a direct connection between two nodes in the network.
 *
 * @since 1.0.0
 * @category Connection Transformers
 */
export const addConnection = /*#__PURE__*/Function.dual(3, (connectionsLayer, from, to) => {
  const oldConnections = Record.get(connectionsLayer.connections, from);
  const newConnections = Function.pipe(oldConnections, Option.map(connections => Array.append(connections, to)), Option.getOrElse(() => Array.of(to)));
  return {
    nodes: connectionsLayer.nodes,
    keys: connectionsLayer.keys,
    wireguardNetworkCidr: connectionsLayer.wireguardNetworkCidr,
    connections: Record.set(connectionsLayer.connections, from, newConnections)
  };
});
/**
 * Generates the allowed IPs for each node in the network based on the
 * connections is has to the other nodes.
 *
 * @since 1.0.0
 * @category AllowedIPs Transformers
 */
export const computeAllowedIPsFromConnections = connectionsLayer => ({
  nodes: connectionsLayer.nodes,
  keys: connectionsLayer.keys,
  connections: connectionsLayer.connections,
  wireguardNetworkCidr: connectionsLayer.wireguardNetworkCidr,
  allowedIPs: Record.map(connectionsLayer.connections, Array.map(ip => ({
    from: ip,
    block: `${ip}/32`
  })))
});
/**
 * Adds an allowed IP to a node in the network.
 *
 * @since 1.0.0
 * @category AllowedIPs Transformers
 */
export const addAllowedIPs = /*#__PURE__*/Function.dual(4, (allowedIPsLayer, nodeToIp, nodeFromIp, cidrs) => {
  const oldCidrs = Record.get(allowedIPsLayer.allowedIPs, nodeToIp);
  const cidrsEncoded = Array.map(cidrs, cidr => Function.pipe(Match.value(cidr), Match.when(Schema.is(InternetSchemas.IPv4CidrBlock), ({
    address,
    mask
  }) => `${address.ip}/${mask}`), Match.when(Schema.is(InternetSchemas.IPv6CidrBlock), ({
    address,
    mask
  }) => `${address.ip}/${mask}`), Match.when(Predicate.isString, Function.identity), Match.exhaustive));
  const mappedCidrs = Array.map(cidrsEncoded, cidr => ({
    from: nodeFromIp,
    block: cidr
  }));
  const newCidrs = Function.pipe(oldCidrs, Option.map(oldCidrs => Array.appendAll(oldCidrs, mappedCidrs)), Option.getOrElse(() => mappedCidrs));
  return {
    nodes: allowedIPsLayer.nodes,
    keys: allowedIPsLayer.keys,
    connections: allowedIPsLayer.connections,
    wireguardNetworkCidr: allowedIPsLayer.wireguardNetworkCidr,
    allowedIPs: Record.set(allowedIPsLayer.allowedIPs, nodeToIp, newCidrs)
  };
});
/**
 * Converts a network into configs.
 *
 * @since 1.0.0
 * @category Generator
 */
export const toConfigs = ({
  allowedIPs,
  connections,
  keys,
  nodes,
  wireguardNetworkCidr
}) => {
  const endpointsByIp = nodesByIp(nodes);
  const peerConfigsByIp = Function.pipe(nodes, nodesByIp, Record.map(node => Effect.gen(function* () {
    const keysForNode = Record.get(keys, ipForNode(node)).pipe(Option.getOrThrow);
    const endpointForNode = Record.get(endpointsByIp, ipForNode(node)).pipe(Option.getOrThrow);
    const endpointForNodeEncoded = yield* Schema.is(InternetSchemas.SetupData)(endpointForNode) ? Schema.encode(InternetSchemas.SetupData)(endpointForNode).pipe(Effect.map(x => x[0])) : Effect.succeed(undefined);
    const address = Schema.is(InternetSchemas.SetupData)(endpointForNode) ? endpointForNode[1].ip : endpointForNode.ip;
    return {
      Address: address,
      Endpoint: endpointForNodeEncoded,
      PublicKey: keysForNode.publicKey,
      PresharedKey: keysForNode.preshareKey
    };
  })));
  const configs = Array.map(nodes, node => Effect.gen(function* () {
    const keysForNode = Record.get(keys, ipForNode(node)).pipe(Option.getOrThrow);
    const endpointForNode = Record.get(endpointsByIp, ipForNode(node)).pipe(Option.getOrThrow);
    const allowedIPsForNode = Record.get(allowedIPs, ipForNode(node)).pipe(Option.getOrThrow);
    const address = Schema.is(InternetSchemas.SetupData)(endpointForNode) ? endpointForNode[1].ip : endpointForNode.ip;
    const listenPort = Schema.is(InternetSchemas.SetupData)(endpointForNode) ? endpointForNode[0].listenPort : 0;
    const directConnectionsForNode = yield* Function.pipe(connections, Record.get(ipForNode(node)), Option.getOrThrow, Array.filterMap(peerAddress => Record.get(peerConfigsByIp, peerAddress)), Effect.allWith());
    const dcsWithAllowedIPs = Array.map(directConnectionsForNode, dc => ({
      Endpoint: dc.Endpoint,
      PublicKey: dc.PublicKey,
      PresharedKey: node === nodes[0] ? dc.PresharedKey : keysForNode.preshareKey,
      AllowedIPs: Function.pipe(allowedIPsForNode, Array.filter(({
        from
      }) => from === dc.Address), Array.map(({
        block
      }) => block), x => new Set(x))
    }));
    return yield* Schema.decode(WireguardConfig.WireguardConfig)({
      ListenPort: listenPort,
      Peers: dcsWithAllowedIPs,
      PrivateKey: keysForNode.privateKey,
      Address: `${address}/${wireguardNetworkCidr.mask}`
    });
  }));
  if (Predicate.isTupleOfAtLeast(configs, 2)) {
    return Effect.all(configs);
  } else {
    return Effect.fail(new WireguardErrors.WireguardError({
      message: "There must be at least two nodes"
    }));
  }
};
/**
 * Use your phone or computer to remotely access just the wireguard server.
 *
 * @since 1.0.0
 * @category Generators
 */
export const generateRemoteAccessToServer = options => Function.flow(generateKeys, addPreshareKeys, generateStarConnections, computeAllowedIPsFromConnections)(options);
/**
 * Builds on "Remote access to server", allowing you to access your entire LAN
 * as well.
 *
 * @since 1.0.0
 * @category Generators
 */
export const generateRemoteAccessToLan = options => Function.flow(generateRemoteAccessToServer, addAllowedIPs(ipForNode(options.nodes[1]), ipForNode(options.nodes[0]), Array.isArray(options.lanNetworkCidr) ? options.lanNetworkCidr : Array.of(options.lanNetworkCidr)))(options);
/**
 * Allows two servers to connect to each other.
 *
 * @since 1.0.0
 * @category Generators
 */
export const generateServerToServerAccess = options => Function.flow(generateKeys, addPreshareKeys, generateHubAndSpokeConnections, computeAllowedIPsFromConnections)(options);
/**
 * Builds on "Server to server access", allowing two entire networks to
 * communicate.
 *
 * @since 1.0.0
 * @category Generators
 */
export const generateLanToLanAccess = options => Function.flow(generateServerToServerAccess, addAllowedIPs(ipForNode(options.nodes[1]), ipForNode(options.nodes[0]), Array.isArray(options.server1Lan) ? options.server1Lan : Array.of(options.server1Lan)), addAllowedIPs(ipForNode(options.nodes[0]), ipForNode(options.nodes[1]), Array.isArray(options.server2Lan) ? options.server2Lan : Array.of(options.server2Lan)))(options);
/**
 * Builds on "Remote access to server", except that all of the VPN clients can
 * connect to each other as well. Note: all traffic between nodes must pass
 * through the server.
 *
 * @since 1.0.0
 * @category Generators
 */
export const generateServerHubAndSpokeAccess = options => {
  const clientNodes = Function.pipe(options.nodes, Array.map(ipForNode), Array.tailNonEmpty);
  const firstFoldOnClientNodes = Array.reduce(clientNodes, Function.identity, (outerAcc, node) => {
    const secondFoldOverOtherNodes = Array.reduce(Array.filter(clientNodes, n => n !== node), Function.identity, (innerAcc, otherNode) => Function.compose(addAllowedIPs(node, ipForNode(options.nodes[0]), [`${otherNode}/32`]), innerAcc));
    return Function.compose(secondFoldOverOtherNodes, outerAcc);
  });
  const networkWithoutSpokeAccess = Function.flow(generateKeys, addPreshareKeys, generateHubAndSpokeConnections, computeAllowedIPsFromConnections)(options);
  return firstFoldOnClientNodes(networkWithoutSpokeAccess);
};
/**
 * Builds on "Server hub and spoke access", allowing you to access your entire
 * LAN as well.
 *
 * @since 1.0.0
 * @category Generators
 */
export const generateLanHubAndSpokeAccess = options => {
  const addAllowedIPsToAllNodes = Function.pipe(options.nodes, Array.map(ipForNode), Array.tailNonEmpty, Array.reduce(Function.identity, (acc, node) => Function.compose(addAllowedIPs(node, ipForNode(options.nodes[0]), Array.isArray(options.lanNetworkCidr) ? options.lanNetworkCidr : Array.of(options.lanNetworkCidr)), acc)));
  return addAllowedIPsToAllNodes(generateServerHubAndSpokeAccess(options));
};
/**
 * Route specific traffic through a commercial WireGuard VPN provider.
 *
 * @since 1.0.0
 * @category Generators
 */
export const generateVpnTunneledAccess = options => Function.flow(generateRemoteAccessToServer, addAllowedIPs(ipForNode(options.nodes[1]), ipForNode(options.nodes[0]), ["0.0.0.0/0"]))(options);
/**
 * Securely access the Internet from untrusted networks by routing all of your
 * traffic through the VPN and out the server's internet connection.
 *
 * @since 1.0.0
 * @category Generators
 */
export const generateRemoteTunneledAccess = options => Function.flow(generateRemoteAccessToLan, addAllowedIPs(ipForNode(options.nodes[1]), ipForNode(options.nodes[0]), ["0.0.0.0/0"]))(options);
//# sourceMappingURL=WireguardGenerate.js.map