import type * as Cause from "effect/Cause";
import type * as PlatformError from "effect/PlatformError";
import type * as Ast from "effect/SchemaAST";
import type * as Scope from "effect/Scope";
import type * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
import type * as Socket from "effect/unstable/socket/Socket";

import * as Array from "effect/Array";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Function from "effect/Function";
import * as Match from "effect/Match";
import * as Number from "effect/Number";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
import * as Predicate from "effect/Predicate";
import * as Result from "effect/Result";
import * as Schedule from "effect/Schedule";
import * as Schema from "effect/Schema";
import * as SchemaGetter from "effect/SchemaGetter";
import * as SchemaIssue from "effect/SchemaIssue";
import * as Stream from "effect/Stream";
import * as String from "effect/String";

import * as assert from "node:assert";
import * as os from "node:os";

import * as InternetSchemas from "effect-schemas/Internet";
import * as ini from "ini";

import * as WireguardControl from "../WireguardControl.ts";
import * as WireguardErrors from "../WireguardErrors.ts";
import * as WireguardKey from "../WireguardKey.ts";
import * as WireguardPeer from "../WireguardPeer.ts";
import * as internalWireguardConfig from "./wireguardConfig.ts";
import * as internalInterface from "./wireguardInterface.ts";

// --------------------------------------------
// WireguardConfig.ts
// --------------------------------------------

export class WireguardConfig extends internalWireguardConfig.WireguardConfigVariantSchema.Class<WireguardConfig>(
    "WireguardIniConfig"
)({
    /** The Address of this peer. */
    Address: InternetSchemas.CidrBlockFromString,

    /** DNS for this peer. */
    Dns: Schema.optional(InternetSchemas.Address),

    /**
     * The value for this is a decimal-string integer corresponding to the
     * listening port of the interface.
     */
    ListenPort: Schema.Union([
        InternetSchemas.Port,
        Schema.NumberFromString.pipe(Schema.decodeTo(InternetSchemas.Port)),
    ]),

    /**
     * The value for this is a decimal-string integer corresponding to the
     * fwmark of the interface. The value may 0 in the case of a set operation,
     * in which case it indicates that the fwmark should be removed.
     */
    FirewallMark: Schema.Number.pipe(Schema.NullOr, Schema.optional),

    /**
     * The value for this key should be a lowercase hex-encoded private key of
     * the interface. The value may be an all zero string in the case of a set
     * operation, in which case it indicates that the private key should be
     * removed.
     */
    PrivateKey: WireguardKey.WireguardKey,

    /** List of peers to add. */
    Peers: internalWireguardConfig.WireguardConfigVariantSchema.Field({
        json: Schema.Array(WireguardPeer.WireguardPeer).pipe(Schema.withDecodingDefault(Effect.succeed([]))),
        uapi: Schema.Array(WireguardPeer.WireguardPeer["uapi"]).pipe(Schema.withDecodingDefault(Effect.succeed([]))),
    }),
}) {
    /**
     * Writes a wireguard interface configuration to an INI file.
     *
     * @since 1.0.0
     * @category Constructors
     * @param file - The path to the INI file.
     */
    public writeToFile: {
        (
            file: string
        ): Effect.Effect<void, Schema.SchemaError | PlatformError.PlatformError, FileSystem.FileSystem | Path.Path>;
    } = (file: string) =>
        Effect.gen({ self: this }, function* () {
            const path = yield* Path.Path;
            const fs = yield* FileSystem.FileSystem;
            const configEncoded = yield* Schema.encodeEffect(WireguardConfig)(this);
            const iniConfigDecoded = yield* Schema.decodeEffect(WireguardIniConfig)(configEncoded);
            yield* fs.makeDirectory(path.dirname(file), { recursive: true });
            yield* fs.writeFileString(file, iniConfigDecoded);
        });

    /**
     * Starts a wireguard tunnel that will continue to run and serve traffic
     * even after the nodejs process exits.
     *
     * @since 1.0.0
     * @category Wireguard
     */
    public up: {
        (
            wireguardInterface?: WireguardInterface | undefined
        ): Effect.Effect<
            WireguardInterface,
            | Socket.SocketError
            | Schema.SchemaError
            | Cause.TimeoutError
            | PlatformError.PlatformError
            | PlatformError.SystemError
            | PlatformError.BadArgument
            | WireguardErrors.WireguardError,
            | FileSystem.FileSystem
            | Path.Path
            | ChildProcessSpawner.ChildProcessSpawner
            | WireguardControl.WireguardControl
        >;
    } = (wireguardInterface?: WireguardInterface | undefined) =>
        Function.pipe(
            wireguardInterface,
            Option.fromUndefinedOr,
            Option.map(Effect.succeed),
            Option.getOrElse(() => WireguardInterface.getNextAvailableInterface),
            Effect.flatMap((io) => up(io, this))
        );

    /**
     * Starts a wireguard tunnel that will be gracefully shutdown and stop
     * serving traffic once the scope is closed.
     *
     * @since 1.0.0
     * @category Wireguard
     */
    public upScoped: {
        (
            wireguardInterface?: WireguardInterface | undefined
        ): Effect.Effect<
            WireguardInterface,
            | Socket.SocketError
            | Schema.SchemaError
            | Cause.TimeoutError
            | PlatformError.PlatformError
            | PlatformError.SystemError
            | PlatformError.BadArgument
            | WireguardErrors.WireguardError,
            | FileSystem.FileSystem
            | Path.Path
            | ChildProcessSpawner.ChildProcessSpawner
            | WireguardControl.WireguardControl
            | Scope.Scope
        >;
    } = (wireguardInterface?: WireguardInterface | undefined) =>
        Function.pipe(
            wireguardInterface,
            Option.fromUndefinedOr,
            Option.map(Effect.succeed),
            Option.getOrElse(() => WireguardInterface.getNextAvailableInterface),
            Effect.flatMap((io) => upScoped(io, this))
        );
}

export const WireguardIniConfig = WireguardConfig.pipe(
    Schema.decodeTo(Schema.String, {
        // Encoding is non-trivial, as we need to handle all the peers individually.
        decode: SchemaGetter.transformOrFail((config: WireguardConfig) =>
            Effect.gen(function* () {
                const listenPort = `ListenPort = ${config.ListenPort}\n`;
                const privateKey = `PrivateKey = ${config.PrivateKey}\n`;
                const address = `Address = ${config.Address.address.ip}/${config.Address.mask}\n`;
                const dns = Predicate.isNotUndefined(config.Dns) ? `Dns = ${config.Dns?.ip}\n` : "";
                const fwmark = Predicate.isNotUndefined(config.FirewallMark)
                    ? `FirewallMark = ${config.FirewallMark}\n`
                    : "";

                const peersConfig = yield* Function.pipe(
                    config.Peers,
                    Array.map((peer) => Schema.encodeEffect(WireguardPeer.WireguardPeer)(peer)),
                    Array.map(Effect.flatMap(Schema.decodeEffect(WireguardPeer.WireguardIniPeer))),
                    Effect.all,
                    Effect.map(Array.join("\n"))
                );

                return `[Interface]\n${dns}${listenPort}${fwmark}${address}${privateKey}\n${peersConfig}`;
            }).pipe(Effect.mapError(({ issue }) => issue))
        ),

        // Decoding is likewise non-trivial, as we need to parse all the peers from the ini config.
        encode: SchemaGetter.transformOrFail((iniConfig: string) =>
            Effect.gen(function* () {
                const sections = iniConfig.split(/(?=\[Peer\])/g);
                const maybeInterfaceSection = Array.findFirst(sections, (text) => text.startsWith("[Interface]"));
                const interfaceSection = Option.getOrThrowWith(
                    maybeInterfaceSection,
                    () => new WireguardErrors.WireguardError({ message: "No [Interface] section found" })
                );

                const peerSections = Function.pipe(
                    sections,
                    Array.filter((text) => text.startsWith("[Peer]")),
                    Array.map((text) => text.replace("[Peer]", ""))
                );

                const parsePeers = yield* Function.pipe(
                    peerSections,
                    Array.map((peer) => Schema.encodeEffect(WireguardPeer.WireguardIniPeer)(peer)),
                    Effect.all
                );

                const parseInterface = Function.pipe(
                    interfaceSection,
                    ini.parse,
                    (jsonConfig) => ({ ...jsonConfig["Interface"], Peers: parsePeers }),
                    ({ Address, Dns, FirewallMark, ListenPort, Peers, PrivateKey }) =>
                        ({
                            Dns,
                            Peers,
                            Address,
                            PrivateKey,
                            ListenPort,
                            FirewallMark: Number.parse(FirewallMark || "").pipe(Option.getOrUndefined),
                        }) as const,
                    Schema.decodeEffect(WireguardConfig)
                );

                return yield* parseInterface;
            }).pipe(Effect.mapError(({ issue }) => issue))
        ),
    }),
    Schema.annotate({
        identifier: "WireguardIniConfig",
        description: "A wireguard ini configuration",
    })
);

// --------------------------------------------
// WireguardInterface.ts
// --------------------------------------------

/** @internal */
export const UnsupportedArchitecture = (arch: string) =>
    Result.fail(new WireguardErrors.WireguardError({ message: `Unsupported architecture ${arch}` }));

/** @internal */
export const InterfaceRegExpForPlatform: Result.Result<RegExp, WireguardErrors.WireguardError> = Function.pipe(
    Match.value(`${process.arch}:${process.platform}`),
    Match.not(
        Predicate.some(Array.map(internalInterface.SupportedArchitectures, (arch) => String.startsWith(`${arch}:`))),
        UnsupportedArchitecture
    ),
    Match.when(String.endsWith(":linux"), () => Result.succeed(internalInterface.LinuxInterfaceNameRegExp)),
    Match.when(String.endsWith(":win32"), () => Result.succeed(internalInterface.WindowsInterfaceNameRegExp)),
    Match.when(String.endsWith(":darwin"), () => Result.succeed(internalInterface.DarwinInterfaceNameRegExp)),
    Match.orElse(UnsupportedArchitecture)
);

/**
 * A wireguard interface name.
 *
 * @since 1.0.0
 * @category Datatypes
 */
export class WireguardInterface extends Schema.Class<WireguardInterface>("WireguardInterface")({
    /**
     * Ensures the interface name matches the platform's interface name regex.
     * These functions need to be fully typed as we are accessing a static
     * method on this same class and otherwise typescript really complains about
     * inference.
     */
    Name: Schema.String.check(
        Result.match(InterfaceRegExpForPlatform, {
            onSuccess: (regex): Ast.Filter<string> => Schema.isPattern(regex),
            onFailure: (error): Ast.Filter<string> =>
                Schema.makeFilter(
                    (input) =>
                        new SchemaIssue.InvalidValue(Option.some(input), {
                            message: error.message,
                        })
                ),
        })
    ),
}) {
    /**
     * @since 1.0.0
     * @category Constructors
     */
    public static readonly getNextAvailableInterface: Effect.Effect<
        WireguardInterface,
        WireguardErrors.WireguardError,
        never
    > = Effect.gen(function* () {
        // Determine all the used interface indexes
        const regex = yield* Effect.fromResult(InterfaceRegExpForPlatform);
        const usedInterfaceIndexes = Function.pipe(
            os.networkInterfaces(),
            Object.keys,
            Array.filter((name) => regex.test(name)),
            Array.map(String.replaceAll(/\D/g, "")),
            Array.map(Number.parse),
            Array.filterMap(Result.fromOption(() => Result.failVoid))
        );

        // Find the next available interface index
        const nextAvailableInterfaceIndex = yield* Function.pipe(
            Stream.iterate(0, (x) => x + 1),
            Stream.filter((x) => !Array.contains(usedInterfaceIndexes, x)),
            Stream.take(1),
            Stream.runCollect,
            Effect.map(Array.head),
            Effect.map(Option.getOrThrow)
        );

        // We know this will be a supported platform now because otherwise
        // the WireguardInterface.InterfaceRegExpForPlatform would have failed
        const platform: (typeof internalInterface.SupportedPlatforms)[number] =
            process.platform as (typeof internalInterface.SupportedPlatforms)[number];

        // Construct the next available interface name
        const fromString = Schema.decodeSync(WireguardInterface);
        switch (platform) {
            case "win32":
                return fromString({ Name: `eth${nextAvailableInterfaceIndex}` });
            case "linux":
                return fromString({ Name: `wg${nextAvailableInterfaceIndex}` });
            case "darwin":
                return fromString({ Name: `utun${nextAvailableInterfaceIndex}` });
            default:
                return Function.absurd<WireguardInterface>(platform);
        }
    });

    /**
     * @since 1.0.0
     * @category Static members
     */
    public static readonly InterfaceRegExpForPlatform = InterfaceRegExpForPlatform;

    /**
     * @since 1.0.0
     * @category Userspace api
     */
    public readonly SocketLocation = Function.pipe(
        Match.type<(typeof internalInterface.SupportedPlatforms)[number]>(),
        Match.when("linux", () => `/var/run/wireguard/${this.Name}.sock`),
        Match.when("darwin", () => `/var/run/wireguard/${this.Name}.sock`),
        Match.when("win32", () => `\\\\.\\pipe\\ProtectedPrefix\\Administrators\\WireGuard\\${this.Name}`),
        Match.exhaustive
    )(process.platform as (typeof internalInterface.SupportedPlatforms)[number]);

    /**
     * Starts a wireguard tunnel that will be gracefully shutdown and stop
     * serving traffic once the scope is closed.
     *
     * @since 1.0.0
     * @category Wireguard control
     */
    public upScoped: {
        (
            config: WireguardConfig
        ): Effect.Effect<
            WireguardInterface,
            | Socket.SocketError
            | Schema.SchemaError
            | PlatformError.PlatformError
            | PlatformError.SystemError
            | PlatformError.BadArgument
            | Cause.TimeoutError,
            | FileSystem.FileSystem
            | Path.Path
            | Scope.Scope
            | ChildProcessSpawner.ChildProcessSpawner
            | WireguardControl.WireguardControl
        >;
    } = (config: WireguardConfig) => upScoped(this, config);

    /**
     * Starts a wireguard tunnel that will continue to run and serve traffic
     * even after the nodejs process exits.
     *
     * @since 1.0.0
     * @category Wireguard control
     */
    public up: {
        (
            config: WireguardConfig
        ): Effect.Effect<
            WireguardInterface,
            | Socket.SocketError
            | Schema.SchemaError
            | PlatformError.PlatformError
            | PlatformError.SystemError
            | PlatformError.BadArgument
            | Cause.TimeoutError,
            | FileSystem.FileSystem
            | Path.Path
            | ChildProcessSpawner.ChildProcessSpawner
            | WireguardControl.WireguardControl
        >;
    } = (config: WireguardConfig) => up(this, config);

    /**
     * Stops a previously started wireguard tunnel.
     *
     * @since 1.0.0
     * @category Wireguard control
     */
    public down: {
        (
            config: WireguardConfig
        ): Effect.Effect<
            WireguardInterface,
            | PlatformError.PlatformError
            | PlatformError.SystemError
            | PlatformError.BadArgument
            | Schema.SchemaError
            | Cause.TimeoutError,
            | FileSystem.FileSystem
            | Path.Path
            | ChildProcessSpawner.ChildProcessSpawner
            | WireguardControl.WireguardControl
        >;
    } = (config: WireguardConfig) => down(this, config);

    /**
     * Sets the config for this wireguard interface.
     *
     * @since 1.0.0
     * @category Wireguard control
     */
    public setConfig: {
        (
            wireguardConfig: WireguardConfig
        ): Effect.Effect<WireguardInterface, Socket.SocketError | Schema.SchemaError, never>;
    } = (wireguardConfig: WireguardConfig) => setConfig(this, wireguardConfig);

    /**
     * Retrieves the config from this wireguard interface.
     *
     * @since 1.0.0
     */
    public getConfig: {
        (
            address: (typeof InternetSchemas.CidrBlockFromString)["Encoded"]
        ): Effect.Effect<
            Schema.Schema.Type<(typeof WireguardConfig)["uapi"]>,
            Socket.SocketError | Schema.SchemaError,
            never
        >;
    } = (address: (typeof InternetSchemas.CidrBlockFromString)["Encoded"]) => getConfig(this, address);

    /**
     * Adds a peer to this interface.
     *
     * @since 1.0.0
     * @category Wireguard control
     */
    public addPeer: {
        (peer: WireguardPeer.WireguardPeer): Effect.Effect<void, Socket.SocketError | Schema.SchemaError, never>;
    } = (peer: WireguardPeer.WireguardPeer) => addPeer(this, peer);

    /**
     * Removes a peer from this interface.
     *
     * @since 1.0.0
     * @category Wireguard control
     */
    public removePeer: {
        (peer: WireguardPeer.WireguardPeer): Effect.Effect<void, Socket.SocketError | Schema.SchemaError, never>;
    } = (peer: WireguardPeer.WireguardPeer) => removePeer(this, peer);

    /**
     * Streams the stats from all the peers on this interface.
     *
     * @since 1.0.0
     * @category Wireguard control
     */
    public streamPeerStats: {
        (): Stream.Stream<
            ReadonlyArray<Schema.Schema.Type<(typeof WireguardPeer.WireguardPeer)["uapi"]>>,
            Socket.SocketError | Schema.SchemaError,
            never
        >;
    } = () => streamPeerStats(this);
}

// --------------------------------------------
// WireguardRpc.ts
// --------------------------------------------

/** @internal */
export const up: {
    (
        wireguardInterface: WireguardInterface,
        wireguardConfig: WireguardConfig
    ): Effect.Effect<
        WireguardInterface,
        | Socket.SocketError
        | Schema.SchemaError
        | PlatformError.PlatformError
        | PlatformError.SystemError
        | PlatformError.BadArgument
        | Cause.TimeoutError,
        FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner | WireguardControl.WireguardControl
    >;
} = (wireguardInterface: WireguardInterface, wireguardConfig: WireguardConfig) =>
    Effect.flatMap(WireguardControl.WireguardControl, (control) => control.up(wireguardConfig, wireguardInterface));

/** @internal */
export const upScoped: {
    (
        wireguardInterface: WireguardInterface,
        wireguardConfig: WireguardConfig
    ): Effect.Effect<
        WireguardInterface,
        | Socket.SocketError
        | Schema.SchemaError
        | PlatformError.PlatformError
        | PlatformError.SystemError
        | PlatformError.BadArgument
        | Cause.TimeoutError,
        | FileSystem.FileSystem
        | Path.Path
        | Scope.Scope
        | ChildProcessSpawner.ChildProcessSpawner
        | WireguardControl.WireguardControl
    >;
} = (wireguardInterface: WireguardInterface, wireguardConfig: WireguardConfig) =>
    Effect.flatMap(WireguardControl.WireguardControl, (control) =>
        control.upScoped(wireguardConfig, wireguardInterface)
    );

/** @internal */
export const down: {
    (
        wireguardInterface: WireguardInterface,
        wireguardConfig: WireguardConfig
    ): Effect.Effect<
        WireguardInterface,
        | PlatformError.BadArgument
        | PlatformError.SystemError
        | PlatformError.PlatformError
        | Schema.SchemaError
        | Cause.TimeoutError,
        FileSystem.FileSystem | Path.Path | ChildProcessSpawner.ChildProcessSpawner | WireguardControl.WireguardControl
    >;
} = (wireguardInterface: WireguardInterface, wireguardConfig: WireguardConfig) =>
    Effect.flatMap(WireguardControl.WireguardControl, (control) => control.down(wireguardConfig, wireguardInterface));

/** @internal */
export const setConfig: {
    (
        wireguardInterface: WireguardInterface,
        wireguardConfig: WireguardConfig
    ): Effect.Effect<WireguardInterface, Socket.SocketError | Schema.SchemaError, never>;
} = (wireguardInterface: WireguardInterface, wireguardConfig: WireguardConfig) =>
    Effect.gen(function* () {
        const listenPort = `listen_port=${wireguardConfig.ListenPort}\n` as const;
        const privateKeyHex = Buffer.from(wireguardConfig.PrivateKey, "base64").toString("hex");
        const privateKey = `private_key=${privateKeyHex}\n` as const;

        const fwmark = Predicate.isNotUndefined(wireguardConfig.FirewallMark)
            ? (`fwmark=${wireguardConfig.FirewallMark}\n` as const)
            : String.empty;

        const peers = yield* Function.pipe(
            wireguardConfig.Peers,
            Array.map((peer) => Schema.encodeEffect(WireguardPeer.WireguardPeer)(peer)),
            Array.map(Effect.flatMap((peer) => Schema.decodeEffect(WireguardPeer.WireguardUapiSetPeer)(peer))),
            Effect.all,
            Effect.map(Array.join("\n"))
        );

        const uapiConfig = `${privateKey}${listenPort}${fwmark}${peers}\n` as const;
        yield* internalInterface.userspaceContact(wireguardInterface, `set=1\n${uapiConfig}\n`);
        return wireguardInterface;
    });

/** @internal */
export const getConfig: {
    (
        wireguardInterface: WireguardInterface,
        address: (typeof InternetSchemas.CidrBlockFromString)["Encoded"]
    ): Effect.Effect<
        Schema.Schema.Type<(typeof WireguardConfig)["uapi"]>,
        Socket.SocketError | Schema.SchemaError,
        never
    >;
} = (wireguardInterface: WireguardInterface, address: (typeof InternetSchemas.CidrBlockFromString)["Encoded"]) =>
    Effect.gen(function* () {
        const uapiConfig = yield* internalInterface.userspaceContact(wireguardInterface, "get=1\n\n");
        const [interfaceConfig, ...peers] = uapiConfig.split("public_key=");
        const { fwmark, listen_port, private_key } = ini.decode(interfaceConfig ?? "");

        const peerConfigs = yield* Function.pipe(
            peers,
            Array.map((peer) => `public_key=${peer}`),
            Array.map((x) => Schema.decodeEffect(WireguardPeer.WireguardUapiGetPeer, { onExcessProperty: "error" })(x)),
            Array.map(Effect.flatMap((x) => Schema.encodeEffect(WireguardPeer.WireguardPeer["uapi"])(x))),
            Effect.all
        );

        return yield* Schema.decodeEffect(WireguardConfig["uapi"], { onExcessProperty: "error" })({
            Address: address,
            ListenPort: listen_port,
            PrivateKey: Buffer.from(private_key, "hex").toString("base64"),
            FirewallMark: Number.parse(fwmark || "").pipe(Option.getOrUndefined),
            Peers: peerConfigs,
        });
    });

/** @internal */
export const addPeer: {
    (
        wireguardInterface: WireguardInterface,
        peer: WireguardPeer.WireguardPeer
    ): Effect.Effect<void, Socket.SocketError | Schema.SchemaError, never>;
} = (wireguardInterface: WireguardInterface, peer: WireguardPeer.WireguardPeer) =>
    Effect.gen(function* () {
        // Get the config before adding this peer and ensure this peer is not present
        const configBefore = yield* getConfig(wireguardInterface, "0.0.0.0/0" as const);
        assert.ok(configBefore.Peers.find((p) => p.PublicKey === peer.PublicKey) === undefined);

        // Add the peer to the interface
        const a = yield* Schema.encodeEffect(WireguardPeer.WireguardPeer)(peer);
        const b = yield* Schema.decodeEffect(WireguardPeer.WireguardUapiSetPeer)(a);
        yield* internalInterface.userspaceContact(wireguardInterface, `set=1\n${b}`);

        // Get the config after adding this peer and ensure this peer is present
        const configAfter = yield* getConfig(wireguardInterface, "0.0.0.0/0" as const);
        assert.ok(configAfter.Peers.find((p) => p.PublicKey === peer.PublicKey) !== undefined);
    });

/** @internal */
export const removePeer: {
    (
        wireguardInterface: WireguardInterface,
        peer: WireguardPeer.WireguardPeer
    ): Effect.Effect<void, Socket.SocketError | Schema.SchemaError, never>;
} = (wireguardInterface: WireguardInterface, peer: WireguardPeer.WireguardPeer) =>
    Effect.gen(function* () {
        // Get the config before removing this peer and ensure this peer is present
        const configBefore = yield* getConfig(wireguardInterface, "0.0.0.0/0" as const);
        assert.ok(configBefore.Peers.find((p) => p.PublicKey === peer.PublicKey) !== undefined);

        // Remove the peer from the interface
        const a = yield* Schema.encodeEffect(WireguardPeer.WireguardPeer)(peer);
        const b = yield* Schema.decodeEffect(WireguardPeer.WireguardUapiSetPeer)(a);
        yield* internalInterface.userspaceContact(wireguardInterface, `set=1\n${b}remove=true\n`);

        // Get the config after removing this peer and ensure this peer is not present
        const configAfter = yield* getConfig(wireguardInterface, "0.0.0.0/0" as const);
        assert.ok(configAfter.Peers.find((p) => p.PublicKey === peer.PublicKey) === undefined);
    });

/** @internal */
export const streamPeerStats = (
    wireguardInterface: WireguardInterface
): Stream.Stream<
    ReadonlyArray<Schema.Schema.Type<(typeof WireguardPeer.WireguardPeer)["uapi"]>>,
    Socket.SocketError | Schema.SchemaError,
    never
> => {
    const pull = getConfig(wireguardInterface, "0.0.0.0/0" as const);
    const schedule = Schedule.spaced("1 second");
    const stream = Stream.fromEffectSchedule(pull, schedule);
    return Stream.map(stream, ({ Peers: peers }) => peers);
};
