login

local git = require "cogit.git"
local scan = require "cogit.scan"
local hash = require "cogit.hash"

local lud = require "ludweb"

local function time_fmt(sig)
    local s = os.date("%Y-%m-%d %H:%M:%S", sig.time_)
    local offset = sig.offset
    local sign
    if offset < 0 then
        offset = -offset
        sign = "-"
    else
        sign = "+"
    end
    local hours = math.floor(offset / 60)
    local mins = offset % 60
    s = s .. (" (UTC%s%02d:%02d)"):format(sign, hours, mins)
    return s
end

local function diff_cb(line_type, line)
    line = lud.template.escape(line:sub(1, #line-1))
    if line_type == " " then
        line = ' <span class="diff_ctx">' .. line .. '</span>'
    elseif line_type == "+" then
        line = '+<span class="diff_add">' .. line .. '</span>'
    elseif line_type == "-" then
        line = '-<span class="diff_del">' .. line .. '</span>'
    elseif line_type == "=" then
        line = ' <span class="diff_nonl diff_ctx_nonl">' .. line .. '</span>'
    elseif line_type == ">" then
        line = ' <span class="diff_nonl diff_old_nonl">' .. line .. '</span>'
    elseif line_type == "<" then
        line = ' <span class="diff_nonl diff_new_nonl">' .. line .. '</span>'
    elseif line_type == "F" then
        line = '\n<span class="diff_file_hdr">' .. line .. '</span>'
    elseif line_type == "H" then
        line = '<span class="diff_hunk_hdr">' .. line .. '</span>'
    elseif line_type == "B" then
        line = '<span class="diff_bin">' .. line .. '</span>'
    else
        line = line_type .. line
    end
    return line .. "\n"
end

local function allowed(is_admin, gname)
    return gname == "public" or is_admin
end

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

local Cogit = {}
Cogit.__index = Cogit

function Cogit:init()
    if not self.initialized then
        git.init()
        self.initialized = true
    end
end

function Cogit:finish()
    git.shutdown()
    self.initialized = false
end

function Cogit:set_password(salt, hash)
    self.salt = salt
    self.hash = hash
end

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

function Cogit:scan()
    self:log(LOG_INFO, "scanning repos in "..self.path)
    self.groups = scan.scanrepos(self.path)
end

function Cogit: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 Cogit:is_admin(cookies)
    local session_id = cookies.sid
    if session_id == nil then
        return false
    end
    return self.sessions[session_id]
end

function Cogit:routes()
    return {
    {"GET", "/?",
    function (req)
        local is_admin = self:is_admin(req.cookies)
        local gnames = {}
        self:scan()
        for gname in pairs(self.groups) do
            if allowed(is_admin, gname) then
                table.insert(gnames, gname)
            end
        end
        local env = {title=self.title, is_admin=is_admin, gnames=gnames}
        return lud.template.render_file("view/home.html", env)
    end},
    {"GET", "/login",
    function (req)
        local is_admin = self:is_admin(req.cookies)
        if is_admin then  -- already logged in
            return "/", 303
        else
            return lud.template.render_file("view/login.html", {title=self.title})
        end
    end},
    {"POST", "/login",
    function (req)
        local pass = req.form.password
        local salt, h
        salt = lud.crypt.b64_dec(self.salt)
        h = hash.hash_pass(pass, salt)
        if h == lud.crypt.b64_dec(self.hash) then
            local session_id = lud.crypt.b64_enc(lud.crypt.uuid4())
            self.sessions[session_id] = true
            self:log(LOG_INFO, "logged in")
            local cookie = {key="sid", val=session_id, path="/", age=self.session_age}
            return "/", 303, "See Other", {cookie}
        else
            self:log(LOG_WARN, "invalid password")
        end
        return "/login", 303
    end},
    {"GET", "/logout",
    function (req)
        local session_id = req.cookies["sid"]
        if session_id ~= nil then
            self:log(LOG_INFO, "logged out")
            self.sessions[session_id] = nil
        end
        return "/", 303
    end},
    {"GET", "/group/([%w_-]+)",
    function (req, gname)
        local is_admin = self:is_admin(req.cookies)
        if not allowed(is_admin, gname) then
            return "/login", 303
        end
        local rnames = {}
        for rname in pairs(self.groups[gname]) do
            table.insert(rnames, rname)
        end
        local env = {title=self.title, is_admin=is_admin, gname=gname, rnames=rnames}
        return lud.template.render_file("view/group.html", env)
    end},
    {"GET", "/group/([%w_-]+)/repo/([%w_-]+)",
    function (req, gname, rname)
        local is_admin = self:is_admin(req.cookies)
        if not allowed(is_admin, gname) then
            return "/login", 303
        end
        local repo = self.groups[gname][rname]
        local bnames = repo:branches()
        local tnames = repo:tags()
        local env = {
            title=self.title, is_admin=is_admin, repo=repo, gname=gname,
            rname=rname, bnames=bnames, tnames=tnames,
        }
        return lud.template.render_file("view/repo.html", env)
    end},
    {"GET", "/group/([%w_-]+)/repo/([%w_-]+)/history/([%w_-]+)",
    function (req, gname, rname, first)
        local is_admin = self:is_admin(req.cookies)
        if not allowed(is_admin, gname) then
            return "/login", 303
        end
        local repo = self.groups[gname][rname]
        local commit = repo:commit(first)
        local prev = repo:find_prev(commit:id(), self.limit)
        local env = {
            title=self.title, is_admin=is_admin, gname=gname, rname=rname,
            bname=bname, commit=commit, limit=self.limit, prev=prev, first=first,
        }
        return lud.template.render_file("view/history.html", env)
    end},
    {"GET", "/group/([%w_-]+)/repo/([%w_-]+)/commit/([%w_-]+)",
    function (req, gname, rname, cid)
        local is_admin = self:is_admin(req.cookies)
        if not allowed(is_admin, gname) then
            return "/login", 303
        end
        local repo = self.groups[gname][rname]
        local commit = repo:commit(cid)
        local prev = repo:find_prev(commit:id(), 1)
        local sig = commit:signature()
        local time_str = time_fmt(sig)
        local diff = repo:diff(commit, diff_cb)
        local env = {
            title=self.title, is_admin=is_admin, gname=gname, rname=rname, bname=bname,
            commit=commit, time_str=time_str, sig=sig, cid=cid, prev=prev, diff=diff,
        }
        return lud.template.render_file("view/commit.html", env)
    end},
    {"GET", "/group/([%w_-]+)/repo/([%w_-]+)/commit/([%w_-]+)/tree/(.*)",
    function (req, gname, rname, cid, path)
        local is_admin = self:is_admin(req.cookies)
        if not allowed(is_admin, gname) then
            return "/login", 303
        end
        local repo = self.groups[gname][rname]
        local commit = repo:commit(cid)
        local node = commit:tree_entry(path)
        if node == nil then
            return "File not found", 404, "Not found"
        end
        local parts = {}
        for part in path:gmatch("[^/]+") do
            table.insert(parts, part)
        end
        local base = req.path
        if base:sub(#base) ~= "/" then
            base = base .. "/"
        end
        local env = {
            title=self.title, is_admin=is_admin, gname=gname, rname=rname, cid=cid,
            path=path, base=base, parts=parts, node=node,
        }
        if node.type_ == "dir" then
            return lud.template.render_file("view/dir.html", env)
        elseif node.type_ == "file" then
            return lud.template.render_file("view/file.html", env)
        end
    end},
} end

local function new_cogit(path, port, title, log_level)
    local log_levels = {ERROR=0, WARN=1, INFO=2, DEBUG=3}
    local self = {
        path=path or ".",
        port=tonumber(port) or 8080,
        title=title or "cogit",
        log_level=log_levels[log_level or "INFO"],
        limit=25,
        session_age=3*24*60*60,
        sessions={},
        initialized=false,
    }
    self = setmetatable(self, Cogit)
    self:init()
    self:scan()
    self.app = lud.app.new_app(self:routes())
    return self
end

return {new_cogit=new_cogit}