import { stringify } from 'querystring';
import {
    getCookie,
    setCookie,
    setCookieRemoval,
    existingSessions,
    createSession,
    pullSession,
    deleteSession,
} from './session';

import { ServerError, pullConfig, currentTime, sleep, Forbidden } from './handler';

const HomeRedirect = async () =>
    new Response(null, {
        status: 302,
        headers: {
            Location: '/',
        },
    });

export interface DiscordUser {
    id: string;
    username: string;
    avatar: string;
    discriminator: string;
    public_flags: number;
    flags: number;
    email: string;
    verified: boolean;
    locale: string;
    mfa_enabled: boolean;
    credentials?: Credentials;
}

export interface DiscordGuild {
    id: string;
    name: string;
    icon: string;
    owner: boolean;
    permissions: number;
}

interface Credentials {
    access_token: string;
    refresh_token: string;
    expires_at: number;
}

interface DiscordOAuth {
    access_token: string;
    token_type: string;
    expires_in: number;
    refresh_token: string;
    scope: string;
}

interface DiscordRatelimit {
    message: string;
    retry_after: number;
    global: boolean;
}

export async function handleLogin(request: Request, url: URL): Promise<Response> {
    const config = await pullConfig();
    const [sessionObj, session] = await pullSession(request, config);

    if (sessionObj.valid && session) {
        // Redirect Home
        return HomeRedirect();
    }

    const response = await redirectResponse(url);

    if (getCookie(request, config.session.cookieName)) {
        setCookieRemoval(response, config.session.cookieName);
    }

    return response;
}

export async function handleDiscord(request: Request, url: URL): Promise<Response> {
    const code = url.searchParams.get('code');

    if (code && code.length < 64) {
        const response = await fetch('https://discordapp.com/api/v6/oauth2/token', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: stringify({
                client_id: DISCORD_CLIENT_ID,
                client_secret: DISCORD_CLIENT_SECRET,
                grant_type: 'authorization_code',
                code,
                redirect_uri: `${url.origin}${url.pathname}`,
                scope: 'identify email connections guilds',
            }),
        });

        switch (response.status) {
            case 200:
                const config = await pullConfig();
                const credentials: DiscordOAuth = await response.json();
                const expiresAt = credentials.expires_in + currentTime();

                const [user, guilds] = await Promise.all([pullUser(credentials), pullUserGuilds(credentials)]);

                if (!user || !guilds) {
                    return ServerError();
                }

                const superUsers = new Set(config.discord.superUsers);

                if (!config.session.enabled && !superUsers.has(user.id)) {
                    return new Response(
                        JSON.stringify({
                            error: 'Session Exists',
                        }),
                        {
                            status: 409,
                        },
                    );
                }

                user.credentials = {
                    access_token: credentials.access_token,
                    refresh_token: credentials.refresh_token,
                    expires_at: expiresAt,
                };

                const sessions = await existingSessions(user);

                if (!sessions) {
                    return ServerError();
                }

                if (sessions.length > 0) {
                    for (const session of sessions) {
                        if (!(await deleteSession(session))) {
                            return ServerError();
                        }
                    }
                }

                const [signedSession, sessionExpiry] = await createSession(request, user, guilds);
                const redirect = await HomeRedirect();
                setCookie(redirect, config.session.cookieName, signedSession, sessionExpiry);

                return redirect;
            case 400 || 401:
                break;
            default:
                return ServerError();
        }
    }

    return handleLogin(request, url);
}

async function pullUser(credentials: DiscordOAuth, retryLimit: number = 5): Promise<DiscordUser | undefined> {
    const response = await fetch('https://discordapp.com/api/v6/users/@me', {
        method: 'GET',
        headers: {
            Authorization: `Bearer ${credentials.access_token}`,
        },
    });

    switch (response.status) {
        case 200:
            return response.json();
        case 429:
            const ratelimit: DiscordRatelimit = await response.json();
            await sleep(ratelimit.retry_after);
        default:
            if (retryLimit > 0) {
                return pullUser(credentials, (retryLimit -= 1));
            }
    }
}

async function pullUserGuilds(credentials: DiscordOAuth, retryLimit: number = 5): Promise<DiscordGuild[] | undefined> {
    const response = await fetch('https://discordapp.com/api/v6/users/@me/guilds', {
        method: 'GET',
        headers: {
            Authorization: `Bearer ${credentials.access_token}`,
        },
    });

    switch (response.status) {
        case 200:
            return response.json();
        case 429:
            const ratelimit: DiscordRatelimit = await response.json();
            await sleep(ratelimit.retry_after);
        default:
            if (retryLimit > 0) {
                return pullUserGuilds(credentials, (retryLimit -= 1));
            }
    }
}

async function redirectResponse(url: URL): Promise<Response> {
    return new Response(null, {
        status: 302,
        headers: {
            Location: encodeURI(
                `https://discordapp.com/api/oauth2/authorize?client_id=${DISCORD_CLIENT_ID}&redirect_uri=https://${url.host}/api/discord&response_type=code&scope=identify email connections guilds`,
            ),
        },
    });
}