/*
 *     The Peacock Project - a HITMAN server replacement.
 *     Copyright (C) 2021-2022 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 { Response, Router } from "express"
import {
    gameDifficulty,
    PEACOCKVERSTRING,
    unlockOrderComparer,
    uuidRegex,
} from "./utils"
import { contractSessions, getSession } from "./eventHandler"
import { missionEnd } from "./scoreHandler"
import { getConfig, getVersionedConfig } from "./configSwizzleManager"
import {
    contractIdToHitObject,
    controller,
    isSuit,
    peacockRecentEscalations,
} from "./controller"
import { makeCampaigns } from "./menus/campaigns"
import {
    createLocationsData,
    destinationsMenu,
    getDestinationCompletion,
} from "./menus/destinations"
import type {
    CommonSelectScreenConfig,
    HitsCategoryCategory,
    IHit,
    MissionManifest,
    PeacockLocationsData,
    RequestWithJwt,
    SafehouseCategory,
    SceneConfig,
    Unlockable,
    UserCentricContract,
} from "./types/types"
import {
    getMenuDetailsForEscalation,
    getUserEscalationProgress,
} from "./contracts/escalations/escalationService"
import {
    complications,
    generateCompletionData,
    generateUserCentric,
} from "./contracts/dataGen"
import { log, LogLevel } from "./loggingInterop"
import { contractsModeHome } from "./contracts/contractsModeRouting"
import random from "random"
import { getUserData } from "./databaseHandler"
import {
    createPlayNextTile,
    getSeasonId,
    orderedMissions,
} from "./menus/playnext"
import { randomUUID } from "crypto"
import { planningView } from "./menus/planning"
import { directRoute, withLookupDialog } from "./menus/favoriteContracts"
import { swapToBrowsingMenusStatus } from "./discordRp"
import axios from "axios"
import { getFlag } from "./flags"
import { fakePlayerRegistry } from "./profileHandler"
import { createInventory } from "./inventory"
import { missionsInLocations } from "./contracts/missionsInLocation"
import { json as jsonMiddleware } from "body-parser"
import { hitsCategoryService } from "./contracts/hitsCategoryService"
import {
    GetCompletionDataForLocationQuery,
    MasteryUnlockableQuery,
    MissionEndRequestQuery,
    StashpointQuery,
} from "./types/gameSchemas"
import assert from "assert"

export const preMenuDataRouter = Router()
const menuDataRouter = Router()

// /profiles/page/

function dashEscalations(req: RequestWithJwt, res: Response) {
    const userData = getUserData(req.jwt.unique_name, req.gameVersion)

    const contracts: UserCentricContract[] = []

    for (const groupId of peacockRecentEscalations) {
        const level = getUserEscalationProgress(userData, groupId)

        const userCentric = generateUserCentric(
            controller.resolveContract(
                controller.escalationMappings[groupId][level],
            )!,
            req.jwt.unique_name,
            req.gameVersion,
        )

        if (!userCentric) {
            continue
        }

        contracts.push(userCentric)
    }

    res.json({
        template: null,
        data: {
            Item: {
                Id: req.params.id,
                Type: "ContractList",
                Title: "ContractList",
                Date: new Date().toISOString(),
                Data: contracts.length > 0 ? contracts : null,
            },
        },
    })
}

menuDataRouter.get(
    "/dashboard/Dashboard_Category_Escalation/:subscriptionId/:type/:id",
    dashEscalations,
)

menuDataRouter.get(
    "/dashboard/Dashboard_Category_Escalation/:subscriptionId/:type/:id/:mode",
    dashEscalations,
)
menuDataRouter.get(
    "/ChallengeLocation",
    (req: RequestWithJwt<{ locationId: string }>, res) => {
        const location = getVersionedConfig<PeacockLocationsData>(
            "LocationsData",
            req.gameVersion,
            true,
        ).children[req.query.locationId]
        res.json({
            template: getVersionedConfig(
                "ChallengeLocationTemplate",
                req.gameVersion,
                false,
            ),
            data: {
                Name: location.DisplayNameLocKey,
                Location: location,
                Children:
                    controller.challengeService.getChallengeDataForLocation(
                        req.query.locationId,
                        req.gameVersion,
                        req.jwt.unique_name,
                    ),
            },
        })
    },
)

menuDataRouter.get("/Hub", (req: RequestWithJwt, res) => {
    swapToBrowsingMenusStatus(req.gameVersion)
    const userdata = getUserData(req.jwt.unique_name, req.gameVersion)

    const theTemplate =
        req.gameVersion === "h3"
            ? null
            : req.gameVersion === "h2"
            ? null
            : req.gameVersion === "scpc"
            ? getConfig("FrankensteinHubTemplate", false)
            : getConfig("LegacyHubTemplate", false)

    if (req.gameVersion === "scpc") {
        req.gameVersion = "h1"
    }

    const contractCreationTutorial = controller.resolveContract(
        "d7e2607c-6916-48e2-9588-976c7d8998bb",
    )!

    const locations = getVersionedConfig<PeacockLocationsData>(
        "LocationsData",
        req.gameVersion,
        true,
    )
    const career = {
        // TODO: Add data on elusive challenges. They are not shown on the Career->Challenges page. What the client does with this information is unclear. They are not supported by Peacock as of v5.6.2.
        ELUSIVES_UNSUPPORTED:
            req.gameVersion === "h3"
                ? {
                      Children: [],
                      Name: "UI_MENU_PAGE_PROFILE_CHALLENGES_CATEGORY_ELUSIVE",
                      Location:
                          locations.parents["LOCATION_PARENT_ICA_FACILITY"],
                  }
                : {},
    }
    for (const parent in locations.parents) {
        career[parent] = {
            Children: [],
            Location: locations.parents[parent],
            Name: locations.parents[parent].DisplayNameLocKey,
        }
    }
    for (const child in locations.children) {
        if (
            child === "LOCATION_ICA_FACILITY_ARRIVAL" ||
            child === "LOCATION_HOKKAIDO_SHIM_MAMUSHI"
        ) {
            continue
        }
        const parent = locations.children[child].Properties.ParentLocation
        const location = locations.children[child]
        const challenges = controller.challengeService.getChallengesForLocation(
            child,
            req.gameVersion,
        )
        const challengeCompletion =
            controller.challengeService.countTotalNCompletedChallenges(
                challenges,
                req.jwt.unique_name,
                req.gameVersion,
            )

        career[parent].Children.push({
            IsLocked: location.Properties.IsLocked,
            Name: location.DisplayNameLocKey,
            Image: location.Properties.Icon,
            Icon: location.Type, // should be "location" for all locations
            CompletedChallengesCount:
                challengeCompletion.CompletedChallengesCount,
            ChallengesCount: challengeCompletion.ChallengesCount,
            CategoryId: child,
            Description: `UI_${child}_PRIMARY_DESC`,
            Location: location,
            ImageLocked: location.Properties.LockedIcon,
            RequiredResources: location.Properties.RequiredResources,
            IsPack: false, // should be false for all locations
            CompletionData: generateCompletionData(
                child,
                req.jwt.unique_name,
                req.gameVersion,
            ),
        })
    }

    res.json({
        template: theTemplate,
        data: {
            ServerTile: {
                title: "The Peacock Project",
                image: "images/contracts/novikov_and_magolis/tile.jpg",
                icon: "story",
                url: "",
                select: {
                    header: "Playing on a Peacock instance",
                    title: "The Peacock Project",
                    icon: "story",
                },
            },
            DashboardData: [],
            DestinationsData:
                req.gameVersion === "h3"
                    ? destinationsMenu(req)
                    : req.gameVersion === "h2"
                    ? getConfig("H2DestinationsData", false)
                    : getConfig("LegacyDestinations", false),
            CreateContractTutorial: generateUserCentric(
                contractCreationTutorial,
                req.jwt.unique_name,
                req.gameVersion,
            ),
            LocationsData: createLocationsData(req.gameVersion),
            ProfileData: {
                ChallengeData: {
                    Children: Object.values(career),
                },
                MasteryData: {},
            },
            StoryData: makeCampaigns(req.gameVersion, req.jwt.unique_name),
            FilterData: getVersionedConfig(
                "FilterData",
                req.gameVersion,
                false,
            ),
            StoreData: getVersionedConfig("StoreData", req.gameVersion, false),
            IOIAccountStatus: {
                IsConfirmed: true,
                LinkedEmail: "mail@example.com",
                IOIAccountId: "00000000-0000-0000-0000-000000000000",
                IOIAccountBaseUrl: "https://account.ioi.dk",
            },
            FinishedFinalTest: true,
            Currency: {
                Balance: 0,
            },
            PlayerProfileXpData: {
                XP: userdata.Extensions.progression.PlayerProfileXP.Total,
                Level: userdata.Extensions.progression.PlayerProfileXP
                    .ProfileLevel,
                MaxLevel: 7500,
            },
        },
    })
})

menuDataRouter.get("/SafehouseCategory", (req: RequestWithJwt, res) => {
    const exts = getUserData(req.jwt.unique_name, req.gameVersion).Extensions

    const inventory = createInventory(
        req.jwt.unique_name,
        req.gameVersion,
        exts.entP,
    )

    const safehouseData = {
        template:
            req.gameVersion === "h1"
                ? getConfig("LegacySafehouseTemplate", false)
                : null,
        data: {
            Category: "_root",
            SubCategories: [],
            IsLeaf: false,
            Data: null,
        } as SafehouseCategory,
    }

    for (const item of inventory) {
        if (req.query.type) {
            // if type is specified in query
            if (item.Unlockable.Type !== req.query.type) {
                continue // skip all items that are not that type
            }

            if (
                req.query.subtype &&
                item.Unlockable.Subtype !== req.query.subtype
            ) {
                // if subtype is specified
                continue // skip all items that are not that subtype
            }
        } else if (
            item.Unlockable.Type === "access" ||
            item.Unlockable.Type === "location" ||
            item.Unlockable.Type === "package" ||
            item.Unlockable.Type === "loadoutunlock" ||
            item.Unlockable.Type === "agencypickup"
        ) {
            continue // these types should not be displayed when not asked for
        }

        if (
            item.Unlockable.Subtype === "disguise" &&
            req.gameVersion === "h3"
        ) {
            continue // I don't want to put this in that elif statement
        }

        let category = safehouseData.data.SubCategories.find(
            (cat) => cat.Category === item.Unlockable.Type,
        )
        let subcategory
        if (!category) {
            category = {
                Category: item.Unlockable.Type,
                SubCategories: [],
                IsLeaf: false,
                Data: null,
            }
            safehouseData.data.SubCategories.push(category)
        }

        subcategory = category.SubCategories.find(
            (cat) => cat.Category === item.Unlockable.Subtype,
        )

        if (!subcategory) {
            subcategory = {
                Category: item.Unlockable.Subtype,
                SubCategories: null,
                IsLeaf: true,
                Data: {
                    Type: item.Unlockable.Type,
                    SubType: item.Unlockable.Subtype,
                    Items: [],
                    Page: 0,
                    HasMore: false,
                },
            }
            category.SubCategories.push(subcategory)
        }

        subcategory.Data.Items.push({
            Item: item,
            ItemDetails: {
                Capabilities: [],
                StatList: item.Unlockable.Properties.Gameplay
                    ? Object.entries(item.Unlockable.Properties.Gameplay).map(
                          ([key, value]) => ({
                              Name: key,
                              Ratio: value,
                          }),
                      )
                    : [],
                PropertyTexts: [],
            },
            Type: item.Unlockable.Type,
            SubType: item.Unlockable.SubType,
        })
    }

    for (const [id, category] of safehouseData.data.SubCategories.entries()) {
        if (category.SubCategories.length === 1) {
            // if category only has one subcategory
            safehouseData.data.SubCategories[id] = category.SubCategories[0] // flatten it
            safehouseData.data.SubCategories[id].Category = category.Category // but keep the top category's name
        }
    }

    if (safehouseData.data.SubCategories.length === 1) {
        // if root has only one subcategory
        safehouseData.data = safehouseData.data.SubCategories[0] // flatten it
    }

    res.json(safehouseData)
})

menuDataRouter.get(
    "/stashpoint",
    (req: RequestWithJwt<StashpointQuery>, res) => {
        // Note: this is handled differently for 2016
        // /stashpoint?contractid=e5b6ccf4-1f29-4ec6-bfb8-2e9b78882c85&slotid=4&slotname=gear4&stashpoint=&allowlargeitems=true&allowcontainers=true
        // /stashpoint?contractid=c1d015b4-be08-4e44-808e-ada0f387656f&slotid=3&slotname=disguise3&stashpoint=&allowlargeitems=true&allowcontainers=true
        // /stashpoint?contractid=&slotid=3&slotname=disguise&stashpoint=&allowlargeitems=true&allowcontainers=false
        // /stashpoint?contractid=5b5f8aa4-ecb4-4a0a-9aff-98aa1de43dcc&slotid=6&slotname=stashpoint6&stashpoint=28b03709-d1f0-4388-b207-f03611eafb64&allowlargeitems=true&allowcontainers=false
        const stashData: {
            template: unknown
            data?: {
                SlotId?: string | number
                LoadoutItemsData?: unknown
                UserCentric?: UserCentricContract
                ShowSlotName?: string | number
            }
        } = {
            template: getVersionedConfig(
                "StashpointTemplate",
                req.gameVersion === "h1" ? "h1" : "h3",
                false,
            ),
        }

        const userData = getUserData(req.jwt.unique_name, req.gameVersion)

        const inventory = createInventory(
            req.jwt.unique_name,
            req.gameVersion,
            userData.Extensions.entP,
        )

        if (!req.query.slotname || !(req.query.slotid ?? undefined)) {
            res.status(400).send("invalid?")
            return
        }

        let contractData: MissionManifest | undefined = undefined
        if (req.query.contractid) {
            contractData = controller.resolveContract(req.query.contractid)
        }

        if (req.query.slotname.endsWith(req.query.slotid!.toString())) {
            req.query.slotname = req.query.slotname.slice(
                0,
                -req.query.slotid!.toString().length,
            ) // weird
        }

        stashData.data = {
            SlotId: req.query.slotid,
            LoadoutItemsData: {
                SlotId: req.query.slotid,
                Items: inventory
                    .filter((item) => {
                        if (
                            req.query.slotname === "gear" &&
                            contractData?.Peacock?.noGear === true
                        ) {
                            return false
                        }

                        if (
                            req.query.slotname === "concealedweapon" &&
                            contractData?.Peacock?.noCarriedWeapon === true
                        ) {
                            return false
                        }

                        if (
                            item.Unlockable.Subtype === "disguise" &&
                            req.gameVersion === "h3"
                        ) {
                            return false
                        }

                        return (
                            item.Unlockable.Properties.LoadoutSlot && // only display items
                            (!req.query.slotname ||
                                ((uuidRegex.test(req.query.slotid as string) || // container
                                    req.query.slotname === "stashpoint") && // stashpoint
                                    item.Unlockable.Properties.LoadoutSlot !==
                                        "disguise") || // container or stashpoint => display all items
                                item.Unlockable.Properties.LoadoutSlot ===
                                    req.query.slotname) && // else: display items for requested slot
                            (req.query.allowcontainers === "true" ||
                                !item.Unlockable.Properties.IsContainer) &&
                            (req.query.allowlargeitems === "true" ||
                                item.Unlockable.Properties.LoadoutSlot !==
                                    "carriedweapon")
                        ) // not sure about this one
                    })
                    .map((item) => ({
                        Item: item,
                        ItemDetails: {
                            Capabilities: [],
                            StatList: item.Unlockable.Properties.Gameplay
                                ? Object.entries(
                                      item.Unlockable.Properties.Gameplay,
                                  ).map(([key, value]) => ({
                                      Name: key,
                                      Ratio: value,
                                  }))
                                : [],
                            PropertyTexts: [],
                        },
                        SlotId: req.query.slotid,
                        SlotName: null,
                    })),
                Page: 0,
                HasMore: false,
                HasMoreLeft: false,
                HasMoreRight: false,
                OptionalData: {
                    stashpoint: req.query.stashpoint || "",
                    AllowLargeItems: req.query.allowlargeitems,
                    AllowContainers: req.query.allowcontainers, //?? true
                },
            },
            ShowSlotName: req.query.slotname,
        }

        if (contractData) {
            stashData.data.UserCentric = generateUserCentric(
                contractData,
                req.jwt.unique_name,
                req.gameVersion,
            )
        }

        res.json(stashData)
    },
)

menuDataRouter.get(
    "/missionrewards",
    (req: RequestWithJwt<{ contractSessionId: string }>, res) => {
        const { contractId } = getSession(req.jwt.unique_name)
        const contractData = controller.resolveContract(contractId)

        res.json({
            template: {
                controller: "group",
                id: "mission_rewards",
                selectable: false,
                pressable: false,
                children: [
                    {
                        view: "menu3.MissionRewardPage",
                        selectable: false,
                        pressable: false,
                        data: {
                            $setup: {
                                "$set Drops": {
                                    "$each $.Drops": "$item $.Unlockable",
                                },
                                $in: "$",
                            },
                        },
                    },
                ],
            },
            data: {
                LevelInfo: [
                    0, 6000, 12000, 18000, 24000, 30000, 36000, 42000, 48000,
                    54000, 60000, 66000, 72000, 78000, 84000, 90000, 96000,
                    102000, 108000, 114000,
                ],
                XP: 0,
                Level: 1,
                Completion: 0,
                XPGain: 0,
                Challenges: Object.values(
                    controller.challengeService.getChallengesForContract(
                        getSession(req.jwt.unique_name).contractId,
                        req.gameVersion,
                    ),
                )
                    .flat()
                    // FIXME: This behaviour may not be accurate to original server
                    .filter(
                        (challengeData) =>
                            controller.challengeService.getPersistentChallengeProgression(
                                req.jwt.unique_name,
                                challengeData.Id,
                                req.gameVersion,
                            ).Completed,
                    )
                    .map((challengeData) =>
                        controller.challengeService.compileRegistryChallengeTreeData(
                            challengeData,
                            controller.challengeService.getPersistentChallengeProgression(
                                req.jwt.unique_name,
                                challengeData.Id,
                                req.gameVersion,
                            ),
                            req.gameVersion,
                            req.jwt.unique_name,
                        ),
                    ),
                Drops: [],
                ContractCompletionBonus: 0,
                GroupCompletionBonus: 0,
                LocationHideProgression: true,
                Difficulty: "normal", // FIXME: is this right?
                CompletionData: generateCompletionData(
                    contractData.Metadata.Location,
                    req.jwt.unique_name,
                    req.gameVersion,
                ),
            },
        })
    },
)

menuDataRouter.get(
    "/scoreoverview",
    async (req: RequestWithJwt<MissionEndRequestQuery>, res) => {
        const resJsonFunc = res.json

        res.json = function fakeJsonBind(input) {
            return resJsonFunc.call(this, {
                template: getConfig("scoreoverviewtemplate", false),
                data: input.data.ScoreOverview,
            })
        }

        await innerMissionEnd(req, res)
    },
)

menuDataRouter.get("/Planning", planningView)

menuDataRouter.get(
    "/selectagencypickup",
    (req: RequestWithJwt<{ contractId: string }>, res) => {
        const pickupData = getConfig<SceneConfig>("AgencyPickups", false)

        const selectagencypickup = {
            template: getVersionedConfig(
                "SelectAgencyPickupTemplate",
                req.gameVersion,
                false,
            ),
        } as CommonSelectScreenConfig

        const exts = getUserData(
            req.jwt.unique_name,
            req.gameVersion,
        ).Extensions

        const inventory = createInventory(
            req.jwt.unique_name,
            req.gameVersion,
            exts.entP,
        )

        const contractData = controller.resolveContract(req.query.contractId)

        if (!contractData) {
            log(
                LogLevel.WARN,
                `Unknown contract on SAP: ${req.query.contractId}`,
            )
            return res.status(404).end()
        }

        const scenePath = contractData.Metadata.ScenePath.toLowerCase()

        log(
            LogLevel.DEBUG,
            `Looking up details for contract - Id:${req.query.contractId} (${scenePath})`,
        )

        if (!Object.prototype.hasOwnProperty.call(pickupData, scenePath)) {
            log(
                LogLevel.ERROR,
                `Could not find AgencyPickup data for ${scenePath}! This may cause an unhandled promise rejection.`,
            )
        }

        if (contractData.Peacock?.noAgencyPickupsActive === true) {
            selectagencypickup.data = {
                Unlocked: [],
                Contract: contractData,
                OrderedUnlocks: [],
                UserCentric: generateUserCentric(
                    contractData,
                    req.jwt.unique_name,
                    req.gameVersion,
                ),
            }

            res.json(selectagencypickup)
            return
        }

        const pickupsInScene = pickupData[scenePath]

        const unlockedAgencyPickups = inventory
            .filter((item) => item.Unlockable.Type === "agencypickup")
            .filter(
                (item) =>
                    item.Unlockable.Properties.Difficulty ===
                    contractData.Metadata.Difficulty,
            )
            .map((i) => i.Unlockable)
            .filter((unlockable) => unlockable.Properties.RepositoryId)

        selectagencypickup.data = {
            Unlocked: unlockedAgencyPickups.map(
                (unlockable) => unlockable.Properties.RepositoryId!,
            ),
            Contract: contractData,
            OrderedUnlocks: unlockedAgencyPickups
                .filter((unlockable) =>
                    pickupsInScene.includes(unlockable.Properties.RepositoryId),
                )
                .sort(unlockOrderComparer),
            UserCentric: generateUserCentric(
                contractData,
                req.jwt.unique_name,
                req.gameVersion,
            ),
        }

        res.json(selectagencypickup)
    },
)

menuDataRouter.get(
    "/selectentrance",
    (req: RequestWithJwt<{ contractId: string }>, res) => {
        const entranceData = getConfig<SceneConfig>("Entrances", false)

        const selectEntrance: CommonSelectScreenConfig = {
            template: getVersionedConfig(
                "SelectEntranceTemplate",
                req.gameVersion,
                true,
            ),
        }

        const exts = getUserData(
            req.jwt.unique_name,
            req.gameVersion,
        ).Extensions

        const inventory = createInventory(
            req.jwt.unique_name,
            req.gameVersion,
            exts.entP,
        )

        const contractData = controller.resolveContract(req.query.contractId)

        if (!contractData) {
            log(LogLevel.WARN, `Unknown contract: ${req.query.contractId}`)
            return res.status(404).end()
        }

        const scenePath = contractData.Metadata.ScenePath.toLowerCase()

        log(
            LogLevel.DEBUG,
            `Looking up details for contract - Id:${req.query.contractId} (${scenePath})`,
        )

        if (!Object.prototype.hasOwnProperty.call(entranceData, scenePath)) {
            log(
                LogLevel.ERROR,
                `Could not find Entrance data for ${scenePath}! This may cause an unhandled promise rejection.`,
            )
        }

        const entrancesInScene = entranceData[scenePath]

        const unlockedEntrances = inventory
            .filter((item) => item.Unlockable.Subtype === "startinglocation")
            .filter(
                (item) =>
                    item.Unlockable.Properties.Difficulty ===
                    contractData.Metadata.Difficulty,
            )
            .map((i) => i.Unlockable)
            .filter((unlockable) => unlockable.Properties.RepositoryId)

        selectEntrance.data = {
            Unlocked: unlockedEntrances.map(
                (unlockable) => unlockable.Properties.RepositoryId!,
            ),
            Contract: contractData,
            OrderedUnlocks: unlockedEntrances
                .filter((unlockable) =>
                    entrancesInScene.includes(
                        unlockable.Properties.RepositoryId!,
                    ),
                )
                .sort(unlockOrderComparer),
            UserCentric: generateUserCentric(
                contractData,
                req.jwt.unique_name,
                req.gameVersion,
            ),
        }

        res.json(selectEntrance)
    },
)

menuDataRouter.get("/missionendready", async (req, res) => {
    const sessionDetails = contractSessions.get(
        req.query.contractSessionId as string,
    )

    const retryCount = 1 + Number(req.query.retryCount) || 0

    if (!sessionDetails?.timerEnd) {
        // not ready
        // wait some time proportional to the amount of retries
        await new Promise<void>((resolve) =>
            setTimeout(() => resolve(undefined), retryCount * 100),
        )

        res.json({
            template: getConfig("MissionEndNotReadyTemplate", false),
            data: {
                contractSessionId: req.query.contractSessionId,
                missionEndReady: false,
                retryCount: retryCount,
            },
        })
    } else {
        // ready
        res.json({
            template: getConfig("MissionEndReadyTemplate", false),
            data: {
                contractSessionId: req.query.contractSessionId,
                missionEndReady: true,
                retryCount: retryCount,
            },
        })
    }
})

menuDataRouter.get("/missionend", innerMissionEnd)

menuDataRouter.get("/scoreoverviewandunlocks", innerMissionEnd)

async function innerMissionEnd(
    req: RequestWithJwt<MissionEndRequestQuery>,
    res: Response,
): Promise<void> {
    const result = controller.hooks.getMissionEnd.call(req, res)

    if (result) {
        return
    }

    await missionEnd(req, res)
}

menuDataRouter.get(
    "/Destination",
    (req: RequestWithJwt<{ locationId: string; difficulty?: string }>, res) => {
        const LOCATION = req.query.locationId

        const locData = getVersionedConfig<PeacockLocationsData>(
            "LocationsData",
            req.gameVersion,
            false,
        )

        const locationData = locData.parents[LOCATION]

        const response = {
            template:
                req.gameVersion === "h1"
                    ? getConfig("LegacyDestinationTemplate", false)
                    : null,
            data: {
                Location: {},
                MissionData: {
                    ...getDestinationCompletion(locationData, req),
                    ...{ SubLocationMissionsData: [] },
                },
                ChallengeData: {
                    Children:
                        controller.challengeService.getChallengeDataForDestination(
                            req.query.locationId,
                            req.gameVersion,
                            req.jwt.unique_name,
                        ),
                },
                MasteryData: [],
                DifficultyData: undefined,
            },
        }

        if (req.gameVersion === "h1") {
            response.data.DifficultyData = {
                AvailableDifficultyModes: [
                    {
                        Name: "normal",
                        Available: true,
                    },
                    {
                        Name: "pro1",
                        Available: true,
                    },
                ],
                Difficulty: req.query.difficulty,
                LocationId: LOCATION,
            }
        }

        if (PEACOCK_DEV) {
            log(LogLevel.DEBUG, `Looking up locations details for ${LOCATION}.`)
        }

        const sublocationsData = Object.values(locData.children).filter(
            (subLocation) => subLocation.Properties.ParentLocation === LOCATION,
        )

        response.data.Location = locationData

        if (req.query.difficulty === "pro1") {
            log(LogLevel.DEBUG, "Adjusting for legacy-pro1.")

            const obj = {
                Location: locationData,
                SubLocation: locationData,
                Missions: [controller.missionsInLocations.pro1[LOCATION]].map(
                    (id) =>
                        contractIdToHitObject(
                            id,
                            req.gameVersion,
                            req.jwt.unique_name,
                        ),
                ),
                SarajevoSixMissions: [],
                ElusiveMissions: [],
                EscalationMissions: [],
                SniperMissions: [],
                PlaceholderMissions: [],
                CampaignMissions: [],
                CompletionData: generateCompletionData(
                    sublocationsData[0].Id,
                    req.jwt.unique_name,
                    req.gameVersion,
                ),
            }

            response.data.MissionData.SubLocationMissionsData.push(obj)

            res.json(response)
            return
        }

        for (const e of sublocationsData) {
            log(LogLevel.DEBUG, `Looking up sublocation details for ${e.Id}`)

            const escalations: IHit[] = []

            // every unique escalation from the sublocation
            const allUniqueEscalations: string[] = [
                ...new Set<string>(
                    controller.missionsInLocations.escalations[e.Id] || [],
                ),
            ]

            for (const escalation of allUniqueEscalations) {
                const details = getMenuDetailsForEscalation(
                    escalation,
                    req.jwt.unique_name,
                    req.gameVersion,
                )

                if (details) {
                    escalations.push(details)
                }
            }

            const sniperMissions: IHit[] = []

            for (const sniperMission of controller.missionsInLocations.sniper[
                e.Id
            ] ?? []) {
                sniperMissions.push(
                    contractIdToHitObject(
                        sniperMission,
                        req.gameVersion,
                        req.jwt.unique_name,
                    ),
                )
            }

            const obj = {
                Location: locationData,
                SubLocation: e,
                Missions: [],
                SarajevoSixMissions: [],
                ElusiveMissions: [],
                EscalationMissions: escalations,
                SniperMissions: sniperMissions,
                PlaceholderMissions: [],
                CampaignMissions: [],
                CompletionData: generateCompletionData(
                    e.Id,
                    req.jwt.unique_name,
                    req.gameVersion,
                ),
            }

            const types = [
                ...[
                    [undefined, "Missions"],
                    ["elusive", "ElusiveMissions"],
                ],
                ...(req.gameVersion === "h1" ||
                (req.gameVersion === "h3" &&
                    missionsInLocations.sarajevo["h3enabled"])
                    ? [["sarajevo", "SarajevoSixMissions"]]
                    : []),
            ]

            for (const t of types) {
                let theMissions = !t[0] // no specific type
                    ? controller.missionsInLocations[e.Id]
                    : controller.missionsInLocations[t[0]][e.Id]

                // edge case: ica facility in h1 was only 1 sublocation, so we merge
                // these into a single array
                if (
                    req.gameVersion === "h1" &&
                    !t[0] &&
                    LOCATION === "LOCATION_PARENT_ICA_FACILITY"
                ) {
                    theMissions = [
                        ...controller.missionsInLocations
                            .LOCATION_ICA_FACILITY_ARRIVAL,
                        ...controller.missionsInLocations
                            .LOCATION_ICA_FACILITY_SHIP,
                        ...controller.missionsInLocations.LOCATION_ICA_FACILITY,
                    ]
                }

                if (theMissions !== undefined) {
                    ;(theMissions as string[])
                        .filter(
                            // removes snow festival on h1, traditions on non-h3
                            (m) =>
                                m &&
                                !(
                                    req.gameVersion === "h1" &&
                                    m === "c414a084-a7b9-43ce-b6ca-590620acd87e"
                                ) &&
                                !(
                                    req.gameVersion !== "h3" &&
                                    m === "90c291f6-7ac3-46de-99b2-082e38fccb24"
                                ),
                        )
                        .forEach((c) => {
                            const mission = contractIdToHitObject(
                                c,
                                req.gameVersion,
                                req.jwt.unique_name,
                            )

                            obj[t[1]].push(mission)
                        })
                }
            }

            response.data.MissionData.SubLocationMissionsData.push(obj)
        }

        res.json(response)
    },
)

menuDataRouter.get(
    "/LookupContractPublicId",
    async (req: RequestWithJwt<{ publicid: string }>, res) => {
        const publicIdRegex = /\d{12}/
        let publicid = req.query.publicid
        const templateLookupContractById = getVersionedConfig(
            "LookupContractByIdTemplate",
            req.gameVersion,
            false,
        )

        if (!publicid) {
            return res.status(400).send("no public id specified!")
        }

        while (publicid.includes("-")) {
            publicid = publicid.replace("-", "")
        }

        if (!publicIdRegex.test(publicid)) {
            res.json({
                template: templateLookupContractById,
                data: {
                    PublicId: req.query.publicid,
                    ErrorReason: "notfound",
                },
            })
            return
        }

        const contract = await controller.contractByPubId(
            publicid,
            req.jwt.unique_name,
            req.gameVersion,
        )

        if (!contract) {
            res.json({
                template: templateLookupContractById,
                data: {
                    PublicId: req.query.publicid,
                    ErrorReason: "notfound",
                },
            })
            return
        }

        const location = (
            getVersionedConfig(
                "allunlockables",
                req.gameVersion,
                false,
            ) as readonly Unlockable[]
        ).find((entry) => entry.Id === contract.Metadata.Location)

        res.json({
            template: templateLookupContractById,
            data: {
                Contract: contract,
                Location: location,
                UserCentricContract: generateUserCentric(
                    contract,
                    req.jwt.unique_name,
                    req.gameVersion,
                ),
            },
        })
    },
)

menuDataRouter.get(
    "/HitsCategory",
    (req: RequestWithJwt<{ type: string; page?: number | string }>, res) => {
        const category = req.query.type

        const response: {
            template: unknown
            data?: HitsCategoryCategory
        } = {
            template:
                req.gameVersion === "h1"
                    ? getConfig("LegacyHitsCategoryTemplate", false)
                    : null,
            data: undefined,
        }

        let pageNumber = req.query.page || 0

        if (typeof pageNumber === "string") {
            pageNumber = parseInt(pageNumber, 10)
        }

        pageNumber = pageNumber < 0 ? 0 : pageNumber

        response.data = hitsCategoryService.paginateHitsCategory(
            category,
            pageNumber as number,
            req.gameVersion,
            req.jwt.unique_name,
        )

        res.json(response)
    },
)

menuDataRouter.get(
    "/PlayNext",
    (req: RequestWithJwt<{ contractId: string }>, res) => {
        if (!req.query.contractId) {
            res.status(400).send("no contract id!")
            return
        }

        const currentIdIndex = orderedMissions.indexOf(req.query.contractId)

        const cats = []

        //#region Main story missions
        if (
            currentIdIndex !== -1 &&
            currentIdIndex !== orderedMissions.length - 1
        ) {
            const nextMissionId = orderedMissions[currentIdIndex + 1]
            const nextSeasonId = getSeasonId(currentIdIndex + 1)

            let shouldContinue = true

            // nextSeasonId > gameVersion's integer
            if (parseInt(nextSeasonId) > parseInt(req.gameVersion[1])) {
                shouldContinue = false
            }

            if (shouldContinue) {
                cats.push(
                    createPlayNextTile(
                        req.jwt.unique_name,
                        nextMissionId,
                        req.gameVersion,
                        {
                            CampaignName: `UI_SEASON_${nextSeasonId}`,
                        },
                    ),
                )
            }
        }
        //#endregion

        //#region Plugin missions
        const pluginData = controller.hooks.getNextCampaignMission.call(
            req.query.contractId,
            req.gameVersion,
        )

        if (pluginData) {
            cats.push(
                createPlayNextTile(
                    req.jwt.unique_name,
                    pluginData.nextContractId,
                    req.gameVersion,
                    pluginData.campaignDetails,
                ),
            )
        }
        //#endregion

        res.json({
            template: getConfig("PlayNextTemplate", false),
            data: {
                Categories: cats,
                ProfileId: req.jwt.unique_name,
            },
        })
    },
)

menuDataRouter.get("/LeaderboardsView", (req, res) => {
    res.json({
        template: getConfig("LeaderboardsViewTemplate", false),
        data: {
            LeaderboardUrl: "leaderboardentries",
            LeaderboardType: "singleplayer",
        },
    })
})

const leaderboardEntries = async (
    req: RequestWithJwt<{ contractid: string; difficultyLevel?: string }>,
    res: Response,
) => {
    let difficulty = "unset"

    const parsedDifficulty = parseInt(req.query?.difficultyLevel || "0")

    if (parsedDifficulty === gameDifficulty.casual) {
        difficulty = "casual"
    }

    if (parsedDifficulty === gameDifficulty.normal) {
        difficulty = "normal"
    }

    if (parsedDifficulty === gameDifficulty.master) {
        difficulty = "master"
    }

    const response = {
        template: getConfig("LeaderboardEntriesTemplate", false),
        data: {
            Entries: [] as ApiLeaderboardEntry[],
            Contract: controller.resolveContract(req.query.contractid),
            Page: 0,
            HasMore: false,
            LeaderboardType: "singleplayer",
        },
    }

    type ApiLeaderboardEntry = {
        LeaderboardData: {
            Player: {
                displayName: string
            }
        }
        gameVersion: {
            id: number
            name: string
        }
        platformId: string
        platform: {
            id: number
            name: string
        }
    }

    const entries = (
        await axios.post<ApiLeaderboardEntry[]>(
            `${getFlag("leaderboardsHost")}/leaderboards/entries/${
                req.query.contractid
            }`,
            {
                gameVersion: req.gameVersion,
                difficulty,
                platform: req.jwt.platform,
            },
            {
                headers: {
                    "Peacock-Version": PEACOCKVERSTRING,
                },
            },
        )
    ).data

    const ids: readonly string[] = entries.map((te) =>
        fakePlayerRegistry.index(
            te.LeaderboardData.Player.displayName,
            te.platform.name,
            te.platformId,
        ),
    )

    entries.forEach((entry, index) => {
        // @ts-expect-error Remapping on different types
        entry.LeaderboardData.Player = ids[index]
        return entry
    })

    response.data.Entries = entries

    res.json(response)
}

menuDataRouter.get("/LeaderboardEntries", leaderboardEntries)

menuDataRouter.get(
    "/DebriefingLeaderboards",
    async (
        req: RequestWithJwt<{ contractid: string; difficulty?: string }>,
        res,
    ) => {
        const debriefingLeaderboardsTemplate = getConfig(
            "DebriefingLeaderboardsTemplate",
            false,
        )

        const resJsonFunc = res.json

        res.json = function (input) {
            return resJsonFunc.call(this, {
                template: debriefingLeaderboardsTemplate,
                data: input.data,
            })
        }

        await leaderboardEntries(req, res)
    },
)

menuDataRouter.get("/Contracts", contractsModeHome)

preMenuDataRouter.get(
    "/contractcreation/planning",
    (
        req: RequestWithJwt<{ contractCreationIdOverwrite: string }>,
        res,
        next,
    ) => {
        const createContractPlanningTemplate = getConfig(
            "CreateContractPlanningTemplate",
            false,
        )

        req.url = "/Planning"
        req.query.contractid = req.query.contractCreationIdOverwrite
        req.query.resetescalation = "false"

        const originalJsonFunc = res.json

        res.json = function (originalData) {
            const d = originalData.data

            // create contract planning isn't supposed to have the following properties
            for (const key of [
                "ElusiveContractState",
                "UserCentric",
                "UserContract",
                "UnlockedEntrances",
                "UnlockedAgencyPickups",
                "Objectives",
                "CharacterLoadoutData",
                "ChallengeData",
                "Currency",
                "PaymentDetails",
                "OpportunityData",
                "PlayerProfileXpData",
            ]) {
                d[key] = undefined
            }

            return originalJsonFunc.call(this, {
                template: createContractPlanningTemplate,
                data: d,
            })
        }

        next("router")
    },
)

menuDataRouter.get("/contractsearchpage", (req: RequestWithJwt, res) => {
    const createContractTutorial = controller.resolveContract(
        "d7e2607c-6916-48e2-9588-976c7d8998bb",
    )

    res.json({
        template: getVersionedConfig(
            "ContractSearchPageTemplate",
            req.gameVersion,
            false,
        ),
        data: {
            CreateContractTutorial: generateUserCentric(
                createContractTutorial!,
                req.jwt.unique_name,
                req.gameVersion,
            ),
            LocationsData: createLocationsData(req.gameVersion),
            FilterData: getVersionedConfig(
                "FilterData",
                req.gameVersion,
                false,
            ),
        },
    })
})

menuDataRouter.post(
    "/ContractSearch",
    jsonMiddleware(),
    async (req: RequestWithJwt<{ sorting?: unknown }, string[]>, res) => {
        const specialContracts: string[] = []

        await controller.hooks.getSearchResults.callAsync(
            req.body,
            specialContracts,
        )

        const contracts: { UserCentricContract: UserCentricContract }[] = []

        for (const contract of specialContracts) {
            const userCentric = generateUserCentric(
                controller.resolveContract(contract),
                req.jwt.unique_name,
                req.gameVersion,
            )

            if (!userCentric) {
                log(LogLevel.ERROR, "UC is null! (contract not registered?)")
                continue
            }

            contracts.push({
                UserCentricContract: userCentric,
            })
        }

        res.json({
            template: getVersionedConfig(
                "ContractSearchResponseTemplate",
                req.gameVersion,
                false,
            ),
            data: {
                Data: {
                    Contracts: contracts,
                    TotalCount: contracts.length,
                    Page: 0,
                    ErrorReason: "",
                    HasPrevious: false,
                    HasMore: false,
                },
            },
        })
    },
)

menuDataRouter.get(
    "/DebriefingChallenges",
    (req: RequestWithJwt<{ contractId: string }>, res) => {
        res.json({
            template: getConfig("DebriefingChallengesTemplate", false),
            data: {
                ChallengeData: {
                    Children:
                        controller.challengeService.getChallengeTreeForContract(
                            req.query.contractId,
                            req.gameVersion,
                            req.jwt.unique_name,
                        ),
                },
            },
        })
    },
)

menuDataRouter.get("/contractcreation/create", (req: RequestWithJwt, res) => {
    let cUuid = randomUUID()
    const createContractReturnTemplate = getConfig(
        "CreateContractReturnTemplate",
        false,
    )

    // if for some reason the id is already in use, generate a new one
    // the math says this is like a one in a billion chance though, I think
    while (controller.resolveContract(cUuid)) {
        cUuid = randomUUID()
    }

    const sesh = getSession(req.jwt.unique_name)

    const one = "1"
    const two = `${random.int(10, 99)}`
    const three = `${random.int(1_000_000, 9_999_999)}`
    const four = `${random.int(10, 99)}`

    const contractId = [one, two, three, four].join("-")
    const joined = [one, two, three, four].join("")

    // See my comment in contractRouting.ts about the Math.ceil call
    const timeLimit = Math.ceil(
        (sesh.timerEnd as number) - (sesh.timerStart as number),
    )
    const timeSeconds = timeLimit % 60
    const timeMinutes = Math.trunc(timeLimit / 60) % 60
    const timeHours = Math.trunc(timeLimit / 3600)
    const timeLimitStr = `${
        timeHours ? `${timeHours}:` : ""
    }${`0${timeMinutes}`.slice(-2)}:${`0${timeSeconds}`.slice(-2)}`

    res.json({
        template: createContractReturnTemplate,
        data: {
            Contract: {
                Title: {
                    $loc: {
                        key: "UI_CONTRACTS_UGC_TITLE",
                        data: [contractId],
                    },
                },
                Description: "UI_CONTRACTS_UGC_DESCRIPTION",
                Targets: Array.from(sesh.kills)
                    .filter((kill) =>
                        sesh.markedTargets.has(kill._RepositoryId),
                    )
                    .map((km) => {
                        return {
                            RepositoryId: km._RepositoryId,
                            Selected: true,
                            Weapon: {
                                RepositoryId: km.KillItemRepositoryId,
                                KillMethodBroad: km.KillMethodBroad,
                                KillMethodStrict: km.KillMethodStrict,
                                RequiredKillMethodType: 3,
                            },
                            Outfit: {
                                RepositoryId: km.OutfitRepoId,
                                Required: true,
                                IsHitmanSuit: isSuit(
                                    km.OutfitRepoId,
                                    req.gameVersion,
                                ),
                            },
                        }
                    }),
                ContractConditions: complications(timeLimitStr),
                PublishingDisabled: false,
                Creator: req.jwt.unique_name,
                ContractId: cUuid,
                ContractPublicId: joined,
            },
        },
    })
})

const createLoadSaveMiddleware =
    (menuTemplate: string) =>
    (req: RequestWithJwt<{ sessionIds?: string }, string[]>, res: Response) => {
        const template = getVersionedConfig(
            menuTemplate,
            req.gameVersion,
            false,
        )
        const doneContracts: string[] = []

        const response = {
            template,
            data: {
                Contracts: [] as UserCentricContract[],
                PaymentEligiblity: {},
            },
        }

        for (const e of req.body) {
            if (e && !doneContracts.includes(e)) {
                doneContracts.push(e)

                const contract = controller.resolveContract(e)

                if (!contract) {
                    log(LogLevel.WARN, `Unknown contract in L/S: ${e}`)
                    continue
                }

                response.data.Contracts.push(
                    generateUserCentric(
                        contract,
                        req.jwt.unique_name,
                        req.gameVersion,
                    )!,
                )
            }
        }

        if (req.gameVersion === "h1") {
            for (const e of req.body) {
                if (e) {
                    response.data.PaymentEligiblity[e] = false
                }
            }
        } else {
            for (const sessionId of req.query.sessionIds?.split(",") || []) {
                response.data.PaymentEligiblity[sessionId] = false
            }
        }

        res.json(response)
    }

menuDataRouter.post(
    "/Load",
    jsonMiddleware(),
    createLoadSaveMiddleware("LoadMenuTemplate"),
)

menuDataRouter.post(
    "/Save",
    jsonMiddleware(),
    createLoadSaveMiddleware("SaveMenuTemplate"),
)

menuDataRouter.get("/PlayerProfile", (req, res) => {
    const playerProfilePage = getConfig("PlayerProfilePage", true)

    res.json(playerProfilePage)
})

menuDataRouter.get(
    // who at IOI decided this was a good route name???!
    "/LookupContractDialogAddOrDeleteFromPlaylist",
    withLookupDialog,
)

menuDataRouter.get(
    // this one is sane Kappa
    "/contractplaylist/addordelete/{contractId}",
    directRoute,
)

menuDataRouter.get("/GetPlayerProfileXpData", (req: RequestWithJwt, res) => {
    const userData = getUserData(req.jwt.unique_name, req.gameVersion)

    res.json({
        template: null,
        data: {
            PlayerProfileXpData: {
                XP: userData.Extensions.progression.PlayerProfileXP.Total,
                Level: userData.Extensions.progression.PlayerProfileXP
                    .ProfileLevel,
                MaxLevel: 7500,
            },
        },
    })
})

menuDataRouter.get(
    "/GetMasteryCompletionDataForLocation",
    (req: RequestWithJwt<GetCompletionDataForLocationQuery>, res) => {
        res.json(
            generateCompletionData(
                req.query.locationId,
                req.jwt.unique_name,
                req.gameVersion,
            ),
        )
    },
)

menuDataRouter.get(
    "/MasteryUnlockable",
    (req: RequestWithJwt<MasteryUnlockableQuery>, res) => {
        let sniperLoadouts = getConfig("SniperLoadouts", false)
        let masteryUnlockTemplate = getConfig(
            "MasteryUnlockablesTemplate",
            false,
        )

        const location = (() => {
            switch (req.query.unlockableId?.split("_").slice(0, 3).join("_")) {
                case "FIREARMS_SC_HERO":
                    return "LOCATION_AUSTRIA"
                case "FIREARMS_SC_SEAGULL":
                    return "LOCATION_SALTY_SEAGULL"
                case "FIREARMS_SC_FALCON":
                    return "LOCATION_CAGED_FALCON"
                default:
                    assert.fail("fell through switch (bad query?)")
            }
        })()

        if (req.gameVersion === "scpc") {
            masteryUnlockTemplate = JSON.parse(
                JSON.stringify(masteryUnlockTemplate).replace(
                    /UI_MENU_PAGE_MASTERY_LEVEL_SHORT+/g,
                    "UI_MENU_PAGE_MASTERY_LEVEL",
                ),
            )

            sniperLoadouts = JSON.parse(
                JSON.stringify(sniperLoadouts).replace(/hawk\/+/g, ""),
            )
        }

        res.json({
            template: masteryUnlockTemplate,
            data: {
                CompletionData: generateCompletionData(
                    location,
                    req.jwt.unique_name,
                    req.gameVersion,
                ),
                Drops: [
                    {
                        IsLevelMarker: false,
                        Unlockable:
                            sniperLoadouts[location][req.query.unlockableId][
                                "Unlockable"
                            ],
                        Level: 20,
                        IsLocked: false,
                        TypeLocaKey:
                            "UI_MENU_PAGE_MASTERY_UNLOCKABLE_NAME_weapon",
                    },
                ],
                Unlockable:
                    sniperLoadouts[location][req.query.unlockableId][
                        "MainUnlockable"
                    ],
            },
        })
    },
)

export { menuDataRouter }