#!/usr/bin/env lua
--[[
    Abrechenbarkeit - A simple trust-based ledger
    Copyright 2024 metamuffin
    Copyright 2024 dasriley

    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, version 3 of the License only.

    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/>.
]] --

-- TODO: allow unicode
-- TODO: somehow remove _opt variants
local matchers = {
    time = "(%d+)",
    user = "([%w_@ -]+)",
    user_opt = "([%w_@ -]*)",
    amount = "(-?%d+)",
    amount_opt = "(-?%d*)",
    comment = "([%w_ -]+)",
    comment_opt = "([%w_ -]*)",
    barcode = "([%w_-]+)",
    barcode_opt = "([%w_-]*)",
    name = "([%w_ -]+)",
    name_opt = "([%w_ -]*)",
}
local matchers_global = (function()
    local s = {}
    for k, v in pairs(matchers) do s[k] = ("^%s$"):format(v) end
    return s
end)()

local function escape(s)
    return s:gsub("<", "&lt;"):gsub("<", "&lt;")
end

local function urldecode(s)
    if s == nil then return nil end
    local t, _ = s:gsub("+", " "):gsub("%%(%x%x)",
        function(cap) return string.char(tonumber(cap, 16)) end)
    return t
end

local function urlencode(s)
    if s == nil then return nil end
    return s:gsub("[^%w]",
        function(cap) return string.format("%%%02x", string.byte(cap, 1)) end)
end

local function parse_query(q)
    if q == nil then return {} end
    local data = {}
    for pair in string.gmatch(q, "([^&]+)") do
        local flag = string.match(pair, "^([^=]+)$")
        if flag ~= nil then
            data[flag] = "1"
        else
            local key, value = string.match(pair, "^([^=]+)=([^=]*)$")
            if key ~= nil and value ~= nil then
                data[key] = urldecode(value)
            end
        end
    end
    return data
end

local function load_config()
    local log = io.open("config", "r")
    if log == nil then return {} end
    local config = {}
    for l in log:lines("l") do
        if l ~= "" and l[0] ~= "#" then
            local key, value = string.match(l, "^([^=]+)=([^=]*)")
            if key ~= nil and value ~= nil then
                config[key] = value
            end
        end
    end
    return config
end

local function load_translations(langs)
    local t = {}
    for _, lcode in ipairs(langs) do
        local file = io.open(string.format("locale/%s.ini", lcode), "r")
        if file ~= nil then
            for l in file:lines("l") do
                if l ~= "" then
                    local key, value = string.match(l, "^([^=%s]+)%s?=%s?([^=]*)")
                    if key ~= nil and value ~= nil then
                        t["+" .. key] = value
                    end
                end
            end
        end
    end
    return t
end

local config = load_config()
local path = os.getenv("PATH_INFO")
local method = os.getenv("REQUEST_METHOD")
local query = parse_query(os.getenv("QUERY_STRING"))
local translations = load_translations({ "en", config.language })

local stylesheet = io.open("style.css"):read("a")
local script = io.open("script.js"):read("a")

local function format(template, params)
    params = params or {}
    if template == nil then return "NIL TEMPLATE" end
    local s, _ = string.gsub(template, "{([%w\\+_\\.!]+)}", function(n)
        local esc = n:sub(1, 1) == "!"
        if esc then n = n:sub(2) end
        local s = params[n] or translations[n] or "NIL PARAM"
        if not esc then s = format(s, params) end
        return esc and escape(s) or s
    end)
    return s
end

local function format_amount(amount, tag, classes)
    local s = format("{+price.amount}", {
        sign = amount >= 0 and "+" or "-",
        amount = string.format("%.2f", math.abs(amount / 100)),
        unit = config.unit or "€"
    })
    if tag == nil then return s end
    return format(
        [[<{tag} class="amount-{sign} {classes}">{content}</{tag}>]], {
            tag = tag,
            sign = amount >= 0 and "pos" or "neg",
            classes = classes or "",
            content = s
        })
end

local function format_list(list)
	if list == nil then
		return nil
	end

	local not_first = false
	local out = ""
	for _, barcode in pairs(list) do
		if not_first then
			out = out .. ", " .. barcode
		else 
			out = barcode
		end

		not_first = true
	end

	return out
end

local function get_user_theme(username)
    local c = ""
    if username == "_jeb" then
        c = "html { animation: 2s jeb infinite; }"
        c = c .. "@keyframes jeb {\n"
        for i = 0, 100 do
            c = c .. string.format("%.02f%% { --hue: %.02f; } \n", i, i / 100 * 360)
        end
        c = c .. "\n}"
    elseif username == "Dinnerbone" then
        c = "html { transform: scale(-1); } "
    end
    return c
end

local function format_duration(t)
    local unit = nil
    local n = nil
    if t > 86400 then
        n = math.floor(t / 86400)
        unit = "day"
    elseif t > 3600 then
        n = math.floor(t / 3600)
        unit = "hour"
    elseif t > 60 then
        n = math.floor(t / 60)
        unit = "minute"
    else
        n = t
        unit = "second"
    end
    return format("{+time.delta_past}",
        { n = n, unit = translations["+time." .. unit .. (n ~= 1 and "s" or "")] })
end

local function respond(status, title, body)
    print(string.format("Status: %d", status))
    print("Content-Type: text/html")
    print("")
    print(format([[
        <!DOCTYPE html>
        <html><head>
            <title>{title}</title>
            <meta charset="utf-8" />
            <style>{style}</style>
            <style>{user_style}</style>
            <script>{script}</script>
            {head_extra}
        </head>
        <body>
            <nav>
                <a class="logo" href="/">
					<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-receipt-cutoff" viewBox="0 0 16 16">
					  <path d="M3 4.5a.5.5 0 0 1 .5-.5h6a.5.5 0 1 1 0 1h-6a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h6a.5.5 0 1 1 0 1h-6a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h6a.5.5 0 1 1 0 1h-6a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5M11.5 4a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h1a.5.5 0 0 0 0-1z"/>
					  <path d="M2.354.646a.5.5 0 0 0-.801.13l-.5 1A.5.5 0 0 0 1 2v13H.5a.5.5 0 0 0 0 1h15a.5.5 0 0 0 0-1H15V2a.5.5 0 0 0-.053-.224l-.5-1a.5.5 0 0 0-.8-.13L13 1.293l-.646-.647a.5.5 0 0 0-.708 0L11 1.293l-.646-.647a.5.5 0 0 0-.708 0L9 1.293 8.354.646a.5.5 0 0 0-.708 0L7 1.293 6.354.646a.5.5 0 0 0-.708 0L5 1.293 4.354.646a.5.5 0 0 0-.708 0L3 1.293zm-.217 1.198.51.51a.5.5 0 0 0 .707 0L4 1.707l.646.647a.5.5 0 0 0 .708 0L6 1.707l.646.647a.5.5 0 0 0 .708 0L8 1.707l.646.647a.5.5 0 0 0 .708 0L10 1.707l.646.647a.5.5 0 0 0 .708 0L12 1.707l.646.647a.5.5 0 0 0 .708 0l.509-.51.137.274V15H2V2.118z"/>
					</svg>
                    {+appname}
                </a>
                <a href="/?users">{+users}</a>
                <a href="/?spus">{+spus}</a>
                <a href="/?products">{+products}</a>
                <a href="/?log">{+log}</a>
                <a href="/?about">{+about}</a>
            </nav>
    ]], {
        title = escape(title),
        style = stylesheet,
        user_style = get_user_theme(path and path:sub(2)),
        script = script,
        head_extra = config.head_extra or ""
    }))
    if config.header ~= nil then
        print(config.header)
    end
    body()
    if config.footer ~= nil then
        print(config.footer)
    end
    print("</body></html>")
end

local function error_box(message, params)
    return string.format([[<div class="notif error"><p>Error: %s</p></div>]], escape(format(message, params)))
end

local function respond_error(message)
    respond(400, "Error", function()
        print(error_box("{!x}", { x = message }))
    end)
end

local function redirect(path)
    print("Status: 307")
    print(string.format("Location: %s", path))
    print()
end

local function form_data()
    return parse_query(io.read())
end

local function read_log()
    local log = io.open("log", "r")
    if log == nil then
        return function() return nil end
    end
    local lines = log:lines("l")
    return function()
        local l = lines()
        if l == "" or l == nil then
            return nil
        end
        local time, user_src, user_dst, amount, pname, pcount, comment = string.match(l,
            format("^{time},{user},{user},{amount},{name_opt},{amount_opt},{comment_opt}$", matchers))
        return tonumber(time), user_src, user_dst, tonumber(amount), pname, tonumber(pcount), comment
    end
end

local function read_products()
    local log = io.open("products", "r")
    if log == nil then
        return function() return nil end
    end
    local lines = log:lines("l")
    return function()
        local l = lines()
        if l == "" or l == nil then
            return nil
        end
        local price, user, name = string.match(l, format("^{amount},{user_opt},{name}$", matchers))
        return tonumber(price), user, name
    end
end

local function read_barcodes()
    local log = io.open("barcodes", "r")
    if log == nil then
        return function() return nil end
    end
    local lines = log:lines("l")
    return function()
        local l = lines()
        if l == "" or l == nil then
            return nil
        end
        local name, barcode = string.match(l, format("^{name},{barcode}$", matchers))
        return name, barcode
    end
end

local function balances()
    local users = {}
    for _, user_src, user_dst, amount, _, _, _ in read_log() do
        users[user_src] = (users[user_src] or 0) - amount
        users[user_dst] = (users[user_dst] or 0) + amount
    end
    return users
end

local function product_balances()
    local products = {}
    for _, _, _, _, pcode, pcount, _ in read_log() do
        if pcode ~= nil and pcount ~= nil then
            products[pcode] = (products[pcode] or 0) + pcount
        end
    end
    return products
end

local function last_txns()
    local users = {}
    for time, user_src, user_dst, _, _, _, _ in read_log() do
        users[user_src] = time
        users[user_dst] = time
    end
    return users
end

local function get_active_users()
    local user_balances = {}
    for time, user_src, user_dst, amount, _, _, _ in read_log() do
        user_balances[user_src] = {
            time = time,
            name = user_src,
            balance = (user_balances[user_src] or { balance = 0 }).balance - amount
        }
        user_balances[user_dst] = {
            time = time,
            name = user_dst,
            balance = (user_balances[user_dst] or { balance = 0 }).balance + amount
        }
    end

    local users = {}
    for _, user in pairs(user_balances) do
        table.insert(users, user)
    end

    table.sort(users, function(a, b) return ((a.time == b.time) and a.name > b.name) or (a.time > b.time) end)
    return users
end

local function r_transaction_post()
    local data = form_data()
    local user_src = data.user_src
    local user_dst = data.user_dst
    local amount = tonumber(data.amount)
    local pcode = data.pcode
    local pcount = tonumber(data.pcount)
    local comment = data.comment
	local barcode_name = nil
    
    if pcode ~= nil and pcode ~= "" then
		-- check if barcode exists
		local exists = false
		for p_name, p_barcode in read_barcodes() do
			if p_barcode == pcode then
				exists = true
				barcode_name = p_name
			end
		end
        if not exists then
            return error_box("{+error.unknown_barcode}")
        end
    
    	-- check if product exists
        local exists = false
        for p_amount, p_user, p_name in read_products() do
            if barcode_name == p_name then
                pcount = (tonumber(data.pcount) or 1) * (data.negate_pcount ~= nil and -1 or 1)
                amount = amount or pcount * p_amount
                user_src = user_src or p_user
                comment = comment or
                    string.format("%s %d %s", pcount < 0 and "Buy" or "Restock", math.abs(pcount or 0), p_name)
                exists = true
            end
        end
        if not exists then
            return error_box("{+error.unknown_product}"..barcode_name)
        end
    end

	-- for outside money
    user_src = user_src or "@Potential"
    if amount == nil then
        return error_box("{+error.invalid_amount}")
    end
    if comment == nil or comment:match(matchers_global.comment_opt) == nil then
        return error_box("{+error.invalid_comment}")
    end
    if user_src == nil or user_src:match(matchers_global.user) == nil then
        return error_box("{+error.invalid_user_src}")
    end
    if user_dst == nil or user_dst:match(matchers_global.user) == nil then
        return error_box("{+error.invalid_user_dst}")
    end
    local log = io.open("log", "a+")
    if log == nil then
        return error_box("{+error.open_log}")
    end
    local time = os.time()
    log:write(string.format("%d,%s,%s,%d,%s,%s,%s\n",
        time, user_src, user_dst, amount, barcode_name or "", pcount or "", comment))
    log:flush()
    log:close()
    return format([[
        <div class="notif"><p>{+user.form.transaction.success}: {amount} ({!comment})</p></div>
        <audio src="{sound}" autoplay></audio>
    ]], {
        sign = amount >= 0 and "pos" or "neg",
        amount = format_amount(amount, "strong"),
        comment = comment,
        sound = config.transaction_sound or ""
    })
end

local function r_user(username)
    local notif = nil
    if method == "POST" then
        notif = r_transaction_post()
    end
    return respond(200, string.format("Abrechenbarheit: %s", username), function()
        local is_special = username:sub(1, 1) == "@"
        local username_display = username:gsub("@", "")
        print(format(is_special and "<h1><i>{!username}</i></h1>" or "<h1>{!username}</h1>",
            { username = username_display }))
        local balance = balances()[username]
        local last_txn = last_txns()[username]
        local new_user = balance == nil
        balance = balance or 0
        if notif then print(notif) end
        if is_special then
            print(format([[<div class="notif"><p><i>{+user.special}</i></p></div>]]))
        end
        if new_user then
            print(format([[<div class="notif"><p><i>{+user.lazy_creation}</i></p></div>]]))
        else
            print([[<div class="backgroundbox userinfo">]])
            print(format([[{+user.balance}: <br>{amount}<br>]],
                { amount = format_amount(balance, "span", "balance-value") }))
            print(format([[{+user.last_txn} <a href="/{username}?log">{+user.view_log}</a>]],
                { time = format_duration(os.time() - last_txn), username = urlencode(username) }))
            print([[</div>]])
        end
        print([[<div class="transactions container firstchildlarge">]])
        print([[<div class="amount-presets backgroundbox">]])
        for _, type in ipairs({ 1, -1 }) do
            for _, amount in ipairs({ 50, 100, 150, 200, 500, 1000 }) do
                local a = amount * type
                print(format([[<form action="" method="POST" class="{disable_class}">
                    <input type="text" name="user_dst" value="{!username}" hidden />
                    <input type="number" name="amount" value="{a_raw}" hidden />
                    <input type="text" name="comment" value="" hidden />
                    <input type="submit" value="{amount}" class="amount-{sign} button" />
                </form>]], {
                    username = username,
                    a_raw = a,
                    amount = format_amount(a),
                    sign = a < 0 and "neg" or "pos",
                    disable_class = is_special and "disabled" or ""
                }))
            end
        end
        print("</div>")
        print(format([[
            <form class="transaction box backgroundbox {disable_class}" action="" method="POST">
                <h3>{+user.form.transaction}</h3>
                <input type="text" name="user_dst" value="{!username}" hidden />
                <label for="amount">Amount (ct): </label>
                <input type="number" name="amount" id="amount" />
                <label for="comment">Comment: </label>
                <input type="text" name="comment" id="comment" />
                <input type="submit" value="{+user.form.transaction.submit}" class="amount-ntr button" />
            </form>
            <form class="transaction box backgroundbox {disable_class}" action="" method="POST" id="buy_product">
                <h3>{+user.form.buy}</h3>
                <input type="text" name="user_dst" value="{!username}" hidden />
                <input type="text" name="negate_pcount" value="1" hidden />
                <label for="pcount">Count: </label>
                <input type="number" name="pcount" id="pcount" value="1" />
                <label for="pcode">{+field.barcode}: </label>
                <input type="text" name="pcode" id="pcode" />
                <input class="amount-neg button" type="submit" value="{+user.form.buy.submit}" />
            </form>
            <form class="transaction box backgroundbox {disable_class}" action="" method="POST" id="buy_product">
                <h3>{+user.form.restock}</h3>
                <input type="text" name="user_dst" value="{!username}" hidden />
                <label for="pcount">{+field.count}: </label>
                <input type="number" name="pcount" id="pcount" value="1" />
                <label for="amount">{+field.upstream_price}: </label>
                <input type="number" name="amount" id="amount" />
                <label for="pcode">{+field.barcode}: </label>
                <input type="text" name="pcode" id="pcode" />
                <input type="submit" value="{+user.form.restock.submit}" class="button amount-pos" />
            </form>
        ]], { username = username, disable_class = is_special and "disabled" or "" }))
        print("</div>")
    end)
end

local function r_log(filter)
    local notif = nil
    if method == "POST" then
        notif = r_transaction_post()
    end
    return respond(200, "Abrechnungen", function()
        if notif then print(notif) end
        print([[<table class="log"]])
        print(format([[<thead><tr>
            <th>{+field.time}</th>
            <th>{+field.username}</th>
            <th>{+field.amount}</th>
            <th>{+field.name}</th>
            <th>{+field.count}</th>
            <th>{+field.comment}</th>
            <th>{+log.actions}</th>
        </tr></thead>]]))
        print("<tbody>")
        for time, user_src, user_dst, amount, pcode, pcount, comment in read_log() do
            if filter == nil or filter == user_src or filter == user_dst then
                print(format([[
                    <tr>
                        <td>{time} ({time_delta})</td>
                        <td><a href="/{user_src_url}">{user_src}</a> → <a href="/{user_dst_url}">{user_dst}</a></td>
                        {amount}
                        <td>{pcode}</td>
                        <td>{pcount}</td>
                        <td>{comment}</td>
                        <td>
                            <form action="/?log" method="POST">
                                <input type="text" name="user_src" value="{user_src}" hidden />
                                <input type="text" name="user_dst" value="{user_dst}" hidden />
                                <input type="number" name="amount" value="{revert_amount}" hidden />
                                <input type="text" name="name" value="{pcode}" hidden />
                                <input type="number" name="pcount" value="{revert_pcount}" hidden />
                                <input type="text" name="comment" value="Revert {comment}" hidden />
                                <input type="submit" class="amount-ntr button" value="{+log.actions.revert}" />
                            </form>
                        </td>
                    </tr>
                ]], {
                    time = os.date("!%Y-%m-%dT%H:%M:%SZ", time),
                    time_delta = format_duration(os.time() - time),
                    user_src = escape(user_src),
                    user_dst = escape(user_dst),
                    user_src_url = urlencode(user_src),
                    user_dst_url = urlencode(user_dst),
                    amount = format_amount(amount, "td"),
                    pcode = escape(pcode),
                    pcount = pcount and tostring(math.abs(pcount)) or "",
                    comment = escape(comment),
                    revert_amount = -amount,
                    revert_pcount = -(pcount or 0),
                }))
            end
        end
        print("</tbody>")
        print("</table>")
    end)
end

local function r_users(show_special, filter_negative)
	if filter_negative ~= nil then
		filter_negative = tonumber(filter_negative) or 0
	end

    return respond(200, "Abrechenbarkeit", function()
        local users = get_active_users()

        if not show_special then
            print(format([[
                <form class="section" action="/" method="GET" id="user_creation">
                    <h3>{+index.form.create_user}</h3>
                    <label for="username">{+field.username}: </label>
                    <input type="text" name="create_user" id="username" />
                    <input type="submit" value="{+index.form.create_user.submit}" class="button amount-ntr" />
                </form>
            ]]))

            -- get first letters
            local firstletters = {}
            local function contains(char)
                for _, firstchar in ipairs(firstletters) do
                    if firstchar == char then
                        return true
                    end
                end

                return false
            end

            for _, user in ipairs(users) do
                user.firstchar = user.name:sub(1, 1):lower()
                if user.firstchar ~= "@" and not contains(user.firstchar) then
                    table.insert(firstletters, user.firstchar)
                end
            end

            table.sort(firstletters)

            print(format([[<section class="firstletterlist"><h3>{+users.filter}</h3><ul>]]))
            for k, letter in ipairs(firstletters) do
                print(format([[<li><a href="?users&prefix={letter}">{letter}</a></li>]], {
                    letter = letter,
                }))
            end
            print([[</ul></section>]])
        end
        print([[<div class="userlist"></div>]]) -- for printing
        print([[<ul class="userlist">]])

        local show_user = function(user)
            local is_spu = user.name:sub(1, 1) == "@"
            local filter_out = query.prefix ~= nil and user.name:sub(1, 1):lower() ~= query.prefix
            if is_spu == show_special and
            	((filter_negative ~= nil and user.balance < filter_negative)
            	or (filter_negative == nil and (not filter_out))) then
                print(format([[<li>
                    <a href="/{username_url}">
                        <span class="name">{!username}</span>
                        {balance}
                    </a>
                </li>]], {
                    username_url = urlencode(user.name),
                    username = user.name:gsub("@", ""),
                    balance = format_amount(user.balance, "span")
                }))
            end
        end
        
        if filter_negative ~= nil then
        	table.sort(users, function(a, b)
        		return a.balance < b.balance
        	end)
        end
        
        local inactive_cutoff = os.time() - (tonumber(config.inactive_cutoff) or (30 * 24 * 60 * 60))
        local embezzlement = 0
        for _, user in ipairs(users) do        
            if filter_negative or user.time > inactive_cutoff then
            	show_user(user)

            	if user.name:sub(1,1) ~= "@" then
					if user.balance < 0 then embezzlement = embezzlement - user.balance end
            	end
            end
        end
        print("</ul>")
        if filter_negative ~= nil then
        	print(format([[<section class="section" style="margin-top: 0">{+users.embezzlement}</section>]], {
        		amount = format_amount(embezzlement)
        	}))

        	return
        end
        print(format([[
            <details {oclass}><summary>{+users.inactive_list}</summary><ul class="userlist">
        ]], { oclass = query.prefix and "open" or "" }))
        for _, user in ipairs(users) do
            if user.time <= inactive_cutoff then show_user(user) end
        end
        print([[</ul></details>]])
    end)
end

local function r_create_user()
    local username = query.create_user
    if username:match(matchers_global.user) == nil then
        return respond_error(format("{+error.invalid_user}"))
    end
    return redirect(string.format("/%s", urlencode(username)))
end

local function r_products_post()
    local data = form_data()

    if data.deletebarcode then
	    local barcode = data.barcode

        -- remove barcode
        local new_barcodes = io.open("barcodes.new", "w+")
        if new_barcodes == nil then
            return error_box("{+error.open_new_barcodes}")
        end
        for name, a_barcode in read_barcodes() do
            if barcode ~= a_barcode then
                new_barcodes:write(string.format("%s,%s\n", name, a_barcode))
            end
        end
        new_barcodes:flush()
        new_barcodes:close()
        os.rename("barcodes.new", "barcodes")
		return
    end
    
    local name = data.name
    if name == nil or name:match("^[%w_-]*$") == nil then
        return error_box("{+error.invalid_name}")
    end
    
    if data.delete then
        local new_products = io.open("products.new", "w+")
        if new_products == nil then
            return error_box("{+error.open_new_products}")
        end
        for price, user, a_name in read_products() do
            if name ~= a_name then
                new_products:write(string.format("%d,%s,%s\n", price, user, a_name))
            end
        end
        new_products:flush()
        new_products:close()
        os.rename("products.new", "products")
    elseif data.addbarcode then
    	-- add barcode
    	local name = data.name
    	local barcode = data.barcode
        if name == nil or name:match(matchers_global.name) == nil then
            return error_box("{+error.invalid_name}")
        end
        if barcode == nil or barcode:match(matchers_global.barcode) == nil then
            return error_box("{+error.invalid_barcode}")
        end
		local exists = false
		for _, a_barcode in read_barcodes() do
			if a_barcode == barcode then
				exists = true
			end
		end

		if exists then
			return error_box("{+error.invalid_barcode}")
		end

    	local barcodes = io.open("barcodes", "a+")
    	if barcodes == nil then
    		return error_box("{+error.open_barcodes}")
    	end
    	barcodes:write(string.format("%s,%s\n", name, barcode))
        barcodes:flush()
        barcodes:close()
    else
        local price = tonumber(data.price)
        local name = data.name
        local user = data.user
        if price == nil then
            return error_box("{+error.invalid_price}")
        end
        if name == nil or name:match(matchers_global.name) == nil then
            return error_box("{+error.invalid_name}")
        end
        if user == nil or user:match(matchers_global.user) == nil then
            return error_box("{+error.invalid_user}")
        end
        local products = io.open("products", "a+")
        if products == nil then
            return error_box("{+error.open_products}")
        end
        products:write(string.format("%d,%s,%s\n", price, user, name))
        products:flush()
        products:close()
    end
end

local function r_products()
    local notif = nil
    if method == "POST" then
        notif = r_products_post()
    end
    respond(200, "Abrechenbare Product List", function()
        print(format("<h1>{+products.title}</h1>"))
        if notif then print(notif) end
        print(format([[
        	<div class="container">
            <form action="/?products" method="POST" class="box backgroundbox">
                <h3>{+products.form.add}</h3>
                <label for="name">{+field.name}: </label>
                <input type="text" name="name" id="name" />
                <label for="price">{+field.price}: </label>
                <input type="number" name="price" id="price" />
                <label for="user">{+field.user}: </label>
                <input type="text" name="user" id="user" />
                <input type="submit" value="{+products.form.add.submit}" class="amount-ntr button" />
            </form>
            <form action="/?products" method="POST" class="box backgroundbox">
                <h3>{+products.form.addbarcode}</h3>
                <input type="text" name="addbarcode" value="1" hidden />
                <label for="name">{+field.name}: </label>
                <select type="text" name="name" id="name">
		]], {
			currency = config.unit or "€",
        }))
        for _, _, name in read_products() do
			print(format([[<option value="{name}">{name}</option>]], {
				name = name,
			}))
		end
        
		print(format([[</select>
                <label for="barcode">{+field.barcode}: </label>
                <input type="text" name="barcode" id="barcode" />
                <input type="submit" value="{+products.form.add.submit}" class="amount-ntr button" />
            </form>
            <form action="/?products" method="POST" class="box backgroundbox">
                <h3>{+products.form.remove}</h3>
                <input type="text" name="delete" value="1" hidden />
                <label for="name">{+field.name}: </label>
                <select type="text" name="name" id="name">
		]], {
			currency = config.unit or "€",
        }))
        for _, _, name in read_products() do
			print(format([[<option value="{name}">{!name}</option>]], {
				name = name,
			}))
		end
		print(format([[</select>
                <input type="submit" value="{+products.form.remove.submit}" class="amount-ntr button" />
            </form>
            <!-- remove barcode  -->
            <form action="/?products" method="POST" class="box backgroundbox">
                <h3>{+products.form.removebarcode}</h3>
                <input type="text" name="deletebarcode" value="1" hidden />
                <label for="barcode">{+field.barcode}: </label>
                <select type="text" name="barcode" id="barcode">
		]], {
			currency = config.unit or "€",
        }))
        
        local barcodes = {}
        for name, barcode in read_barcodes() do
			print(format([[<option value="{barcode}">{!barcode} ({!name})</option>]], {
				name = name,
				barcode = barcode,
			}))
			if barcodes[name] ~= nil then
				table.insert(barcodes[name], barcode)
			else
				barcodes[name] = {}
				table.insert(barcodes[name], barcode)				
			end
		end

		print(format([[</select>
                <input type="submit" value="{+products.form.remove.submit}" class="amount-ntr button" />
            </form>
            </div>
        ]], {
            currency = config.unit or "€",
        }))
        print(format([[<table class="productlist"><tr>
            <th>{+field.name}</th>
            <th>{+field.price}</th>
            <th>{+field.count}</th>
            <th>{+field.user}</th>
            <th>{+field.barcode}</th>
        </tr>]]))
        local pbals = product_balances()
        for price, user, name in read_products() do
            print(format([[<tr>
                <td>{!name}</td>
                {price}
                <td>{!count}</td>
                <td>{!user}</td>
                <td>{barcodes}</td>
            </tr>]], {
                name = name,
                price = format_amount(-price, "td"),
                count = tostring(pbals[barcode] or 0),
                user = user,
                barcodes = format_list(barcodes[name]) or "{+field.not_set}",
            }))
		end
        
        print("</table>")
    end)
end

local function r_about()
    respond(200, "About Abrechenbarkeit", function()
        print(format([[
            <h1>{+about.title}</h1>
            <p>{+about.desc}</p>
            <p>{+about.license}<p>
            <p>{+about.source}</p>
            <p>{+about.thanks}</p>
        ]], {
            issues = [[<a href="https://codeberg.org/metamuffin/strichliste/issues">]],
            codeberg = [[<a href="https://codeberg.org/metamuffin/strichliste">]],
            ae = [[</a>]],
        }))
    end)
end

local function extract_username()
    if path == nil then
        return respond_error(format("{+error.no_path}"))
    end
    local username = urldecode(path:sub(2))
    if username == nil or username:match(matchers_global.user) == nil then
        return nil
    end
    return username
end

local function r_export_log()
    local log = io.open("log", "r")
    if log == nil then
        return function() return nil end
    end
    print("Status: 200")
    print("Content-Type: text/csv")
    print("")
    for l in log:lines("l") do
        print(l)
    end
end
local function r_export_products()
    local log = io.open("products", "r")
    if log == nil then
        return function() return nil end
    end
    print("Status: 200")
    print("Content-Type: text/csv")
    print("")
    for l in log:lines("l") do
        print(l)
    end
end
local function r_export_balances()
    print("Status: 200")
    print("Content-Type: text/csv")
    print("")
    for user, balance in pairs(balances()) do
        print(string.format("%s,%d", user, balance))
    end
end

if path == "/" then
    if query.about then
        return r_about()
    elseif query.products then
        if query.export then
            return r_export_products()
        else
            return r_products()
        end
    elseif query.log then
        if query.export then
            return r_export_log()
        else
            return r_log()
        end
    elseif query.create_user then
        return r_create_user()
    elseif query.spus then
        return r_users(true, nil)
    elseif query.users and query.export then
		return r_export_balances()
    else
        return r_users(false, query.negative and (query.maximum or 0))
    end
else
    local username = extract_username()
    if username == nil then
        return respond_error(format("{+error.invalid_user}"))
    elseif query.log then
        return r_log(username)
    else
        return r_user(username)
    end
end