--Copyright 2021 The casbin Authors. All Rights Reserved.
--
--Licensed under the Apache License, Version 2.0 (the "License");
--you may not use this file except in compliance with the License.
--You may obtain a copy of the License at
--
--    http://www.apache.org/licenses/LICENSE-2.0
--
--Unless required by applicable law or agreed to in writing, software
--distributed under the License is distributed on an "AS IS" BASIS,
--WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
--See the License for the specific language governing permissions and
--limitations under the License.

local DefaultEffector = require("src.effect.DefaultEffector")
local Effect = require("src.effect.Effect")
local FunctionMap = require("src.model.FunctionMap")
local Model = require("src.model.Model")
local Adapter = require("src.persist.Adapter")
local FileAdapter = require("src.persist.file_adapter.FileAdapter")
local DefaultRoleManager = require("src.rbac.DefaultRoleManager")
local BuiltInFunctions = require("src.util.BuiltInFunctions")
local Log = require("src.util.Log")
local Util = require("src.util.Util")

local luaxp = require("modules.luaxp")

local CoreEnforcer = {
    enabled = false,
    autoSave = false,
    autoBuildRoleLinks = false,
    autoNotifyWatcher = true,
    autoNotifyDispatcher = true,
}
CoreEnforcer.__index = CoreEnforcer

function CoreEnforcer:new(model, adapter)
    local o = {}
    setmetatable(o, self)
    self.__index = self
    self.logger = Log.getLogger()

    if type(model) == "string" then
        if type(adapter) == "string" then
            o:initWithFile(model, adapter)
        else
            o:initWithAdapter(model, adapter)
        end
    else
        if type(adapter) == "string" then
            error("Invalid parameters for Enforcer.")
        else
            o:initWithModelAndAdapter(model, adapter)
        end
    end
    return o
end

function CoreEnforcer:newEnforcerFromText(modelText, policyText)
    local o = {}
    setmetatable(o, self)
    self.__index = self
    o.logger = Log.getLogger()

    local m = o:newModel("", modelText)
    m.logger =  o.logger

    local a = {}
    setmetatable(a, Adapter)
    o.model = m
    o.adapter = a

    o.model:printModel()
    o:initialize()

    o.model:clearPolicy()
 
	string.gsub(policyText, "[^\r\n]+", function(line)
        o.adapter.loadPolicyLine(Util.trim(line), o.model)
    end)

    o.model:sortPoliciesByPriority()

    if o.autoBuildRoleLinks then
        o:buildRoleLinks()
    end

    return o
end

function CoreEnforcer:initWithFile(modelPath, policyPath)
    local a = FileAdapter:new(policyPath)
    self:initWithAdapter(modelPath, a)
end

function CoreEnforcer:initWithAdapter(modelPath, adapter)
    local m = self:newModel(modelPath)
    self:initWithModelAndAdapter(m, adapter)
    self.modelPath = modelPath
end

function CoreEnforcer:initWithModelAndAdapter(m, adapter)
    self.adapter = adapter
    self.model = m
    self.model.logger = self.logger
    self.model:printModel()

    self:initialize()
    if self.adapter and not self:isFiltered() then
        self:loadPolicy()
    end
end

function CoreEnforcer:initialize()
    self.rmMap = {}
    self.enabled = true
    self.autoSave = true
    self.autoBuildRoleLinks = true
    self.autoNotifyDispatcher = true
    self:initBuildRoleLinks()
end

-- 
function CoreEnforcer:initBuildRoleLinks()
    if self.model.model["g"] then
        for ptype, _ in pairs(self.model.model["g"]) do
            self.rmMap[ptype] = DefaultRoleManager:new(10)
        end
    end
end

--[[
     * newModel creates a model.
     *
     * @param modelPath the path of the model file.
     * @param unused unused parameter, just for differentiating with
     *               newModel(String text).
     * @return the model.
]]
function CoreEnforcer:newModel(modelPath, text)
    local m = Model:new()
    if modelPath ~= "" then
        m:loadModel(modelPath)
    else
        m:loadModelFromText(text)
    end
    return m
end

--[[
     * loadModel reloads the model from the model CONF file.
     * Because the policy is attached to a model, so the policy is invalidated
     * and needs to be reloaded by calling LoadPolicy().
]]
function CoreEnforcer:loadModel()
    self.model = self.model:loadModel(self.modelPath)
    self.model.logger = self.logger
    self.model:printModel()

    self:initialize()
end

--[[
     * getModel gets the current model.
     *
     * @return the model of the enforcer.
]]
function CoreEnforcer:getModel()
    return self.model
end

--[[
     * setModel sets the current model.
     *
     * @param model the model.
]]
function CoreEnforcer:setModel(model)
    self.model = model
end

--[[
     * getAdapter gets the current adapter.
     *
     * @return the adapter of the enforcer.
]]
function CoreEnforcer:getAdapter()
    return self.adapter
end

--[[
     * setAdapter sets the current adapter.
     *
     * @param adapter the adapter.
]]
function CoreEnforcer:setAdapter(adapter)
    self.adapter = adapter
end

--[[
     * setWatcher sets the current watcher.
     *
     * @param watcher the watcher.
]]
function CoreEnforcer:setWatcher(watcher)
    self.watcher = watcher
end

--[[
     * setDispatcher sets the current dispatcher.
     *
     * @param dispatcher jCasbin dispatcher
]]
function CoreEnforcer:setDispatcher(dispatcher)
    self.dispatcher = dispatcher
end

--[[
     * SetRoleManager sets the current role manager.
     *
     * @param rm the role manager.
]]
function CoreEnforcer:setRoleManager(rm)
    self.rmMap["g"] = rm
end

--[[
     * setEffector sets the current effector.
     *
     * @param eft the effector.
]]
function CoreEnforcer:setEffector(eft)
    self.eft = eft
end

--[[
     * clearPolicy clears all policy.
]]
function CoreEnforcer:clearPolicy()
    self.model:clearPolicy()
end

--[[
     * loadPolicy reloads the policy from file/database.
]]
function CoreEnforcer:loadPolicy()
    self.model:clearPolicy()
    self.adapter:loadPolicy(self.model)

    self.model:sortPoliciesByPriority()
    self.model:printPolicy()

    if self.autoBuildRoleLinks then
        self:buildRoleLinks()
    end
end

--[[
     * loadFilteredPolicy reloads a filtered policy from file/database.
     *
     * @param filter the filter used to specify which type of policy should be loaded.
]]
function CoreEnforcer:loadFilteredPolicy(filter)
    self.model:clearPolicy()

    self.adapter:loadFilteredPolicy(self.model, filter)

    self:initBuildRoleLinks()
    self.model:printPolicy()
    if self.autoBuildRoleLinks then
        self:buildRoleLinks()
    end
end

--[[
     * isFiltered returns true if the loaded policy has been filtered.
     *
     * @return if the loaded policy has been filtered.
]]
function CoreEnforcer:isFiltered()
    return self.adapter.isFiltered
end

--[[
     * savePolicy saves the current policy (usually after changed with
     * Casbin API) back to file/database.
]]
function CoreEnforcer:savePolicy()
    if self:isFiltered() then
        error("cannot save a filtered policy")
    end

    self.adapter:savePolicy(self.model)

    if self.watcher then
        if self.watcher.updateForSavePolicy then
            self.watcher:updateForSavePolicy(self.model)
        else
            self.watcher:update()
        end
    end
end

--[[
     * enableEnforce changes the enforcing state of Casbin, when Casbin is
     * disabled, all access will be allowed by the enforce() function.
     *
     * @param enable whether to enable the enforcer.
]]
function CoreEnforcer:enableEnforce(enable)
    self.enable = enable
end

-- setLogger changes the current enforcer's logger.
function CoreEnforcer:setLogger(logger)
    self.logger = logger
    self.model.logger = logger
    for _, rm in pairs(self.rmMap) do
        rm.logger = logger
    end
end

--[[
     * enableLog changes whether to print Casbin log to the standard output.
     *
     * @param enable whether to enable Casbin's log.
]]
function CoreEnforcer:enableLog(enable)
    self.logger.enabled = enable
end

-- returns the current logger's enabled status
function CoreEnforcer:isLogEnabled()
    return self.logger.enabled
end

--[[
     * enableAutoSave controls whether to save a policy rule automatically to
     * the adapter when it is added or removed.
     *
     * @param autoSave whether to enable the AutoSave feature.
]]
function CoreEnforcer:enableAutoSave(autoSave)
    self.autoSave = autoSave
end

--[[
     * enableAutoBuildRoleLinks controls whether to save a policy rule
     * automatically to the adapter when it is added or removed.
     *
     * @param autoBuildRoleLinks whether to automatically build the role links.
]]
function CoreEnforcer:enableAutoBuildRoleLinks(autoBuildRoleLinks)
    self.autoBuildRoleLinks  = autoBuildRoleLinks
end

--[[
     * buildRoleLinks manually rebuild the
     * role inheritance relations.
]]
function CoreEnforcer:buildRoleLinks()
    for _, rm in pairs(self.rmMap) do
        rm:clear()
    end

    self.model:buildRoleLinks(self.rmMap)
end

--[[
     * enforce decides whether a "subject" can access a "object" with
     * the operation "action", input parameters are usually: (sub, obj, act).
     *
     * @param rvals the request needs to be mediated, usually an array
     *              of strings, can be class instances if ABAC is used.
     * @return whether to allow the request.
]]
function CoreEnforcer:enforceEx(...)
    local rvals = {...}
    if type(rvals[1]) == "table" and #rvals == 1 then
        rvals = rvals[1]
    end

    if not self.enabled then
        return false
    end
    
    local functions = FunctionMap:new()

    if self.model.model["g"] then
        for key, ast in pairs(self.model.model["g"]) do
            local rm = ast.RM
            functions[key] = BuiltInFunctions.generateGFunction(rm)
        end
    end

    if not self.model.model["m"] then
        error("model is undefined")
    end

    if not self.model.model["m"]["m"] then
        error("model is undefined")
    end

    local rTokens = self.model.model["r"]["r"].tokens
    local pTokens = self.model.model["p"]["p"].tokens

    if #rTokens ~= #rvals then
        error("invalid request size")
    end

    local expString = self.model.model["m"]["m"].value
    local hasEval = Util.hasEval(expString)

    local policyLen = #self.model.model["p"]["p"].policy

    local policyEffects = {}

    expString = Util.replaceInOfMatcher(expString)
    local compiledExpression = luaxp.compile(expString)

    if policyLen ~=0 then
        for i, pvals in pairs(self.model.model["p"]["p"].policy) do
            if #pTokens ~= #pvals then
                error("invalid policy size")
            end

            local context = {}
            for k, v in pairs(functions) do
                context[k] = v
            end
            for k, v in pairs(rTokens) do
                context[v] = rvals[k]
            end
            for k, v in pairs(pTokens) do
                context[v] = pvals[k]
            end

            local tExpString = expString
            if hasEval then
                tExpString = Util.findAndReplaceEval(expString, context)
            end

            local res, err
            if tExpString == expString then
                res, err = luaxp.run(compiledExpression, context)
            else
                res, err = luaxp.evaluate(tExpString, context)
            end
            if err then
                error("evaluation error: " .. err.message)
            end

            local c = true
            if type(res) == "boolean" then
                if not res then
                    table.insert(policyEffects, Effect.INDETERMINATE)
                    c = false
                end
            elseif type(res) == "number" then
                if res == 0 then
                    table.insert(policyEffects, Effect.INDETERMINATE)
                    c = false
                end
            else
                error("matcher result should be boolean or number")
            end
            
            if context["p_eft"] and c then
                local eft = context["p_eft"]
                if eft == "allow" then
                    table.insert(policyEffects, Effect.ALLOW)
                elseif eft == "deny" then
                    table.insert(policyEffects, Effect.DENY)
                else
                    table.insert(policyEffects, Effect.INDETERMINATE)
                end
            elseif c then
                table.insert(policyEffects, Effect.ALLOW)
            end
        end
    else
        local context = {}
        for k, v in pairs(functions) do
            context[k] = v
        end
        for k, v in pairs(rTokens) do
            context[v] = rvals[k]
        end
        for k, v in pairs(pTokens) do
            context[v] = ""
        end
            
        local res, err = luaxp.run(compiledExpression, context)
        if err then
            error("evaluation error: " .. err.message)
        end

        if res then
            table.insert(policyEffects, Effect.ALLOW)
        else
            table.insert(policyEffects, Effect.INDETERMINATE)
        end
    end
    
    local finalResult, explainIndex = DefaultEffector:mergeEffects(self.model.model["e"]["e"].value, policyEffects)

    local explainPolicy = {}

    -- Logging request
    if self.logger.enabled then
        local req = "Request: "
        for _, v in pairs(rvals) do
            if type(v)=="table" then
                req = req .. Util.printTable(v) .. ", "
            else
                req = req .. tostring(v) .. ", "
            end
        end
        req = string.sub(req, 1, -3)
        req = req .. " ---> " .. tostring(finalResult) .. "\n"
        if explainIndex~=-1 and #self.model.model["p"]["p"].policy>=explainIndex then
            req = req .. "Hit Policy: "
            req = req .. Util.printTable(self.model.model["p"]["p"].policy[explainIndex])

            explainPolicy = Util.deepCopy(self.model.model["p"]["p"].policy[explainIndex])
        end

        self.logger:info(req)
    end
    
    return finalResult, explainPolicy
end

function CoreEnforcer:enforce(...)
    local res, _ =  self:enforceEx(...)
    return res
end

function CoreEnforcer:isAutoNotifyWatcher()
    return self.autoNotifyWatcher
end

function CoreEnforcer:setAutoNotifyWatcher(autoNotifyWatcher)
    self.autoNotifyWatcher = autoNotifyWatcher
end

function CoreEnforcer:isAutoNotifyDispatcher()
    return self.autoNotifyDispatcher
end

function CoreEnforcer:setAutoNotifyDispatcher(autoNotifyDispatcher)
    self.autoNotifyDispatcher = autoNotifyDispatcher
end

-- BatchEnforce enforce in batches and returns table of results
function CoreEnforcer:BatchEnforce(requests)
    local results = {}
    for _, request in pairs(requests) do
        local res = self:enforce(request)
        table.insert(results, res)
    end

    return results
end

-- AddNamedMatchingFunc add MatchingFunc by ptype RoleManager
function CoreEnforcer:AddNamedMatchingFunc(ptype, fn)
    if self.rmMap[ptype] then
        self.rmMap[ptype].matchingFunc = fn
        return true
    end
    return false
end

-- AddNamedDomainMatchingFunc add MatchingFunc by ptype to RoleManager
function CoreEnforcer:AddNamedDomainMatchingFunc(ptype, fn)
    if self.rmMap[ptype] then
        self.rmMap[ptype].domainMatchingFunc = fn
        return true
    end
    return false
end

return CoreEnforcer