/*
 *     The Peacock Project - a HITMAN server replacement.
 *     Copyright (C) 2021-2023 The Peacock Project Team
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU Affero General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU Affero General Public License for more details.
 *
 *     You should have received a copy of the GNU Affero General Public License
 *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

import type { Response } from "express"
import { decode, sign } from "jsonwebtoken"
import { extractToken, uuidRegex } from "./utils"
import type { GameVersion, RequestWithJwt, UserProfile } from "./types/types"
import { getVersionedConfig } from "./configSwizzleManager"
import { log, LogLevel } from "./loggingInterop"
import {
    STEAM_NAMESPACE_2018,
    STEAM_NAMESPACE_2021,
} from "./platformEntitlements"
import {
    getExternalUserData,
    getUserData,
    loadUserData,
    writeExternalUserData,
    writeNewUserData,
} from "./databaseHandler"
import { OfficialServerAuth, userAuths } from "./officialServerAuth"
import { randomUUID } from "crypto"
import { getFlag } from "./flags"
import { clearInventoryFor } from "./inventory"
import {
    EpicH1Strategy,
    EpicH3Strategy,
    IOIStrategy,
    SteamH1Strategy,
    SteamH2Strategy,
    SteamScpcStrategy,
} from "./entitlementStrategies"

export async function handleOauthToken(
    req: RequestWithJwt,
    res: Response,
): Promise<void> {
    const isFrankenstein = req.body.gs === "scpc-prod"

    const signOptions = {
        notBefore: -60000,
        expiresIn: 6000,
        issuer: "auth.hitman.io",
        audience: isFrankenstein ? "scpc-prod" : "pc_prod_8",
        noTimestamp: true,
    }

    //#region Refresh tokens
    if (req.body.grant_type === "refresh_token") {
        // send back the token from the request (re-signed so the timestamps update)
        extractToken(req) // init req.jwt
        // remove signOptions from existing jwt
        // ts-expect-error Non-optional, we're reassigning.
        delete req.jwt.nbf // notBefore
        // ts-expect-error Non-optional, we're reassigning.
        delete req.jwt.exp // expiresIn
        // ts-expect-error Non-optional, we're reassigning.
        delete req.jwt.iss // issuer
        // ts-expect-error Non-optional, we're reassigning.
        delete req.jwt.aud // audience

        if (getFlag("officialAuthentication") === true && !isFrankenstein) {
            if (userAuths.has(req.jwt.unique_name)) {
                userAuths
                    .get(req.jwt.unique_name)!
                    ._doRefresh()
                    .then(() => undefined)
                    .catch(() => {
                        log(LogLevel.WARN, "Failed authentication refresh.")
                        userAuths.get(req.jwt.unique_name)!.initialized = false
                    })
            }
        }

        res.json({
            access_token: sign(req.jwt, "secret", signOptions),
            token_type: "bearer",
            expires_in: 5000,
            refresh_token: randomUUID(),
        })

        return
    }
    //#endregion

    let external_platform: "steam" | "epic",
        external_userid: string,
        external_users_folder: "steamids" | "epicids",
        external_appid: string

    if (req.body.grant_type === "external_steam") {
        if (!/^\d{1,20}$/.test(req.body.steam_userid)) {
            res.status(400).end() // invalid steam user id
            return
        }

        external_platform = "steam"
        external_userid = req.body.steam_userid
        external_users_folder = "steamids"
        external_appid = req.body.steam_appid
    } else if (req.body.grant_type === "external_epic") {
        if (!/^[\da-f]{32}$/.test(req.body.epic_userid)) {
            res.status(400).end() // invalid epic user id
            return
        }

        const epic_token = decode(
            req.body.access_token.replace(/^eg1~/, ""),
        ) as {
            appid: string
            app: string
        }

        if (!epic_token || !(epic_token.appid || epic_token.app)) {
            res.status(400).end() // invalid epic access token
            return
        }

        external_appid = epic_token.appid || epic_token.app
        external_platform = "epic"
        external_userid = req.body.epic_userid
        external_users_folder = "epicids"
    } else {
        res.status(406).end() // unsupported auth method
        return
    }

    if (req.body.pId && !uuidRegex.test(req.body.pId)) {
        res.status(400).end() // pId is not a GUID
        return
    }

    const isHitman3 =
        external_appid === "fghi4567xQOCheZIin0pazB47qGUvZw4" ||
        external_appid === STEAM_NAMESPACE_2021

    const gameVersion: GameVersion = isFrankenstein
        ? "scpc"
        : isHitman3
        ? "h3"
        : external_appid === STEAM_NAMESPACE_2018
        ? "h2"
        : "h1"

    if (!req.body.pId) {
        // if no profile id supplied
        try {
            req.body.pId = (
                await getExternalUserData(
                    external_userid,
                    external_users_folder,
                    gameVersion,
                )
            ).toString()
        } catch (e) {
            req.body.pId = randomUUID()
            await writeExternalUserData(
                external_userid,
                external_users_folder,
                req.body.pId,
                gameVersion,
            )
        }
    } else {
        // if a profile id is supplied
        getExternalUserData(external_userid, external_users_folder, gameVersion)
            .then(() => null)
            .catch(async () => {
                // external id is not yet linked to this profile
                await writeExternalUserData(
                    external_userid,
                    external_users_folder,
                    req.body.pId,
                    gameVersion,
                )
            })
    }

    try {
        await loadUserData(req.body.pId, gameVersion)
    } catch (e) {
        log(LogLevel.DEBUG, "Unable to load profile information.")
    }

    if (getFlag("officialAuthentication") === true && !isFrankenstein) {
        const authContainer = new OfficialServerAuth(
            gameVersion,
            req.body.access_token,
        )

        log(LogLevel.DEBUG, `Setting up container with ID ${req.body.pId}.`)

        userAuths.set(req.body.pId, authContainer)

        await authContainer._initiallyAuthenticate(req)
    }

    if (getUserData(req.body.pId, gameVersion) === undefined) {
        // User does not exist, create new profile from default:
        log(LogLevel.DEBUG, `Create new profile ${req.body.pId}`)

        const userData = getVersionedConfig(
            "UserDefault",
            gameVersion,
            true,
        ) as UserProfile
        userData.Id = req.body.pId
        userData.LinkedAccounts[external_platform] = external_userid

        if (external_platform === "steam") {
            userData.SteamId = req.body.steam_userid
        } else if (external_platform === "epic") {
            userData.EpicId = req.body.epic_userid
        }

        // eslint-disable-next-line no-inner-declarations
        async function getEntitlements(): Promise<string[]> {
            if (isFrankenstein) {
                return new SteamScpcStrategy().get()
            }

            if (gameVersion === "h1") {
                if (external_platform === "steam") {
                    return new SteamH1Strategy().get()
                } else if (external_platform === "epic") {
                    return new EpicH1Strategy().get()
                } else {
                    log(LogLevel.ERROR, "Unsupported platform.")
                    return []
                }
            }

            if (gameVersion === "h2") {
                return new SteamH2Strategy().get()
            }

            if (gameVersion === "h3") {
                if (external_platform === "epic") {
                    return await new EpicH3Strategy().get(
                        req.body.access_token,
                        req.body.epic_userid,
                    )
                } else if (external_platform === "steam") {
                    return await new IOIStrategy(
                        gameVersion,
                        STEAM_NAMESPACE_2021,
                    ).get(req.body.pId)
                } else {
                    log(LogLevel.ERROR, "Unsupported platform.")
                    return []
                }
            }

            log(LogLevel.ERROR, "Unsupported platform.")
            return []
        }

        userData.Extensions.entP = await getEntitlements()

        if (
            Object.prototype.hasOwnProperty.call(
                userData.Extensions,
                "inventory",
            )
        ) {
            // @ts-expect-error No longer in the typedefs.
            delete userData.Extensions.inventory
        }

        writeNewUserData(req.body.pId, userData, gameVersion)
    }

    // Format here follows steam_external, Epic jwt has some different fields
    const userinfo = {
        "auth:method": req.body.grant_type,
        roles: "user",
        sub: req.body.pId,
        unique_name: req.body.pId,
        userid: external_userid,
        platform: external_platform,
        locale: req.body.locale,
        rgn: req.body.rgn,
        pis: external_appid,
        cntry: req.body.locale,
    }

    clearInventoryFor(req.body.pId)

    res!.json({
        access_token: sign(userinfo, "secret", signOptions),
        token_type: "bearer",
        expires_in: 5000,
        refresh_token: randomUUID(),
    })
}