package be.iminds.jfed.gts_highlevel.controller;

import be.iminds.ilabt.jfed.lowlevel.JFedException;
import be.iminds.ilabt.jfed.lowlevel.api.RestApi;
import be.iminds.jfed.gts_highlevel.GtsService;
import be.iminds.jfed.gts_highlevel.GtsServiceFactory;
import be.iminds.jfed.gts_highlevel.model.*;
import be.iminds.jfed.gts_highlevel.model.helpers.*;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.xml.bind.*;
import java.io.StringReader;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * User: twalcari
 * Date: 9/22/2015
 * Time: 12:10
 */

public class GtsReservationManager {
    private static final Logger LOG = LoggerFactory.getLogger(GtsReservationManager.class);

    private final GtsCredentials gtsCredentials;

    private final be.iminds.ilabt.jfed.log.Logger logger;
    private final GtsService gtsService;
    private final GtsConnectionProvider gtsConnectionProvider;
    private final GtsModel gtsModel;

    private final Unmarshaller unmarshaller;

    GtsReservationManager(@Nonnull GtsCredentials gtsCredentials,
                          be.iminds.ilabt.jfed.log.Logger logger, GtsServiceFactory gtsServiceFactory,
                          @GtsReservationManagerConnectionProvider GtsConnectionProvider gtsConnectionProvider,
                          GtsModel gtsModel) {
        this.gtsCredentials = gtsCredentials;
        this.logger = logger;
        this.gtsService = gtsServiceFactory.createGtsService(logger);
        this.gtsConnectionProvider = gtsConnectionProvider;
        this.gtsModel = gtsModel;

        try {
            JAXBContext jaxbContext = JAXBContext.newInstance(GtsXmlComposite.class,
                    GtsInfrastructure.class,
                    GtsXmlProject.class,
                    GtsXmlReservation.class,
                    GtsXmlResource.class,
                    GtsXmlType.class,
                    GtsXmlUser.class,
                    GtsUsers.class,
                    GtsResources.class,
                    GtsReservations.class,
                    GtsTypes.class,
                    GtsProjects.class,
                    GtsInfrastructures.class);

            unmarshaller = jaxbContext.createUnmarshaller();

            unmarshaller.setEventHandler(event -> {
                if (event.getSeverity() == ValidationEvent.FATAL_ERROR) {
                    LOG.error("FATAL GTS-JAXB[{}]: {}", event.getLocator(), event.getMessage());
                    return false;
                }
                if (event.getSeverity() == ValidationEvent.ERROR)
                    LOG.error("GTS-JAXB[{}]: {}", event.getLocator(), event.getMessage());
                if (event.getSeverity() == ValidationEvent.WARNING)
                    LOG.warn("GTS-JAXB[{}]: {}", event.getLocator(), event.getMessage());

                return true;
            });

        } catch (JAXBException e) {
            throw new RuntimeException("Could not instantiate JAXB Context", e);
        }
    }

    public List<GtsXmlProject> listProjects() throws GtsException {
        RestApi.RestReply<String> reply = null;
        try {
            reply =
                    gtsService.queryProjects(gtsConnectionProvider.getConnection(), gtsCredentials.getToken());

            GtsProjects projects = (GtsProjects) unmarshaller.unmarshal(new StringReader(reply.getValue()));

            if (projects != null && projects.getProjects() != null) {
                seeProjects(projects.getProjects());
                return projects.getProjects();
            } else
                throw new GtsException("Did not receive a list of projects");
        } catch (JAXBException e) {
            throw new GtsException("Could not parse list of projects", e, reply);
        } catch (JFedException e) {
            throw new GtsException("Exception while listing projects", e);
        }
    }


    public List<GtsInfrastructure> listInfrastructures() throws GtsException {
        RestApi.RestReply<String> reply = null;
        try {
            reply =
                    gtsService.queryRCAs(gtsConnectionProvider.getConnection(), gtsCredentials.getToken());

            GtsInfrastructures infrastructures =
                    (GtsInfrastructures) unmarshaller.unmarshal(new StringReader(reply.getValue()));

            if (infrastructures != null && infrastructures.getInfrastructures() != null) {
                seeInfrastructures(infrastructures.getInfrastructures());
                return infrastructures.getInfrastructures();
            } else
                throw new GtsException("Did not receive a list of infrastructures");
        } catch (JAXBException e) {
            throw new GtsException("Could not parse list of infrastructures", e, reply);
        } catch (JFedException e) {
            throw new GtsException("Exception while listing infrastructures", e);
        }
    }

    public List<GtsXmlUser> listUsers() throws GtsException {
        RestApi.RestReply<String> reply = null;
        try {
            reply =
                    gtsService.queryUsers(gtsConnectionProvider.getConnection(), gtsCredentials.getToken());

            GtsUsers users = (GtsUsers) unmarshaller.unmarshal(new StringReader(reply.getValue()));

            if (users != null && users.getUsers() != null)
                return users.getUsers();
            else
                throw new GtsException("Did not receive a list of users");
        } catch (JAXBException e) {
            throw new GtsException("Could not parse list of users", e, reply);
        } catch (JFedException e) {
            throw new GtsException("Exception while listing users.", e);
        }
    }

    public List<GtsXmlType> listTypes(@Nonnull GtsXmlProject project) throws GtsException {
        Objects.requireNonNull(project);
        RestApi.RestReply<String> reply = null;
        try {
            reply =
                    gtsService.queryTypes(gtsConnectionProvider.getConnection(), gtsCredentials.getToken(), project.getId());

            GtsTypes types = (GtsTypes) unmarshaller.unmarshal(new StringReader(reply.getValue()));
            if (types != null && types.getTypes() != null) {
                seeTypes(types.getTypes(), project);
                return types.getTypes();
            } else
                throw new GtsException("Did not receive a list of types");

        } catch (JFedException e) {
            throw new GtsException("Exception while listing types for" + project.getId(), e);
        } catch (JAXBException e) {
            throw new GtsException("Could not parse list of types for " + project.getId(), e, reply);
        }
    }

    public List<GtsXmlReservation> listReservations(@Nonnull GtsXmlProject project) throws GtsException {
        Objects.requireNonNull(project);

        RestApi.RestReply<String> reply = null;
        try {
            reply =
                    gtsService.queryReservations(gtsConnectionProvider.getConnection(), gtsCredentials.getToken(), project.getId());

            GtsReservations reservations = (GtsReservations) unmarshaller.unmarshal(new StringReader(reply.getValue()));
            if (reservations != null && reservations.getReservations() != null) {
                seeReservations(reservations.getReservations());
                return reservations.getReservations();
            } else
                throw new GtsException("Did not receive a list of reservations");

        } catch (JFedException e) {

            throw new GtsException("Exception while listing reservations for" + project.getId(), e);
        } catch (JAXBException e) {
            throw new GtsException("Could not parse list of reservations for " + project.getId(), e, reply);
        }
    }

    public List<GtsXmlResource> listResources() throws GtsException {
        RestApi.RestReply<String> reply = null;
        try {
            reply =
                    gtsService.queryAllResources(gtsConnectionProvider.getConnection(), gtsCredentials.getToken());

            GtsResources resources = (GtsResources) unmarshaller.unmarshal(new StringReader(reply.getValue()));
            if (resources != null && resources.getResources() != null) {
                seeResources(resources.getResources());
                return resources.getResources();
            } else
                throw new GtsException("Did not receive a list of resources");

        } catch (JFedException e) {

            throw new GtsException("Exception while listing resources", e);
        } catch (JAXBException e) {
            throw new GtsException("Could not parse list of resources", e, reply);
        }
    }


    public List<GtsXmlResource> listResources(@Nonnull GtsXmlProject project) throws GtsException {
        Objects.requireNonNull(project);
        RestApi.RestReply<String> reply = null;
        try {
            reply =
                    gtsService.queryProjectResources(gtsConnectionProvider.getConnection(), gtsCredentials.getToken(), project.getId());

            GtsResources resources = (GtsResources) unmarshaller.unmarshal(new StringReader(reply.getValue()));
            if (resources != null && resources.getResources() != null) {
                seeResources(resources.getResources());
                return resources.getResources();
            } else
                throw new GtsException("Did not receive a list of resources for project "+ project.getId());

        } catch (JFedException e) {

            throw new GtsException("Exception while listing resources of project " + project.getId(), e);
        } catch (JAXBException e) {
            throw new GtsException("Could not parse list of resources of project " + project.getId(), e, reply);
        }
    }

    public GtsInfrastructure getInfrastructure(@Nonnull String id) throws GtsException {
        Objects.requireNonNull(id);
        RestApi.RestReply<String> reply = null;
        try {
            reply =
                    gtsService.queryRCA(gtsConnectionProvider.getConnection(), gtsCredentials.getToken(), id);

            final GtsInfrastructure result =
                    (GtsInfrastructure) unmarshaller.unmarshal(new StringReader(reply.getValue()));
            if (result != null)
                gtsModel.addOrUpdateInfrastructure(result);
            else
                LOG.warn("Got null after querying infrastructure {}", id);
            return result;
        } catch (JFedException e) {
            throw new GtsException("Exception while getting infrastructure " + id, e);
        } catch (JAXBException e) {
            throw new GtsException("Could not parse infrastructure " + id, e, reply);
        }
    }

    public GtsXmlResource getResource(int id) throws GtsException {
        RestApi.RestReply<String> reply = null;
        try {
            reply =
                    gtsService.queryResource(gtsConnectionProvider.getConnection(), gtsCredentials.getToken(), Integer.toString(id));

            final GtsXmlResource result = (GtsXmlResource) unmarshaller.unmarshal(new StringReader(reply.getValue()));
            if (result != null)
                gtsModel.addOrUpdateResource(result);
            else
                LOG.warn("Got null after querying resource {}", id);
            return result;
        } catch (JFedException e) {
            throw new GtsException("Exception while listing resources", e);
        } catch (JAXBException e) {
            throw new GtsException("Could not parse list of resources", e, reply);
        }
    }

    public GtsXmlProject getProject(@Nonnull String id) throws GtsException {
        Objects.requireNonNull(id);
        RestApi.RestReply<String> reply = null;
        try {
            reply =
                    gtsService.queryProject(gtsConnectionProvider.getConnection(), gtsCredentials.getToken(), id);

            final GtsXmlProject result = (GtsXmlProject) unmarshaller.unmarshal(new StringReader(reply.getValue()));
            if (result != null)
                gtsModel.addOrUpdateProject(result);
            else
                LOG.warn("Got null after querying project {}", id);
            return result;
        } catch (JFedException e) {
            throw new GtsException("Exception while requesting project " + id, e);
        } catch (JAXBException e) {
            throw new GtsException("Could not parse list of project " + id, e, reply);
        }
    }

    /**
     * @return type-name
     * @throws GtsException when adding type fails
     */
    public String addType(@Nonnull String gtsScript, @Nonnull GtsXmlProject project) throws GtsException {
        Objects.requireNonNull(gtsScript);
        Objects.requireNonNull(project);

        try {
            RestApi.RestReply<String> reply =
                    gtsService.addType(gtsConnectionProvider.getConnection(), gtsCredentials.getToken(), project.getId(), gtsScript);
            if (reply != null && reply.getHttpCallDetails().getResultHttpStatusCode() == 200 && reply.getValue() != null) {
                GtsXmlType type = new GtsXmlType(reply.getValue());
                type.setScript(gtsScript);
                gtsModel.addOrUpdateType(type, project);

                return reply.getValue();
            } else
                throw new GtsException("A server-side error occured while adding the type", reply);
        } catch (JFedException e) {
            throw new GtsException("Exception while adding type", e);
        }
    }

    /**
     * @return type-name
     * @throws GtsException when adding type fails
     */
    public String addProject(@Nonnull GtsXmlProject project) throws GtsException {
        Objects.requireNonNull(project);

        try {
            RestApi.RestReply<String> reply =
                    gtsService.addProject(gtsConnectionProvider.getConnection(), gtsCredentials.getToken(), project.getId(),
                            project.getVpnUser(),
                            project.getVpnPassword(),
                            project.getUserLogins(),
                            null);
            if (reply != null && reply.getHttpCallDetails().getResultHttpStatusCode() == 200 && reply.getValue() != null) {
                project.setId(reply.getValue());
                gtsModel.addOrUpdateProject(project);

                return reply.getValue();
            } else
                throw new GtsException("A server-side error occured while adding the type", reply);
        } catch (JFedException e) {
            throw new GtsException("Exception while adding type", e);
        }
    }

    public void removeType(@Nonnull GtsXmlProject project, @Nonnull GtsXmlType type) throws GtsException {
        Objects.requireNonNull(project);
        Objects.requireNonNull(type);

        removeType(project, type.getName());
    }

    public void removeType(@Nonnull GtsXmlProject project, @Nonnull String typeName) throws GtsException {
        Objects.requireNonNull(project);
        Objects.requireNonNull(typeName);

        try {
            RestApi.RestReply<String> reply =
                    gtsService.removeType(gtsConnectionProvider.getConnection(), gtsCredentials.getToken(), project.getId(), typeName);
            if (reply != null && reply.getHttpCallDetails().getResultHttpStatusCode() == 200 && reply.getValue() != null) {
                if (Objects.equals(reply.getValue(), typeName)) {
                    gtsModel.removeType(typeName, project);
                } else
                    throw new GtsException("Got an unexpected result when removing " + typeName, reply);
            } else
                throw new GtsException("A server-side error occurred while removing type" + typeName, reply);
        } catch (JFedException e) {
            throw new GtsException("Exception while adding type", e);
        }
    }

    public int reserveType(@Nonnull GtsXmlProject project, @Nonnull GtsXmlType type) throws GtsException {
        Objects.requireNonNull(project);
        Objects.requireNonNull(type);

        RestApi.RestReply<String> reply = null;
        try {
            reply =
                    gtsService.reserveType(gtsConnectionProvider.getConnection(), gtsCredentials.getToken(), type.getName(), project.getId());
            if (reply != null && reply.getHttpCallDetails().getResultHttpStatusCode() == 200 && reply.getValue() != null) {
                final int reservationId = Integer.parseInt(reply.getValue());
                gtsModel.registerReservationOfType(reservationId, new GtsModel.TypeId(project.getId(), type.getName()));
                return reservationId;
            } else
                throw new GtsException("A server-side error occurred while reserved the type", reply);
        } catch (JFedException e) {
            throw new GtsException("Exception while reserving type", e);
        } catch (NumberFormatException e) {
            throw new GtsException("Could not process result while reserving type", e, reply);
        }
    }

    public void activateReservation(int reservationId) throws GtsException {
        try {
            RestApi.RestReply<String> reply =
                    gtsService.activateReservation(gtsConnectionProvider.getConnection(), gtsCredentials.getToken(), Integer.toString(reservationId));
            if (reply != null && reply.getHttpCallDetails().getResultHttpStatusCode() == 204) {
                //set status to activating in model
                GtsXmlReservation res = gtsModel.getReservation(reservationId);
                if (res != null) {
                    updateReservationAndChildrenStatus(res, GtsXmlReservation.Status.ACTIVATING);
                } else {
                    LOG.warn("Could not find reservation {} to activate in GTSModel", reservationId);
                }
            } else
                throw new GtsException("A server-side error occurred while activating reservation " + reservationId, reply);
        } catch (JFedException e) {
            throw new GtsException("Exception while activating reservation " + reservationId, e);
        }
    }

    public void deactivateReservation(int reservationId) throws GtsException {
        try {
            RestApi.RestReply<String> reply =
                    gtsService.deactivateReservation(gtsConnectionProvider.getConnection(), gtsCredentials.getToken(), Integer.toString(reservationId));
            if (reply != null && reply.getHttpCallDetails().getResultHttpStatusCode() == 204) {
                //set status to deactivating in model
                GtsXmlReservation res = gtsModel.getReservation(reservationId);
                if (res != null) {
                    updateReservationAndChildrenStatus(res, GtsXmlReservation.Status.DEACTIVATING);
                } else {
                    LOG.warn("Could not find reservation {} to deactivate in GTSModel", reservationId);
                }
            } else
                throw new GtsException("A server-side error occurred while deactivating reservation " + reservationId, reply);
        } catch (JFedException e) {
            throw new GtsException("Exception while deactivating reservation " + reservationId, e);
        }
    }

    public void releaseReservation(int reservationId) throws GtsException {
        try {
            RestApi.RestReply<String> reply =
                    gtsService.releaseReservation(gtsConnectionProvider.getConnection(), gtsCredentials.getToken(), Integer.toString(reservationId));
            if (reply != null && reply.getHttpCallDetails().getResultHttpStatusCode() == 204) {
                boolean removed = gtsModel.removeReservation(reservationId);

                GtsXmlReservation res = gtsModel.getReservation(reservationId);
                if (removed) {
                    updateReservationAndChildrenStatus(res, GtsXmlReservation.Status.RELEASING);
                    gtsModel.deregisterReservationOfType(reservationId);
                } else {
                    LOG.warn("Could not find reservation {} to delete in GTSModel", reservationId);
                }
            } else
                throw new GtsException("A server-side error occurred while releasing reservation " + reservationId, reply);
        } catch (JFedException e) {
            throw new GtsException("Exception while releasing reservation " + reservationId, e);
        }
    }

    public void removeProject(@Nonnull String projectName) throws GtsException {
        Objects.requireNonNull(projectName);

        try {
            RestApi.RestReply<String> reply =
                    gtsService.removeProject(gtsConnectionProvider.getConnection(), gtsCredentials.getToken(), projectName);
            if (reply != null && reply.getHttpCallDetails().getResultHttpStatusCode() == 200 && reply.getValue() != null) {
                if (Objects.equals(reply.getValue(), projectName)) {
                    boolean removed = gtsModel.removeProject(projectName);
                    if (!removed)
                        LOG.warn("Could not find project {} to delete in GTSModel", projectName);
                } else
                    throw new GtsException("Got an unexpected result when removing project " + projectName, reply);
            } else
                throw new GtsException("A server-side error occurred while removing project " + projectName, reply);
        } catch (JFedException e) {
            throw new GtsException("Exception while removing project " + projectName, e);
        }
    }

    /*
    Helpers
     */

    private void updateReservationAndChildrenStatus(GtsXmlReservation reservation, GtsXmlReservation.Status newStatus){
        if(reservation == null) {
            LOG.warn("Got a null reservation!");
            return;
        }

        gtsModel.addOrUpdateReservation(new GtsXmlReservation(reservation, newStatus));

        reservation.getChildren().forEach(child -> updateReservationAndChildrenStatus(child, newStatus));
    }

    /*
    Listeners
     */

    private void seeProjects(List<GtsXmlProject> projects) {
        seeCollection(projects,
                gtsModel.getProjects(),
                GtsXmlProject::getId,
                gtsModel::addOrUpdateProject,
                gtsModel::removeProject);
    }

    private void seeReservations(List<GtsXmlReservation> reservations) {
        LOG.info("Seeing reservations {}", reservations);
        seeCollection(reservations,
                gtsModel.getReservations(),
                GtsXmlReservation::getId,
                gtsModel::addOrUpdateReservation,
                gtsModel::removeReservation);

    }

    private void seeResources(List<GtsXmlResource> resources) {
        seeCollection(resources,
                gtsModel.getResources(),
                GtsXmlResource::getProviderId,
                gtsModel::addOrUpdateResource,
                gtsModel::removeResource);

    }

    private void seeTypes(List<GtsXmlType> types, GtsXmlProject project) {
        seeCollection(types,
                gtsModel.getTypesInProject(project),
                type -> new GtsModel.TypeId(type.getName(), project.getId()),
                type -> gtsModel.addOrUpdateType(type, project),
                type -> gtsModel.removeType(type, project));

    }

    private void seeInfrastructures(List<GtsInfrastructure> infrastructures) {
        seeCollection(infrastructures, gtsModel.getInfrastructures(),
                GtsInfrastructure::getProviderId,
                gtsModel::addOrUpdateInfrastructure,
                gtsModel::removeInfrastructure);
    }

    private <K, V> void seeCollection(List<V> sourceList,
                                      Map<K, V> targetMap,
                                      Function<V, K> keyFunction,
                                      Consumer<V> addOrUpdateFunction,
                                      Consumer<V> removeFunction) {
        try {
            Map<K, V> sourceMap =
                    sourceList.stream().collect(Collectors.toMap(keyFunction, Function.identity()));

            MapDifference<K, V> diff = Maps.difference(sourceMap, targetMap);

            diff.entriesOnlyOnRight().values().forEach(removeFunction);
            diff.entriesOnlyOnLeft().values().forEach(addOrUpdateFunction);
            diff.entriesDiffering().values().forEach(vd -> addOrUpdateFunction.accept(vd.leftValue()));
        } catch (IllegalStateException ex) {
            LOG.error("Error while processing data: " + ex.getMessage(), ex);
        }
    }

}
