package org.bdware.sc.engine.hook;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.bdware.doip.codec.doipMessage.DoipMessage;
import org.bdware.doip.codec.doipMessage.DoipMessageFactory;
import org.bdware.doip.codec.doipMessage.DoipResponseCode;
import org.bdware.doip.codec.operations.BasicOperations;
import org.bdware.sc.JSEngine;
import org.bdware.sc.bean.ContractRequest;
import org.bdware.sc.boundry.ScriptReturnException;
import org.bdware.sc.entity.DoipMessagePacker;
import org.bdware.sc.node.AnnotationHook;
import org.bdware.sc.node.ArgPacks;
import org.bdware.sc.node.FunctionNode;

import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class DOOPHandler implements AnnotationHook {
    public static Map<String, BasicOperations> funcNameToDoipOperations;
    public static DOOPHandler instance;
    public DOOPHandler() {
        funcNameToDoipOperations = new HashMap<>();
    }

    public static DOOPHandler createDOOPHandler() {
        if(instance == null) {
            instance = new DOOPHandler();
        }

        return instance;
    }

    public void putFuncNameAndDoipOperationsMapping(FunctionNode fn) {
        String basicOperationsString = fn.getDoipOperationInfo().operationType;
        BasicOperations operation = BasicOperations.Unknown;
        for(BasicOperations basicOperation : BasicOperations.values()) {
            if(basicOperation.toString().equals(basicOperationsString)) {
                operation = basicOperation;
            }
        }
        funcNameToDoipOperations.put(fn.getFunctionName(), operation);
    }

    @Override
    public ArgPacks handle(JSEngine desktopEngine, ArgPacks argPacks) {
        Object arg = argPacks.arg;
        DoipMessagePacker doipMsgPackerArg = new DoipMessagePacker();

        if (arg instanceof DoipMessagePacker){
            doipMsgPackerArg = (DoipMessagePacker)arg;
        } else {
            // validate http request's params
            ContractRequest httpReq = argPacks.request;
            validateHTTPRequestArgs(httpReq);

            // set doipMsgPackerArg struct's params
            doipMsgPackerArg.setSource("http");
            doipMsgPackerArg.rawDoipMsg = convertHttpRequestToDoipMessage(httpReq);
        }

        argPacks.arg = doipMsgPackerArg;
        return argPacks;
    }

    public static void validateHTTPRequestArgs(ContractRequest httpReq) {
        JsonElement originArgs = httpReq.getArg();
        JsonElement httpArgs = JsonParser.parseString(originArgs.getAsString());
        BasicOperations curOp = funcNameToDoipOperations.get(httpReq.getAction());

        // get args rules and validate http args
        JsonElement httpArgsRules = getRulesForHTTPRequest(curOp);
        ArgSchemaVisitor visitor = new ArgSchemaVisitor(httpArgs);
        validateJsonElementRulesByArgSchemaVisitor(httpArgsRules, visitor);
    }

    public static JsonElement getRulesForHTTPRequest(BasicOperations basicOperation) {
        switch(basicOperation) {
            case Hello:
            case Delete:
            case ListOps:
                return JsonParser.parseString("{\"!header\":{\"!identifier\":\"string\"}}");
            case Create:
            case Update:
                return JsonParser.parseString("{\"!header\":{\"!identifier\":\"string\"}, \"!body\":\"string\"}");
            case Search:
                return JsonParser.parseString("{\"!header\":{\"!identifier\":\"string\", \"!attributes\":{\"!query\":\"string\", \"!pageNum\":\"int\", \"!pageSize\":\"int\", \"!type\":\"string\"}}}");
            case Retrieve:
                return JsonParser.parseString("{\"!header\":{\"!identifier\":\"string\", \"attributes\":{\"element\":\"string\", \"includeElementData\":\"boolean\"}}}");
            case Extension:
            case Unknown:
            default:
                return null;
        }
    }

    public static JsonElement getRulesForJsonResponse(BasicOperations basicOperations) {
        switch (basicOperations) {
            case Hello:
            case Retrieve:
            case Create:
            case Update:
            case Search:
            case ListOps:
                return JsonParser.parseString("{\"!header\":{\"!response\":\"string\"}, \"!body\":\"string\"}");
            case Delete:
                return JsonParser.parseString("{\"!header\":{\"!response\":\"string\"}, \"body\":\"string\"}");
            case Extension:
            case Unknown:
            default:
                return null;
        }
    }

    public static DoipMessage convertHttpRequestToDoipMessage(ContractRequest httpReq) {
        BasicOperations httpOperation = funcNameToDoipOperations.get(httpReq.getAction());
        JsonObject jsonParams = JsonParser.parseString(httpReq.getArg().getAsString()).getAsJsonObject();
        // taking Extension into consideration
        JsonObject header = jsonParams.get("header") != null ? jsonParams.get("header").getAsJsonObject() : null;
        JsonObject body = jsonParams.get("body") != null ? jsonParams.get("body").getAsJsonObject() : null;
        DoipMessage doipMessage = null;
        switch(httpOperation) {
            case Hello:
            case Delete:
            case ListOps:
                doipMessage = new DoipMessageFactory.DoipMessageBuilder().createRequest(header.get("identifier").getAsString(), httpOperation.getName()).create();
                break;
            case Create:
            case Update:
                doipMessage = new DoipMessageFactory.DoipMessageBuilder()
                        .createRequest(header.get("identifier").getAsString(), httpOperation.getName())
                        .setBody(body.getAsString().getBytes(StandardCharsets.UTF_8))
                        .create();
                break;
            case Search:
                DoipMessageFactory.DoipMessageBuilder searchBuilder = new DoipMessageFactory.DoipMessageBuilder()
                        .createRequest(header.get("identifier").getAsString(), httpOperation.getName());
                JsonElement query = header.get("query");
                if(query != null) searchBuilder.addAttributes("query", query.getAsString());
                JsonElement pageNum = header.get("pageNum");
                if(pageNum != null) searchBuilder.addAttributes("pageNum", pageNum.getAsInt());
                JsonElement pageSize = header.get("pageSize");
                if(pageSize != null) searchBuilder.addAttributes("pageSize", pageSize.getAsInt());
                JsonElement type = header.get("type");
                if(type != null) searchBuilder.addAttributes("type", type.getAsString());

                doipMessage = searchBuilder.create();
                break;
            case Retrieve:
                doipMessage = new DoipMessageFactory.DoipMessageBuilder().createRequest(header.get("identifier").getAsString(), httpOperation.getName()).create();
                JsonElement element = header.get("element");
                JsonElement includeElementData = header.get("includeElementData");
                if(element != null) doipMessage.header.parameters.addAttribute("element", element.getAsString());
                if(includeElementData != null && includeElementData.getAsBoolean()) doipMessage.header.parameters.addAttribute("includeElementData", "true");
                break;
            case Extension:
                DoipMessageFactory.DoipMessageBuilder extensionBuilder = new DoipMessageFactory.DoipMessageBuilder();
                if(header != null) {
                    if(header.get("identifier") != null) {
                        extensionBuilder = extensionBuilder
                                .createRequest(header.get("identifier").getAsString(), httpOperation.getName());
                    }

                    Set<Map.Entry<String, JsonElement>> entries = header.entrySet();
                    for (Map.Entry<String, JsonElement> entry : entries) {
                        extensionBuilder.addAttributes(entry.getKey(), entry.getValue());
                    }
                }

                if(body != null) {
                    extensionBuilder.setBody(body.getAsString().getBytes(StandardCharsets.UTF_8));
                }

                doipMessage = extensionBuilder.create();
                break;
            case Unknown:
            default:
                break;
        }

        return doipMessage;
    }

    public static DoipMessage convertJsonResponseToDoipMessage(FunctionNode fn, JsonElement jsonResponse, DoipMessage msg) {
        BasicOperations curOp = funcNameToDoipOperations.get(fn.getFunctionName());
        JsonObject jsonParams = jsonResponse.getAsJsonObject();

        // validate json response
        JsonElement jsonResponseRules = getRulesForJsonResponse(curOp);
        ArgSchemaVisitor visitor = new ArgSchemaVisitor(jsonResponse);
        validateJsonElementRulesByArgSchemaVisitor(jsonResponseRules, visitor);

        JsonObject header = jsonParams.get("header") != null ? jsonParams.get("header").getAsJsonObject() : null;
        String body = jsonParams.get("body") != null ? jsonParams.get("body").getAsString() : null;

        if(header != null) {
            String headerRespCode = header.get("response") != null ? header.get("response").getAsString() : null;
            if(headerRespCode != null) {
                for (DoipResponseCode responseCode : DoipResponseCode.values()) {
                    if(responseCode.toString().equals(headerRespCode)) {
                        msg.header.parameters.response = responseCode;
                        break;
                    }
                }
            }
        }

        if(body != null) {
            msg.body.encodedData = body.getBytes(StandardCharsets.UTF_8);
        }

        return msg;
    }

    public static void validateJsonElementRulesByArgSchemaVisitor(JsonElement jsonElement, ArgSchemaVisitor visitor) {
        visitor.visit(jsonElement);
        if (!visitor.getStatus()) {
            JsonObject jo = new JsonObject();
            jo.addProperty("msg", visitor.getException());
            jo.addProperty("code", visitor.errorCode);
            throw new ScriptReturnException(jo);
        }
    }
}
