import { none, type Option, some } from "@shared/utils/option.ts";
import { errAsync, okAsync, ResultAsync } from "@shared/utils/resultasync.ts";
import { InferSchemaType, Schema, z } from "@shared/utils/validator.ts";

const CONNECTION_TIMEOUT_MS = 2000;
const PING_INTERVAL_MS = 1000;
const PING_CHECK_INTERVAL_MS = 15000;
const MAX_PING_ATTEMPTS = 5;
const MAX_RECONNECTION_ATTEMPTS = 5;

export class WebSocketWrapper<
    R extends Schema<any> = Schema<unknown>,
    S extends Schema<any> = Schema<unknown>,
> {
    private _ws: Option<WebSocket> = none;
    get ws(): Option<WebSocket> {
        return this._ws;
    }
    set ws(ws: Option<WebSocket>) {
        this._ws = ws;
        if (ws.isSome()) {
            ws.value.addEventListener("close", this.handleWebSocketClose);
            ws.value.addEventListener("error", this.handleWebSocketError);
            ws.value.addEventListener("message", this.onMessage!);
            this.onConnectSucc!();
            this.pingAndWait();
        } else {
            this.onDisconnect!();
        }
    }

    private handleWebSocketClose = () => {
        this._ws = none;
        this.connect();
    };

    private handleWebSocketError = () => {
        this._ws = none;
        this.connect();
    };

    private pingAndWait = async () => {
        const r = await this.ping();

        if (r.isErr()) {
            clearTimeout(this.pingTimer);
            this.ws = none;
        }

        this.pingTimer = setTimeout(this.pingAndWait, PING_CHECK_INTERVAL_MS);
    };

    private pingTimer?: number;

    public onConnectInit?: () => void;
    public onConnectSucc?: () => void;
    public onConnectFail?: () => void;

    public onDisconnect?: () => void;

    public onMessage?: (ev: MessageEvent<any>) => void;

    private isConnecting = false;

    constructor(
        public readonly url: string,
        public readonly schema: {
            receive: R;
            send: S;
        },
        private readonly timeout = CONNECTION_TIMEOUT_MS,
        private readonly pingInterval = PING_INTERVAL_MS,
    ) {}

    public ping(): ResultAsync<void, void> {
        if (this.ws.isNone()) {
            return errAsync();
        }

        const ws = this.ws.value;

        return ResultAsync.from((resolve, reject) => {
            let timer: number;

            const listener = (e: MessageEvent<any>) => {
                if (e.data === "pong") {
                    ws.removeEventListener("message", listener);
                    clearTimeout(timer);
                    resolve();
                }
            };

            ws.addEventListener("message", listener);

            let attempts = 0;
            const pingAndWait = () => {
                if (++attempts > MAX_PING_ATTEMPTS) reject();

                ws.send("ping");

                timer = setTimeout(pingAndWait, this.pingInterval);
            };
            pingAndWait();
        });
    }

    private createWebSocketConnection(): ResultAsync<WebSocket, void> {
        const ws = new WebSocket(this.url);

        return ResultAsync.from((resolve, reject) => {
            const handleError = () => {
                reject();
            };

            ws.addEventListener("open", () => {
                ws.removeEventListener("error", handleError);
                resolve(ws);
            });

            ws.addEventListener("error", handleError);
        });
    }

    public connect(): ResultAsync<void, void> {
        if (this.isConnecting) {
            return errAsync();
        }

        return ResultAsync.fromSafePromise(
            this.ping().match(
                () => okAsync(),
                () => {
                    this.isConnecting = true;
                    this.onConnectInit!();
                    this.tryReconnect();
                },
            ),
        ).flatten();
    }

    private tryReconnect(): ResultAsync<void, void> {
        return ResultAsync.from((resolve, reject) => {
            let attempt = 0;

            let timer: number;

            const tryConnect = async () => {
                console.log(`attempt ${attempt + 1}`);
                if (++attempt >= MAX_RECONNECTION_ATTEMPTS) {
                    this.onConnectFail!();
                    this.isConnecting = false;
                    console.error("Failed to connect");
                    reject();
                    return;
                }

                const ws = await this.createWebSocketConnection();

                if (ws.isOk()) {
                    this.ws = some(ws.value);
                    clearTimeout(timer);
                    this.isConnecting = false;
                    resolve();
                } else {
                    timer = setTimeout(
                        tryConnect,
                        this.timeout,
                    );
                }
            };

            tryConnect();
        });
    }

    send(data: InferSchemaType<S>): ResultAsync<void, void> {
        if (this.ws.isNone()) {
            return errAsync();
        }
    }
}

const sendSchema = z.obj({
    id: z.number(),
    kind: z.enum(["up"]),
});
