/**
 *
 */
package com.thinkbiganalytics.metadata.modeshape.sla;

/*-
 * #%L
 * thinkbig-metadata-modeshape
 * %%
 * Copyright (C) 2017 ThinkBig Analytics
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.thinkbiganalytics.metadata.api.sla.FeedServiceLevelAgreementProvider;
import com.thinkbiganalytics.metadata.modeshape.BaseJcrProvider;
import com.thinkbiganalytics.metadata.modeshape.MetadataRepositoryException;
import com.thinkbiganalytics.metadata.modeshape.common.JcrEntity;
import com.thinkbiganalytics.metadata.modeshape.common.JcrPropertyConstants;
import com.thinkbiganalytics.metadata.modeshape.feed.JcrFeedPrecondition;
import com.thinkbiganalytics.metadata.modeshape.sla.JcrServiceLevelAgreement.SlaId;
import com.thinkbiganalytics.metadata.modeshape.support.JcrPropertyUtil;
import com.thinkbiganalytics.metadata.modeshape.support.JcrQueryUtil;
import com.thinkbiganalytics.metadata.modeshape.support.JcrUtil;
import com.thinkbiganalytics.metadata.sla.api.Metric;
import com.thinkbiganalytics.metadata.sla.api.Obligation;
import com.thinkbiganalytics.metadata.sla.api.ObligationGroup.Condition;
import com.thinkbiganalytics.metadata.sla.api.ServiceLevelAgreement;
import com.thinkbiganalytics.metadata.sla.api.ServiceLevelAgreement.ID;
import com.thinkbiganalytics.metadata.sla.api.ServiceLevelAgreementActionConfig;
import com.thinkbiganalytics.metadata.sla.api.ServiceLevelAgreementActionConfiguration;
import com.thinkbiganalytics.metadata.sla.spi.ObligationBuilder;
import com.thinkbiganalytics.metadata.sla.spi.ObligationGroupBuilder;
import com.thinkbiganalytics.metadata.sla.spi.ServiceLevelAgreementBuilder;
import com.thinkbiganalytics.metadata.sla.spi.ServiceLevelAgreementCheck;
import com.thinkbiganalytics.metadata.sla.spi.ServiceLevelAgreementCheckBuilder;
import com.thinkbiganalytics.metadata.sla.spi.ServiceLevelAgreementProvider;

import org.modeshape.jcr.api.JcrTools;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.UUID;

import javax.inject.Inject;
import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.query.QueryResult;

/**
 *
 */
public class JcrServiceLevelAgreementProvider extends BaseJcrProvider<ServiceLevelAgreement, ServiceLevelAgreement.ID> implements ServiceLevelAgreementProvider {

    public static final String SLA_PATH = "/metadata/sla";

    private final JcrTools jcrTools = new JcrTools();

    @Inject
    private FeedServiceLevelAgreementProvider feedServiceLevelAgreementProvider;

    @Override
    public Class<? extends ServiceLevelAgreement> getEntityClass() {
        return JcrServiceLevelAgreement.class;
    }

    @Override
    public Class<? extends JcrEntity> getJcrEntityClass() {
        return JcrServiceLevelAgreement.class;
    }

    @Override
    public String getNodeType(Class<? extends JcrEntity> jcrEntityType) {
        return "tba:sla";
    }


    /* (non-Javadoc)
     * @see com.thinkbiganalytics.metadata.sla.spi.ServiceLevelAgreementProvider#resolve(java.io.Serializable)
     */
    @Override
    public ID resolve(Serializable ser) {
        return resolveId(ser);
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.metadata.sla.spi.ServiceLevelAgreementProvider#getAgreements()
     */
    @Override
    /**
     * Find all Agreements that are not Preconditions
     */
    public List<ServiceLevelAgreement> getAgreements() {
        try {
            Session session = getSession();
            Node slasNode = session.getNode(SLA_PATH);
            @SuppressWarnings("unchecked")
            Iterator<Node> itr = (Iterator<Node>) slasNode.getNodes("sla-*");

            return Lists.newArrayList(Iterators.transform(itr, (slaNode) -> {
                return JcrUtil.createJcrObject(slaNode, JcrServiceLevelAgreement.class);
            }));
        } catch (RepositoryException e) {
            throw new MetadataRepositoryException("Failed to retrieve the ServiceLevelAgreements", e);
        }

    }

    /**
     * Return All SLAs that are not Precondition SLAs
     */
    public List<ServiceLevelAgreement> getNonPreconditionAgreements() {
        try {

            //query for the SLAs
            String query = "SELECT * FROM [" + getNodeType(getJcrEntityClass()) + "] as sla "
                           + "LEFT JOIN [" + JcrFeedPrecondition.NODE_TYPE + "] as precondition on precondition.[" + JcrFeedPrecondition.SLA + "] = sla.[jcr:uuid] "
                           + " WHERE precondition.[jcr:uuid] is NULL ";

            QueryResult result = JcrQueryUtil.query(getSession(), query, null);

            return JcrQueryUtil.queryRowItrNodeResultToList(result, ServiceLevelAgreement.class, "sla");

        } catch (RepositoryException e) {
            throw new MetadataRepositoryException("Failed to retrieve the obligation nodes", e);
        }

    }


    /* (non-Javadoc)
     * @see com.thinkbiganalytics.metadata.sla.spi.ServiceLevelAgreementProvider#getAgreement(com.thinkbiganalytics.metadata.sla.api.ServiceLevelAgreement.ID)
     */
    @Override
    public ServiceLevelAgreement getAgreement(ID id) {
        try {
            Session session = getSession();
            SlaId slaId = (SlaId) id;
            return new JcrServiceLevelAgreement(session.getNodeByIdentifier(slaId.getIdValue()));
        } catch (ItemNotFoundException e) {
            return null;
        } catch (RepositoryException e) {
            throw new MetadataRepositoryException("Failed to retrieve the SLA node", e);
        }
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.metadata.sla.spi.ServiceLevelAgreementProvider#findAgreementByName(java.lang.String)
     */
    @Override
    public ServiceLevelAgreement findAgreementByName(String slaName) {
        String query = "SELECT * FROM [" + getNodeType(getJcrEntityClass()) + "] as sla WHERE sla.[" + JcrPropertyConstants.TITLE + "] = $slaName";
        return JcrQueryUtil.findFirst(getSession(), query, ImmutableMap.of("slaName", slaName), getEntityClass());
    }


    /* (non-Javadoc)
     * @see com.thinkbiganalytics.metadata.sla.spi.ServiceLevelAgreementProvider#removeAgreement(com.thinkbiganalytics.metadata.sla.api.ServiceLevelAgreement.ID)
     */
    @Override
    public boolean removeAgreement(ID id) {
        try {
            Session session = getSession();
            SlaId slaId = (SlaId) id;
            Node slaNode = session.getNodeByIdentifier(slaId.getIdValue());
            if (slaNode != null) {
                JcrServiceLevelAgreement sla = new JcrServiceLevelAgreement(slaNode);

                //remove any other relationships
                feedServiceLevelAgreementProvider.removeAllRelationships(id);
                slaNode.remove();
            }
            return true;
        } catch (ItemNotFoundException e) {
            return false;
        } catch (RepositoryException e) {
            throw new MetadataRepositoryException("Failed to retrieve the SLA node", e);
        }
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.metadata.sla.spi.ServiceLevelAgreementProvider#builder()
     */
    @Override
    public ServiceLevelAgreementBuilder builder() {
        try {
            Session session = getSession();
            Node slasNode = session.getNode(SLA_PATH);
            Node slaNode = slasNode.addNode("sla-" + UUID.randomUUID(), "tba:sla");

            return builder(slaNode);
        } catch (RepositoryException e) {
            throw new MetadataRepositoryException("Failed to create an sla node", e);
        }
    }

    public ServiceLevelAgreementBuilder builder(ServiceLevelAgreement.ID id) {
        try {
            Session session = getSession();
            Node slaNode = session.getNodeByIdentifier(id.toString());
            if (slaNode == null || !slaNode.isNodeType("tba:sla")) {
                throw new MetadataRepositoryException("Failed to get sla node for " + id);
            }
            //clear out any obligations/metrics associated to this node as the builder will bring in new ones
            JcrServiceLevelAgreement sla = new JcrServiceLevelAgreement(slaNode);
            sla.clear();
            return builder(slaNode);
        } catch (RepositoryException e) {
            throw new MetadataRepositoryException("Failed to create an sla node", e);
        }
    }

    /**
     * Returns a builder that constructs an SLA rooted by the given node.  This method is exposed to support
     * other JCR-based providers that may construct object that have embedded SLA's that are not managed by
     * this provider.
     *
     * @param slaNode the root node of the SLA
     * @return a builder to construct the sla
     */
    public ServiceLevelAgreementBuilder builder(Node slaNode) throws RepositoryException {
        return new SLABuilderImpl(slaNode);
    }


    @Override
    public ID resolveId(Serializable ser) {
        if (ser instanceof JcrServiceLevelAgreement.SlaId) {
            return (JcrServiceLevelAgreement.SlaId) ser;
        } else {
            return new JcrServiceLevelAgreement.SlaId(ser);
        }
    }

    public ServiceLevelAgreementCheckBuilder slaCheckBuilder(ServiceLevelAgreement.ID slaId) {
        try {
            Session session = getSession();
            Node n = session.getNodeByIdentifier(slaId.toString());
            return new SLACheckBuilderImpl(n);
        } catch (RepositoryException e) {
            throw new MetadataRepositoryException("Unable to create slaCheckBuilder. Error attempting to find related SLA by id of " + slaId);
        }
    }

    private static class ObligationBuilderImpl<B> implements ObligationBuilder<B> {

        private Node obNode;

        private SLABuilderImpl slaBuilder;
        private ObligationGroupBuilderImpl groupBuilder;
        private String description;
        private Set<Metric> metrics = new HashSet<Metric>();

        public ObligationBuilderImpl(Node node, SLABuilderImpl bldr) {
            this.obNode = node;
            this.slaBuilder = bldr;
        }

        public ObligationBuilderImpl(Node node, ObligationGroupBuilderImpl bldr) {
            this.obNode = node;
            this.groupBuilder = bldr;
        }

        @Override
        public ObligationBuilder<B> description(String descr) {
            this.description = descr;
            return this;
        }

        @Override
        public ObligationBuilder<B> metric(Metric metric, Metric... more) {
            this.metrics.add(metric);
            for (Metric another : more) {
                this.metrics.add(another);
            }
            return this;
        }

        @Override
        public ObligationBuilder<B> metric(Collection<Metric> metrics) {
            this.metrics.addAll(metrics);
            return this;
        }

        @Override
        @SuppressWarnings("unchecked")
        public B build() {
            try {
                JcrPropertyUtil.setProperty(this.obNode, "tba:description", this.description);

                for (Metric metric : this.metrics) {
                    Node metricNode = this.obNode.addNode(JcrObligation.METRICS, JcrObligation.METRIC_TYPE);

                    JcrPropertyUtil.setProperty(metricNode, JcrObligation.NAME, metric.getClass().getSimpleName());
                    JcrPropertyUtil.setProperty(metricNode, JcrObligation.DESCRIPTION, metric.getDescription());
                    JcrUtil.addGenericJson(metricNode, JcrObligation.JSON, metric);
                }
            } catch (RepositoryException e) {
                throw new MetadataRepositoryException("Failed to build the obligation", e);
            }

            if (this.groupBuilder != null) {
                return (B) this.groupBuilder;
            } else {
                return (B) this.slaBuilder;
            }
        }
    }

    private static class ObligationGroupBuilderImpl implements ObligationGroupBuilder {

        private Node groupNode;
        private Condition condition;
        private SLABuilderImpl slaBuilder;

        public ObligationGroupBuilderImpl(Node node, Condition cond, SLABuilderImpl slaBuilder) {
            this.groupNode = node;
            this.condition = cond;
            this.slaBuilder = slaBuilder;
        }

        @Override
        public ObligationGroupBuilder obligation(Obligation obligation) {
            // TODO Does not to work in the current JCR implementation.  Perhaps it should not be supported at all in the builder.
            throw new UnsupportedOperationException();
        }

        @Override
        public ObligationBuilder<ObligationGroupBuilder> obligationBuilder() {
            try {
                Node obNode = this.groupNode.addNode(JcrObligationGroup.OBLIGATIONS, JcrObligationGroup.OBLIGATION_TYPE);
                return new ObligationBuilderImpl<ObligationGroupBuilder>(obNode, this);
            } catch (RepositoryException e) {
                throw new MetadataRepositoryException("Failed to create the obligation group builder", e);
            }
        }

        @Override
        public ServiceLevelAgreementBuilder build() {
            JcrPropertyUtil.setProperty(this.groupNode, JcrObligationGroup.CONDITION, this.condition);
            return this.slaBuilder;

        }
    }

    private static class SLACheckBuilderImpl implements ServiceLevelAgreementCheckBuilder {


        private JcrServiceLevelAgreement sla;
        private Node slaCheckNode;

        private String cronExpression;

        private List<ServiceLevelAgreementActionConfiguration> serviceLevelAgreementActionConfigurations;

        public SLACheckBuilderImpl(Node slaNode) {
            this.sla = new JcrServiceLevelAgreement(slaNode);
        }

        public SLACheckBuilderImpl removeSlaChecks() {
            try {
                Iterator<Node> nodesItr = (Iterator<Node>) this.sla.getNode().getNodes(JcrServiceLevelAgreement.SLA_CHECKS);
                while (nodesItr != null && nodesItr.hasNext()) {
                    Node action = nodesItr.next();
                    action.remove();
                }
            } catch (RepositoryException e) {
                throw new MetadataRepositoryException("Failed to remove the SLA Checks", e);
            }
            return this;
        }

        @Override
        public ServiceLevelAgreementCheckBuilder actionConfiguration(ServiceLevelAgreementActionConfiguration configuration) {
            if (this.serviceLevelAgreementActionConfigurations == null) {
                this.serviceLevelAgreementActionConfigurations = new ArrayList<>();
            }
            if (configuration != null) {
                this.serviceLevelAgreementActionConfigurations.add(configuration);
            }
            return this;
        }

        @Override
        public ServiceLevelAgreementCheckBuilder actionConfigurations(List<ServiceLevelAgreementActionConfiguration> configurations) {
            if (this.serviceLevelAgreementActionConfigurations == null) {
                this.serviceLevelAgreementActionConfigurations = new ArrayList<>();
            }
            if (configurations != null) {
                this.serviceLevelAgreementActionConfigurations.addAll(configurations);
            }
            return this;
        }

        @Override
        public ServiceLevelAgreementCheckBuilder cronExpression(String cronExpression) {
            this.cronExpression = cronExpression;
            return this;
        }


        @Override
        public ServiceLevelAgreementCheck build() {
            try {
                //create the Check Node

                this.slaCheckNode = JcrUtil.getOrCreateNode(this.sla.getNode(), JcrServiceLevelAgreement.SLA_CHECKS, JcrServiceLevelAgreementCheck.NODE_TYPE);

                String slaName = sla.getName();
                JcrPropertyUtil.setProperty(slaCheckNode, JcrPropertyConstants.DESCRIPTION, "SLA Check for " + slaName + " using schedule " + cronExpression);
                JcrPropertyUtil.setProperty(slaCheckNode, JcrPropertyConstants.TITLE, "SLA Check");
                JcrPropertyUtil.setProperty(slaCheckNode, JcrServiceLevelAgreementCheck.CRON_SCHEDULE, cronExpression);

                if (this.serviceLevelAgreementActionConfigurations != null && !this.serviceLevelAgreementActionConfigurations.isEmpty()) {
                    for (ServiceLevelAgreementActionConfiguration actionConfiguration : this.serviceLevelAgreementActionConfigurations) {
                        Node node = this.slaCheckNode.addNode(JcrServiceLevelAgreementCheck.ACTION_CONFIGURATIONS, JcrServiceLevelAgreementCheck.ACTION_CONFIGURATION_TYPE);

                        JcrPropertyUtil.setProperty(node, JcrPropertyConstants.TITLE, actionConfiguration.getClass().getSimpleName());
                        ServiceLevelAgreementActionConfig annotation = actionConfiguration.getClass().getAnnotation(ServiceLevelAgreementActionConfig.class);
                        String desc = actionConfiguration.getClass().getSimpleName();
                        if (annotation != null) {
                            desc = annotation.description();
                        }
                        JcrPropertyUtil.setProperty(node, JcrPropertyConstants.DESCRIPTION, desc);
                        JcrUtil.addGenericJson(node, JcrPropertyConstants.JSON, actionConfiguration);


                    }
                }
            } catch (RepositoryException e) {
                throw new MetadataRepositoryException("Failed to build the SLA Check", e);
            }

            return new JcrServiceLevelAgreementCheck(this.slaCheckNode);
        }
    }

    protected class SLABuilderImpl implements ServiceLevelAgreementBuilder {

        private Node slaNode;

        private String name;
        private String description;
        private List<? extends ServiceLevelAgreementActionConfiguration> serviceLevelAgreementActionConfigurations;

        public SLABuilderImpl(Node node) throws RepositoryException {
            this.slaNode = node;
        }


        @Override
        public ServiceLevelAgreementBuilder name(String name) {
            this.name = name;
            return this;
        }

        @Override
        public ServiceLevelAgreementBuilder description(String description) {
            this.description = description;
            return this;
        }

        @Override
        public ServiceLevelAgreementBuilder obligation(Obligation obligation) {
            // TODO This isn't going to work in the current JCR implementation.  Perhaps it should not be supported at all in the builder.
            throw new UnsupportedOperationException();
        }

        @Override
        public ObligationBuilder<ServiceLevelAgreementBuilder> obligationBuilder() {
            try {
                Node groupNode = null;

                if (this.slaNode.hasProperty(JcrServiceLevelAgreement.DEFAULT_GROUP)) {
                    groupNode = this.slaNode.getProperty(JcrServiceLevelAgreement.DEFAULT_GROUP).getNode();
                } else {
                    groupNode = this.slaNode.addNode(JcrServiceLevelAgreement.GROUPS, JcrServiceLevelAgreement.GROUP_TYPE);
                    this.slaNode.setProperty(JcrServiceLevelAgreement.DEFAULT_GROUP, groupNode);
                }

                Node obNode = groupNode.addNode(JcrObligationGroup.OBLIGATIONS, JcrObligationGroup.OBLIGATION_TYPE);

                return new ObligationBuilderImpl<ServiceLevelAgreementBuilder>(obNode, this);
            } catch (RepositoryException e) {
                throw new MetadataRepositoryException("Failed to build the obligation node", e);
            }
        }

        @Override
        public ObligationBuilder<ServiceLevelAgreementBuilder> obligationBuilder(Condition condition) {
            try {
                Node groupNode = this.slaNode.getProperty(JcrServiceLevelAgreement.DEFAULT_GROUP).getNode();
                groupNode.setProperty(JcrObligationGroup.CONDITION, condition.name());
                Node obNode = groupNode.addNode(JcrObligationGroup.OBLIGATIONS, JcrObligationGroup.OBLIGATION_TYPE);

                return new ObligationBuilderImpl<ServiceLevelAgreementBuilder>(obNode, this);
            } catch (RepositoryException e) {
                throw new MetadataRepositoryException("Failed to build the obligation group node", e);
            }
        }

        @Override
        public ObligationGroupBuilder obligationGroupBuilder(Condition condition) {
            try {
                Node groupNode = this.slaNode.addNode(JcrServiceLevelAgreement.GROUPS, JcrServiceLevelAgreement.GROUP_TYPE);

                return new ObligationGroupBuilderImpl(groupNode, condition, this);
            } catch (RepositoryException e) {
                throw new MetadataRepositoryException("Failed to build the obligation group node", e);
            }
        }

        public ServiceLevelAgreementBuilder actionConfigurations(List<? extends ServiceLevelAgreementActionConfiguration> actionConfigurations) {
            this.serviceLevelAgreementActionConfigurations = actionConfigurations;
            return this;
        }

        @Override
        public ServiceLevelAgreement build() {
            JcrPropertyUtil.setProperty(this.slaNode, JcrServiceLevelAgreement.NAME, this.name);
            JcrPropertyUtil.setProperty(this.slaNode, JcrServiceLevelAgreement.DESCRIPTION, this.description);
            JcrServiceLevelAgreement agreement = new JcrServiceLevelAgreement(this.slaNode);
            //always make it enabled by default
            agreement.setEnabled(true);
            return agreement;
        }
    }


}
