/*
 * Copyright 2015 Network New Technologies Inc.
 *
 * 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.
 */

package com.networknt.light.rule.rule;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import com.networknt.light.rule.AbstractRule;
import com.networknt.light.rule.Rule;
import com.networknt.light.server.DbService;
import com.networknt.light.util.ServiceLocator;
import com.orientechnologies.orient.core.Orient;
import com.orientechnologies.orient.core.db.document.ODatabaseDocumentTx;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.index.OIndex;
import com.orientechnologies.orient.core.metadata.schema.OSchema;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.serialization.serializer.OJSONWriter;
import com.orientechnologies.orient.core.sql.OCommandSQL;
import com.orientechnologies.orient.core.sql.query.OSQLSynchQuery;
import com.tinkerpop.blueprints.Vertex;
import com.tinkerpop.blueprints.impls.orient.OrientGraph;
import com.tinkerpop.blueprints.impls.orient.OrientGraphNoTx;
import com.tinkerpop.blueprints.impls.orient.OrientVertex;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.concurrent.ConcurrentMap;

/**
 * Created by husteve on 10/8/2014.
 */
public abstract class AbstractRuleRule extends AbstractRule implements Rule {
    static final org.slf4j.Logger logger = LoggerFactory.getLogger(AbstractRuleRule.class);

    public abstract boolean execute (Object ...objects) throws Exception;


    protected void addRule(Map<String, Object> data) throws Exception {
        OrientVertex access = null;
        String ruleClass = (String)data.get("ruleClass");
        OrientGraph graph = ServiceLocator.getInstance().getGraph();
        try {
            graph.begin();
            Vertex createUser = graph.getVertexByKey("User.userId", data.remove("createUserId"));
            OrientVertex rule = graph.addVertex("class:Rule", data);
            createUser.addEdge("Create", rule);
            if(rule != null) {
                // add the rule into compile map
                Map<String, Object> compileMap = ServiceLocator.getInstance().getMemoryImage("compileMap");
                ConcurrentMap<String, String> cache = (ConcurrentMap<String, String>)compileMap.get("cache");
                if(cache == null) {
                    cache = new ConcurrentLinkedHashMap.Builder<String, String>()
                            .maximumWeightedCapacity(10000)
                            .build();
                    compileMap.put("cache", cache);
                }
                cache.put(ruleClass, (String)data.get("sourceCode"));
            }

            // For all the newly added rules, the default security access is role based and only
            // owner can access. For some of the rules, like getForm, getMenu, they are granted
            // to anyone in the db script. Don't overwrite if access exists for these rules.

            // check if access exists for the ruleClass and add access if not.
            if(getAccessByRuleClass(ruleClass) == null) {
                access = graph.addVertex("class:Access");
                access.setProperty("ruleClass", ruleClass);
                if(ruleClass.contains("Abstract") || ruleClass.contains("_")) {
                    access.setProperty("accessLevel", "N"); // abstract rule and internal beta tester rule
                } else if(ruleClass.endsWith("EvRule")) {
                    access.setProperty("accessLevel", "A"); // event rule can be only called internally.
                } else {
                    access.setProperty("accessLevel", "R"); // role level access
                    List roles = new ArrayList();
                    roles.add("owner");  // give owner access for the rule by default.
                    access.setProperty("roles", roles);
                }
                access.setProperty("createDate", data.get("createDate"));
                createUser.addEdge("Create", access);
            }
            graph.commit();

            if(access != null) {
                Map<String, Object> accessMap = ServiceLocator.getInstance().getMemoryImage("accessMap");
                ConcurrentMap<Object, Object> cache = (ConcurrentMap<Object, Object>)accessMap.get("cache");
                if(cache == null) {
                    cache = new ConcurrentLinkedHashMap.Builder<Object, Object>()
                            .maximumWeightedCapacity(1000)
                            .build();
                    accessMap.put("cache", cache);
                }
                cache.put(ruleClass, mapper.readValue(access.getRecord().toJSON(),
                        new TypeReference<HashMap<String, Object>>() {
                        }));
            }
        } catch (Exception e) {
            logger.error("Exception:", e);
            graph.rollback();
            throw e;
        } finally {
            graph.shutdown();
        }
    }

    protected void impRule(Map<String, Object> data) throws Exception {
        String ruleClass = (String)data.get("ruleClass");
        OrientGraph graph = ServiceLocator.getInstance().getGraph();
        try {
            graph.begin();
            Vertex rule = graph.getVertexByKey("Rule.ruleClass", ruleClass);
            boolean toCompile = true;
            // remove the existing rule if there is.
            if(rule != null) {
                graph.removeVertex(rule);
                // This is to replace existing rule in memory but due to class loader issue, the
                // new rule will replace the old one after restart the server. don't compile it.
                toCompile = false;
            }
            // create a new rule
            Vertex createUser = graph.getVertexByKey("User.userId", data.remove("createUserId"));
            rule = graph.addVertex("class:Rule", data);
            createUser.addEdge("Create", rule);

            if(rule != null && toCompile) {
                // add the rule into compile map
                Map<String, Object> compileMap = ServiceLocator.getInstance().getMemoryImage("compileMap");
                ConcurrentMap<String, String> cache = (ConcurrentMap<String, String>)compileMap.get("cache");
                if(cache == null) {
                    cache = new ConcurrentLinkedHashMap.Builder<String, String>()
                            .maximumWeightedCapacity(10000)
                            .build();
                    compileMap.put("cache", cache);
                }
                cache.put(ruleClass, (String)data.get("sourceCode"));
            }

            // For all the newly added rules, the default security access is role based and only
            // owner can access. For some of the rules, like getForm, getMenu, they are granted
            // to anyone in the db script. Don't overwrite if access exists for these rules.
            // Also, if ruleClass contains "Abstract" then its access level should be N.

            // check if access exists for the ruleClass and add access if not.
            OrientVertex access = null;
            if(getAccessByRuleClass(graph, ruleClass) == null) {
                access = graph.addVertex("class:Access");
                access.setProperty("ruleClass", ruleClass);
                if(ruleClass.contains("Abstract") || ruleClass.contains("_")) {
                    access.setProperty("accessLevel", "N"); // abstract and internal beta tester rule
                } else if(ruleClass.endsWith("EvRule")) {
                    access.setProperty("accessLevel", "A"); // event rule can be only called internally.
                } else {
                    access.setProperty("accessLevel", "R"); // role level access
                    List roles = new ArrayList();
                    roles.add("owner");  // give owner access for the rule by default.
                    access.setProperty("roles", roles);
                }
                access.setProperty("createDate", data.get("createDate"));
            }
            graph.commit();

            if(access != null) {
                Map<String, Object> accessMap = ServiceLocator.getInstance().getMemoryImage("accessMap");
                ConcurrentMap<Object, Object> cache = (ConcurrentMap<Object, Object>)accessMap.get("cache");
                if(cache == null) {
                    cache = new ConcurrentLinkedHashMap.Builder<Object, Object>()
                            .maximumWeightedCapacity(1000)
                            .build();
                    accessMap.put("cache", cache);
                }
                cache.put(ruleClass, mapper.readValue(access.getRecord().toJSON(),
                        new TypeReference<HashMap<String, Object>>() {
                        }));
            }
        } catch (Exception e) {
            logger.error("Exception:", e);
            graph.rollback();
            throw e;
        } finally {
            graph.shutdown();
        }
    }

    protected String updateValidation(Map<String, Object> inputMap, Map<String, Object> eventData) {
        Map<String, Object> data = (Map<String, Object>)inputMap.get("data");
        Map<String, Object> user = (Map<String, Object>) inputMap.get("user");
        String rid = (String)data.get("@rid");
        String ruleClass = (String)data.get("ruleClass");
        String error = null;
        String host = (String)user.get("host");
        if(host != null) {
            if(!host.equals(data.get("host"))) {
                error = "You can only update rule for host: " + host;
                inputMap.put("responseCode", 403);
            } else {
                // make sure the ruleClass contains the host.
                if(host != null && !ruleClass.contains(host)) {
                    // you are not allowed to update rule as it is not owned by the host.
                    error = "ruleClass is not owned by the host: " + host;
                    inputMap.put("responseCode", 403);
                }
            }
        }
        if(error == null) {
            OrientGraph graph = ServiceLocator.getInstance().getGraph();
            try {
                Vertex rule = DbService.getVertexByRid(graph, rid);
                if(rule == null) {
                    error = "Rule with @rid " + rid + " cannot be found";
                    inputMap.put("responseCode", 404);
                } else {
                    eventData.put("ruleClass", ruleClass);
                    eventData.put("updateUserId", user.get("userId"));
                }
            } catch (Exception e) {
                logger.error("Exception:", e);
                throw e;
            } finally {
                graph.shutdown();
            }
        }
        return error;
    }

    protected void updEtag(Map<String, Object> data) throws Exception {
        String ruleClass = (String)data.get("ruleClass");
        OrientGraph graph = ServiceLocator.getInstance().getGraph();
        try {
            graph.begin();
            Vertex rule = graph.getVertexByKey("Rule.ruleClass", ruleClass);
            if(rule != null) {
                rule.setProperty("enableEtag", data.get("enableEtag"));
                String cacheControl = (String)data.get("cacheControl");
                if(cacheControl != null) {
                    rule.setProperty("cacheControl", cacheControl);
                } else {
                    rule.removeProperty("cacheControl");
                }
                rule.setProperty("updateDate", data.get("updateDate"));
                Map<String, Object> ruleMap = ServiceLocator.getInstance().getMemoryImage("ruleMap");
                ConcurrentMap<String, Map<String, Object>> cache = (ConcurrentMap<String, Map<String, Object>>)ruleMap.get("cache");
                if(cache != null) {
                    cache.remove(ruleClass);
                }
            }
            graph.commit();
        } catch (Exception e) {
            logger.error("Exception:", e);
            graph.rollback();
            throw e;
        } finally {
            graph.shutdown();
        }
    }

    protected void updSchema(Map<String, Object> data) throws Exception {
        String ruleClass = (String)data.get("ruleClass");
        OrientGraph graph = ServiceLocator.getInstance().getGraph();
        try {
            graph.begin();
            Vertex rule = graph.getVertexByKey("Rule.ruleClass", ruleClass);
            if(rule != null) {
                String schema = (String)data.get("schema");
                if(schema != null && schema.length() > 0) {
                    // convert it to map before setProperty.
                    Map<String, Object> schemaMap = mapper.readValue(schema,
                        new TypeReference<HashMap<String, Object>>() {});
                    rule.setProperty("schema", schemaMap);
                } else {
                    rule.removeProperty("schema");
                }
                rule.setProperty("updateDate", data.get("updateDate"));
                Map<String, Object> ruleMap = ServiceLocator.getInstance().getMemoryImage("ruleMap");
                ConcurrentMap<String, Map<String, Object>> cache = (ConcurrentMap<String, Map<String, Object>>)ruleMap.get("cache");
                if(cache != null) {
                    cache.remove(ruleClass);
                }
            }
            graph.commit();
        } catch (Exception e) {
            logger.error("Exception:", e);
            graph.rollback();
            throw e;
        } finally {
            graph.shutdown();
        }
    }

    protected void updCors(Map<String, Object> data) throws Exception {
        String ruleClass = (String)data.get("ruleClass");
        OrientGraph graph = ServiceLocator.getInstance().getGraph();
        try {
            graph.begin();
            Vertex rule = graph.getVertexByKey("Rule.ruleClass", ruleClass);
            if(rule != null) {
                rule.setProperty("enableCors", data.get("enableCors"));
                String corsHosts = (String)data.get("corsHosts");
                if(corsHosts != null) {
                    rule.setProperty("corsHosts", corsHosts);
                } else {
                    rule.removeProperty("corsHosts");
                }
                rule.setProperty("updateDate", data.get("updateDate"));
                Map<String, Object> ruleMap = ServiceLocator.getInstance().getMemoryImage("ruleMap");
                ConcurrentMap<String, Map<String, Object>> cache = (ConcurrentMap<String, Map<String, Object>>)ruleMap.get("cache");
                if(cache != null) {
                    cache.remove(ruleClass);
                }
            }
            graph.commit();
        } catch (Exception e) {
            logger.error("Exception:", e);
            graph.rollback();
            throw e;
        } finally {
            graph.shutdown();
        }
    }

    protected void updPublisher(Map<String, Object> data) throws Exception {
        String ruleClass = (String)data.get("ruleClass");
        OrientGraph graph = ServiceLocator.getInstance().getGraph();
        try {
            graph.begin();
            Vertex rule = graph.getVertexByKey("Rule.ruleClass", ruleClass);
            if(rule != null) {
                rule.setProperty("isPublisher", data.get("isPublisher"));
                rule.setProperty("updateDate", data.get("updateDate"));
                Map<String, Object> ruleMap = ServiceLocator.getInstance().getMemoryImage("ruleMap");
                ConcurrentMap<String, Map<String, Object>> cache = (ConcurrentMap<String, Map<String, Object>>)ruleMap.get("cache");
                if(cache != null) {
                    cache.remove(ruleClass);
                }
            }
            graph.commit();
        } catch (Exception e) {
            logger.error("Exception:", e);
            graph.rollback();
            throw e;
        } finally {
            graph.shutdown();
        }
    }

    protected void updSubscriber(Map<String, Object> data) throws Exception {
        String ruleClass = (String)data.get("ruleClass");
        OrientGraph graph = ServiceLocator.getInstance().getGraph();
        try {
            graph.begin();
            Vertex rule = graph.getVertexByKey("Rule.ruleClass", ruleClass);
            if(rule != null) {
                rule.setProperty("isSubscriber", data.get("isSubscriber"));
                rule.setProperty("updateDate", data.get("updateDate"));
                Vertex updateUser = graph.getVertexByKey("User.userId", data.get("updateUserId"));
                if(updateUser != null) {
                    updateUser.addEdge("Update", rule);
                }
                Map<String, Object> ruleMap = ServiceLocator.getInstance().getMemoryImage("ruleMap");
                ConcurrentMap<String, Map<String, Object>> cache = (ConcurrentMap<String, Map<String, Object>>)ruleMap.get("cache");
                if(cache != null) {
                    cache.remove(ruleClass);
                }
            }
            graph.commit();
        } catch (Exception e) {
            logger.error("Exception:", e);
            graph.rollback();
            throw e;
        } finally {
            graph.shutdown();
        }
    }

    protected void updRule(Map<String, Object> data) throws Exception {
        OrientGraph graph = ServiceLocator.getInstance().getGraph();
        try {
            graph.begin();
            Vertex rule = graph.getVertexByKey("Rule.ruleClass", data.get("ruleClass"));
            if(rule != null) {
                String sourceCode = (String)data.get("sourceCode");
                if(sourceCode != null && !sourceCode.equals(rule.getProperty("sourceCode"))) {
                    rule.setProperty("sourceCode", sourceCode);
                }
                rule.setProperty("updateDate", data.get("updateDate"));
                Vertex updateUser = graph.getVertexByKey("User.userId", data.get("updateUserId"));
                if(updateUser != null) {
                    updateUser.addEdge("Update", rule);
                }
                // there is no need to put updated rule into compileMap.
            }
            graph.commit();
        } catch (Exception e) {
            logger.error("Exception:", e);
            graph.rollback();
            throw e;
        } finally {
            graph.shutdown();
        }
    }

    protected void delRule(Map<String, Object> data) throws Exception {
        String ruleClass = (String)data.get("ruleClass");
        OrientGraph graph = ServiceLocator.getInstance().getGraph();
        try {
            graph.begin();
            Vertex rule = graph.getVertexByKey("Rule.ruleClass", ruleClass);
            if(rule != null) {
                graph.removeVertex(rule);
            }
            graph.commit();
            // check if the rule is in compile cache, remove it.
            Map<String, Object> compileMap = ServiceLocator.getInstance().getMemoryImage("compileMap");
            ConcurrentMap<String, String> cache = (ConcurrentMap<String, String>)compileMap.get("cache");
            if(cache != null) {
                cache.remove(ruleClass);
            }
        } catch (Exception e) {
            logger.error("Exception:", e);
            graph.rollback();
            throw e;
        } finally {
            graph.shutdown();
        }
    }

    protected String getRules(String host) {
        String sql = "SELECT FROM Rule";
        if(host != null) {
            sql = sql + " WHERE host = '" + host;
        }
        String json = null;
        OrientGraph graph = ServiceLocator.getInstance().getGraph();
        try {
            OSQLSynchQuery<ODocument> query = new OSQLSynchQuery<>(sql);
            List<ODocument> rules = graph.getRawGraph().command(query).execute();
            json = OJSONWriter.listToJSON(rules, null);
        } catch (Exception e) {
            logger.error("Exception:", e);
            throw e;
        } finally {
            graph.shutdown();
        }
        return json;
    }

    public static void loadCompileCache() {
        String sql = "SELECT FROM Rule";
        Map<String, Object> compileMap = ServiceLocator.getInstance().getMemoryImage("compileMap");
        ConcurrentMap<String, String> cache = (ConcurrentMap<String, String>)compileMap.get("cache");
        if(cache == null) {
            cache = new ConcurrentLinkedHashMap.Builder<String, String>()
                    .maximumWeightedCapacity(10000)
                    .build();
            compileMap.put("cache", cache);
        }
        OrientGraph graph = ServiceLocator.getInstance().getGraph();
        try {
            for (Vertex rule : (Iterable<Vertex>) graph.command(new OCommandSQL(sql)).execute()) {
                cache.put((String) rule.getProperty("ruleClass"), (String) rule.getProperty("sourceCode"));
            }
        } catch (Exception e) {
            logger.error("Exception:", e);
            throw e;
        } finally {
            graph.shutdown();
        }
    }

    protected String getRuleMap(OrientGraph graph, String host) throws Exception {
        String sql = "SELECT FROM Rule";
        if(host != null) {
            sql = sql + " WHERE host = '" + host;
        }
        String json = null;
        try {
            Map<String, String> ruleMap = new HashMap<String, String> ();
            for (Vertex rule : (Iterable<Vertex>) graph.command(new OCommandSQL(sql)).execute()) {
                ruleMap.put((String) rule.getProperty("ruleClass"), (String) rule.getProperty("sourceCode"));
            }
            json = mapper.writeValueAsString(ruleMap);
        } catch (Exception e) {
            logger.error("Exception:", e);
            throw e;
        } finally {
            graph.shutdown();
        }
        return json;
    }

    protected String getRuleDropdown(String host) {
        String sql = "SELECT FROM Rule";
        if(host != null) {
            sql = sql + " WHERE host = '" + host + "' OR host IS NULL";
        }
        String json = null;
        OrientGraph graph = ServiceLocator.getInstance().getGraph();
        try {
            OSQLSynchQuery<ODocument> query = new OSQLSynchQuery<>(sql);
            List<ODocument> rules = graph.getRawGraph().command(query).execute();
            if(rules.size() > 0) {
                List<Map<String, String>> list = new ArrayList<Map<String, String>>();
                for(ODocument doc: rules) {
                    Map<String, String> map = new HashMap<String, String>();
                    String ruleClass = doc.field("ruleClass");
                    map.put("label", ruleClass);
                    map.put("value", ruleClass);
                    list.add(map);
                }
                json = mapper.writeValueAsString(list);
            }

        } catch (Exception e) {
            logger.error("Exception:", e);
            //throw e;
        } finally {
            graph.shutdown();
        }
        return json;
    }

    protected void updReqTransform(Map<String, Object> data) throws Exception {
        String ruleClass = (String)data.get("ruleClass");
        OrientGraph graph = ServiceLocator.getInstance().getGraph();
        try {
            graph.begin();
            Vertex rule = graph.getVertexByKey("Rule.ruleClass", ruleClass);
            if(rule != null) {
                rule.setProperty("reqTransforms", data.get("reqTransforms"));
                rule.setProperty("updateDate", data.get("updateDate"));
                Vertex updateUser = graph.getVertexByKey("User.userId", data.get("updateUserId"));
                if(updateUser != null) {
                    updateUser.addEdge("Update", rule);
                }
                Map<String, Object> ruleMap = ServiceLocator.getInstance().getMemoryImage("ruleMap");
                ConcurrentMap<String, Map<String, Object>> cache = (ConcurrentMap<String, Map<String, Object>>)ruleMap.get("cache");
                if(cache != null) {
                    cache.remove(ruleClass);
                }
            }
            graph.commit();
        } catch (Exception e) {
            logger.error("Exception:", e);
            graph.rollback();
            throw e;
        } finally {
            graph.shutdown();
        }
    }

    protected void updResTransform(Map<String, Object> data) throws Exception {
        String ruleClass = (String)data.get("ruleClass");
        OrientGraph graph = ServiceLocator.getInstance().getGraph();
        try {
            graph.begin();
            Vertex rule = graph.getVertexByKey("Rule.ruleClass", ruleClass);
            if(rule != null) {
                rule.setProperty("resTransforms", data.get("resTransforms"));
                rule.setProperty("updateDate", data.get("updateDate"));
                Vertex updateUser = graph.getVertexByKey("User.userId", data.get("updateUserId"));
                if(updateUser != null) {
                    updateUser.addEdge("Update", rule);
                }
                Map<String, Object> ruleMap = ServiceLocator.getInstance().getMemoryImage("ruleMap");
                ConcurrentMap<String, Map<String, Object>> cache = (ConcurrentMap<String, Map<String, Object>>)ruleMap.get("cache");
                if(cache != null) {
                    cache.remove(ruleClass);
                }
            }
            graph.commit();
        } catch (Exception e) {
            logger.error("Exception:", e);
            graph.rollback();
            throw e;
        } finally {
            graph.shutdown();
        }
    }
}