package com.avg.zaap.crashlibrary.automation.cap;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import fi.iki.elonen.NanoHTTPD;

/**
 * Class that mocks behaviour of cap for test purposes.
 * <p/>
 * Created by marek.osvald on 20.7.2016.
 */
public class CapServer extends NanoHTTPD {

    private static final String REQUEST = "/request";
    private static final String UPLOAD = "/upload";
    private static final String FILE = "/file";
    private static final String DELETE = "/delete";
    private static final String FILES = "/files";
    private static final String UPLOADING = "/uploading";
    private static final String ENABLE_SERVER = "/enable-server";
    private static final String DISABLE_SERVER = "/disable-server";
    private static final String CRLF = "\n";

    private static final String GUID = "guid";
    private static final String PRD = "prd";
    private static final String PRV = "prv";
    private static final String PROTVER = "protver";
    private static final String EXCPRE = "excpre";
    private static final String UPFILES = "upfiles[]";

    public static final String KEY_BCK = "BCK";
    public static final String KEY_EXC = "EXC";
    public static final String KEY_PFN = "PFN";
    public static final String KEY_MDN = "MDN";
    public static final String KEY_FSO = "FSO";
    public static final String KEY_PFV = "PFV";
    public static final String KEY_MDV = "MDV";

    private static final String GUID_REGEX = "^([0-9a-z]){8}-(([0-9a-z]){4}-){3}([0-9a-z]){12}$";
    private static final String PRD_REGEX = "ANDR";
    private static final String PRV_REGEX = "^Crash Simulator (([0-9])+\\.){2}([0-9])+$";
    private static final String PROTVER_REGEX = "2";

    private static final String BCK_REGEX = "([0-9a-f]){40}";
    private static final String EXC_REGEX = "0x([0-9a-f]){8}";
    private static final String PFN_REGEX = "native|(([0-9a-zA-Z])*\\.)*([0-9a-zA-Z])+";
    // FIXME: 21.7.2016 this will be uncommented in later version after merge with new format (maybe)
//    private static final String MDN_REGEX = "MDN (native|([0-9a-zA-Z])+\\.java(:([0-9a-zA-Z])+){2})";
    // TODO: 21.7.2016 temporary, remove after uncommenting ^
    private static final String MDN_REGEX = "([0-9a-zA-Z\\._])+";
    private static final String FSO_REGEX = "0x([0-9a-f]){1,8}";
    private static final String PFV_REGEX = "(([0-9])+\\.){2}([0-9])+";
    private static final String MDV_REGEX = "Crash Simulator (([0-9])+\\.){2}([0-9])+$";

    private static final String REQUEST_OK = "1 ok";
    private static final String UPLOAD_OK = "0 ok";
    private static final String ERROR = "-1 error";

    public static final String TRUE = "true";
    public static final String FALSE = "false";

    private Map<String, Item> mItems = new LinkedHashMap<>();
    private boolean mUploading = false;
    private ResponseType mResponseType = ResponseType.OK;

    private class Item {
        public String guid;
        public String filePath;
        private int mAttributeCount = 0;
        private static final int GOAL = 5;

        public void newAttribute() {
            mAttributeCount++;
        }

        public boolean isCorrect() {
            return mAttributeCount == GOAL;
        }

        public void copy(Item item) {
            guid = item.guid;
            filePath = item.filePath;
        }

        @Override
        public String toString() {
            return "Item { " +
                    "guid='" + guid + '\'' +
                    ", filePath='" + filePath + '\'' +
                    ", mAttributeCount=" + mAttributeCount +
                    " }";
        }
    }

    public CapServer(int port) throws IOException {
        super(port);
        start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
        Logger.d("Server is running on: http://localhost:%s/", port);
    }

    @SuppressWarnings("deprecation")
    @Override
    public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) {
        Logger.d("path is: %s", uri);
        printMap(headers, "headers");
        printMap(parms, "params");
        printMap(files, "files");

        switch (uri) {
            case REQUEST:
                // check whether the response has been overridden
                if (mResponseType == ResponseType.ERROR) {
                    return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, null);
                }

                if (processRequest(files)) {
                    mUploading = true;
                    return newFixedLengthResponse(REQUEST_OK);
                } else {
                    return newFixedLengthResponse(ERROR);
                }

            case UPLOAD:
                // check whether the response has been overridden
                if (mResponseType == ResponseType.ERROR) {
                    return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, null);
                }

                if (parms.containsKey(UPFILES) && processUpload(files, parms.get(UPFILES))) {
                    mUploading = false;
                    return newFixedLengthResponse(UPLOAD_OK);
                } else {
                    return newFixedLengthResponse(ERROR);
                }

            case FILE:
                if (parms.containsKey(GUID)) {
                    String guid = parms.get(GUID);

                    try {
                        File file = new File(mItems.get(guid).filePath);
                        final FileInputStream fileInputStream = new FileInputStream(file);
                        return newChunkedResponse(Response.Status.OK, "application/octet-stream", fileInputStream);
                    } catch (FileNotFoundException ex) {
                        return newFixedLengthResponse(ERROR);
                    }
                } else {
                    return newFixedLengthResponse(ERROR);
                }

            case DELETE:
                if (parms.containsKey(GUID)) {
                    final String guid = parms.get(GUID);

                    if (mItems.containsKey(guid)) {
                        final Item item = mItems.get(guid);

                        final File file = new File(item.filePath);
                        if (!file.delete()) {
                            return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "Unable to delete file.");
                        }

                        mItems.remove(guid);
                        return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, TRUE);
                    } else {
                        return newFixedLengthResponse(Response.Status.BAD_REQUEST, MIME_PLAINTEXT, "");
                    }
                } else {
                    return newFixedLengthResponse(Response.Status.BAD_REQUEST, MIME_PLAINTEXT, "Request requires the mandatory guid query parameter.");
                }

            case FILES:
                return newFixedLengthResponse(String.join(CRLF, mItems.keySet()));

            case UPLOADING:
                return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, mUploading ? TRUE : FALSE);

            case ENABLE_SERVER:
                mResponseType = ResponseType.OK;
                return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, TRUE);

            case DISABLE_SERVER:
                mResponseType = ResponseType.ERROR;
                return newFixedLengthResponse(Response.Status.OK, MIME_PLAINTEXT, TRUE);

            default:
                Logger.d("wrong URI: %s", uri);
                return newFixedLengthResponse(ERROR + "wrong URI: " + uri);
        }
    }

    private boolean processUpload(Map<String, String> files, String filename) {
        Item item = new Item();
        for (Map.Entry<String, String> entry : files.entrySet()) {
            String value = readValue(entry.getValue());
            if (value != null) {
                if (!checkUpload(entry.getKey(), value, item, entry.getValue(), filename)) {
                    return false;
                }
                if (entry.getKey().equals(UPFILES)) {
                    try {
                        Files.copy(
                                Paths.get((new File(entry.getValue())).toURI()),
                                Paths.get((new File(filename)).toURI()),
                                StandardCopyOption.REPLACE_EXISTING
                        );
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        //noinspection ConstantConditions
        if (item != null) {
            mItems.get(item.guid).filePath = filename;
            Logger.d("parsing done");
            return true;
        }
        return false;
    }

    private boolean checkUpload(String key, String attribute, Item item, String srcPath, String dstPath) {
        Logger.d("+ key: %s", key);
//        Logger.d("|-- value: %s", attribute);
        switch (key) {
            case GUID:
                if (attribute.matches(GUID_REGEX + CRLF)) {
                    item.copy(mItems.get(attribute.trim()));
                    return true;
                }
                break;
            case PRD:
                // regex not needed, constant value
                if (attribute.equals(PRD_REGEX + CRLF)) {
                    return true;
                }
                break;
            case UPFILES:
                try {
                    Files.copy(
                            Paths.get((new File(srcPath)).toURI()),
                            Paths.get((new File(dstPath)).toURI()),
                            StandardCopyOption.REPLACE_EXISTING);
                } catch (IOException e) {
                    e.printStackTrace();
                    return false;
                }
                return true;
        }
        return false;
    }

    private boolean processRequest(Map<String, String> files) {
        Item item = new Item();
        for (Map.Entry<String, String> entry : files.entrySet()) {
            String value = readValue(entry.getValue());
            if (value != null) {
                checkRequest(entry.getKey(), value, item);
            }
        }
        if (!item.isCorrect()) {
            Logger.d("Invalid item.");
            return false;
        }

        mItems.put(item.guid, item);
        Logger.d("parsing done");
        return true;
    }

    private void checkRequest(String key, String attribute, Item item) {
        Logger.d("+ key: %s", key);
        Logger.d("|-- value: %s", attribute);
        switch (key) {
            case GUID:
                if (attribute.matches(GUID_REGEX + CRLF)) {
                    item.guid = attribute.trim();
                    item.newAttribute();
                } else {
                    Logger.d(String.format("GUID invalid: %s: attribute.", attribute));
                }

                break;

            case PRD:
                // regex not needed, constant value
                if (attribute.equals(PRD_REGEX + CRLF)) {
                    item.newAttribute();
                } else {
                    Logger.d(String.format("PRD invalid: %s: attribute.", attribute));
                }

                break;

            case PRV:
                if (attribute.matches(PRV_REGEX + CRLF)) {
                    item.newAttribute();
                } else {
                    Logger.d(String.format("PRV invalid: %s: attribute.", attribute));
                }

                break;

            case PROTVER:
                // regex not needed, constant value
                if (attribute.equals(PROTVER_REGEX + CRLF)) {
                    item.newAttribute();
                } else {
                    Logger.d(String.format("PROTVER invalid: %s: attribute.", attribute));
                }

                break;

            case EXCPRE:
                Map<String, String> argumentMap = new HashMap<>();

                final String[] lines = attribute.split(CRLF);

                for (String line : lines) {
                    final String[] parts = line.split(" ", 2);

                    if (parts.length == 2) {
                        argumentMap.put(parts[0], parts[1]);
                    } else {
                        Logger.d(String.format("Invalid header value %s", attribute));
                    }
                }

                // BCK
                if (!argumentMap.containsKey(KEY_BCK) || !argumentMap.get(KEY_BCK).matches(BCK_REGEX)) {
                    Logger.d("Invalid BCK. Missing or does not match regex.");
                    break;
                }

                // EXC
                if (!argumentMap.containsKey(KEY_EXC) || !argumentMap.get(KEY_EXC).matches(EXC_REGEX)) {
                    Logger.d("Invalid EXC. Missing or does not match regex.");
                    break;
                }

                // PFN
                if (!argumentMap.containsKey(KEY_PFN) || !argumentMap.get(KEY_PFN).matches(PFN_REGEX)) {
                    Logger.d("Invalid PFN. Missing or does not match regex.");
                    break;
                }

                // MDN
                if (!argumentMap.containsKey(KEY_MDN) || !argumentMap.get(KEY_MDN).matches(MDN_REGEX)) {
                    Logger.d("Invalid MDN. Missing or does not match regex.");
                    break;
                }

                // FSO
                if (!argumentMap.containsKey(KEY_FSO) || !argumentMap.get(KEY_FSO).matches(FSO_REGEX)) {
                    Logger.d("Invalid FSO. Missing or does not match regex.");
                    break;
                }

                // PFV
                if (!argumentMap.containsKey(KEY_PFV) || !argumentMap.get(KEY_PFV).matches(PFV_REGEX)) {
                    Logger.d("Invalid PFV. Missing or does not match regex.");
                    break;
                }

                // MDV
                if (!argumentMap.containsKey(KEY_MDV) || !argumentMap.get(KEY_MDV).matches(MDV_REGEX)) {
                    Logger.d("=" + attribute + "=");
                    Logger.d("argumentMap: " + argumentMap.containsKey(KEY_MDV));
                    Logger.d("key: " + argumentMap.get(KEY_MDV));
                    Logger.d("Invalid MDV. Missing or does not match regex.");
                    break;
                }

                item.newAttribute();
                break;

            default:
                Logger.d(String.format("NO MATCH for key: %s, attribute: %s", key, attribute));

        }
    }

    /**
     * Read file with given path/name.
     *
     * @param name path to file to read content.
     * @return content of file or null if any error occurs.
     */
    private String readValue(String name) {
        File file = new File(name);
        try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
            StringBuilder builder = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                builder.append(line).append(CRLF);
            }
            return builder.toString();
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    private void printMap(Map<String, String> data, String title) {
        Logger.d("_____________________________%s_________________________________________", title);
        for (Map.Entry<String, String> entry : data.entrySet()) {
            Logger.d("+ key: %s", entry.getKey());
            Logger.d("|-- value: %s", entry.getValue());
        }
        Logger.d("_____________________________________________________________________________");
    }

    private enum ResponseType {
        OK, ERROR
    }

}
