package com.smockin.mockserver.service;

import com.smockin.admin.dto.UserKeyValueDataDTO;
import com.smockin.admin.service.SmockinUserService;
import com.smockin.admin.service.UserKeyValueDataService;
import com.smockin.mockserver.exception.InboundParamMatchException;
import com.smockin.mockserver.service.enums.ParamMatchTypeEnum;
import com.smockin.utils.GeneralUtils;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import spark.Request;

import java.text.SimpleDateFormat;

/**
 * Created by mgallina on 09/08/17.
 */
@Service
public class InboundParamMatchServiceImpl implements InboundParamMatchService {

    private final Logger logger = LoggerFactory.getLogger(InboundParamMatchServiceImpl.class);

    @Autowired
    private SmockinUserService smockinUserService;

    @Autowired
    private UserKeyValueDataService userKeyValueDataService;


    private static final String GENERAL_ERROR = "Error processing inbound param matching. Please check your token syntax";

    @Override
    public String enrichWithInboundParamMatches(final Request req,
                                                final String mockPath,
                                                final String responseBody,
                                                final String userCtxPath,
                                                final long mockOwnerUserId) throws InboundParamMatchException {

        if (responseBody == null) {
            return null;
        }

        final String sanitizedUserCtxInboundPath = GeneralUtils.sanitizeMultiUserPath(smockinUserService.getUserMode(), req.pathInfo(), userCtxPath);

        String enrichedResponseBody = responseBody;

        final int MAX = 10000;
        int index = 0;

        while (true) {

            if (index > MAX) {
                logger.error("Error MAX iterations reached in 'while loop', whilst trying to swap out inbound param tokens.");
                throw new InboundParamMatchException(GENERAL_ERROR);
            }

            final String r;

            try {
                r = processParamMatch(req, mockPath, enrichedResponseBody, sanitizedUserCtxInboundPath, mockOwnerUserId);
            } catch (Throwable ex) {
                logger.error(ex.getMessage());
                throw new InboundParamMatchException(GENERAL_ERROR);
            }

            if (r == null) {
                break;
            }

            enrichedResponseBody = r;

            index++;
        }

        return enrichedResponseBody;
    }

    String processParamMatch(final Request req,
                             final String mockPath,
                             final String responseBody,
                             final String sanitizedUserCtxInboundPath,
                             final long mockOwnerUserId) {

        // Look up for any 'inbound param token' matches
        final Pair<ParamMatchTypeEnum, Integer> matchResult = findInboundParamMatch(responseBody);

        if (matchResult == null) {
            // No tokens found so do nothing.
            return null;
        }

        final ParamMatchTypeEnum paramMatchType = matchResult.getLeft();
        final int matchStartingPosition = matchResult.getRight();

        // Determine the matching token type, is it a requestHeader, requestParameter, pathVar, etc...
        if (ParamMatchTypeEnum.lookUpKvp.equals(paramMatchType)) {
            return processKvp(matchStartingPosition, sanitizedUserCtxInboundPath, mockPath, req, responseBody, mockOwnerUserId);
        }

        if (ParamMatchTypeEnum.requestHeader.equals(paramMatchType)) {
            return processRequestHeader(matchStartingPosition, req, responseBody);
        }

        if (ParamMatchTypeEnum.requestParameter.equals(paramMatchType)) {
            return processRequestParameter(matchStartingPosition, req, responseBody);
        }

        if (ParamMatchTypeEnum.pathVar.equals(paramMatchType)) {
            return processPathVariable(sanitizedUserCtxInboundPath, matchStartingPosition, mockPath, responseBody);
        }

        if (ParamMatchTypeEnum.requestBody.equals(paramMatchType)) {
            return StringUtils.replaceIgnoreCase(responseBody,
                    ParamMatchTypeEnum.PARAM_PREFIX + ParamMatchTypeEnum.requestBody,
                    (req.body() != null) ? req.body() : "",
                    1);
        }

        if (ParamMatchTypeEnum.isoDate.equals(paramMatchType)) {
            return StringUtils.replaceIgnoreCase(responseBody,
                    ParamMatchTypeEnum.PARAM_PREFIX + ParamMatchTypeEnum.isoDate,
                    new SimpleDateFormat(GeneralUtils.ISO_DATE_FORMAT).format(GeneralUtils.getCurrentDate()),
                    1);
        }

        if (ParamMatchTypeEnum.isoDatetime.equals(paramMatchType)) {
            return StringUtils.replaceIgnoreCase(responseBody,
                    ParamMatchTypeEnum.PARAM_PREFIX + ParamMatchTypeEnum.isoDatetime,
                    new SimpleDateFormat(GeneralUtils.ISO_DATETIME_FORMAT).format(GeneralUtils.getCurrentDate()),
                    1);
        }

        if (ParamMatchTypeEnum.uuid.equals(paramMatchType)) {
            return StringUtils.replaceIgnoreCase(responseBody,
                    ParamMatchTypeEnum.PARAM_PREFIX + ParamMatchTypeEnum.uuid,
                    GeneralUtils.generateUUID(),
                    1);
        }

        if (ParamMatchTypeEnum.randomNumber.equals(paramMatchType)) {
            return processRandomNumber(matchStartingPosition, responseBody);
        }

        throw new IllegalArgumentException("Unsupported token : " + matchResult);
    }

    Pair<ParamMatchTypeEnum, Integer> findInboundParamMatch(final String responseBody) {

        if (responseBody == null) {
            return null;
        }

        for (ParamMatchTypeEnum p : ParamMatchTypeEnum.values()) {
            final int pos = StringUtils.indexOf(responseBody, ParamMatchTypeEnum.PARAM_PREFIX + p.name() + ((p.takesArg()) ? "(" : ""));
            if (pos > -1) {
                return Pair.of(p, pos + ((p.takesArg()) ? 1 : 0));
            }
        }

        return null;
    }

    String extractArgName(final int matchStartPos, final ParamMatchTypeEnum paramMatchType, final String responseBody, final boolean isNested) {

        final int start = matchStartPos + (ParamMatchTypeEnum.PARAM_PREFIX + paramMatchType).length();
        final int closingPos = StringUtils.indexOf(responseBody, (isNested) ? "))" : ")", start);

        return StringUtils.substring(responseBody, start, closingPos);
    }

    String sanitiseArgName(String argName) {

        argName = StringUtils.remove(argName, "'");

        return StringUtils.remove(argName, "\"");
    }

    String processKvp(final int matchStartingPosition,
                      final String sanitizedUserCtxInboundPath,
                      final String mockPath,
                      final Request req,
                      final String responseBody,
                      final long mockOwnerUserId) {

        // Determine the matching token type, is it a requestHeader, requestParameter, pathVar, etc...

        final String kvpKey = extractArgName(matchStartingPosition, ParamMatchTypeEnum.lookUpKvp, responseBody, false);
        String sanitisedKvpKey = sanitiseArgName(kvpKey);

        if (sanitisedKvpKey.contains("(") && !sanitisedKvpKey.contains(")")) {
            sanitisedKvpKey = sanitisedKvpKey.concat(")");
        }

        if (logger.isDebugEnabled()) {
            logger.debug("RAW KVP: " + kvpKey);
            logger.debug("Cleaned KVP : " + sanitisedKvpKey);
        }

        // Check if kvpKey is a nested ParamMatchTypeEnum itself
        final Pair<ParamMatchTypeEnum, Integer> kvpMatchResult = findInboundParamMatch(sanitisedKvpKey);
        final boolean isNested = (kvpMatchResult != null);

        if (isNested) {

            if (logger.isDebugEnabled()) {
                logger.debug("Nested KVP request type: " + kvpMatchResult.getLeft());
            }

            final String nestedRequestKey = extractArgName(kvpMatchResult.getRight(), kvpMatchResult.getLeft(), sanitisedKvpKey, isNested);

            if (logger.isDebugEnabled()) {
                logger.debug("Nested KVP request key: " + nestedRequestKey);
            }

            switch (kvpMatchResult.getLeft()) {
                case requestHeader:
                    sanitisedKvpKey = GeneralUtils.findHeaderIgnoreCase(req, sanitiseArgName(nestedRequestKey));
                    break;
                case requestParameter:
                    sanitisedKvpKey = GeneralUtils.findRequestParamIgnoreCase(req, sanitiseArgName(nestedRequestKey));
                    break;
                case pathVar:
                    sanitisedKvpKey = GeneralUtils.findPathVarIgnoreCase(sanitizedUserCtxInboundPath, mockPath, sanitiseArgName(nestedRequestKey));
                    break;
                case requestBody:
                    sanitisedKvpKey = req.body();
                    break;
                default:
                    sanitisedKvpKey = null;
                    break;
            }
        }

        final UserKeyValueDataDTO userKeyValueDataDTO = (sanitisedKvpKey != null)
                ? userKeyValueDataService.loadByKey(sanitisedKvpKey, mockOwnerUserId)
                : null;

        if (logger.isDebugEnabled()) {
            logger.debug("KVP value: " + ((userKeyValueDataDTO != null) ? userKeyValueDataDTO.getValue() : null));
        }

        return StringUtils.replaceIgnoreCase(responseBody,
                ParamMatchTypeEnum.PARAM_PREFIX + ParamMatchTypeEnum.lookUpKvp + "(" + kvpKey + ((kvpKey.contains("(")) ? "))" : ")"),
                (userKeyValueDataDTO != null) ? userKeyValueDataDTO.getValue() : "",
                1);
    }

    String processRequestHeader(final int matchStartingPosition, final Request req, final String responseBody) {

        final String headerName = extractArgName(matchStartingPosition, ParamMatchTypeEnum.requestHeader, responseBody, false);
        final String headerValue = GeneralUtils.findHeaderIgnoreCase(req, sanitiseArgName(headerName));

        if (logger.isDebugEnabled()) {
            logger.debug("raw header: " + headerName);
            logger.debug("cleaned header: " + sanitiseArgName(headerName));
            logger.debug("header value: " + headerValue);
        }

        return StringUtils.replaceIgnoreCase(responseBody,
                ParamMatchTypeEnum.PARAM_PREFIX + ParamMatchTypeEnum.requestHeader + "(" + headerName + ")",
                (headerValue != null) ? headerValue : "",
                1);
    }

    String processRequestParameter(final int matchStartingPosition, final Request req, final String responseBody) {

        final String requestParamName = extractArgName(matchStartingPosition, ParamMatchTypeEnum.requestParameter, responseBody, false);
        final String requestParamValue = GeneralUtils.findRequestParamIgnoreCase(req, sanitiseArgName(requestParamName));

        if (logger.isDebugEnabled()) {
            logger.debug("RAW request param: " + requestParamName);
            logger.debug("Cleaned request param: " + sanitiseArgName(requestParamName));
            logger.debug("Request param value: " + requestParamValue);
        }

        return StringUtils.replaceIgnoreCase(responseBody,
                ParamMatchTypeEnum.PARAM_PREFIX + ParamMatchTypeEnum.requestParameter + "(" + requestParamName + ")",
                (requestParamValue != null) ? requestParamValue : "",
                1);

    }

    String processPathVariable(final String sanitizedUserCtxInboundPath, final int matchStartingPosition, final String mockPath, final String responseBody) {

        final String pathVariableName = extractArgName(matchStartingPosition, ParamMatchTypeEnum.pathVar, responseBody, false);
        final String pathVariableValue = GeneralUtils.findPathVarIgnoreCase(sanitizedUserCtxInboundPath, mockPath, sanitiseArgName(pathVariableName));

        if (logger.isDebugEnabled()) {
            logger.debug("RAW path var: " + pathVariableName);
            logger.debug("Cleaned path var : " + sanitiseArgName(pathVariableName));
            logger.debug("Path var value: " + pathVariableValue);
        }

        return StringUtils.replaceIgnoreCase(responseBody,
                ParamMatchTypeEnum.PARAM_PREFIX + ParamMatchTypeEnum.pathVar + "(" + pathVariableName + ")",
                (pathVariableValue != null) ? pathVariableValue : "",
                1);

    }

    String processRandomNumber(final int matchStartingPosition, final String responseBody) {

        final String randomNumberContent = extractArgName(matchStartingPosition, ParamMatchTypeEnum.randomNumber, responseBody, false);

        if (logger.isDebugEnabled()) {
            logger.debug("Random number params: " + randomNumberContent);
        }

        if (randomNumberContent == null) {
            throw new IllegalArgumentException(ParamMatchTypeEnum.randomNumber.name() + " is missing args");
        }

        final String[] randomNumberContentParams = StringUtils.split(randomNumberContent, ",");

        if (randomNumberContentParams.length == 0) {
            throw new IllegalArgumentException(ParamMatchTypeEnum.randomNumber.name() + " is missing args");
        }

        if (randomNumberContentParams.length > 2) {
            throw new IllegalArgumentException(ParamMatchTypeEnum.randomNumber.name() + " has too many args");
        }

        final int startInc = (randomNumberContentParams.length == 2) ? Integer.parseInt(randomNumberContentParams[0].trim()) : 0;
        final int endExcl = (randomNumberContentParams.length == 2) ? Integer.parseInt(randomNumberContentParams[1].trim()) : Integer.parseInt(randomNumberContentParams[0].trim());
        final int randomValue = RandomUtils.nextInt(startInc, endExcl);

        if (logger.isDebugEnabled()) {
            logger.debug("Random number value: " + randomValue);
        }

        return StringUtils.replaceIgnoreCase(responseBody,
                ParamMatchTypeEnum.PARAM_PREFIX + ParamMatchTypeEnum.randomNumber + "(" + randomNumberContent + ")",
                String.valueOf(randomValue),
                1);
    }

}