package io.fabric8.mockwebserver.crud;

import com.fasterxml.jackson.databind.JsonNode;
import io.fabric8.mockwebserver.Context;
import io.fabric8.zjsonpatch.JsonPatch;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.RecordedRequest;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class CrudDispatcher extends Dispatcher {

    private static final String POST = "POST";
    private static final String PUT = "PUT";
    private static final String PATCH = "PATCH";
    private static final String GET = "GET";
    private static final String DELETE = "DELETE";

    protected Map<AttributeSet, String> map = new HashMap<>();

    protected final Context context;
    protected final AttributeExtractor attributeExtractor;
    protected final ResponseComposer responseComposer;

    public CrudDispatcher(Context context, AttributeExtractor attributeExtractor, ResponseComposer responseComposer) {
        this.context = context;
        this.attributeExtractor = attributeExtractor;
        this.responseComposer = responseComposer;
    }

    @Override
    public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
        String path = request.getPath();
        String method = request.getMethod();

        switch (method.toUpperCase()) {
            case POST:
            case PUT:
                return handleCreate(path, request.getBody().readUtf8());
            case PATCH:
                return handlePatch(path, request.getBody().readUtf8());
            case GET:
                return handleGet(path);
            case DELETE:
                return handleDelete(path);
            default:
                return null;
        }
    }

    /**
     * Adds the specified object to the in-memory db.
     *
     * @param path
     * @param s
     * @return
     */
    public MockResponse handleCreate(String path, String s) {
        MockResponse response = new MockResponse();
        AttributeSet features = AttributeSet.merge(attributeExtractor.fromPath(path), attributeExtractor.fromResource(s));
        map.put(features, s);
        response.setBody(s);
        response.setResponseCode(202);
        return response;
    }

    /**
     * Patches the specified object to the in-memory db.
     *
     * @param path
     * @param s
     * @return
     */
    public MockResponse handlePatch(String path, String s) {
        MockResponse response = new MockResponse();
        String body = doGet(path);
        if (body == null) {
            response.setResponseCode(404);
        } else {
            try {
                JsonNode patch = context.getMapper().readTree(s);
                JsonNode source = context.getMapper().readTree(body);
                JsonNode updated = JsonPatch.apply(patch, source);
                String updatedAsString = context.getMapper().writeValueAsString(updated);
                AttributeSet features = AttributeSet.merge(attributeExtractor.fromPath(path),
                        attributeExtractor.fromResource(updatedAsString));
                map.put(features, updatedAsString);
                response.setResponseCode(202);
                response.setBody(updatedAsString);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

        }
        return response;
    }

    /**
     * Updates the specified object to the in-memory db.
     *
     * @param path
     * @param s
     * @return
     */
    public MockResponse handleUpdate(String path, String s) {
        return handleCreate(path, s);
    }

    /**
     * Performs a get for the corresponding object from the in-memory db.
     *
     * @param path The path.
     * @return The {@link MockResponse}
     */
    public MockResponse handleGet(String path) {
        MockResponse response = new MockResponse();

        String body = doGet(path);
        if (body == null) {
            response.setResponseCode(404);
        } else {
            response.setResponseCode(200);
            response.setBody(body);
        }
        return response;
    }


    /**
     * Performs a delete for the corresponding object from the in-memory db.
     *
     * @param path
     * @return
     */
    public MockResponse handleDelete(String path) {
        MockResponse response = new MockResponse();
        List<AttributeSet> items = new ArrayList<>();
        AttributeSet query = attributeExtractor.extract(path);

        for (Map.Entry<AttributeSet, String> entry : map.entrySet()) {
            if (entry.getKey().matches(query)) {
                items.add(entry.getKey());
            }
        }
        if (!items.isEmpty()) {
            for (AttributeSet item : items) {
                map.remove(item);
            }
            response.setResponseCode(200);
        } else {
            response.setResponseCode(404);
        }
        return response;
    }

    public Map<AttributeSet, String> getMap() {
        return map;
    }

    public AttributeExtractor getAttributeExtractor() {
        return attributeExtractor;
    }

    public ResponseComposer getResponseComposer() {
        return responseComposer;
    }


    private String doGet(String path) {
        List<String> items = new ArrayList<>();
        AttributeSet query = attributeExtractor.extract(path);
        for (Map.Entry<AttributeSet, String> entry : map.entrySet()) {
            if (entry.getKey().matches(query)) {
                items.add(entry.getValue());
            }
        }

        if (items.isEmpty()) {
            return null;
        } else if (items.size() == 1) {
            return items.get(0);
        } else {
            return responseComposer.compose(items);
        }
    }
}