import type * as Cause from "effect/Cause";
import type * as Schema from "effect/Schema";
import type * as Socket from "effect/unstable/socket/Socket";

import * as Array from "effect/Array";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Function from "effect/Function";
import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
import * as PlatformError from "effect/PlatformError";
import * as Schedule from "effect/Schedule";
import * as Scope from "effect/Scope";
import * as Stream from "effect/Stream";
import * as String from "effect/String";
import * as Tuple from "effect/Tuple";
import * as ChildProcess from "effect/unstable/process/ChildProcess";
import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";

import type * as WireguardConfig from "../WireguardConfig.ts";
import type * as _WireguardControl from "../WireguardControl.ts";
import type * as WireguardInterface from "../WireguardInterface.ts";

/** @internal */
export const TypeId: _WireguardControl.TypeId = Symbol.for(
    "@leonitousconforti/the-wireguard-effect/WireguardControl"
) as _WireguardControl.TypeId;

/** @internal */
export const WireguardControl = Context.Service<_WireguardControl.WireguardControl>(
    "@leonitousconforti/the-wireguard-effect/WireguardControl"
);

/**
 * @since 1.0.0
 * @category Constructors
 */
export const makeUserspaceLayer: Effect.Effect<_WireguardControl.WireguardControl, never, FileSystem.FileSystem> =
    Effect.gen(function* () {
        const fs = yield* FileSystem.FileSystem;

        const up: _WireguardControl.WireguardControl["up"] = (
            wireguardConfig: WireguardConfig.WireguardConfig,
            wireguardInterface: WireguardInterface.WireguardInterface
        ) => wireguardInterface.setConfig(wireguardConfig);

        const down: _WireguardControl.WireguardControl["down"] = (
            _wireguardConfig: WireguardConfig.WireguardConfig,
            wireguardInterface: WireguardInterface.WireguardInterface
        ) => fs.remove(wireguardInterface.SocketLocation).pipe(Effect.as(wireguardInterface));

        const upScoped: _WireguardControl.WireguardControl["upScoped"] = (
            wireguardConfig: WireguardConfig.WireguardConfig,
            wireguardInterface: WireguardInterface.WireguardInterface
        ) => {
            const _down = down(wireguardConfig, wireguardInterface).pipe(Effect.orDie);
            const _up = wireguardInterface.setConfig(wireguardConfig);
            return Effect.acquireRelease(_up, () => _down);
        };

        return WireguardControl.of({
            [TypeId]: TypeId,
            up,
            down,
            upScoped,
        });
    });

/**
 * @since 1.0.0
 * @category Constructors
 */
export const makeBundledWgQuickLayer = (options: {
    sudo: boolean;
}): Effect.Effect<
    _WireguardControl.WireguardControl,
    never,
    ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path
> =>
    Effect.gen(function* () {
        const executor = yield* ChildProcessSpawner.ChildProcessSpawner;
        const fs = yield* FileSystem.FileSystem;
        const path = yield* Path.Path;

        // process.platform === "win32" && command.includes("-wireguard-go.exe")
        const execCommandWireguardGoWindows = (
            command: string,
            ...args: Array<string>
        ): Effect.Effect<
            ChildProcessSpawner.ChildProcessHandle,
            PlatformError.PlatformError | Cause.TimeoutError,
            never
        > =>
            Effect.gen(function* () {
                const scope = Scope.makeUnsafe();

                const subprocess = yield* ChildProcess.make(command, args, {
                    forceKillAfter: "5 seconds",
                    stdin: "ignore",
                    detached: true,
                }).pipe(Scope.provide(scope));

                yield* subprocess.unref.pipe(Effect.asVoid);
                yield* subprocess.all.pipe(
                    Stream.decodeText({
                        encoding: "utf-8",
                    }),
                    Stream.splitLines,
                    Stream.takeUntil(String.includes("UAPI listener started")),
                    Stream.timeout("1 minutes"),
                    Stream.runDrain
                );

                return subprocess;
            }).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, executor));

        const execCommand = (
            command: string,
            ...args: Array<string>
        ): Effect.Effect<void, PlatformError.SystemError | PlatformError.PlatformError | Cause.TimeoutError, never> =>
            Function.pipe(
                executor.spawn(
                    options.sudo && process.platform !== "win32"
                        ? ChildProcess.make("sudo", [command, ...args])
                        : ChildProcess.make(command, args)
                ),
                Effect.flatMap((process) =>
                    Effect.all(
                        [
                            process.stdout.pipe(
                                Stream.decodeText({
                                    encoding: "utf-8",
                                }),
                                Stream.splitLines,
                                Stream.runCollect
                            ),
                            process.stderr.pipe(
                                Stream.decodeText({
                                    encoding: "utf-8",
                                }),
                                Stream.splitLines,
                                Stream.runCollect
                            ),
                            process.exitCode,
                        ],
                        { concurrency: 3 }
                    )
                ),
                Effect.flatMap(([stdout, stderr, exitCode]) => {
                    return exitCode === 0
                        ? Effect.void
                        : Effect.fail(
                              new PlatformError.SystemError({
                                  _tag: "Unknown",
                                  module: "Command",
                                  pathOrDescriptor: command,
                                  method: `${command} ${args.join(" ")}`,
                                  description: `Process exited with code ${exitCode}: ${stdout.join("\n")}, ${stderr.join("\n")}`,
                              })
                          );
                }),
                Effect.timeout("10 seconds"),
                Effect.scoped
            );

        const internalUp: (
            wireguardConfig: WireguardConfig.WireguardConfig,
            wireguardInterface: WireguardInterface.WireguardInterface
        ) => Effect.Effect<
            | readonly [wireguardInterface: WireguardInterface.WireguardInterface]
            | readonly [
                  wireguardInterface: WireguardInterface.WireguardInterface,
                  wireguardGoSubprocess: ChildProcessSpawner.ChildProcessHandle,
              ],
            | Socket.SocketError
            | Schema.SchemaError
            | PlatformError.PlatformError
            | PlatformError.BadArgument
            | PlatformError.SystemError
            | Cause.TimeoutError,
            never
        > = (
            wireguardConfig: WireguardConfig.WireguardConfig,
            wireguardInterface: WireguardInterface.WireguardInterface
        ) =>
            Effect.gen(function* () {
                const tempDirectory = yield* fs.makeTempDirectory();
                const file = path.join(tempDirectory, `${wireguardInterface.Name}.conf`);
                yield* wireguardConfig
                    .writeToFile(file)
                    .pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path));

                const arch = process.arch === "x64" ? "amd64" : process.arch;
                const extension = process.platform === "win32" ? ".exe" : "";
                const wireguardGoUrl = new URL(
                    `../${process.platform}-${arch}-wireguard-go${extension}`,
                    import.meta.url
                );
                const bundledWireguardGoExecutablePath = yield* path.fromFileUrl(wireguardGoUrl);
                yield* fs.access(bundledWireguardGoExecutablePath, { ok: true });

                const wgQuickUrl = new URL(`../${process.platform}-wg-quick`, import.meta.url);
                const bundledWgQuickExecutablePath = yield* path.fromFileUrl(wgQuickUrl);
                if (process.platform !== "win32") yield* fs.access(bundledWgQuickExecutablePath, { ok: true });

                const wgWindowsUrl = new URL(`../win32-${arch}-wireguard.exe`, import.meta.url);
                const bundledWgWindowsExecutablePath = yield* path.fromFileUrl(wgWindowsUrl);
                if (process.platform === "win32") yield* fs.access(bundledWgWindowsExecutablePath, { ok: true });

                const wgQuickCommandNix = [bundledWgQuickExecutablePath, "up", file] as const;
                const wgQuickCommandWin = [bundledWgWindowsExecutablePath, "/installtunnelservice", file] as const;
                const wgQuickCommand = process.platform === "win32" ? wgQuickCommandWin : wgQuickCommandNix;

                if (process.platform === "win32") {
                    const retrySchedule = Schedule.max([Schedule.recurs(5), Schedule.exponential(2_000)]);
                    const runningWireguardGoProcess = yield* execCommandWireguardGoWindows(
                        bundledWireguardGoExecutablePath,
                        wireguardInterface.Name
                    );
                    yield* Effect.retry(wireguardInterface.setConfig(wireguardConfig), retrySchedule);
                    yield* execCommand(wgQuickCommand[0], ...wgQuickCommand.slice(1));
                    return Tuple.make(wireguardInterface, runningWireguardGoProcess);
                } else {
                    yield* execCommand(bundledWireguardGoExecutablePath, wireguardInterface.Name);
                    yield* wireguardInterface.setConfig(wireguardConfig);
                    yield* execCommand(wgQuickCommand[0], ...wgQuickCommand.slice(1));
                    return Tuple.make(wireguardInterface);
                }
            });

        const down: _WireguardControl.WireguardControl["down"] = (
            wireguardConfig,
            wireguardInterface,
            wireguardGoSubprocess
        ) =>
            Effect.gen(function* () {
                if (wireguardGoSubprocess) yield* wireguardGoSubprocess.kill();
                const tempDirectory = yield* fs.makeTempDirectory();
                const file = path.join(tempDirectory, `${wireguardInterface.Name}.conf`);
                yield* wireguardConfig
                    .writeToFile(file)
                    .pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path));

                const arch = process.arch === "x64" ? "amd64" : process.arch;
                const wgQuickUrl = new URL(`../${process.platform}-wg-quick`, import.meta.url);
                const bundledWgQuickExecutablePath = yield* path.fromFileUrl(wgQuickUrl);
                if (process.platform !== "win32") yield* fs.access(bundledWgQuickExecutablePath, { ok: true });

                const wgWindowsUrl = new URL(`../win32-${arch}-wireguard.exe`, import.meta.url);
                const bundledWgWindowsExecutablePath = yield* path.fromFileUrl(wgWindowsUrl);
                if (process.platform === "win32") yield* fs.access(bundledWgWindowsExecutablePath, { ok: true });

                const wgQuickCommandWin = [
                    bundledWgWindowsExecutablePath,
                    "/uninstalltunnelservice",
                    wireguardInterface.Name,
                ] as const;
                const wgQuickCommandNix = [bundledWgQuickExecutablePath, "down", file] as const;
                const wgQuickCommand = process.platform === "win32" ? wgQuickCommandWin : wgQuickCommandNix;
                yield* execCommand(wgQuickCommand[0], ...wgQuickCommand.slice(1));
                return wireguardInterface;
            });

        const upScoped: _WireguardControl.WireguardControl["upScoped"] = (wireguardConfig, wireguardInterface) => {
            const _down = (wireguardGoSubprocess?: ChildProcessSpawner.ChildProcessHandle | undefined) =>
                down(wireguardConfig, wireguardInterface, wireguardGoSubprocess).pipe(Effect.orDie);
            const _up = internalUp(wireguardConfig, wireguardInterface);
            return Effect.map(
                Effect.acquireRelease(_up, (data) => {
                    if (Array.isArray(data) && Tuple.isTupleOf(data, 2)) {
                        const wireguardGoSubprocess = data[1];
                        return _down(wireguardGoSubprocess);
                    } else {
                        return _down();
                    }
                }),
                (data) => data[0]
            );
        };

        const up: _WireguardControl.WireguardControl["up"] = (wireguardConfig, wireguardInterface) =>
            Effect.map(internalUp(wireguardConfig, wireguardInterface), (data) => data[0]);

        return WireguardControl.of({
            [TypeId]: TypeId,
            up,
            down,
            upScoped,
        });
    });

/**
 * @since 1.0.0
 * @category Layers
 */
export const UserspaceLayer = Layer.effect(WireguardControl, makeUserspaceLayer);

/**
 * @since 1.0.0
 * @category Layers
 */
export const BundledWgQuickLayer = Layer.effect(
    WireguardControl,
    makeBundledWgQuickLayer({
        sudo: true,
    })
);
