package be.iminds.ilabt.jfed.lowlevel.api;

import be.iminds.ilabt.jfed.log.Logger;
import be.iminds.ilabt.jfed.lowlevel.*;
import be.iminds.ilabt.jfed.lowlevel.connection.HttpConnection;
import be.iminds.ilabt.jfed.preferences.JFedPreferences;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.GeniUrnTypeAdapter;
import be.iminds.ilabt.jfed.util.RFC3339Util;
import com.google.gson.*;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import org.apache.http.HttpHeaders;
import org.apache.http.entity.ContentType;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.lang.reflect.Type;
import java.net.MalformedURLException;
import java.text.ParseException;
import java.util.*;

/**
 * User: twalcari
 * Date: 10/29/2015
 * Time: 15:23
 */
public class ReputationService extends RestApi {
    private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(ReputationService.class);

    public static final String SERVICE_AVAILABILITY = "availability";
    public static final String SERVICE_OVERALL = "overall";
    public static final String SERVICE_QUALITY = "quality";

    private static final Type SERVICES_MAP_TYPE = new TypeToken<Map<String, Double>>() {
    }.getType();

    private final Gson gson;

    /**
     * @param logger the logger to use. May be null if no logger is used.
     */
    public ReputationService(@Nonnull Logger logger, @Nonnull JFedPreferences jFedPreferences) {
        super(logger, new ApiInfo.Api(ApiInfo.ApiName.FED4FIRE_SLA_COLLECTOR, 1), jFedPreferences);


        GsonBuilder builder = new GsonBuilder();
        builder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
        builder.registerTypeAdapter(Date.class, new DateRFC3339Adapter());
        builder.registerTypeAdapter(GeniUrn.class, new GeniUrnTypeAdapter());
        builder.registerTypeAdapter(SERVICES_MAP_TYPE, new ServicesMapTypeAdapter());
        this.gson = builder.create();
    }

    @Override
    public String getName() {
        return getApiName();
    }

    /**
     * A human readable name for the implemented API
     *
     * @return "SLA Collector";
     */
    static public String getApiName() {
        return "Reputation Service";
    }

    @SuppressWarnings("unused")
    public static class SliceReputation {

        private GeniUrn userUrn;
        private GeniUrn sliceUrn;
        private Date startTime;
        private Date endTime;

        private List<GeniUrn> resources;

        @SerializedName("user_eval")
        private Map<String, Integer> evaluation;

        public SliceReputation() {
        }

        public SliceReputation(GeniUrn userUrn, GeniUrn sliceUrn, Date startTime, Date endTime) {
            this.userUrn = userUrn;
            this.sliceUrn = sliceUrn;
            this.startTime = startTime;
            this.endTime = endTime;
        }

        public GeniUrn getUserUrn() {
            return userUrn;
        }

        public void setUserUrn(GeniUrn userUrn) {
            this.userUrn = userUrn;
        }

        public GeniUrn getSliceUrn() {
            return sliceUrn;
        }

        public void setSliceUrn(GeniUrn sliceUrn) {
            this.sliceUrn = sliceUrn;
        }

        public Date getStartTime() {
            return startTime;
        }

        public void setStartTime(Date startTime) {
            this.startTime = startTime;
        }

        public Date getEndTime() {
            return endTime;
        }

        public void setEndTime(Date endTime) {
            this.endTime = endTime;
        }

        public List<GeniUrn> getResources() {
            return resources;
        }

        public void setResources(List<GeniUrn> resources) {
            this.resources = resources;
        }

        public Map<String, Integer> getEvaluation() {
            return evaluation;
        }

        public void setEvaluation(Map<String, Integer> evaluation) {
            this.evaluation = evaluation;
        }
    }

    public static class TestbedReputation {

        public String testbed;
        public Map<String, Double> services;


        public String getTestbed() {
            return testbed;
        }

        public void setTestbed(String testbed) {
            this.testbed = testbed;
        }

        public Map<String, Double> getServices() {
            return services;
        }

        public void setServices(Map<String, Double> services) {
            this.services = services;
        }
    }

    @ApiMethod(order = 1, hint = "POST")
    public RestReply<List<SliceReputation>> registerExperiment(
            @Nonnull HttpConnection con,
            @ApiMethodParameter(name = "User URN", parameterType = ApiMethodParameterType.USER_URN, hint = "User URN")
            @Nonnull GeniUrn userUrn,
            @ApiMethodParameter(name = "Slice URN", parameterType = ApiMethodParameterType.SLICE_URN, hint = "Slice URN")
            @Nonnull GeniUrn sliceUrn,
            @ApiMethodParameter(name = "Start time", parameterType = ApiMethodParameterType.DATE, hint = "Start time")
            @Nonnull Date startTime,
            @ApiMethodParameter(name = "End time", parameterType = ApiMethodParameterType.SLICE_URN, hint = "End time")
            @Nonnull Date endTime) throws JFedException {
        String relUrl = "exp";
        Map<String, Object> methodParameters = makeMethodParameters(
                "userUrn", userUrn,
                "sliceUrn", sliceUrn,
                "startTime", startTime,
                "endTime", endTime);


        Map<String, Object> request = new HashMap<>();

        SliceReputation experiment =
                new SliceReputation(userUrn, sliceUrn, startTime, endTime);

        request.put("experiments", Collections.singletonList(experiment));


        String url = con.getServerUrl() + relUrl;
        HttpCallDetails reply;
        try {
            reply = executeHttpCall(con, relUrl, HttpVerb.POST, url,
                    ContentType.APPLICATION_JSON, gson.toJson(request),
                    relUrl, methodParameters,
                    EMPTY_URL_PARAMS);
        } catch (MalformedURLException e) {
            throw new JFedException("Error constructing relative URL \"" + url + "\"", e);
        }

        String jsonString = reply.getResultHttpContent();
        try {
            SliceReputation[] sliceReputations =
                    gson.fromJson(jsonString, SliceReputation[].class);
            RestReply<List<SliceReputation>> res =
                    new RestReply<>(reply, Arrays.asList(sliceReputations));

            logNonSfa(reply, res, relUrl, relUrl, con, methodParameters, null);
            return res;

        } catch (JsonSyntaxException ex) {
            LOG.error("Error while deserializing answer from reputation service", ex);

            logNonSfa(reply, null, relUrl, relUrl, con, methodParameters, ex);
            return new RestReply<>(reply, null);
        }

    }


    @ApiMethod(order = 2, hint = "GET")
    public RestReply<TestbedReputation> getReputation(
            @Nonnull HttpConnection con,
            @ApiMethodParameter(name = "Testbed", parameterType = ApiMethodParameterType.AM_URN, hint = "Aggregate URN")
            @Nonnull GeniUrn aggregateUrn) throws JFedException {
        String relUrl = "reputation/show";
        Map<String, Object> methodParameters = makeMethodParameters("testbed", aggregateUrn);
        Map<String, Object> urlParams = new HashMap<>();
        urlParams.put("testbed", aggregateUrn.getEncodedTopLevelAuthority());


        String url = con.getServerUrl() + relUrl;
        HttpCallDetails reply;
        try {
            reply = executeHttpCall(con, relUrl, HttpVerb.GET, url, null, null,
                    relUrl, methodParameters, urlParams);
        } catch (MalformedURLException e) {
            throw new JFedException("Error constructing relative URL \"" + url + "\"", e);
        }


        String jsonString = reply.getResultHttpContent();
        try {
            TestbedReputation[] testbedReputations =
                    gson.fromJson(jsonString, TestbedReputation[].class);
            RestReply<TestbedReputation> res =
                    new RestReply<>(reply, testbedReputations[0]);

            logNonSfa(reply, res, relUrl, relUrl, con, methodParameters, null);
            return res;

        } catch (JsonSyntaxException ex) {
            LOG.error("Error while deserializing answer from reputation service", ex);

            logNonSfa(reply, null, relUrl, relUrl, con, methodParameters, ex);
            return new RestReply<>(reply, null);
        }
    }

    @ApiMethod(order = 3, hint = "GET")
    public RestReply<List<TestbedReputation>> getReputations(@Nonnull HttpConnection con) throws JFedException {
        String relUrl = "reputation/showrep";
        Map<String, Object> methodParameters = Collections.emptyMap();

        String url = con.getServerUrl() + relUrl;
        HttpCallDetails reply;
        try {
            reply = executeHttpCall(con, relUrl, HttpVerb.GET, url, null, null,
                    relUrl, methodParameters, EMPTY_URL_PARAMS);
        } catch (MalformedURLException e) {
            throw new JFedException("Error constructing relative URL \"" + url + "\"", e);
        }


        String jsonString = reply.getResultHttpContent();
        try {
            TestbedReputation[] testbedReputations = gson.fromJson(jsonString, TestbedReputation[].class);
            RestReply<List<TestbedReputation>> res =
                    new RestReply<>(reply, Arrays.asList(testbedReputations));
            logNonSfa(reply, res, relUrl, relUrl, con, methodParameters, null);
            return res;
        } catch (JsonSyntaxException ex) {
            LOG.error("Error while deserializing answer from reputation service", ex);
            logNonSfa(reply, null, relUrl, relUrl, con, methodParameters, ex);
            return new RestReply<>(reply, null);
        }
    }


    @ApiMethod(order = 4, hint = "POST")
    public RestReply<String> registerUserQOE(
            @Nonnull HttpConnection con,
            @ApiMethodParameter(name = "User URN", parameterType = ApiMethodParameterType.USER_URN, hint = "User URN")
            @Nonnull GeniUrn userUrn,
            @ApiMethodParameter(name = "Slice URN", parameterType = ApiMethodParameterType.SLICE_URN, hint = "Slice URN")
            @Nonnull GeniUrn sliceUrn,
            @ApiMethodParameter(name = "Start time", parameterType = ApiMethodParameterType.DATE, hint = "Start time")
            @Nonnull Date startTime,
            @ApiMethodParameter(name = "End time", parameterType = ApiMethodParameterType.SLICE_URN, hint = "End time")
            @Nonnull Date endTime,
            @ApiMethodParameter(name = "Resources", parameterType = ApiMethodParameterType.LIST_OF_URN, hint = "List of resources")
            @Nonnull List<GeniUrn> resources,
            @ApiMethodParameter(name = "Overall score", parameterType = ApiMethodParameterType.INTEGER, hint = "Overall score")
                    int overallScore,
            @ApiMethodParameter(name = "Quality score", parameterType = ApiMethodParameterType.INTEGER, hint = "Quality score")
                    int qualityScore,
            @ApiMethodParameter(name = "Availability score", parameterType = ApiMethodParameterType.INTEGER, hint = "Availability score")
                    int availabilityScore) throws JFedException {
        String relUrl = "userqoe";

        Map<String, Object> methodParameters = makeMethodParameters(
                "userUrn", userUrn,
                "sliceUrn", sliceUrn,
                "startTime", startTime,
                "endTime", endTime,
                "resources", resources,
                "overallScore", overallScore,
                "qualityScore", qualityScore,
                "availabilityScore", availabilityScore);

        SliceReputation experiment = new SliceReputation(userUrn, sliceUrn, startTime, endTime);
        experiment.setResources(resources);

        Map<String, Integer> scores = new HashMap<>();
        scores.put(SERVICE_OVERALL, overallScore);
        scores.put(SERVICE_QUALITY, qualityScore);
        scores.put(SERVICE_AVAILABILITY, availabilityScore);
        experiment.setEvaluation(scores);

        return internalPost(con, relUrl, relUrl, methodParameters, EMPTY_URL_PARAMS,
                ContentType.APPLICATION_JSON, gson.toJson(experiment),
                Collections.singletonMap(HttpHeaders.CONTENT_TYPE, "application/json"));
    }

    private class ServicesMapTypeAdapter implements JsonDeserializer<Map<String, Double>> {

        @Override
        public Map<String, Double> deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
            if (!jsonElement.isJsonArray()) {
                throw new JsonParseException("Expected an JSON Array as outer object for services-map");
            }

            Map<String, Double> result = new HashMap<>();
            for (JsonElement element : jsonElement.getAsJsonArray()) {

                for (Map.Entry<String, JsonElement> entry : element.getAsJsonObject().entrySet()) {
                    if (!entry.getValue().isJsonPrimitive() || !entry.getValue().getAsJsonPrimitive().isNumber()) {
                        throw new JsonParseException("Expected a number as value of " + entry.getKey());
                    }
                    result.put(entry.getKey(), entry.getValue().getAsJsonPrimitive().getAsDouble());
                }

            }
            return result;
        }
    }

    public final class DateRFC3339Adapter extends TypeAdapter<Date> {

        public DateRFC3339Adapter() {
        }

        @Override
        public final void write(JsonWriter out, Date value) throws IOException {
            if (value == null) {
                out.nullValue();
                return;
            }

            out.value(RFC3339Util.dateToRFC3339String(value));
        }

        @Override
        public final Date read(JsonReader in) throws IOException {
            if (in.peek() == JsonToken.NULL)
                return null;

            try {
                return RFC3339Util.rfc3339StringToDate(in.nextString());
            } catch (ParseException e) {
                throw new IOException("Unexpected input for Date", e);
            }
        }

    }
}
