// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you 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.cloud.api.response;

import com.cloud.api.ApiDBUtils;
import com.cloud.api.ApiResponseGsonHelper;
import com.cloud.api.ApiServer;
import com.cloud.serializer.Param;
import com.cloud.server.ManagementServerImpl;
import com.cloud.user.Account;
import com.cloud.utils.HttpUtils;
import com.cloud.utils.encoding.URLEncoder;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.exception.ExceptionProxyObject;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;

import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.ResponseObject;
import org.apache.cloudstack.api.response.AsyncJobResponse;
import org.apache.cloudstack.api.response.AuthenticationCmdResponse;
import org.apache.cloudstack.api.response.CreateCmdResponse;
import org.apache.cloudstack.api.response.ExceptionResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ApiResponseSerializer {
    protected static Logger LOGGER = LogManager.getLogger(ApiResponseSerializer.class);

    public static String toSerializedString(ResponseObject result, String responseType) {
        LOGGER.trace("===Serializing Response===");
        if (HttpUtils.RESPONSE_TYPE_JSON.equalsIgnoreCase(responseType)) {
            return toJSONSerializedString(result, new StringBuilder());
        } else {
            return toXMLSerializedString(result, new StringBuilder());
        }
    }

    public static String toSerializedStringWithSecureLogs(ResponseObject result, String responseType, StringBuilder log) {
        LOGGER.trace("===Serializing Response===");
        if (HttpUtils.RESPONSE_TYPE_JSON.equalsIgnoreCase(responseType)) {
            return toJSONSerializedString(result, log);
        } else {
            return toXMLSerializedString(result, log);
        }
    }

    private static final Pattern s_unicodeEscapePattern = Pattern.compile("\\\\u([0-9A-Fa-f]{4})");

    public static String unescape(String escaped) {
        String str = escaped;
        Matcher matcher = s_unicodeEscapePattern.matcher(str);
        while (matcher.find()) {
            str = str.replaceAll("\\" + matcher.group(0), Character.toString((char)Integer.parseInt(matcher.group(1), 16)));
        }
        return str;
    }

    public static String toJSONSerializedString(ResponseObject result, StringBuilder log) {
        if (result != null && log != null) {
            Gson responseBuilder = ApiResponseGsonHelper.getBuilder().excludeFieldsWithModifiers(Modifier.TRANSIENT, Modifier.STATIC).create();
            Gson logBuilder = ApiResponseGsonHelper.getLogBuilder().excludeFieldsWithModifiers(Modifier.TRANSIENT, Modifier.STATIC).create();

            StringBuilder sb = new StringBuilder();

            sb.append("{\"").append(result.getResponseName()).append("\":");
            log.append("{\"").append(result.getResponseName()).append("\":");
            if (result instanceof ListResponse) {
                List<? extends ResponseObject> responses = ((ListResponse)result).getResponses();
                Integer count = ((ListResponse)result).getCount();
                boolean nonZeroCount = (count != null && count.longValue() != 0);
                if (nonZeroCount) {
                    sb.append("{\"").append(ApiConstants.COUNT).append("\":").append(count);
                    log.append("{\"").append(ApiConstants.COUNT).append("\":").append(count);
                }

                if ((responses != null) && !responses.isEmpty()) {
                    String jsonStr = responseBuilder.toJson(responses.get(0));
                    jsonStr = unescape(jsonStr);
                    String logStr = logBuilder.toJson(responses.get(0));
                    logStr = unescape(logStr);

                    if (nonZeroCount) {
                        sb.append(",\"").append(responses.get(0).getObjectName()).append("\":[").append(jsonStr);
                        log.append(",\"").append(responses.get(0).getObjectName()).append("\":[").append(logStr);
                    }

                    for (int i = 1; i < ((ListResponse)result).getResponses().size(); i++) {
                        jsonStr = responseBuilder.toJson(responses.get(i));
                        jsonStr = unescape(jsonStr);
                        logStr = logBuilder.toJson(responses.get(i));
                        logStr = unescape(logStr);
                        sb.append(",").append(jsonStr);
                        log.append(",").append(logStr);
                    }
                    sb.append("]}");
                    log.append("]}");
                } else  {
                    if (!nonZeroCount) {
                        sb.append("{");
                        log.append("{");
                    }

                    sb.append("}");
                    log.append("}");
                }
            } else if (result instanceof SuccessResponse || result instanceof ExceptionResponse) {
                final String jsonErrorText = unescape(responseBuilder.toJson(result));
                sb.append(jsonErrorText);
                log.append(jsonErrorText);
            } else {
                String jsonStr = responseBuilder.toJson(result);
                if (jsonStr != null && !jsonStr.isEmpty()) {
                    jsonStr = unescape(jsonStr);
                    if (result instanceof AsyncJobResponse || result instanceof CreateCmdResponse || result instanceof AuthenticationCmdResponse) {
                        sb.append(jsonStr);
                    } else {
                        sb.append("{\"").append(result.getObjectName()).append("\":").append(jsonStr).append("}");
                    }
                } else {
                    sb.append("{}");
                }
                String logStr = logBuilder.toJson(result);
                if (logStr != null && !logStr.isEmpty()) {
                    logStr = unescape(logStr);
                    if (result instanceof AsyncJobResponse || result instanceof CreateCmdResponse || result instanceof AuthenticationCmdResponse) {
                        log.append(logStr);
                    } else {
                        log.append("{\"").append(result.getObjectName()).append("\":").append(logStr).append("}");
                    }
                } else {
                    log.append("{}");
                }
            }
            sb.append("}");
            log.append("}");
            return sb.toString();
        }
        return null;
    }

    private static String toXMLSerializedString(ResponseObject result, StringBuilder log) {
        if (result != null && log != null) {
            StringBuilder sb = new StringBuilder();
            sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
            log.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");

            sb.append("<").append(result.getResponseName());
            log.append("<").append(result.getResponseName());

            boolean authenticated = CallContext.current().getCallingAccount().getId() != Account.ACCOUNT_ID_SYSTEM;
            if (ManagementServerImpl.exposeCloudStackVersionInApiXmlResponse.value() && authenticated) {
                sb.append(" cloud-stack-version=\"").append(ApiDBUtils.getVersion()).append("\"");
                log.append(" cloud-stack-version=\"").append(ApiDBUtils.getVersion()).append("\"");
            }
            sb.append(">");
            log.append(">");

            if (result instanceof ListResponse) {
                Integer count = ((ListResponse)result).getCount();

                if (count != null && count != 0) {
                    sb.append("<").append(ApiConstants.COUNT).append(">").append(((ListResponse)result).getCount()).append("</").append(ApiConstants.COUNT).append(">");
                    log.append("<").append(ApiConstants.COUNT).append(">").append(((ListResponse)result).getCount()).append("</").append(ApiConstants.COUNT).append(">");
                }
                List<? extends ResponseObject> responses = ((ListResponse)result).getResponses();
                if ((responses != null) && !responses.isEmpty()) {
                    for (ResponseObject obj : responses) {
                        serializeResponseObjXML(sb, log, obj);
                    }
                }
            } else {
                if (result instanceof CreateCmdResponse || result instanceof AsyncJobResponse || result instanceof AuthenticationCmdResponse) {
                    serializeResponseObjFieldsXML(sb, log, result);
                } else {
                    serializeResponseObjXML(sb, log, result);
                }
            }

            sb.append("</").append(result.getResponseName()).append(">");
            log.append("</").append(result.getResponseName()).append(">");
            return sb.toString();
        }
        return null;
    }

    private static void serializeResponseObjXML(StringBuilder sb, StringBuilder log, ResponseObject obj) {
        if (!(obj instanceof SuccessResponse) && !(obj instanceof ExceptionResponse)) {
            sb.append("<").append(obj.getObjectName()).append(">");
            log.append("<").append(obj.getObjectName()).append(">");
        }
        serializeResponseObjFieldsXML(sb, log, obj);
        if (!(obj instanceof SuccessResponse) && !(obj instanceof ExceptionResponse)) {
            sb.append("</").append(obj.getObjectName()).append(">");
            log.append("</").append(obj.getObjectName()).append(">");
        }
    }

    private static Field[] getFlattenFields(Class<?> clz) {
        List<Field> fields = new ArrayList<Field>();
        fields.addAll(Arrays.asList(clz.getDeclaredFields()));
        if (clz.getSuperclass() != null) {
            fields.addAll(Arrays.asList(getFlattenFields(clz.getSuperclass())));
        }
        return fields.toArray(new Field[] {});
    }

    private static void serializeResponseObjFieldsXML(StringBuilder sb, StringBuilder log, ResponseObject obj) {
        boolean isAsync = false;
        if (obj instanceof AsyncJobResponse)
            isAsync = true;

        Field[] fields = getFlattenFields(obj.getClass());
        for (Field field : fields) {
            if ((field.getModifiers() & Modifier.TRANSIENT) != 0) {
                continue; // skip transient fields
            }

            SerializedName serializedName = field.getAnnotation(SerializedName.class);
            if (serializedName == null) {
                continue; // skip fields w/o serialized name
            }

            boolean logField = true;
            Param param = field.getAnnotation(Param.class);
            if (param != null) {
                RoleType[] allowedRoles = param.authorized();
                if (allowedRoles.length > 0) {
                    boolean permittedParameter = false;
                    Account caller = CallContext.current().getCallingAccount();
                    for (RoleType allowedRole : allowedRoles) {
                        if (allowedRole.getAccountType() == caller.getType()) {
                            permittedParameter = true;
                            break;
                        }
                    }
                    if (!permittedParameter) {
                        LOGGER.trace("Ignoring parameter " + param.name() + " as the caller is not authorized to see it");
                        continue;
                    }
                }
                if (param.isSensitive()) {
                    logField = false;
                }
            }

            field.setAccessible(true);
            Object fieldValue = null;
            try {
                fieldValue = field.get(obj);
            } catch (IllegalArgumentException e) {
                throw new CloudRuntimeException("how illegal is it?", e);
            } catch (IllegalAccessException e) {
                throw new CloudRuntimeException("come on...we set accessible already", e);
            }
            if (fieldValue != null) {
                if (fieldValue instanceof ResponseObject) {
                    ResponseObject subObj = (ResponseObject)fieldValue;
                    if (isAsync) {
                        sb.append("<jobresult>");
                        log.append("<jobresult>");
                    }
                    serializeResponseObjXML(sb, log, subObj);
                    if (isAsync) {
                        sb.append("</jobresult>");
                        log.append("</jobresult>");
                    }
                } else if (fieldValue instanceof Collection<?>) {
                    Collection<?> subResponseList = (Collection<?>)fieldValue;
                    boolean usedUuidList = false;
                    for (Object value : subResponseList) {
                        if (value instanceof ResponseObject) {
                            ResponseObject subObj = (ResponseObject)value;
                            if (serializedName != null) {
                                subObj.setObjectName(serializedName.value());
                            }
                            serializeResponseObjXML(sb, log, subObj);
                        } else if (value instanceof ExceptionProxyObject) {
                            // Only exception responses carry a list of
                            // ExceptionProxyObject objects.
                            ExceptionProxyObject idProxy = (ExceptionProxyObject)value;
                            // If this is the first IdentityProxy field
                            // encountered, put in a uuidList tag.
                            if (!usedUuidList) {
                                sb.append("<" + serializedName.value() + ">");
                                log.append("<" + serializedName.value() + ">");
                                usedUuidList = true;
                            }
                            sb.append("<" + "uuid" + ">" + idProxy.getUuid() + "</" + "uuid" + ">");
                            log.append("<" + "uuid" + ">" + idProxy.getUuid() + "</" + "uuid" + ">");
                            // Append the new descriptive property also.
                            String idFieldName = idProxy.getDescription();
                            if (idFieldName != null) {
                                sb.append("<" + "uuidProperty" + ">" + idFieldName + "</" + "uuidProperty" + ">");
                                log.append("<" + "uuidProperty" + ">" + idFieldName + "</" + "uuidProperty" + ">");
                            }
                        } else if (value instanceof String) {
                            sb.append("<").append(serializedName.value()).append(">").append(value).append("</").append(serializedName.value()).append(">");
                            if (logField) {
                                log.append("<").append(serializedName.value()).append(">").append(value).append("</").append(serializedName.value()).append(">");
                            }
                        }
                    }
                    if (usedUuidList) {
                        // close the uuidList.
                        sb.append("</").append(serializedName.value()).append(">");
                        log.append("</").append(serializedName.value()).append(">");
                    }
                } else if (fieldValue instanceof Date) {
                    sb.append("<").append(serializedName.value()).append(">").append(BaseCmd.getDateString((Date)fieldValue)).append("</").append(serializedName.value()).append(">");
                    log.append("<").append(serializedName.value()).append(">").append(BaseCmd.getDateString((Date)fieldValue)).append("</").append(serializedName.value()).append(">");
                } else {
                    String resultString = escapeSpecialXmlChars(fieldValue.toString());
                    if (!(obj instanceof ExceptionResponse)) {
                        resultString = encodeParam(resultString);
                    }

                    sb.append("<").append(serializedName.value()).append(">").append(resultString).append("</").append(serializedName.value()).append(">");
                    if (logField) {
                        log.append("<").append(serializedName.value()).append(">").append(resultString).append("</").append(serializedName.value()).append(">");
                    }
                }
            }
        }
    }

    private static String escapeSpecialXmlChars(String originalString) {
        char[] origChars = originalString.toCharArray();
        StringBuilder resultString = new StringBuilder();

        for (char singleChar : origChars) {
            if (singleChar == '"') {
                resultString.append("&quot;");
            } else if (singleChar == '\'') {
                resultString.append("&apos;");
            } else if (singleChar == '<') {
                resultString.append("&lt;");
            } else if (singleChar == '>') {
                resultString.append("&gt;");
            } else if (singleChar == '&') {
                resultString.append("&amp;");
            } else {
                resultString.append(singleChar);
            }
        }

        return resultString.toString();
    }

    private static String encodeParam(String value) {
        if (!ApiServer.isEncodeApiResponse()) {
            return value;
        }
        try {
            return new URLEncoder().encode(value).replaceAll("\\+", "%20");
        } catch (Exception e) {
            LOGGER.warn("Unable to encode: " + value, e);
        }
        return value;
    }

}
