login

local lud = require "ludweb"

local data = require "data"
local auth = require "auth"

local LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG = 0, 1, 2, 3

local App = {}
App.__index = App

function App:init()
    self.model:create_tables()
    if #self.model:get_states() == 0 then
        self.model:create_states()
        local uuid = self.model:create_invite()
        self:log(LOG_INFO, "root invite: "..uuid)
    else
        self.model:expire_invites()
    end
end

function App:run()
    self:log(LOG_INFO, "server running on port "..self.port)
    self.app:run(self.port)
end

function App:log(level, msg)
    local level_str = {"ERROR", "WARN", "INFO", "DEBUG"}
    if self.log_level >= level then
        io.stderr:write(("[%s] %s\n"):format(level_str[level+1], msg))
    end
end

function App:get_user(req)
    local session_id = req.cookies.sid
    local user
    if session_id ~= nil then
        user = self.model:get_user(self.sessions[session_id])
    end
    return user
end

function App:routes()
    return {
    {"GET", "/?",
    function (req)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        -- TODO: set proj_id
        local columns = self.model:get_board(user.id)
        local env = {title=self.title, user=user, columns=columns}
        return lud.template.render_file("view/home.html", env)
    end},
    {"GET", "/join",
    function (req)
        local uuid = req.query.invite or ""
        local env = {title=self.title, uuid=uuid}
        return lud.template.render_file("view/join.html", env)
    end},
    {"POST", "/join",
    function (req)
        local uuid = req.form.invite
        local nick = req.form.username
        local name = req.form.realname
        local pass = req.form.password
        local fail_path = "/join?invite="..uuid
        if #pass == 0 then  -- empty password
            return fail_path, 303
        end
        if self.model:get_user(nick) ~= nil then  -- user already exists
            return fail_path, 303
        end
        if not self.model:use_invite(uuid) then -- invalid/expired invite
            self:log(LOG_WARN, "attempt to use invalid invite: "..uuid)
            return fail_path, 303
        end
        self.model:create_user(nick, name, pass)
        self:log(LOG_INFO, "new user joined: "..nick)
        return "/login", 303
    end},
    {"GET", "/login",
    function (req)
        local env = {title=self.title}
        return lud.template.render_file("view/login.html", env)
    end},
    {"POST", "/login",
    function (req)
        local nick = req.form.username
        local pass = req.form.password
        local user = self.model:get_user(nick)
        local salt, hash
        if user == nil then
            -- hash something as if we're trying to login anyway
            salt = auth.get_salt()
            hash = auth.hash_pass(pass, salt)
            self:log(LOG_WARN, "invalid username: "..nick)
        else
            salt = auth.b64_dec(user.salt)
            hash = auth.hash_pass(pass, salt)
            if hash == auth.b64_dec(user.hash) then
                local session_id = auth.b64_enc(auth.uuid4())
                self.sessions[session_id] = nick
                self:log(LOG_INFO, "logged in as "..nick)
                local cookie = {key="sid", val=session_id, path="/", age=3*24*60*60}
                return "/", 303, "See Other", {cookie}
            else
                self:log(LOG_WARN, "invalid password for "..nick)
            end
        end
        return "/login", 303
    end},
    {"GET", "/i",
    function (req)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local invites = self.model:get_invites(user.id)
        local env = {title=self.title, invites=invites}
        return lud.template.render_file("view/invites.html", env)
    end},
    {"POST", "/i",
    function (req)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        self.model:create_invite(user.id)
        self:log(LOG_INFO, "user "..user.nick.." generated a new invite")
        return "/i", 303
    end},
    {"POST", "/i/([%x]+)/del",
    function (req, uuid)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        self.model:del_invite(user.id, uuid)
        self:log(LOG_INFO, "user "..user.nick.." canceled invite "..uuid)
        return "/i", 303
    end},
    {"GET", "/p",
    function (req)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local projs = self.model:get_user_projects(user.id)
        local env = {title=self.title, user=user, projs=projs}
        return lud.template.render_file("view/projs.html", env)
    end},
    {"GET", "/p/new",
    function (req)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local env = {title=self.title, user=user}
        return lud.template.render_file("view/proj_form.html", env)
    end},
    {"GET", "/p/([-_%w]+)",
    function (req, name)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local columns = self.model:get_board(user.id, proj.id)
        local env = {title=self.title, user=user, proj=proj, columns=columns}
        return lud.template.render_file("view/proj.html", env)
    end},
    {"POST", "/p",
    function (req)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local name = req.form.name
        local desc = req.form.desc
        local goal = req.form.goal
        local color = req.form.color
        local priority = tonumber(req.form.priority or 0)
        self.model:create_project(user.id, name, desc, goal, color, priority)
        self:log(LOG_INFO, "user "..user.nick.." created project "..name)
        return "/p", 303
    end},
    {"GET", "/p/([-_%w]+)/edit",
    function (req, name)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local env = {title=self.title, user=user, proj=proj}
        return lud.template.render_file("view/proj_form.html", env)
    end},
    {"POST", "/p/([-_%w]+)/put",
    function (req, old_name)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local new_name = req.form.name
        local desc = req.form.desc
        local goal = req.form.goal
        local color = req.form.color
        local priority = tonumber(req.form.priority or 0)
        self.model:update_project(old_name, new_name, desc, goal, color, priority)
        self:log(LOG_INFO, "user "..user.nick.." edited project "..old_name)
        return "/p", 303
    end},
    {"POST", "/p/([-_%w]+)/del",
    function (req, name)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local proj = self.model:get_project(name)
        if proj ~= nil then
            self.model:del_project(proj.id)
            self:log(LOG_INFO, "user "..user.nick.." deleted project "..name)
        end
        return "/p", 303
    end},
    {"GET", "/p/([-_%w]+)/t/new",
    function (req, name)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local env = {title=self.title, user=user, proj=proj}
        return lud.template.render_file("view/ticket_form.html", env)
    end},
    {"GET", "/p/([-_%w]+)/t/(%d+)",
    function (req, name, code)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local tick = self.model:get_ticket(proj.id, tonumber(code))
        if tick == nil then return "not found", 404 end
        local users = self.model:get_indexed_users()
        local states = self.model:get_indexed_states()
        -- TODO: load comments
        local comments = {}
        local env = {
            title=self.title, user=user, proj=proj, tick=tick,
            users=users, states=states, comments=comments
        }
        return lud.template.render_file("view/ticket.html", env)
    end},
    {"POST", "/p/([-_%w]+)/t",
    function (req, name)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local title = req.form.title
        local desc = req.form.desc
        local priority = tonumber(req.form.priority or 0)
        local code = self.model:create_ticket(user.id, proj.id, title, desc, priority)
        self:log(LOG_INFO, "user "..user.nick.." created ticket "..name.."#"..code)
        return "/p/"..name.."/t/"..code, 303
    end},
    {"GET", "/p/([-_%w]+)/t/(%d+)/edit",
    function (req, name, code)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local tick = self.model:get_ticket(proj.id, code)
        if tick == nil then return "not found", 404 end
        local env = {title=self.title, user=user, proj=proj, tick=tick}
        return lud.template.render_file("view/ticket_form.html", env)
    end},
    {"POST", "/p/([-_%w]+)/t/(%d+)/put",
    function (req, name, code)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local tick = self.model:get_ticket(proj.id, code)
        if tick == nil then return "not found", 404 end
        local title = req.form.title
        local desc = req.form.desc
        local priority = tonumber(req.form.priority or 0)
        self.model:update_ticket(tick.id, title, desc, priority)
        self:log(LOG_INFO, "user "..user.nick.." edited ticket "..name.."#"..code)
        return "/p/"..name.."/t/"..code, 303
    end},
    {"POST", "/p/([-_%w]+)/t/(%d+)/del",
    function (req, name, code)
        local user = self:get_user(req)
        if user == nil then return "/login", 303 end
        local proj = self.model:get_user_project(user.id, name)
        if proj == nil then return "not found", 404 end
        local tick = self.model:get_ticket(proj.id, code)
        if tick ~= nil then
            self.model:del_ticket(tick.id)
            self:log(LOG_INFO, "user "..user.nick.." deleted ticket "..name.."#"..code)
        end
        return "/p/"..name, 303
    end},
} end

local function new_app(db_path, port, title, log_level)
    local log_levels = {ERROR=0, WARN=1, INFO=2, DEBUG=3}
    db_path = db_path or ":memory:"
    local self = {
        port=tonumber(port) or 8080,
        title=title or "skopos",
        log_level=log_levels[log_level or "INFO"],
        sessions={},
    }
    local self = setmetatable(self, App)
    self.model = data.open(db_path)
    self.app = lud.app.new_app(self:routes())
    self:init()
    return self
end

local app = new_app(unpack(arg))
app:run()