import { okAsync, ResultAsync } from "@shared/utils/resultasync.ts";
import { err, getMessageFromError, ok } from "@shared/utils/result.ts";
import { errAsync } from "@shared/utils/index.ts";
import log from "@shared/utils/logger.ts";
import {
    fromNullableVal,
    none,
    type Option,
    some,
} from "@shared/utils/option.ts";

class CommandExecutionError extends Error {
    code = "CommandExecutionError";
    constructor(msg: string) {
        super(msg);
    }
}

class DeviceDoesNotExistError extends Error {
    code = "DeviceDoesNotExist";
    constructor(msg: string) {
        super(msg);
    }
}

class DeviceAlreadyBoundError extends Error {
    code = "DeviceAlreadyBound";
    constructor(msg: string) {
        super(msg);
    }
}

class DeviceNotBound extends Error {
    code = "DeviceNotBound";
    constructor(msg: string) {
        super(msg);
    }
}

class UsbipUknownError extends Error {
    code = "UsbipUknownError";
    constructor(msg: string) {
        super(msg);
    }
}

type UsbipCommonError = DeviceDoesNotExistError | UsbipUknownError;

class UsbipManager {
    private readonly listDeatiledCmd = new Deno.Command("usbip", {
        args: ["list", "-l"],
    });
    private readonly listParsableCmd = new Deno.Command("usbip", {
        args: ["list", "-pl"],
    });
    private readonly decoder = new TextDecoder();

    private readonly usbidRegex = /[0-9abcdef]{4}:[0-9abcdef]{4}/;
    private readonly busidRegex =
        /(?:[0-9]+(?:\.[0-9]+)*-)*[0-9]+(?:\.[0-9]+)*/;

    private executeCommand(
        cmd: Deno.Command,
    ): ResultAsync<CommandOutput, CommandExecutionError> {
        const promise = cmd.output();

        return ResultAsync.fromPromise(
            promise,
            (e) => new CommandExecutionError(getMessageFromError(e)),
        )
            .map(({ stdout, stderr, code }) =>
                new CommandOutput(
                    this.decoder.decode(stdout).trim(),
                    this.decoder.decode(stderr).trim(),
                    code,
                )
            );
    }

    private handleCommonErrors(stderr: string): UsbipCommonError {
        if (
            stderr.includes("device with the specified bus ID does not exist")
        ) {
            return new DeviceDoesNotExistError(stderr);
        }

        return new UsbipUknownError(stderr);
    }

    private parseDetailedList(stdout: string): Option<DeviceDetailed[]> {
        const devices: DeviceDetailed[] = [];

        const deviceEntries = stdout.trim().split("\n\n");

        for (const deviceEntry of deviceEntries) {
            const busid = deviceEntry.match(this.busidRegex)?.shift();

            if (!busid) {
                log.error(
                    `Failed to parse busid of a device:\n ${deviceEntry}`,
                );
                continue;
            }

            const usbid = fromNullableVal(
                deviceEntry.match(this.usbidRegex)?.shift(),
            );

            const [_, line2] = deviceEntry.split("\n");

            const [vendorVal, nameVal] = line2
                ? line2.split(" : ").map((s) => s.trim())
                : [undefined, undefined];

            const vendor = fromNullableVal(vendorVal);
            const name = nameVal
                ? some(
                    nameVal.replace(
                        usbid.isSome() ? usbid.value : this.usbidRegex,
                        "",
                    ).replace("()", "")
                        .trim(),
                )
                : none;

            [["usbid", usbid], ["vendor", vendor], ["name", name]].filter((v) =>
                (v[1] as Option<string>).isNone()
            ).map((v) => log.warn(`Failed to parse ${v[0]}:\n ${deviceEntry}`));

            devices.push({
                busid,
                usbid,
                vendor,
                name,
            });
        }

        return devices.length > 0 ? some(devices) : none;
    }

    public getDevicesDetailed(): ResultAsync<
        Option<DeviceDetailed[]>,
        CommandExecutionError | UsbipUknownError
    > {
        return this.executeCommand(this.listDeatiledCmd).andThen(
            ({ stdout, stderr, success }) => {
                if (success) {
                    if (stderr) {
                        log.warn(
                            `usbip list -l succeeded but encountered an error: ${stderr}`,
                        );
                    }
                    return ok(this.parseDetailedList(stdout));
                }

                return err(new UsbipUknownError(stderr));
            },
        );
    }

    private parseParsableList(stdout: string): Option<Device[]> {
        const devices: Device[] = [];

        const devicesEntries = stdout.trim().split("\n");

        for (const deviceEntry of devicesEntries) {
            const [busid, usbid] = deviceEntry
                .slice(0, -1)
                .split("#")
                .map((v) => v.split("=")[1].trim() || undefined);

            if (!busid) {
                log.error(
                    `Failed to parse busid of a device:\n ${deviceEntry}`,
                );
                continue;
            }

            if (!usbid) {
                log.warn(
                    `Failed to parse usbid of a device:\n ${deviceEntry}`,
                );
            }

            devices.push({
                busid,
                usbid: fromNullableVal(usbid),
            });
        }

        return devices.length > 0 ? some(devices) : none;
    }

    public getDevices(): ResultAsync<
        Option<Device[]>,
        CommandExecutionError | UsbipUknownError
    > {
        return this.executeCommand(this.listParsableCmd).andThenAsync(
            ({ stdout, stderr, success }) => {
                if (success) {
                    if (stderr) {
                        log.warn(
                            `usbip list -lp succeeded but encountered an error: ${stderr}`,
                        );
                    }
                    return okAsync(this.parseParsableList(stdout));
                }
                return errAsync(new UsbipUknownError(stderr));
            },
        );
    }

    public bindDevice(
        busid: string,
    ): ResultAsync<
        string,
        UsbipCommonError | DeviceAlreadyBoundError | CommandExecutionError
    > {
        const cmd = new Deno.Command("usbip", { args: ["bind", "-b", busid] });

        return this.executeCommand(cmd).andThen(
            ({ stderr, success }) => {
                if (success) {
                    return ok(stderr.trim() || "Device bound successfully");
                }

                if (stderr.includes("is already bound to usbip-host")) {
                    return err(new DeviceAlreadyBoundError(stderr));
                }

                return err(this.handleCommonErrors(stderr));
            },
        );
    }

    public unbindDevice(
        busid: string,
    ): ResultAsync<
        string,
        CommandExecutionError | DeviceNotBound | UsbipCommonError
    > {
        const cmd = new Deno.Command("usbip", {
            args: ["unbind", "-b", busid],
        });

        return this.executeCommand(cmd).andThen(({ stderr, success }) => {
            if (success) {
                return ok(stderr.trim() || "Device unbound successfully");
            }

            if (stderr.includes("device is not bound to usbip-host driver")) {
                return err(new DeviceNotBound(stderr));
            }

            return err(this.handleCommonErrors(stderr));
        });
    }
}

class CommandOutput {
    constructor(
        public readonly stdout: string,
        public readonly stderr: string,
        public readonly code: number,
    ) {}
    get success(): boolean {
        return this.code === 0;
    }
}

interface DeviceDetailed {
    busid: string;
    usbid: Option<string>;
    vendor: Option<string>;
    name: Option<string>;
}

interface Device {
    busid: string;
    usbid: Option<string>;
}

export default UsbipManager;
