term.clear()
term.setCursorPos(1, 1)
-- Find modems
for _, sModem in ipairs(peripheral.getNames()) do
    if peripheral.getType(sModem) == "modem" then rednet.open(sModem) end
end

local knownHosts = settings.get("knownhosts")
if knownHosts == nil then
	settings.set("knownhosts", {})
	settings.save()
end

local isClient = true
local PK = nil
local crypto = require("ccryptolib")
crypto.random.initWithTiming()
local to = arg[1] -- Who we're trying to talk to (computer id)
write("Your Nickname: ")
local from = read()
if to == nil then print("You need to provide a computer ID!") end
local hexPrivate = settings.get("keys.private")
local hexPublic = settings.get("keys.public")

if hexPrivate == nil then
    settings.set("keys.private", crypto.util.toHex(crypto.random.random(32)))
    hexPrivate = settings.get("keys.private")
	settings.save()
end

if hexPublic == nil then
	settings.set("keys.public", crypto.util.toHex(crypto.x25519.publicKey(crypto.util.fromHex(settings.get("keys.private")))))
	hexPublic = settings.get("keys.public")
    settings.save()
end

local privateKey = crypto.util.fromHex(hexPrivate)
local publicKey = crypto.util.fromHex(hexPublic)

local termX, termY = term.getSize()

function string.starts(String, Start)
    return string.sub(String, 1, string.len(Start)) == Start
end

function rx()
    while true do
        sleep(0)
        sender, rawData = rednet.receive("encChat")
        if sender ~= tonumber(to) then return end
        data = textutils.unserialise(crypto.chacha20.crypt(PK, rawData.nonce, rawData.data))
        if not data.test then return end -- Failed decrypt, possible bad key exchange?
        term.setCursorPos(1, termY)
        term.clearLine()
        print(data.from .. ":" .. data.msg)
		term.blit("> ", "30", "ff")
        term.setCursorPos(3, termY)
    end
end

function tx()
    while true do
        sleep(0)
        term.setCursorPos(1, termY)
        term.blit("> ", "30", "ff")
        local msg = read()
        if (string.starts(msg, "/")) then
            -- Command handler
			msg = string.lower(msg)
			if msg == ("/q" or "/quit" or "/exit") then
				error("Goodbye!")
			end
        else
            term.scroll(-1)
            term.clearLine()
            write(from .. ":" .. msg)
            term.scroll(1)
            nonce = crypto.random.random(12)
            msgData = {
                timestamp = os.epoch("utc"),
                msg = msg,
                from = from,
                test = true
            }
            rednet.send(tonumber(to), {
                nonce = nonce,
                data = crypto.chacha20
                    .crypt(PK, nonce, textutils.serialise(msgData))
            }, "encChat")
        end
    end
end

function ping()
    local fails = 0
    if isClient then
        while true do
            sleep(1)
            nonce = crypto.random.random(12)
            msgData = {timestamp = os.epoch("utc"), test = true}
            rednet.send(tonumber(to), {
                nonce = nonce,
                data = crypto.chacha20
                    .crypt(PK, nonce, textutils.serialise(msgData))
            }, "encPing")

            -- Wait for reply
            sender, reply = rednet.receive("encPingReply", 5)
            if fails > 2 then
                error(
                    "No response from other user, They likely closed their client exiting...")
            end
            if sender == nil or data == nil then
                fails = fails + 1
            elseif crypto.chacha20.crypt(PK, reply.nonce, reply.data) ~=
                tostring(msgData.timestamp) then
                error("Invalid reply from ping, exiting for security.")
            else
                fails = 0
            end
        end
    else
        while true do
            sleep(1)
            sender, data = rednet.receive("encPing", 10)
            if fails > 1 then
                error("No response from other user, They likely closed their client exiting...")
            end
            if sender == nil or data == nil then
                fails = fails + 1
            elseif sender == tonumber(to) then
                timestamp = textutils.unserialise(crypto.chacha20.crypt(PK, data.nonce, data.data)).timestamp
                if os.epoch("utc") > timestamp then
                    fails = 0
                    local nonce = crypto.random.random(12)
                    rednet.send(sender, {
                        nonce = nonce,
                        data = crypto.chacha20
                            .crypt(PK, nonce, tostring(timestamp))
                    }, "encPingReply")
                end
            end
        end
    end
end

function exchange()
    print("sending ping to " .. to)
    rednet.send(tonumber(to), publicKey, "keyExClient")
    sender, data = rednet.receive("keyEx", 1)
    if sender ~= tonumber(to) then -- Switch to server
        print("no response from " .. to .. ". listening for ping...")
        isClient = false
        while true do
            sender, data = rednet.receive("keyExClient")
            if sender == tonumber(to) then
                print("ping from " .. sender .. ". sending and generating keys...")
                rednet.send(tonumber(to), publicKey, "keyEx")
				if knownHosts[tonumber(to)] == nil then
					write("remote pubkey is " .. crypto.util.toHex(data) ..". Do you trust it? (Y/N)")
					local trust = string.lower(read())
					if trust == "y" then
						knownHosts[tonumber(to)] = data
						settings.set("knownhosts", knownHosts)
						settings.save()
						PK = crypto.x25519.exchange(privateKey, data)
					else
						error("Exiting...")
					end
				elseif knownHosts[tonumber(to)] ~= data then
					error("==DANGER== Remote pubkey differs from previous interaction!")
				else
					PK = crypto.x25519.exchange(privateKey, data)
				end
                break
            end
        end
    else
        print("response from " .. to)
		if knownHosts[tonumber(to)] == nil then
			write("remote pubkey is " .. crypto.util.toHex(data) ..". Do you trust it? (Y/N)")
			local trust = string.lower(read())
			if trust == "y" then
				knownHosts[tonumber(to)] = data
				settings.set("knownhosts", knownHosts)
				settings.save()
				PK = crypto.x25519.exchange(privateKey, data)
			else
				error("Exiting...")
			end
		elseif knownHosts[tonumber(to)] ~= data then
			error("==DANGER== Remote pubkey differs from previous interaction!")
		else
			PK = crypto.x25519.exchange(privateKey, data)
		end
        PK = crypto.x25519.exchange(privateKey, data)
    end
	term.clear()
	term.setCursorPos(1,1)
    print("==BEGIN ENCRYPTED CONVERSATION==")
end

exchange()

parallel.waitForAll(rx, tx, ping)
