--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| 	Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]

local pcall = pcall
local util = util
local ix = ix
local string = string
local net = net
local ipairs = ipairs
local IsValid = IsValid
local os = os
local L = L
local math = math
local table = table
local pairs = pairs
local print = print
local CHTTP = CHTTP
local player = player
local CAMI = CAMI

if (!CHTTP) then
    pcall(require, "chttp")
end

local PLUGIN = PLUGIN
PLUGIN.TYPE_UNKNOWN = 1
PLUGIN.TYPE_KNOWN = 2

local COOKIE_KEY = "qvFT4QSSST8K4tZF"

local fileChecks = {
    {"hl2_misc_001.vpk", "hl2"},
    {"ep2_pak_008.vpk", "ep2"},
    {"cstrike_pak_003.vpk", "css", "cstrike"},
    {"bin/client_panorama.dll", "csgo"},
    {"detail.vbsp", "gmod", "garrysmod"}
}

util.AddNetworkString("RecieveDupe")

ix.util.Include("sv_antialt_db.lua")

ix.lang.AddTable("english", {
    altSignupProxy = "Vous essayez de rejoindre Willard Networks en tant que nouvel utilisateur tout en utilisant un proxy ou un VPN. Pour lutter contre les autres comptes, nous ne l'autorisons pas.\n\nVeuillez désactiver votre proxy/VPN et rejoindre le serveur. Après avoir rejoint le serveur avec succès une fois, vous pouvez réactiver votre proxy/VPN pour de futures visites.\n\nSi vous n'utilisez pas de proxy ou de VPN, veuillez contacter un membre de l'équipe du STAFF sur nos forums ou discord"
})

ix.log.AddType("altNewClient", function(client)
    return string.format("[AltChecker] %s rejoint pour la première fois.", client:SteamName())
end)
ix.log.AddType("altKnownNewCookie", function(client)
    return string.format("[AltChecker] %s rejoint avec une installation inconnue.", client:SteamName())
end)
ix.log.AddType("altKnownCookieMatched", function(client, matched, total)
    return string.format("[AltChecker] %s rejoint sans cookie, mais l'installation correspond au cookie existant. %d/%d", client:SteamName(), matched, total)
end)

hook.Add("PlayerInitialSpawn", "bastionAntiAlt", function(client)
    if (client:IsBot()) then return end

    client.ixAltData = {
        checks = 4 + #fileChecks,
        altLogging = 0,
        error = false,
        received = false,
        checkComplete = false,
        newAltFound = false,

        mode = 0,
        localCookie = "",
        cookies = false,
        ip = false,
        timestamps = false,

        otherCookies = {},
        otherIPs = {},
        otherTimestamps = {},
        otherTimestampsNZ = {},
        otherTimeMatches = {},

        discordAlert = {
            cookies = {},
            timestamps = {},
            ips = {},
            altIDs = {}
        }
    }

    -- Lookup user data
    PLUGIN:AltLoadUserData(client)

    if (PLUGIN.API_KEY) then
    -- Lookup user IP
        PLUGIN:AltLookupIP(client)
    else
        client.ixAltData.checkes = client.ixAltData.checks - 1
    end

    -- Check for IP matches
    PLUGIN:AltLookupIPMatches(client)

    -- Request cookie and install timestamps
    PLUGIN:RequestClientData(client)
end)

function PLUGIN:RequestClientData(client)
    net.Start("RecieveDupe")
    net.WriteUInt(1, 3)
    net.WriteString(COOKIE_KEY)
    for _, v in ipairs(fileChecks) do
        net.WriteString(v[1])
        net.WriteString(v[3] or v[2])
    end
    net.Send(client)
end

net.Receive("RecieveDupe", function(len, client)
    if (!IsValid(client) or !client.ixAltData or client.ixAltData.received) then return end

    local data = client.ixAltData
    data.received = true
    -- set cookie
    data.localCookie = net.ReadString()
    -- set file timestamps
    data.timestamps = {}
    for i = 1, #fileChecks do
        data.timestamps[i] = net.ReadUInt(32)
    end

    -- Check for cookie matches
    if (data.localCookie != "") then
        PLUGIN:AltLookupCookieMatches(client, data)
    else
        PLUGIN:AltPreFinalChecking(client)
    end

    -- Check for install timestamp matches
    data.otherTimestamps = {}
    data.otherTimestampsNZ = {}
    for i = 1, #fileChecks do
        PLUGIN:AltLookupTimestampMatches(client, data, fileChecks[i][2], data.timestamps[i])
    end
end)

function PLUGIN:AltPreFinalChecking(client)
    if (!IsValid(client)) then return end

    local data = client.ixAltData
    -- if we errored, don't do anything
    -- check will be reexecuted when the client rejoins
    if (data.error) then return end

    -- check if all queries finished
    data.checks = data.checks - 1
    if (data.checks != 0) then return end

    -- cookie matches (STRONG)
    if (#data.otherCookies > 0) then
        for _, v in ipairs(data.otherCookies) do
            self:AltFound(client, v[2], "cookie", "cookie")
            data.discordAlert.cookies[#data.discordAlert.cookies + 1] = (v[2] or "").." ("..(v[1] or "")..")"
            data.discordAlert.altIDs[v[2]] = true
        end
    end

    -- IP (WEAK)
    if (#data.otherIPs > 0) then
        for _, v in ipairs(data.otherIPs) do
            data.discordAlert.ips[#data.discordAlert.ips + 1] = v[2]
            data.discordAlert.altIDs[v[2]] = true
        end
    end

    -- time matches (STRONG-MEDIUM)
    self:AggregateTimestampMatches(data)

    -- If no local cookie and player is known, check if a known cookie was time-matched
    if (data.localCookie == "" and data.mode == self.TYPE_KNOWN) then
        self:FindMatchingCookie(client, data)
    end

    if (#data.otherTimeMatches > 0) then
        -- go looking for the other clients that own our matched timestamps
        self:AltLookupCookieForTimestamps(client, data, #fileChecks)
    else
        -- else we don't need to wait for the lookup above
        data.checkComplete = true
        self:AltFinalChecking(client)
    end
end

function PLUGIN:AltFinalChecking(client)
    if (!IsValid(client)) then return end

    local data = client.ixAltData
    if (!data.checkComplete or data.altLogging != 0) then return end

    self:DiscordAlert(client)

    -- update IP list
    local steamID = client:SteamID64()
    local ip = self.GetIPAddress(client)
    local query = mysql:Select("bastion_antialt_userips")
        query:Where("steamid", steamID)
        query:Where("ip", ip)
        query:Callback(function(result)
            if (!result or #result == 0) then
                local query2 = mysql:Insert("bastion_antialt_userips")
                    query2:Insert("steamid", steamID)
                    query2:Insert("ip", ip)
                    query2:Insert("last_seen", os.time())
                query2:Execute()
            else
                local query2 = mysql:Update("bastion_antialt_userips")
                    query2:Where("id", result[1].id)
                    query2:Update("last_seen", os.time())
                query2:Execute()
            end
        end)
    query:Execute()

    -- Kick if new player on proxy/vpn
    if (data.mode == self.TYPE_UNKNOWN) then
        if (self.API_KEY and (data.ip.proxy == "yes" or (data.ip.risk or 0) > 60)) then
            if (ix.config.Get("VPNKick")) then
                self:ProxyAlert(client)
                client:Kick(L("altSignupProxy", client))
            else
                if (!self:NotifyProxyJoin(client) or ix.config.Get("ProxyAlwaysAlert")) then
                    self:ProxyAlert(client)
                end
            end
        elseif (data.localCookie == "") then
            ix.log.Add(client, "altNewClient")
            self:GenerateCookie(client, data)
        else
            -- Update the cookie's timestamps
            self:StoreCookieInfo(data, fileChecks)
            -- Add this cookie to client as well so it can be time matched/timestamps updated
            self:StoreClientCookie(client, data)
        end
    elseif (data.localCookie == "") then
        ix.log.Add(client, "altKnownNewCookie")
        self:GenerateCookie(client, data)
    else
        -- Update the cookie's timestamps
        self:StoreCookieInfo(data, fileChecks)
        -- Add this cookie to client as well so it can be time matched/timestamps updated
        self:StoreClientCookie(client, data)
    end

    if (sam) then
        timer.Simple(3, function()
            if (!IsValid(client)) then return end
            local query1 = mysql:Select("bastion_antialt_alts")
                query1:Where("steamid", client:SteamID64())
                query1:Select("alt_id")
                query1:Callback(function(result)
                    if (!IsValid(client)) then return end
                    if (!result or #result == 0) then return end
                    local query2 = mysql:Select("bastion_antialt_alts")
                        query2:Where("alt_id", result[1].alt_id)
                        query2:WhereNotEqual("steamid", client:SteamID64())
                        query2:Select("steamid")
                        query2:Callback(function(result2)
                            if (!IsValid(client)) then return end
                            if (!result2 or #result2 == 0) then return end

                            for k, v in ipairs(result2) do
                                sam.player.is_banned(util.SteamIDFrom64(v.steamid), function(banned)
                                    if (!banned or !IsValid(client)) then return end
                                    client:Kick("You have a banned alt account.")
                                    return
                                end)
                            end
                        end)
                    query2:Execute()
                end)
            query1:Execute()
        end)
    end
end

--[[
    COOKIE STUFF
]]
function PLUGIN:WhitelistPlayer(client)
    local data = client.ixAltData
    if (!data) then return end

    if (data.localCookie) then
        ix.log.Add(client, "altNewClient")
        self:GenerateCookie(client, data)

        return true
    end
end

function PLUGIN.RandomString()
    local result = {} -- The empty table we start with
    while (#result != 64) do
        local char = string.char(math.random(32, 126))
        if (string.find(char, "%w")) then
            result[#result + 1] = char
        end
    end

    return table.concat(result)
end

function PLUGIN:GenerateCookie(client, data)
    local cookie = self.RandomString()
    self:UpdateLocalCookie(client, data, cookie)

    local query = mysql:Insert("bastion_antialt_users")
        query:Insert("steamid", client:SteamID64())
        query:Insert("steam_name", client:SteamName())
        query:Insert("cookie", cookie)
    query:Execute()
end

function PLUGIN:UpdateLocalCookie(client, data, cookie)
    data.localCookie = cookie

    net.Start("RecieveDupe")
        net.WriteUInt(2, 3)
        net.WriteString(COOKIE_KEY)
        net.WriteString(cookie)
    net.Send(client)

    self:StoreCookieInfo(data, fileChecks)
end

function PLUGIN:FindMatchingCookie(client, data)
    for _, v in ipairs(data.otherTimeMatches) do
        for _, v1 in ipairs(data.cookies) do
            if (v1.cookie == v[1]) then
                -- found a timestamp match belonging to the client, restore cookie
                -- in case of e.g. gmod reinstall
                ix.log.Add(client, "altKnownCookieMatched", v[2], #fileChecks)
                self:UpdateLocalCookie(client, data, v[1])
                return
            end
        end
    end
end

--[[
    LOGGING AND ALERTING
]]
local ALT_OFFSET = 0
function PLUGIN:AltFound(client, steamid, type, info)
    local ids = {client:SteamID64(), steamid}
    local data = client.ixAltData

    data.altLogging = data.altLogging + 1

    local query = mysql:Select("bastion_antialt_alts")
        query:WhereIn("steamid", ids)
        query:Callback(function(result)
            if (!result or #result == 0) then
                local alt_id = os.time() + ALT_OFFSET
                ALT_OFFSET = ALT_OFFSET + 1

                local text = table.concat(ids,"+")..": "..info.." (alt-id: "..alt_id..")"
                self:InsertNewAlt(ids[1], alt_id, type, text)
                self:InsertNewAlt(steamid, alt_id, type, text)

                data.newAltFound = true
            elseif (#result == 1) then
                self:InsertNewAlt(
                    result[1].steamid == steamid and ids[1] or steamid,
                    result[1].alt_id,
                    type,
                    table.concat(ids,"+")..": "..info.." (alt-id: "..result[1].alt_id..")"
                )

                data.newAltFound = true
            elseif (result[2].alt_id != result[1].alt_id) then
                self:MergeAlts(
                    result[2].alt_id,
                    result[1].alt_id,
                    table.concat(ids,"+")..": "..info.." (alt-id: "..result[1].alt_id..")"
                )

                data.newAltFound = true
            end

            data.altLogging = data.altLogging - 1
            self:AltFinalChecking(client)
        end)
    query:Execute()
end

function PLUGIN:DiscordAlert(client)
    if (!self.DISCORD_WEBHOOK_ALTS or self.DISCORD_WEBHOOK_ALTS == "") then return end

    local data = client.ixAltData
    if (!data.newAltFound) then return end

    local tbl = {
        embeds = {{
            title = "Alt found for "..client:SteamName(),
            description = "An alt account match was found for **"..client:SteamName().."** (*"..client:SteamID64().."*)\n\n__COOKIE__: 99.99% certainty via installed cookie\nShown as 'SteamID64 (SteamName)'\n__TIMESTAMP__: check via installation date/time or absense of mounted games (hl2,ep2,css,csgo,gmod)\nMore matches = more certainty, especially if all/most are installed\nShown as 'SteamID64 (SteamName; date/time matches - installed matches)'\n__IP__: users that connected from the same IP address at any point\nShown as 'SteamID64'",
            color = 13632027,
            timestamp = os.date("%Y-%m-%d %X%z"),
            footer = {
                text = "Bastion Alt Checker for GMod by Gr4Ss"
            },
            author = {
                name = "Bastion Alt Checker"
            },
            fields = {
            }
        }}
    }

    if (data.discordAlert.cookies and #data.discordAlert.cookies > 0) then
        table.insert(tbl.embeds[1].fields, {name = "COOKIE", value = table.concat(data.discordAlert.cookies, "\n")})
    end
    if (data.discordAlert.timestamps and #data.discordAlert.timestamps > 0) then
        table.insert(tbl.embeds[1].fields, {name = "TIMESTAMP", value = table.concat(data.discordAlert.timestamps, "\n")})
    end
    if (data.discordAlert.ips and #data.discordAlert.ips > 0) then
        table.insert(tbl.embeds[1].fields, {
            name = "IP",
            value = string.format("Address: [%s](https://proxycheck.io/threats/%s)\n", data.ipAddress, data.ipAddress)..
                table.concat(data.discordAlert.ips, "\n")
        })
    end
    local ipLinks = {"["..client:SteamID64().."](https://steamidfinder.com/lookup/"..client:SteamID64().."/)"}
    for k in pairs(data.discordAlert.altIDs) do
        ipLinks[#ipLinks + 1] = "["..k.."](https://steamidfinder.com/lookup/"..k.."/)"
    end
    table.insert(tbl.embeds[1].fields, {
        name = "SteamID Finder Links",
        value = table.concat(ipLinks,"\n")
    })

    for _, field in ipairs(tbl.embeds[1].fields) do
        if (string.len(field.value) > 1024) then
            field.value = string.sub(field.value, 1, 1024)
        end
    end

    local request = {
        failed = function(error) print("discord error", error) end,
        success = function(code, body, headers)
            if (code != 200) then print("discord error", code, body) end
        end,
        method = "post",
        url = self.DISCORD_WEBHOOK_ALTS,
        body = util.TableToJSON(tbl),
        type = "application/json; charset=utf-8"
    }

    CHTTP(request)
end

function PLUGIN:ProxyAlert(client)
    if (!self.DISCORD_WEBHOOK_ALTS or self.DISCORD_WEBHOOK_ALTS == "") then return end

    local ip, steamID = client.ixAltData.ipAddress, client:SteamID64()
    local tbl = {
        embeds = {{
            title = "New player using VPN - "..client:SteamName(),
            description = client:SteamName().." joined WN for the first time, but was using a proxy/VPN. They have been kicked.\n\n"..
            string.format("More info: [%s](https://proxycheck.io/threats/%s) & [%s](https://steamidfinder.com/lookup/%s/)", ip, ip, steamID, steamID),
            color = 16312092,
            timestamp = os.date("%Y-%m-%d %X%z"),
            footer = {
                text = "Bastion Alt Checker for GMod by Gr4Ss"
            },
            author = {
                name = "Bastion Alt Checker"
            }
        }}
    }

    local request = {
        failed = function(error) print("discord error", error) end,
        method = "post",
        url = self.DISCORD_WEBHOOK_ALTS,
        body = util.TableToJSON(tbl),
        type = "application/json; charset=utf-8"
    }

    CHTTP(request)
end

function PLUGIN:NotifyProxyJoin(client)
    local bSend = false
    for _, v in ipairs(player.GetAll()) do
        if (CAMI.PlayerHasAccess(v, "Helix - Proxy Notify")) then
            bSend = true
            v:NotifyLocalized("bastionProxyNotify", client:SteamName())
        end
    end

    return bSend
end