import usbip, { DeviceDetailed } from "@src/lib/usbip.ts";
import { none, Option, some } from "@shared/utils/option.ts";
import { InferSchemaType, z } from "@shared/utils/validator.ts";
import log from "@shared/utils/logger.ts";
import { ResultAsync } from "@shared/utils/resultasync.ts";
import { err, Ok, ok, Result } from "@shared/utils/result.ts";
import {
    CommandExecutionError,
    DeviceDoesNotExistError,
    deviceDoesNotExistError,
    UsbipUnknownError,
} from "@src/lib/errors.ts";

type FailedToAccessDevices = CommandExecutionError | UsbipUnknownError;

const DEFAULT_STATE = 0;

class Devices {
    private devices: Result<
        Map<string, Device>,
        CommandExecutionError | UsbipUnknownError
    > = ok(new Map());

    public update(
        busid: string,
        update: Partial<DeviceMutables>,
    ): Result<void, DeviceDoesNotExistError | FailedToAccessDevices> {
        return this.devices.andThen((devices) => {
            const device = devices.get(busid);

            if (device === undefined) {
                return err(
                    deviceDoesNotExistError(
                        `Device with busid ${busid} does not exist`,
                    ),
                );
            }

            for (const key of Object.keys(update) as (keyof DeviceMutables)[]) {
                if (update[key] !== undefined) {
                    switch (key) {
                        case "status":
                            device.status = update.status ?? device.status;
                            break;
                        case "displayName":
                            device.displayName = update.displayName ?? none;
                            break;
                        case "description":
                            device.description = update.description ?? none;
                            break;
                    }
                }
            }

            return ok();
        });
    }

    public updateDevices(): ResultAsync<
        void,
        FailedToAccessDevices
    > {
        return usbip.getDevicesDetailed()
            .mapErr((e) => {
                log.error("Failed to update devices!");
                this.devices = err(e);
                return e;
            })
            .map((d) => d.unwrapOr([] as DeviceDetailed[]))
            .map(
                (devices) => {
                    const current = new Set(devices.map((d) => d.busid));
                    const old = new Set(
                        this.devices.unwrapOrElse(() => {
                            this.devices = ok(new Map());
                            return this.devices.unwrap();
                        }).keys(),
                    );

                    const connected = current.difference(old);
                    const disconnected = old.difference(current);

                    for (const device of devices) {
                        if (connected.has(device.busid)) {
                            this.devices.unwrap().set(
                                device.busid,
                                this.deviceFromDetailed(device),
                            );
                        }
                    }

                    for (const device of disconnected) {
                        this.devices.unwrap().delete(device);
                    }
                },
            );
    }

    private deviceFromDetailed(d: DeviceDetailed): Device {
        return {
            busid: d.busid,
            usbid: d.usbid,
            vendor: d.vendor,
            name: d.name,
            status: DEFAULT_STATE,
            displayName: none,
            description: none,
            connectedAt: new Date(),
        };
    }

    public list(): Result<Option<Device[]>, FailedToAccessDevices> {
        return this.devices.map((devices) => devices.values().toArray()).map((
            devices,
        ) => devices.length > 0 ? some(devices) : none);
    }
}

export const deviceSchema = z.obj({
    busid: z.string(),
    usbid: z.option(z.string()),
    vendor: z.option(z.string()),
    name: z.option(z.string()),
    status: z.enum([0, 1, 2]), // 0 - private, 1 - public, 2 - exported
    displayName: z.option(z.string()),
    description: z.option(z.string()),
    connectedAt: z.date(),
}).strict();

export const deviceMutablesSchema = deviceSchema.pick({
    status: true,
    displayName: true,
    description: true,
});

export type DeviceMutables = InferSchemaType<typeof deviceMutablesSchema>;

export type Device = InferSchemaType<typeof deviceSchema>;

const devices = new Devices();

devices.updateDevices();

export default devices;
