login

local lud = require "ludweb"

local auth = require "auth"

--[[
common data representations:
    time INTEGER - unix timestamp
    color INTEGER - 24-bits RGB; MSB is red, LSB is blue
    priority INTEGER - no bounds; highest number <-> highest priority
]]
local schema = [[
CREATE TABLE IF NOT EXISTS User (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    nick TEXT NOT NULL UNIQUE,
    name TEXT NOT NULL,
    salt TEXT,
    hash TEXT,
    invited_by INTEGER REFERENCES User(id)
);
CREATE TABLE IF NOT EXISTS Invite (
    uuid TEXT PRIMARY KEY,
    expire INTEGER NOT NULL,
    user_id INTEGER REFERENCES User(id)
);
CREATE TABLE IF NOT EXISTS Project (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    time INTEGER NOT NULL,
    name TEXT NOT NULL UNIQUE,
    desc TEXT,
    goal TEXT,
    color TEXT,
    priority INTEGER NOT NULL,
    is_active INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS Membership (
    proj_id INTEGER REFERENCES Project(id),
    user_id INTEGER REFERENCES User(id),
    PRIMARY KEY(proj_id, user_id)
);
CREATE TABLE IF NOT EXISTS Ticket (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    proj_id INTEGER NOT NULL REFERENCES Project(id),
    user_id INTEGER NOT NULL REFERENCES User(id),
    state_id INTEGER NOT NULL,
    time INTEGER NOT NULL,
    code INTEGER NOT NULL,
    title TEXT NOT NULL,
    desc TEXT,
    priority INTEGER NOT NULL,
    is_active INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS Block (
    blocked INTEGER REFERENCES Ticket(id),
    blocker INTEGER REFERENCES Ticket(id),
    PRIMARY KEY(blocked, blocker)
);
CREATE TABLE IF NOT EXISTS Shift (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    ticket_id INTEGER NOT NULL REFERENCES Ticket(id),
    old_stt_id INTEGER NOT NULL,
    new_stt_id INTEGER NOT NULL,
    user_id INTEGER NOT NULL REFERENCES User(id),
    time INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS Comment (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    ticket_id INTEGER NOT NULL REFERENCES Ticket(id),
    user_id INTEGER NOT NULL REFERENCES User(id),
    time INTEGER NOT NULL,
    code INTEGER NOT NULL,
    text TEXT NOT NULL
);
]]

local Model = {}
Model.__index = Model

function Model:create_tables()
    self.db:execute_many(schema)
end

function Model:create_user(nick, name, password, uuid)
    self:expire_invites()
    local inv = self.db:execute("SELECT * FROM Invite WHERE uuid = ?;", uuid)[1]
    if inv == nil then return false end
    local salt = auth.get_salt()
    local hash = auth.hash_pass(password, salt)
    salt = auth.b64_enc(salt)
    hash = auth.b64_enc(hash)
    local query = "INSERT INTO User(nick, name, salt, hash, invited_by) VALUES (?, ?, ?, ?, ?);"
    self.db:execute(query, nick, name, salt, hash, inv.user_id)
    self.db:execute("DELETE FROM Invite WHERE uuid = ?;", uuid)
    return true
end

function Model:get_user_count()
    return self.db:execute("SELECT COUNT(*) as count FROM User;")[1].count
end

function Model:get_user(nick)
    return self.db:execute("SELECT * FROM User WHERE nick = ?;", nick)[1]
end

function Model:get_indexed_users()
    local query = "SELECT id, nick, name FROM User;"
    local users = {}
    for _, user in ipairs(self.db:execute(query)) do
        users[user.id] = {nick=user.nick, name=user.name}
    end
    return users
end

function Model:create_invite(user_id)
    local uuid = auth.hex(auth.uuid4())
    local expire = "unixepoch('now', '+2 days')"
    local query = "INSERT INTO Invite(uuid, expire, user_id) VALUES (?, "..expire..", ?);"
    self.db:execute(query, uuid, user_id)
    return uuid
end

function Model:get_invites(user_id)
    return self.db:execute("SELECT * FROM Invite WHERE user_id = ?;", user_id)
end

function Model:expire_invites()
    self.db:execute("DELETE FROM Invite WHERE expire < unixepoch();")
end

function Model:del_invite(user_id, uuid)
    self.db:execute("DELETE FROM Invite WHERE user_id = ? AND uuid = ?;", user_id, uuid)
end

function Model:create_project(user_id, name, desc, goal, color, priority)
    local query = [[
        INSERT INTO Project(time, name, desc, goal, color, priority, is_active)
        VALUES (unixepoch(), ?, ?, ?, ?, ?, 1);
    ]]
    self.db:execute(query, name, desc, goal, color, priority)
    local proj_id = self.db:execute("SELECT id FROM Project WHERE name = ?;", name)[1].id
    self.db:execute("INSERT INTO Membership(proj_id, user_id) VALUES (?, ?);", proj_id, user_id)
    return proj_id
end

function Model:get_project(name)
    return self.db:execute("SELECT * FROM Project WHERE name = ?;", name)[1]
end

function Model:get_user_project(user_id, proj_name)
    return self.db:execute([[
        SELECT * FROM Project JOIN Membership ON Project.id = Membership.proj_id
        WHERE Membership.user_id = ? AND Project.name = ? ORDER BY priority DESC;
    ]], user_id, proj_name)[1]
end

function Model:get_user_projects(user_id)
    return self.db:execute([[
        SELECT * FROM Project JOIN Membership ON Project.id = Membership.proj_id
        WHERE Membership.user_id = ? ORDER BY priority DESC;
    ]], user_id)
end

function Model:update_project(old_name, new_name, desc, goal, color, priority)
    local query = [[
        UPDATE Project SET name = ?, desc = ?, goal = ?, color = ?, priority = ?
        WHERE name = ?;
    ]]
    self.db:execute(query, new_name, desc, goal, color, priority, old_name)
end

function Model:del_project(proj_id)
    self.db:execute("DELETE FROM Membership WHERE proj_id = ?;", proj_id)
    self.db:execute("DELETE FROM Ticket WHERE proj_id = ?;", proj_id)
    -- TODO: delete associated comments and shifts
    self.db:execute("DELETE FROM Project WHERE id = ?;", proj_id)
end

-- generate new code for a child object
-- ticket_code = get_next_ticket_code("Ticket", "proj_id", proj_id)
-- comment_code = get_next_ticket_code("Comment", "ticket_id", ticket_id)
function Model:get_next_code(class, parent_row, parent_id)
    local query = "SELECT code FROM %s WHERE %s = ? ORDER BY code DESC LIMIT 1;"
    query = query:format(class, parent_row)
    local last_child = self.db:execute(query, parent_id)[1]
    local last_code
    if last_child == nil then
        last_code = 0
    else
        last_code = last_child.code
    end
    return last_code + 1
end

function Model:create_ticket(user_id, proj_id, title, desc, priority)
    local code = self:get_next_code("Ticket", "proj_id", proj_id)
    self.db:execute([[
        INSERT INTO Ticket(proj_id, user_id, state_id, time, code, title, desc, priority, is_active)
        VALUES (?, ?, 1, unixepoch(), ?, ?, ?, ?, 1);
    ]], proj_id, user_id, code, title, desc, priority)
    return code
end

function Model:get_ticket(proj_id, code, full)
    local query
    if full then
        query = [[
            SELECT Ticket.*, User.nick AS author_nick, User.name AS author_name
            FROM Ticket JOIN User ON Ticket.user_id = User.id
            WHERE Ticket.proj_id = ? AND Ticket.code = ?;
        ]]
    else
        query = "SELECT * FROM Ticket WHERE proj_id = ? AND code = ?;"
    end
    return self.db:execute(query, proj_id, code)[1]
end

function Model:get_tickets(proj_id, state_id)
    local query = "SELECT * FROM Ticket WHERE proj_id = ? AND state_id = ?;"
    return self.db:execute(query, proj_id, state_id)
end

function Model:update_ticket(tick_id, title, desc, priority)
    local query = "UPDATE Ticket SET title = ?, desc = ?, priority = ? WHERE id = ?;"
    self.db:execute(query, title, desc, priority, tick_id)
end

function Model:shift_ticket(user_id, ticket, state_id)
    local query = "SELECT * FROM %s WHERE ticket_id = ? ORDER BY time DESC LIMIT 1;"
    local last_comment = self.db:execute(query:format("Comment"), ticket.id)[1]
    local last_shift = self.db:execute(query:format("Shift"), ticket.id)[1]
    if last_comment ~= nil and last_shift ~= nil and last_shift.time < last_comment.time then
        -- don't undo last shift if a comment has been added after it
        last_shift = nil
    end
    if last_shift ~= nil and last_shift.old_stt_id == state_id then
        -- we're undoing the last shift: delete it from DB
        self.db:execute("DELETE FROM Shift WHERE id = ?;", last_shift.id)
    else
        -- we're doing a new shift: add it to DB
        self.db:execute([[
            INSERT INTO Shift(ticket_id, old_stt_id, new_stt_id, user_id, time)
            VALUES (?, ?, ?, ?, unixepoch());
        ]], ticket.id, ticket.state_id, state_id, user_id)
    end
    self.db:execute("UPDATE Ticket SET state_id = ? WHERE id = ?;", state_id, ticket.id)
end

function Model:del_ticket(tick_id)
    self.db:execute("DELETE FROM Ticket WHERE id = ?;", tick_id)
end

function Model:add_comment(user_id, ticket_id, text)
    local code = self:get_next_code("Comment", "ticket_id", ticket_id)
    self.db:execute([[
        INSERT INTO Comment(ticket_id, user_id, time, code, text)
        VALUES (?, ?, unixepoch(), ?, ?);
    ]], ticket_id, user_id, code, text)
    return code
end

function Model:get_comment(ticket_id, code)
    local query = "SELECT * FROM Comment WHERE ticket_id = ? AND code = ?;"
    return self.db:execute(query, ticket_id, code)[1]
end

function Model:get_comments(ticket_id, full)
    local query
    if full then
        query = [[
            SELECT Comment.*, User.nick AS author_nick, User.name AS author_name
            FROM Comment JOIN User ON Comment.user_id = User.id
            WHERE Comment.ticket_id = ? ORDER BY code ASC;
        ]]
    else
        query = "SELECT * FROM Comment WHERE ticket_id = ? ORDER BY code ASC;"
    end
    return self.db:execute(query, ticket_id)
end

function Model:update_comment(comm_id, text)
    self.db:execute("UPDATE Comment SET text = ? WHERE id = ?;", text, comm_id)
end

function Model:del_comment(comm_id)
    self.db:execute("DELETE FROM Comment WHERE id = ?;", comm_id)
end

-- return a list of columns ordered by state ID
-- each column is a list of cards ordered by priority
-- each card is a ticket DB object with some keys added:
--   author_nick, author_name, proj_name, proj_color, proj_priority
function Model:get_board(user_id, proj_id)
    local filter = ""
    if proj_id ~= nil then
        filter = " AND Ticket.proj_id = ?"
    end
    local query = [[
    SELECT
        Ticket.*,
        User.nick AS author_nick,
        User.name AS author_name,
        Project.name AS proj_name,
        Project.color AS proj_color,
        Project.priority AS proj_priority
    FROM Membership
    JOIN Project ON Membership.proj_id = Project.id
    JOIN Ticket ON Project.id = Ticket.proj_id
    JOIN User ON Ticket.user_id = User.id
    WHERE Membership.user_id = ?]]..filter..[[
    ORDER BY Ticket.priority DESC, proj_priority DESC;
    ]]
    local tickets
    if proj_id ~= nil then
        tickets = self.db:execute(query, user_id, proj_id)
    else
        tickets = self.db:execute(query, user_id)
    end
    local columns = {}
    for i = 1, #self.states do
        table.insert(columns, {})
    end
    for i, ticket in ipairs(tickets) do
        table.insert(columns[ticket.state_id], ticket)
    end
    return columns
end

function Model:close()
    self.db:close()
end

local function open(path)
    local self = setmetatable({path=path}, Model)
    self.db = lud.sqlite.open(path)
    self.states = {"backlog", "design", "progress", "review", "done"}
    return self
end

return {open=open}