/**
 * Tools to help generate wireguard configs for common situations.
 *
 * @since 1.0.0
 */

import type * as ParseResult from "effect/ParseResult";

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";

import * as internalInternalSchemas from "./internal/internetSchemas.js";

/** A node in the network can either have an ipv4 or ipv6 address. */
type WireguardIPv4RoamingPeer = InternetSchemas.IPv4;
type WireguardIPv6RoamingPeer = InternetSchemas.IPv6;
type WireguardRoamingPeer = WireguardIPv4RoamingPeer | WireguardIPv6RoamingPeer;

/**
 * A server in the network can either have an ipv4 address pool or ipv6 address
 * pool.
 */
type WireguardIPv4Server = InternetSchemas.IPv4SetupData | InternetSchemas.HostnameIPv4SetupData;
type WireguardIPv6Server = InternetSchemas.IPv6SetupData | InternetSchemas.HostnameIPv6SetupData;
type WireguardServer = WireguardIPv4Server | WireguardIPv6Server;

/**
 * Every node in the network must have a public+private key pair and possibly a
 * preshare key as well.
 */
type Keys = {
    publicKey: WireguardKey.WireguardKey;
    privateKey: WireguardKey.WireguardKey;
    preshareKey?: WireguardKey.WireguardKey;
};

/**
 * The nodes in the network must be homogeneous (either all ipv4 or all ipv6).
 * If you want dual stack, you must have two separate networks.
 */
type WireguardIPv4Node = WireguardIPv4Server | WireguardIPv4RoamingPeer;
type WireguardIPv6Node = WireguardIPv6Server | WireguardIPv6RoamingPeer;
type WireguardNode = WireguardIPv4Node | WireguardIPv6Node;

/**
 * Base layer containing just the nodes in the network.
 *
 * @since 1.0.0
 * @category WireguardGenerate
 */
export type NodesLayer<
    Nodes extends
        | readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
        | readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
> = {
    nodes: Nodes;
    wireguardNetworkCidr: Nodes[0] extends WireguardIPv4Node
        ? InternetSchemas.IPv4CidrBlock
        : Nodes[0] extends WireguardIPv6Node
          ? InternetSchemas.IPv6CidrBlock
          : never;
};

/**
 * Layer containing the keys for each node in the network.
 *
 * @since 1.0.0
 * @category WireguardGenerate
 */
export type keysLayer<
    Nodes extends
        | readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
        | readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
> = NodesLayer<Nodes> & {
    keys: Record.ReadonlyRecord<
        Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
        Keys
    >;
};

/**
 * Layer containing the connections for each node in the network.
 *
 * @since 1.0.0
 * @category WireguardGenerate
 */
export type ConnectionsLayer<
    Nodes extends
        | readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
        | readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
> = keysLayer<Nodes> & {
    connections: Record.ReadonlyRecord<
        Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
        Array.NonEmptyReadonlyArray<
            Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"]
        >
    >;
};

/**
 * Layer containing the allowed IPs for each node in the network.
 *
 * @since 1.0.0
 * @category WireguardGenerate
 */
export type AllowedIPsLayer<
    Nodes extends
        | readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
        | readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
> = ConnectionsLayer<Nodes> & {
    allowedIPs: Record.ReadonlyRecord<
        Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
        Array.NonEmptyReadonlyArray<{
            block: InternetSchemas.CidrBlockFromStringEncoded;
            from: Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"];
        }>
    >;
};

/**
 * The final network type.
 *
 * @since 1.0.0
 * @category WireguardGenerate
 */
export type WireguardNetwork<
    Nodes extends
        | readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
        | readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
> = AllowedIPsLayer<Nodes>;

/** @internal */
export const ipForNode = Function.pipe(
    Match.type<WireguardIPv4Node | WireguardIPv6Node>(),
    Match.when(Schema.is(InternetSchemas.IPv4), ({ ip }) => ip),
    Match.when(Schema.is(InternetSchemas.IPv6), ({ ip }) => ip),
    Match.whenOr(
        Schema.is(InternetSchemas.IPv4SetupData),
        Schema.is(InternetSchemas.IPv6SetupData),
        Schema.is(InternetSchemas.HostnameIPv4SetupData),
        Schema.is(InternetSchemas.HostnameIPv6SetupData),
        ([_, { ip }]) => ip
    ),
    Match.exhaustive
);

/** @internal */
export const nodesByIp = <
    Nodes extends
        | readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
        | readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>(
    nodes: Nodes
): Record.ReadonlyRecord<
    Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
    Nodes[number]
> =>
    Function.pipe(
        nodes,
        Array.map((node) => Tuple.make(ipForNode(node), node)),
        Record.fromEntries
    );

/** @internal */
export const makeEmptyContext = <
    Nodes extends
        | readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
        | readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>(
    nodes: Nodes
): Record.ReadonlyRecord<
    Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
    undefined
> => 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 = <
    Nodes extends
        | readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
        | readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>(
    nodesLayer: NodesLayer<Nodes>
): keysLayer<Nodes> => ({
    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 = <
    Nodes extends
        | readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
        | readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>(
    keysLayer: keysLayer<Nodes>
): keysLayer<Nodes> => ({
    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 = <
    Nodes extends
        | readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
        | readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>(
    keysLayer: keysLayer<Nodes>
): ConnectionsLayer<Nodes> => {
    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 = <
    Nodes extends
        | readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
        | readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>(
    keysLayer: keysLayer<Nodes>
): ConnectionsLayer<Nodes> => ({
    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 internalInternalSchemas.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 = Function.dual<
    <
        Nodes extends
            | readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
            | readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
    >(
        from: Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
        to: Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"]
    ) => (connectionsLayer: ConnectionsLayer<Nodes>) => ConnectionsLayer<Nodes>,
    <
        Nodes extends
            | readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
            | readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
    >(
        connectionsLayer: ConnectionsLayer<Nodes>,
        from: Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
        to: Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"]
    ) => ConnectionsLayer<Nodes>
>(
    3,
    <
        Nodes extends
            | readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
            | readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
    >(
        connectionsLayer: ConnectionsLayer<Nodes>,
        from: Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
        to: Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"]
    ): ConnectionsLayer<Nodes> => {
        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 = <
    Nodes extends
        | readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
        | readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>(
    connectionsLayer: ConnectionsLayer<Nodes>
): AllowedIPsLayer<Nodes> => ({
    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` as const }))
    ),
});

/**
 * Adds an allowed IP to a node in the network.
 *
 * @since 1.0.0
 * @category AllowedIPs Transformers
 */
export const addAllowedIPs = Function.dual<
    <
        Nodes extends
            | readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
            | readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
    >(
        nodeToIp: Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
        nodeFromIp:
            | Extract<Nodes[number], WireguardRoamingPeer>["ip"]
            | Extract<Nodes[number], WireguardServer>[1]["ip"],
        cidrs: Array.NonEmptyReadonlyArray<
            InternetSchemas.IPv4CidrBlock | InternetSchemas.IPv6CidrBlock | InternetSchemas.CidrBlockFromStringEncoded
        >
    ) => (allowedIPsLayer: AllowedIPsLayer<Nodes>) => AllowedIPsLayer<Nodes>,
    <
        Nodes extends
            | readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
            | readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
    >(
        allowedIPsLayer: AllowedIPsLayer<Nodes>,
        nodeToIp: Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
        nodeFromIp:
            | Extract<Nodes[number], WireguardRoamingPeer>["ip"]
            | Extract<Nodes[number], WireguardServer>[1]["ip"],
        cidrs: Array.NonEmptyReadonlyArray<
            InternetSchemas.IPv4CidrBlock | InternetSchemas.IPv6CidrBlock | InternetSchemas.CidrBlockFromStringEncoded
        >
    ) => AllowedIPsLayer<Nodes>
>(
    4,
    <
        Nodes extends
            | readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
            | readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
    >(
        allowedIPsLayer: AllowedIPsLayer<Nodes>,
        nodeToIp: Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
        nodeFromIp:
            | Extract<Nodes[number], WireguardRoamingPeer>["ip"]
            | Extract<Nodes[number], WireguardServer>[1]["ip"],
        cidrs: Array.NonEmptyReadonlyArray<
            InternetSchemas.IPv4CidrBlock | InternetSchemas.IPv6CidrBlock | InternetSchemas.CidrBlockFromStringEncoded
        >
    ): AllowedIPsLayer<Nodes> => {
        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 }): InternetSchemas.CidrBlockFromStringEncoded => `${address.ip}/${mask}` as const
                ),
                Match.when(
                    Schema.is(InternetSchemas.IPv6CidrBlock),
                    ({ address, mask }): InternetSchemas.CidrBlockFromStringEncoded => `${address.ip}/${mask}` as const
                ),
                Match.when(Predicate.isString, Function.identity<InternetSchemas.CidrBlockFromStringEncoded>),
                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 = <
    Nodes extends
        | readonly [node1: WireguardIPv4Node, node2: WireguardIPv4Node, ...rest: Array<WireguardIPv4Node>]
        | readonly [node1: WireguardIPv6Node, node2: WireguardIPv6Node, ...rest: Array<WireguardIPv6Node>],
>({
    allowedIPs,
    connections,
    keys,
    nodes,
    wireguardNetworkCidr,
}: WireguardNetwork<Nodes>): Effect.Effect<
    readonly [
        WireguardConfig.WireguardConfig,
        WireguardConfig.WireguardConfig,
        ...ReadonlyArray<WireguardConfig.WireguardConfig>,
    ],
    ParseResult.ParseError | WireguardErrors.WireguardError,
    never
> => {
    const endpointsByIp = nodesByIp(nodes) as Record.ReadonlyRecord<
        Extract<Nodes[number], WireguardRoamingPeer>["ip"] | Extract<Nodes[number], WireguardServer>[1]["ip"],
        WireguardNode
    >;

    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,
                } as const;
            })
        )
    );

    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 = <
    Nodes extends
        | readonly [server: WireguardIPv4Server, client: WireguardIPv4Node]
        | readonly [server: WireguardIPv6Server, client: WireguardIPv6Node],
    NetworkCidr extends Nodes[0] extends WireguardIPv4Node
        ? InternetSchemas.IPv4CidrBlock
        : Nodes[0] extends WireguardIPv6Node
          ? InternetSchemas.IPv6CidrBlock
          : never,
>(options: {
    nodes: Nodes;
    wireguardNetworkCidr: NetworkCidr;
}): WireguardNetwork<Nodes> =>
    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 = <
    Nodes extends
        | readonly [server: WireguardIPv4Server, client: WireguardIPv4Node]
        | readonly [server: WireguardIPv6Server, client: WireguardIPv6Node],
    NetworkCidr1 extends Nodes[0] extends WireguardIPv4Node
        ? InternetSchemas.IPv4CidrBlock
        : Nodes[0] extends WireguardIPv6Node
          ? InternetSchemas.IPv6CidrBlock
          : never,
    NetworkCidr2 extends Nodes[0] extends WireguardIPv4Server
        ? InternetSchemas.IPv4CidrBlock | Array.NonEmptyArray<InternetSchemas.IPv4CidrBlock>
        : Nodes[0] extends WireguardIPv6Server
          ? InternetSchemas.IPv6CidrBlock | Array.NonEmptyArray<InternetSchemas.IPv6CidrBlock>
          : never,
>(options: {
    nodes: Nodes;
    wireguardNetworkCidr: NetworkCidr1;
    lanNetworkCidr: NetworkCidr2;
}): WireguardNetwork<Nodes> =>
    Function.flow(
        generateRemoteAccessToServer,
        addAllowedIPs(
            ipForNode(options.nodes[1]),
            ipForNode(options.nodes[0]),
            Array.isArray(options.lanNetworkCidr)
                ? options.lanNetworkCidr
                : Array.of(options.lanNetworkCidr as InternetSchemas.CidrBlockBase<InternetSchemas.Family>)
        )
    )(options);

/**
 * Allows two servers to connect to each other.
 *
 * @since 1.0.0
 * @category Generators
 */
export const generateServerToServerAccess = <
    Nodes extends
        | readonly [server1: WireguardIPv4Server, server2: WireguardIPv4Server]
        | readonly [server1: WireguardIPv6Server, server2: WireguardIPv6Server],
    NetworkCidr extends Nodes[0] extends WireguardIPv4Node
        ? InternetSchemas.IPv4CidrBlock
        : Nodes[0] extends WireguardIPv6Node
          ? InternetSchemas.IPv6CidrBlock
          : never,
>(options: {
    nodes: Nodes;
    wireguardNetworkCidr: NetworkCidr;
}): WireguardNetwork<Nodes> =>
    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 = <
    Nodes extends
        | readonly [server1: WireguardIPv4Server, server2: WireguardIPv4Server]
        | readonly [server1: WireguardIPv6Server, server2: WireguardIPv6Server],
    NetworkCidr1 extends Nodes[0] extends WireguardIPv4Node
        ? InternetSchemas.IPv4CidrBlock
        : Nodes[0] extends WireguardIPv6Node
          ? InternetSchemas.IPv6CidrBlock
          : never,
    NetworkCidr2 extends Nodes[0] extends WireguardIPv4Server
        ? InternetSchemas.IPv4CidrBlock | Array.NonEmptyArray<InternetSchemas.IPv4CidrBlock>
        : Nodes[0] extends WireguardIPv6Server
          ? InternetSchemas.IPv6CidrBlock | Array.NonEmptyArray<InternetSchemas.IPv6CidrBlock>
          : never,
    NetworkCidr3 extends Nodes[1] extends WireguardIPv4Server
        ? InternetSchemas.IPv4CidrBlock | Array.NonEmptyArray<InternetSchemas.IPv4CidrBlock>
        : Nodes[1] extends WireguardIPv6Server
          ? InternetSchemas.IPv6CidrBlock | Array.NonEmptyArray<InternetSchemas.IPv6CidrBlock>
          : never,
>(options: {
    nodes: Nodes;
    server1Lan: NetworkCidr2;
    server2Lan: NetworkCidr3;
    wireguardNetworkCidr: NetworkCidr1;
}): WireguardNetwork<Nodes> =>
    Function.flow(
        generateServerToServerAccess,
        addAllowedIPs(
            ipForNode(options.nodes[1]),
            ipForNode(options.nodes[0]),
            Array.isArray(options.server1Lan)
                ? options.server1Lan
                : Array.of(options.server1Lan as InternetSchemas.CidrBlockBase<InternetSchemas.Family>)
        ),
        addAllowedIPs(
            ipForNode(options.nodes[0]),
            ipForNode(options.nodes[1]),
            Array.isArray(options.server2Lan)
                ? options.server2Lan
                : Array.of(options.server2Lan as InternetSchemas.CidrBlockBase<InternetSchemas.Family>)
        )
    )(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 = <
    Nodes extends
        | readonly [server: WireguardIPv4Server, ...nodes: Array.NonEmptyReadonlyArray<WireguardIPv4Node>]
        | readonly [server: WireguardIPv6Server, ...nodes: Array.NonEmptyReadonlyArray<WireguardIPv6Node>],
    NetworkCidr extends Nodes[0] extends WireguardIPv4Node
        ? InternetSchemas.IPv4CidrBlock
        : Nodes[0] extends WireguardIPv6Node
          ? InternetSchemas.IPv6CidrBlock
          : never,
>(options: {
    nodes: Nodes;
    wireguardNetworkCidr: NetworkCidr;
}): WireguardNetwork<Nodes> => {
    const clientNodes = Function.pipe(options.nodes, Array.map(ipForNode), Array.tailNonEmpty);

    const firstFoldOnClientNodes = Array.reduce(
        clientNodes,
        Function.identity<AllowedIPsLayer<Nodes>>,
        (outerAcc, node) => {
            const secondFoldOverOtherNodes = Array.reduce(
                Array.filter(clientNodes, (n) => n !== node),
                Function.identity<AllowedIPsLayer<Nodes>>,
                (innerAcc, otherNode) =>
                    Function.compose(
                        addAllowedIPs(node, ipForNode(options.nodes[0]), [`${otherNode}/32`] as const),
                        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 = <
    Nodes extends
        | readonly [server: WireguardIPv4Server, ...nodes: Array.NonEmptyReadonlyArray<WireguardIPv4Node>]
        | readonly [server: WireguardIPv6Server, ...nodes: Array.NonEmptyReadonlyArray<WireguardIPv6Node>],
    NetworkCidr extends Nodes[0] extends WireguardIPv4Node
        ? InternetSchemas.IPv4CidrBlock
        : Nodes[0] extends WireguardIPv6Node
          ? InternetSchemas.IPv6CidrBlock
          : never,
    NetworkCidr2 extends Nodes[0] extends WireguardIPv4Server
        ? InternetSchemas.IPv4CidrBlock | Array.NonEmptyArray<InternetSchemas.IPv4CidrBlock>
        : Nodes[0] extends WireguardIPv6Server
          ? InternetSchemas.IPv6CidrBlock | Array.NonEmptyArray<InternetSchemas.IPv6CidrBlock>
          : never,
>(options: {
    nodes: Nodes;
    lanNetworkCidr: NetworkCidr2;
    wireguardNetworkCidr: NetworkCidr;
}): WireguardNetwork<Nodes> => {
    const addAllowedIPsToAllNodes = Function.pipe(
        options.nodes,
        Array.map(ipForNode),
        Array.tailNonEmpty,
        Array.reduce(Function.identity<AllowedIPsLayer<Nodes>>, (acc, node) =>
            Function.compose(
                addAllowedIPs(
                    node,
                    ipForNode(options.nodes[0]),
                    Array.isArray(options.lanNetworkCidr)
                        ? options.lanNetworkCidr
                        : Array.of(options.lanNetworkCidr as InternetSchemas.CidrBlockBase<InternetSchemas.Family>)
                ),
                acc
            )
        )
    );

    return addAllowedIPsToAllNodes(generateServerHubAndSpokeAccess(options));
};

/**
 * Route specific traffic through a commercial WireGuard VPN provider.
 *
 * @since 1.0.0
 * @category Generators
 */
export const generateVpnTunneledAccess = <
    Nodes extends
        | readonly [server: WireguardIPv4Server, client: WireguardIPv4Node]
        | readonly [server: WireguardIPv6Server, client: WireguardIPv6Node],
    NetworkCidr extends Nodes[0] extends WireguardIPv4Node
        ? InternetSchemas.IPv4CidrBlock
        : Nodes[0] extends WireguardIPv6Node
          ? InternetSchemas.IPv6CidrBlock
          : never,
>(options: {
    nodes: Nodes;
    wireguardNetworkCidr: NetworkCidr;
}): WireguardNetwork<Nodes> =>
    Function.flow(
        generateRemoteAccessToServer,
        addAllowedIPs(ipForNode(options.nodes[1]), ipForNode(options.nodes[0]), ["0.0.0.0/0"] as const)
    )(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 = <
    Nodes extends
        | readonly [server: WireguardIPv4Server, client: WireguardIPv4Node]
        | readonly [server: WireguardIPv6Server, client: WireguardIPv6Node],
    NetworkCidr1 extends Nodes[0] extends WireguardIPv4Node
        ? InternetSchemas.IPv4CidrBlock
        : Nodes[0] extends WireguardIPv6Node
          ? InternetSchemas.IPv6CidrBlock
          : never,
    NetworkCidr2 extends Nodes[0] extends WireguardIPv4Server
        ? InternetSchemas.IPv4CidrBlock | Array.NonEmptyArray<InternetSchemas.IPv4CidrBlock>
        : Nodes[0] extends WireguardIPv6Server
          ? InternetSchemas.IPv6CidrBlock | Array.NonEmptyArray<InternetSchemas.IPv6CidrBlock>
          : never,
>(options: {
    nodes: Nodes;
    lanNetworkCidr: NetworkCidr2;
    wireguardNetworkCidr: NetworkCidr1;
}): WireguardNetwork<Nodes> =>
    Function.flow(
        generateRemoteAccessToLan,
        addAllowedIPs(ipForNode(options.nodes[1]), ipForNode(options.nodes[0]), ["0.0.0.0/0"] as const)
    )(options);
