-- gitano.lace
--
-- Interface to Lace for rules management
--
-- Copyright 2012-2017 Daniel Silverstone <dsilvers@digital-scurf.org>
-- All rights reserved.
--
-- Redistribution and use in source and binary forms, with or without
-- modification, are permitted provided that the following conditions
-- are met:
-- 1. Redistributions of source code must retain the above copyright
--    notice, this list of conditions and the following disclaimer.
-- 2. Redistributions in binary form must reproduce the above copyright
--    notice, this list of conditions and the following disclaimer in the
--    documentation and/or other materials provided with the distribution.
-- 3. Neither the name of the author nor the names of their contributors
--    may be used to endorse or promote products derived from this software
--    without specific prior written permission.
--
-- THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-- ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-- SUCH DAMAGE.
--

local lace = require 'lace'
local util = require 'gitano.util'
local gall = require 'gall'
local log = require 'gitano.log'
local i18n = require 'gitano.i18n'
local pat = require 'gitano.patterns'

local pcre = require "rex_pcre"

local global_lookaside = {
   ["_basis"] = [[
include global:core
]]
}

local function _loader(ctx, _name)
   local global_name = _name:match(pat.LACE_GLOBAL_DEFINITION)
   local name, tree, sha = global_name or _name
   if not global_name then
      -- Project load
      if ctx.project and not ctx.project_tree then
         ctx.project_tree = gall.tree.flatten(ctx.project.content.tree.content)
      end
      if ctx.project_tree then
         tree = ctx.project_tree
         sha = ctx.project.sha
      end
   else
      -- Global load
      if global_lookaside[global_name] then
         local resolved = global_lookaside[global_name]
         if type(resolved) == "function" then
            resolved = resolved(ctx)
         end
         return "global_lookaside::" .. global_name, resolved
      end
      if not ctx.global_tree then
         ctx.global_tree = gall.tree.flatten(ctx.global.content.tree.content)
      end
      tree = ctx.global_tree
      sha = ctx.global.sha
   end
   if not tree then
      log.ddebug(i18n.expand("DEBUG_EMPTY_OBJECT_NASCENT"))
      return "empty", "# Nothing to see here\n"
   end
   local blob_name = "rules/" .. name .. ".lace"
   local real_name = sha .. "::" .. blob_name
   if not tree[blob_name] or tree[blob_name].type ~= "blob" then
      local m = i18n.expand("ERROR_UNABLE_TO_FIND_RULE",
                            { name=blob_name, sha=sha, inname=_name })
      log.ddebug(m)
      return lace.error.error(m)
   end
   return real_name, tree[blob_name].obj.content
end

local match_types = {
   exact = function(want, have)
      return want == have
   end,
   prefix = function(want, have)
      return have:sub(1, #want) == want
   end,
   suffix = function(want, have)
      return have:sub(-#want) == want
   end,
   pattern = function(want, have)
      return (have:match(want) ~= nil)
   end,
   pcre = function(want, have)
      return (pcre.match(have, want) ~= nil)
   end
}

-- Match aliases (auto-inverted)
match_types.is = match_types.exact
match_types.starts = match_types.prefix
match_types.ends = match_types.suffix
match_types.startswith = match_types.prefix
match_types.endswith = match_types.suffix

-- Inverted matches
do
   local inverted_matches = {}
   for k, v in pairs(match_types) do
      inverted_matches["!" .. k] = function(...) return not v(...) end
   end
   for k, v in pairs(inverted_matches) do
      match_types[k] = v
   end
end

local function _do_simple_match(ctx, key, matchtype, value)
   value = util.process_expansion(ctx, value)
   local kk = ctx[key] or ""
   local check = match_types[matchtype]
   if type(kk) == "function" then
      -- Realise the value first
      ctx[key] = kk(ctx)
      kk = ctx[key]
   end
   if type(kk) == "string" then
      return check(value, kk)
   else
      local ret = false
      for k in pairs(kk) do
         -- Only bother if the key is a string (set-form)
         if type(k) == "string" then
            ret = ret or check(value, k)
         end
      end
      if matchtype:sub(1,1) == "!" then
         ret = not ret
      end
      return ret
   end
end

local function _simple_match(ctx, key, matchtype, value, guard)
   if guard ~= nil then
      return lace.error.error(i18n.expand("ERROR_UNEXPECTED_ADDITIONAL_ARGUMENT"), {4})
   end
   if value == nil then
      return lace.error.error(i18n.expand("ERROR_MISSING_MATCHTYPE_OR_VALUE"), {1})
   end
   if match_types[matchtype] == nil then
      return lace.error.error(i18n.expand("ERROR_UNKNOWN_MATCHTYPE"), {2})
   end
   return {
      fn = _do_simple_match,
      args = { key, matchtype, util.prep_expansion(value) }
   }
end

local simples = {
   -- Trivial strings/lists
   "operation", "ref", "group", "user", "repository",
   "source", "oldsha", "newsha", "target",
   -- Trees for update operations (flat lists)
   "start_tree", "target_tree",
   -- Tree diffs for update operations (flat lists)
   "treediff/targets", "treediff/added",
   "treediff/deleted", "treediff/modified", "treediff/renamed",
   "treediff/renamedto",
   -- Stuff for 'as' users
   "as_user", "as_group",
   -- Stuff for site admin commands
   "targetuser", "targetgroup", "member",
   -- Stuff from update hooks for object types
   "oldtype", "oldtaggedtype", "oldtaggedsha", "oldsigned",
   "newtype", "newtaggedtype", "newtaggedsha", "newsigned",
   -- Stuff for keyring command
   "keyringname",
   -- Stuff for config command
   "key", "value",
}

local matchers = {}

for _, s in ipairs(simples) do
   matchers[s] = _simple_match
end

local base_compcontext = {
   _lace = {
      loader = _loader,
      controltype = matchers,
   }
}

local function cloddly_bless(ctx)
   local function indexer(tab, name)
      if name:sub(1,7) == "config/" then
         tab[name] = _simple_match
         return _simple_match
      end
   end
   setmetatable(ctx._lace.controltype, {__index = indexer})
   return ctx
end

local function compile_ruleset(repo, adminsha, globaladminsha)
   -- repo is a gitano repository object.
   -- We trust that we can compile the repo's ruleset which involves
   -- finding the admin sha for the repo (unless given) and then the global
   -- admin sha (unless given) and using that to compile a full ruleset.
   local compcontext = cloddly_bless(util.deep_copy(base_compcontext))
   compcontext.repo = repo
   if not repo.is_nascent then
      if not adminsha then
         compcontext.project = repo.git:get("refs/gitano/admin")
      else
         compcontext.project = repo.git:get(adminsha)
      end
   end
   if not globaladminsha then
      compcontext.global = repo.config.commit
   else
      compcontext.global = repo.config.repo.git:get(globaladminsha)
   end

   return lace.compiler.compile(compcontext, "global:_basis")
end

local function run_ruleset(ruleset, ctx)
   -- First check if we're running as bypass
   if ctx.as_user == "gitano-bypass" then
      log.state(i18n.expand("LACE_BYPASS_BANNER_HEADER"))
      log.state(i18n.expand("LACE_BYPASS_ALERT_MESSAGE"))
      log.state(i18n.expand("LACE_BYPASS_BANNER_FOOTER"))
      return "allow", i18n.expand("LACE_BYPASSED")
   end
   if ctx.as_user == nil and ctx.user == "gitano-bypass" then
      log.state(i18n.expand("LACE_BYPASS_BANNER_HEADER"))
      log.state(i18n.expand("LACE_BYPASS_ALERT_MESSAGE"))
      log.state(i18n.expand("LACE_BYPASS_BANNER_FOOTER"))
      return "allow", i18n.expand("LACE_BYPASSED")
   end
   return lace.engine.run(ruleset, ctx)
end

return {
   compile = compile_ruleset,
   run = run_ruleset,
}
