import type * as Scope from "effect/Scope";
import type * as HttpClient from "effect/unstable/http/HttpClient";

import * as Effect from "effect/Effect";
import * as Function from "effect/Function";
import * as Layer from "effect/Layer";
import * as Socket from "effect/unstable/socket/Socket";

import type * as net from "node:net";

import type * as MobyConnection from "../../MobyConnection.js";
import type * as ssh2 from "ssh2";
import type * as undici from "undici";

import * as internalAgnostic from "./agnostic.js";
import * as internalConnection from "./connection.js";

/** @internal */
export const makeDispatcher: (
    agentOptions?: undici.Agent.Options | undefined
) => Effect.Effect<undici.Agent, never, Scope.Scope> = Effect.fnUntraced(function* (
    agentOptions?: undici.Agent.Options | undefined
) {
    const undiciLazy = yield* Effect.promise(() => import("undici"));
    const acquire = Effect.sync(() => new undiciLazy.Agent(agentOptions));
    const release = (agent: undici.Dispatcher) => Effect.promise(() => agent.destroy());
    const resource = Effect.acquireRelease(acquire, release);
    return yield* resource;
});

/** @internal */
export const makeSshDispatcher: (
    connectionOptions: MobyConnection.SshConnectionOptions
) => Effect.Effect<undici.Agent, never, Scope.Scope> = Effect.fnUntraced(function* (
    connectionOptions: MobyConnection.SshConnectionOptions
) {
    const ssh2Lazy = yield* Effect.promise(() => import("ssh2"));
    const undiciLazy = yield* Effect.promise(() => import("undici"));
    const sshClient = new ssh2Lazy.Client();

    let sshConnection: "unopened" | "connecting" | "open-failed" | "ready" = "unopened";
    let openFailedError: Error | null = null;

    const connector: undici.buildConnector.connector = (
        // oxlint-disable-next-line only-used-in-recursion
        _options: undici.buildConnector.Options,
        callback: undici.buildConnector.Callback
    ) => {
        const onError = (error: Error): void => callback(error, null);

        // No connection yet, start connecting
        if (sshConnection === "unopened") {
            sshConnection = "connecting";
            const unableToOpen = (error: Error & ssh2.ClientErrorExtensions) => {
                sshConnection = "open-failed";
                openFailedError = error;
                onError(error);
            };
            sshClient
                .once("ready", () => {
                    sshConnection = "ready";
                    sshClient.off("error", unableToOpen);
                    connector(_options, callback);
                })
                .once("error", unableToOpen)
                .connect(connectionOptions);
        }

        // Already tried to connect but failed
        else if (sshConnection === "open-failed") {
            callback(openFailedError!, null);
        }

        // Another connection attempt is already in progress, wait for it
        else if (sshConnection === "connecting") {
            sshClient
                .once("ready", () => {
                    sshClient.off("error", onError);
                    connector(_options, callback);
                })
                .once("error", onError);
        }

        // We are connected to the ssh server, now forward our stream local
        else if (sshConnection === "ready") {
            sshClient
                .openssh_forwardOutStreamLocal(
                    connectionOptions.remoteSocketPath,
                    (error: Error | undefined, stream: ssh2.ClientChannel) => {
                        sshClient.off("error", onError);
                        if (error) return callback(error, null);
                        else return callback(null, stream as unknown as net.Socket);
                    }
                )
                .once("error", onError);
        }

        // Should never happen
        else {
            return Function.absurd<undefined>(sshConnection);
        }
    };

    const acquire = Effect.sync(() => new undiciLazy.Agent({ connect: connector }));
    const release = (_agent: undici.Dispatcher) =>
        Effect.promise(async () => {
            sshClient.end();
            sshClient.destroy();
            // await agent.close();
            // await agent.destroy();
        });

    const resource = Effect.acquireRelease(acquire, release);
    return yield* resource;
});

/** @internal */
export const getUndiciDispatcher: (
    connectionOptions: MobyConnection.MobyConnectionOptions
) => Effect.Effect<undici.Dispatcher, never, Scope.Scope> = internalConnection.MobyConnectionOptions.$match({
    ssh: (options) => makeSshDispatcher(options),
    socket: (options) =>
        makeDispatcher({
            connect: {
                socketPath: options.socketPath,
            },
        }),
    http: (options) =>
        makeDispatcher({
            connect: {
                host: options.host,
                port: options.port,
                path: options.path,
            },
        }),
    https: (options) =>
        makeDispatcher({
            connect: {
                ca: options.ca,
                key: options.key,
                cert: options.cert,
                passphrase: options.passphrase,
                host: options.host,
                port: options.port,
                path: options.path,
            },
        }),
});

/** @internal */
export const getUndiciWebsocketConstructor = (
    connectionOptions: MobyConnection.MobyConnectionOptions
): Effect.Effect<(url: string, protocols?: string | Array<string> | undefined) => globalThis.WebSocket, never, never> =>
    Function.pipe(
        Effect.promise(() => import("ws")),
        Effect.map((ws) => {
            const prependedUrl = internalAgnostic.makeWebsocketRequestUrl(connectionOptions);
            return (url: string, protocols?: string | Array<string> | undefined) =>
                new ws.WebSocket(`${prependedUrl}${url}`, protocols) as unknown as globalThis.WebSocket;
        })
    );

/** @internal */
export const makeUndiciHttpClientLayer = (
    connectionOptions: MobyConnection.MobyConnectionOptions
): Layer.Layer<HttpClient.HttpClient | Socket.WebSocketConstructor, never, never> => {
    const dispatcherLive = Function.pipe(
        Effect.promise(() => import("@effect/platform-node/NodeHttpClient")),
        Effect.map((nodeHttpClientLazy) =>
            Layer.effect(nodeHttpClientLazy.Dispatcher, getUndiciDispatcher(connectionOptions))
        ),
        Layer.unwrap
    );

    const websocketConstructorLive = Layer.effect(
        Socket.WebSocketConstructor,
        getUndiciWebsocketConstructor(connectionOptions)
    );

    const undiciHttpClientLive = Function.pipe(
        Effect.promise(() => import("@effect/platform-node/NodeHttpClient")),
        Effect.map((nodeHttpClientLazy) => nodeHttpClientLazy.layerUndiciNoDispatcher),
        Layer.unwrap
    );

    const agnosticHttpClientLayer = internalAgnostic.makeAgnosticHttpClientLayer(connectionOptions);

    return agnosticHttpClientLayer.pipe(
        Layer.merge(websocketConstructorLive),
        Layer.provide(undiciHttpClientLive),
        Layer.provide(dispatcherLive)
    );
};
