import Elysia from "elysia";
import open from 'open';
import z from "zod";
import os from 'node:os';
import { cachePath, config, events, taskQueue } from "./app";
import { getAppVersion, isSteamDeck, openExternal } from "../utils";
import fs from 'node:fs/promises';
import buildNotificationsStream from "./notifications";
import path, { dirname } from "node:path";
import { SystemInfoSchema, DirSchema, DownloadsDrive } from '@simeonradivoev/gameflow-sdk/shared';
import { getDevices, getDevicesCurated } from "./drives";
import getFolderSize from "get-folder-size";
import si from 'systeminformation';
import { getStoreFolder } from "./store/services/gamesService";
import ReloadPluginsJob from "./jobs/reload-plugins-job";
import { semver } from "bun";
import { getOrCachedGithubRelease } from "./cache";
import SelfUpdateJob from "./jobs/self-update-job";

async function checkUpdate (force?: boolean)
{
    const latest = await getOrCachedGithubRelease('simeonradivoev/gameflow-deck', force);
    if (!latest || !latest.tag_name) return {
        hasUpdate: 0,
        version: getAppVersion()
    };
    const hasUpdate = semver.order(latest.tag_name, getAppVersion());
    return { hasUpdate, version: latest.tag_name, info: latest.body };
}

export const system = new Elysia({ prefix: '/api/system' })
    .post('/show_keyboard', async ({ body: { XPosition, YPosition, Width, Height } }) =>
    {
        if (await isSteamDeck())
        {
            const url = new URL('steam://open/keyboard');
            if (XPosition) url.searchParams.set('XPosition', String(XPosition));
            if (YPosition) url.searchParams.set('YPosition', String(YPosition));
            if (Width) url.searchParams.set('Width', String(Width));
            if (Height) url.searchParams.set('Height', String(Height));
            open(url.href);
        }
    }, {
        body: z.object({
            XPosition: z.coerce.number().optional(),
            YPosition: z.coerce.number().optional(),
            Width: z.coerce.number().optional(),
            Height: z.coerce.number().optional()
        })
    })
    .get('/info', async () =>
    {
        let source = 'unknown';
        if (process.env.APPIMAGE === 'true')
            source = "AppImage";
        if (process.env.FLATPAK === 'true')
            source = "Flatpak";

        return {
            homeDir: os.homedir(),
            user: os.userInfo().username,
            arch: os.arch(),
            platform: os.platform(),
            hostname: os.hostname(),
            steamDeck: process.env.SteamDeck,
            machine: os.machine(),
            source,
            cacheSize: (await fs.stat(cachePath)).size,
            storeSize: (await getFolderSize(getStoreFolder())).size,
            version: getAppVersion()
        };
    })
    .get('/notifications', ({ set }) =>
    {
        set.headers["content-type"] = 'text/event-stream';
        set.headers["cache-control"] = 'no-cache';
        set.headers['connection'] = 'keep-alive';
        return new Response(buildNotificationsStream());
    })
    .get('/notifications/all', ({ }) =>
    {

    })
    .ws('/info/system', {
        response: z.discriminatedUnion('type', [
            z.object({ type: z.literal('info'), data: SystemInfoSchema }),
            z.object({ type: z.literal('focus') }),
            z.object({ type: z.literal('loading'), progress: z.number(), state: z.string().optional() }),
            z.object({ type: z.literal('activeTask'), progress: z.number().nullable() }),
            z.object({ type: z.literal('loaded') }),
        ]),
        async open (ws)
        {
            const existingLoading = taskQueue.findJob(ReloadPluginsJob.id, ReloadPluginsJob);
            if (existingLoading) ws.send({ type: 'loading', progress: existingLoading.progress, state: existingLoading.state });
            else ws.send({ type: 'loaded' });

            ws.send({ type: 'activeTask', progress: taskQueue.getActiveJobs()[0]?.progress });

            const startInfo = async () =>
            {
                const battery = await si.battery();
                const wifi = await si.wifiConnections();
                const bluetooth = await si.bluetoothDevices();
                ws.send({
                    type: 'info',
                    data: {
                        battery: battery,
                        wifiConnections: wifi,
                        bluetoothDevices: bluetooth
                    }
                }, true);
            };
            startInfo();

            const handleFocus = () => ws.send({ type: 'focus' });
            events.on('focus', handleFocus);
            const dispose: (() => void)[] = [];

            dispose.push(taskQueue.on('progress', e =>
            {
                ws.send({ type: 'activeTask', progress: e.progress });

                if (e.id === ReloadPluginsJob.id)
                {
                    ws.send({ type: "loading", progress: e.progress, state: e.state });
                }
                else if (e.id === SelfUpdateJob.id)
                {
                    ws.send({ type: "loading", progress: e.progress, state: e.state });
                }
            }));
            dispose.push(taskQueue.on('started', e =>
            {
                ws.send({ type: 'activeTask', progress: 0 });

                if (e.id === ReloadPluginsJob.id)
                    ws.send({ type: "loading", progress: e.job.progress, state: e.job.state });
                else if (e.id === SelfUpdateJob.id)
                    ws.send({ type: "loading", progress: e.job.progress, state: e.job.state });
            }));
            dispose.push(taskQueue.on('ended', e =>
            {
                ws.send({ type: 'activeTask', progress: null });
                if (e.id !== ReloadPluginsJob.id && e.id !== SelfUpdateJob.id) return;
                ws.send({ type: "loaded" });
            }));

            (ws.data as any).dispose = [...dispose, () =>
            {
                events.removeListener('focus', handleFocus);
            }];
            (ws.data as any).observer = setInterval(async () =>
            {
                const battery = await si.battery();
                const wifi = await si.wifiConnections();
                const bluetooth = await si.bluetoothDevices();
                ws.send({
                    type: 'info',
                    data: {
                        battery: battery,
                        wifiConnections: wifi,
                        bluetoothDevices: bluetooth
                    }
                }, true);
            }, 1000 * 30);
        },
        close (ws)
        {
            clearInterval((ws.data as any).observer);
            (ws.data as any).dispose.forEach((dispose: any) => dispose());
        }
    })
    .get('/drives', async () =>
    {
        const drives = await getDevices();
        if (process.platform === 'win32')
            return drives.map(d =>
            {
                d.mountPoint += '/';
                return d;
            });
        return drives;
    })
    // Drives that are vaiable for downloads
    .get('/drives/download', async () =>
    {
        const drives = await getDevicesCurated();
        let downloadsPath = config.get('downloadPath');
        if (!path.isAbsolute(downloadsPath))
        {
            downloadsPath = path.resolve(process.cwd(), downloadsPath);
        }
        const currentDownloadsSize = await getFolderSize(downloadsPath);
        let used = false;
        const drivesDownload: DownloadsDrive[] = drives
            .filter(d => !!d.mountPoint)
            .map(d => ({ ...d, depth: d.mountPoint!.split(path.sep).length }))
            .sort((a, b) => b.depth - a.depth)
            .map(d =>
            {
                const drive: DownloadsDrive = {
                    device: d.device,
                    label: d.label,
                    mountPoint: path.join(d.mountPoint!, 'gameflow'),
                    isRemovable: d.isRemovable,
                    size: d.size,
                    used: d.used,
                    isCurrentlyUsed: false,
                    unusableReason: null
                };

                if (!used && d.mountPoint && downloadsPath.startsWith(d.mountPoint))
                {
                    drive.isCurrentlyUsed = true;
                    used = true;
                }

                if (!drive.isCurrentlyUsed && currentDownloadsSize && drive.size - drive.used <= currentDownloadsSize.size)
                {
                    drive.unusableReason = 'not_enough_space';
                }
                else if (drive.isCurrentlyUsed && downloadsPath === drive.mountPoint)
                {
                    drive.unusableReason = 'already_used';
                }

                return drive;
            });
        return {
            downloadsSize: currentDownloadsSize.size,
            configPath: dirname(config.path),
            drives: drivesDownload,
        };
    })
    // Create Folder
    .put('/dirs', async ({ body: { dirname, name } }) =>
    {
        await fs.mkdir(path.join(dirname, name));
    }, {
        body: z.object({ dirname: z.string(), name: z.string() })
    })
    .get('/dirs', async ({ query: { path: startingPath } }) =>
    {
        let currentPath = startingPath ?? dirname(process.cwd());
        if (!path.isAbsolute(currentPath))
        {
            currentPath = path.resolve(process.cwd(), currentPath);
        }
        const currentPathExists = await fs.exists(currentPath);
        if (!currentPathExists) currentPath = dirname(process.cwd());
        const currentPathStat = await fs.stat(currentPath);
        if (!currentPathStat.isDirectory()) currentPath = dirname(currentPath);
        const paths = await fs.readdir(currentPath, { withFileTypes: true });
        return {
            name: path.basename(currentPath),
            parentPath: path.dirname(currentPath),
            dirs: paths.sort((a, b) => (b.isDirectory() ? 1 : 0) - (a.isDirectory() ? 1 : 0)).map(p =>
            ({
                name: p.name,
                parentPath: p.parentPath,
                isDirectory: p.isDirectory()
            }))
        };
    },
        {
            query: z.object({ path: z.string().optional() }),
            response: z.object({
                name: z.string(),
                parentPath: z.string(),
                dirs: z.array(DirSchema)
            })
        })
    .post('/exit', () =>
    {
        events.emit('exitapp');
    })
    .post('/open', async ({ body: { url } }) =>
    {
        await openExternal(url);
    }, {
        body: z.object({ url: z.string() })
    })
    .get('/update', async () =>
    {
        return checkUpdate();
    })
    .post('/update', async () =>
    {
        return taskQueue.enqueue(SelfUpdateJob.id, new SelfUpdateJob());
    })
    .post('/update/check', async () =>
    {
        return checkUpdate(true);
    });