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.thinkbiganalytics.metadata.api.MetadataAccess;
import com.thinkbiganalytics.metadata.api.PostMetadataConfigAction;
import com.thinkbiganalytics.metadata.api.extension.ExtensibleEntity;
import com.thinkbiganalytics.metadata.api.extension.ExtensibleEntityProvider;
import com.thinkbiganalytics.metadata.api.extension.ExtensibleType;
import com.thinkbiganalytics.metadata.api.extension.ExtensibleTypeProvider;
import com.thinkbiganalytics.metadata.api.extension.FieldDescriptor;
import com.thinkbiganalytics.metadata.api.feed.Feed;
import com.thinkbiganalytics.metadata.api.feed.FeedProvider;
import com.thinkbiganalytics.metadata.api.sla.FeedServiceLevelAgreement;
import com.thinkbiganalytics.metadata.api.sla.FeedServiceLevelAgreementProvider;
import com.thinkbiganalytics.metadata.api.sla.FeedServiceLevelAgreementRelationship;
import com.thinkbiganalytics.metadata.modeshape.JcrMetadataAccess;
import com.thinkbiganalytics.metadata.modeshape.MetadataRepositoryException;
import com.thinkbiganalytics.metadata.modeshape.extension.JcrExtensibleEntity;
import com.thinkbiganalytics.metadata.modeshape.feed.JcrFeed;
import com.thinkbiganalytics.metadata.modeshape.support.JcrQueryUtil;
import com.thinkbiganalytics.metadata.sla.api.ServiceLevelAgreement;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

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

/**
 */
public class JcrFeedServiceLevelAgreementProvider implements FeedServiceLevelAgreementProvider, PostMetadataConfigAction {

    @Inject
    FeedProvider feedProvider;
    @Inject
    private ExtensibleTypeProvider typeProvider;
    @Inject
    private ExtensibleEntityProvider entityProvider;
    @Inject
    private JcrMetadataAccess metadata;

    private AtomicBoolean typeCreated = new AtomicBoolean(false);

    /**
     * Finds All Service Level Agreements and also adds in an related feeds
     * Filter out the Precondition SLAs as those are not to be managed via the SLA screen.
     */
    @Override
    public List<FeedServiceLevelAgreement> findAllAgreements() {
        String query = "SELECT * from [" + JcrServiceLevelAgreement.NODE_TYPE + "] AS sla "
                       + "JOIN [" + JcrFeedServiceLevelAgreementRelationship.NODE_TYPE + "] AS feedSla ON feedSla.[" + JcrFeedServiceLevelAgreementRelationship.SLA + "] = sla.[jcr:uuid] ";
        return queryToList(query, null);

    }

    @Override
    public void run() {
        createType();
    }

    private List<FeedServiceLevelAgreement> queryToList(String query, Map<String, String> params) {
        QueryResult result = null;
        try {
            result = JcrQueryUtil.query(getSession(), query, params);

            List<FeedServiceLevelAgreement> entities = new ArrayList<>();

            if (result != null) {
                try {
                    RowIterator rowIterator = result.getRows();
                    while (rowIterator.hasNext()) {
                        Row row = rowIterator.nextRow();
                        Node slaNode = row.getNode("sla");
                        Set<JcrFeed> feeds = null;

                        Node feedSlaNode = row.getNode("feedSla");
                        //Left Join will result in the node being NULL if its not there
                        if (feedSlaNode != null) {
                            JcrFeedServiceLevelAgreementRelationship feedServiceLevelAgreementRelationship = new JcrFeedServiceLevelAgreementRelationship(feedSlaNode);
                            feeds = (Set<JcrFeed>) feedServiceLevelAgreementRelationship.getFeeds();
                        }
                        JcrFeedServiceLevelAgreement entity = new JcrFeedServiceLevelAgreement(slaNode, feeds);
                        entities.add(entity);
                    }
                } catch (RepositoryException e) {
                    throw new MetadataRepositoryException("Unable to parse QueryResult to List for feedSla", e);

                }
            }
            return entities;


        } catch (RepositoryException e) {
            throw new MetadataRepositoryException("Unable to execute Feed SLA Query.  Query is: " + query, e);
        }
    }

    public FeedServiceLevelAgreement findAgreement(ServiceLevelAgreement.ID id) {
        String query = "SELECT * from [" + JcrServiceLevelAgreement.NODE_TYPE + "] AS sla "
                       + "LEFT JOIN [" + JcrFeedServiceLevelAgreementRelationship.NODE_TYPE + "] AS feedSla ON feedSla.[" + JcrFeedServiceLevelAgreementRelationship.SLA + "] = sla.[jcr:uuid] "
                       + "WHERE sla.[jcr:uuid] = $slaId ";
        Map<String, String> bindParams = new HashMap<>();
        bindParams.put("slaId", id.toString());
        List<FeedServiceLevelAgreement> list = queryToList(query, bindParams);
        if (list != null && !list.isEmpty()) {
            return list.get(0);
        }
        return null;

    }


    public List<FeedServiceLevelAgreement> findFeedServiceLevelAgreements(Feed.ID feedId) {
        String query = "SELECT * from [" + JcrServiceLevelAgreement.NODE_TYPE + "] AS sla "
                       + "JOIN [" + JcrFeedServiceLevelAgreementRelationship.NODE_TYPE + "] AS feedSla ON feedSla.[" + JcrFeedServiceLevelAgreementRelationship.SLA + "] = sla.[jcr:uuid] "
                       + "WHERE feedSla.[" + JcrFeedServiceLevelAgreementRelationship.FEEDS + "] IN ('" + feedId.toString() + "')";
        List<FeedServiceLevelAgreement> list = queryToList(query, null);
        return list;
    }


    public List<ExtensibleEntity> findAllRelationships() {
        return entityProvider.getEntities(JcrFeedServiceLevelAgreementRelationship.NODE_TYPE);
    }

    public FeedServiceLevelAgreementRelationship findRelationship(ServiceLevelAgreement.ID id) {
        List<? extends ExtensibleEntity> entities = entityProvider.findEntitiesMatchingProperty(JcrFeedServiceLevelAgreementRelationship.NODE_TYPE, JcrFeedServiceLevelAgreementRelationship.SLA, id);
        if (entities != null && !entities.isEmpty()) {
            return new JcrFeedServiceLevelAgreementRelationship((JcrExtensibleEntity) entities.get(0));
        }
        return null;
    }

    /**
     * Returns the relationship object for SLA to a Set of Feeds
     */
    public FeedServiceLevelAgreementRelationship getRelationship(ExtensibleEntity.ID id) {
        ExtensibleEntity entity = entityProvider.getEntity(id);
        return new JcrFeedServiceLevelAgreementRelationship((JcrExtensibleEntity) entity);
    }


    /**
     * Creates the Extensible Entity Type
     */
    public String createType() {
        // TODO service?
        if (typeCreated.compareAndSet(false, true)) {
            return metadata.commit(() -> {
                ExtensibleType feedSla = typeProvider.getType(JcrFeedServiceLevelAgreementRelationship.TYPE_NAME);
                if (feedSla == null) {
                    feedSla = typeProvider.buildType(JcrFeedServiceLevelAgreementRelationship.TYPE_NAME)
                        // @formatter:off
                         .field(JcrFeedServiceLevelAgreementRelationship.FEEDS)
                            .type(FieldDescriptor.Type.WEAK_REFERENCE)
                            .displayName("Feeds")
                            .description("The Feeds referenced on this SLA")
                            .required(false)
                            .collection(true)
                            .add()
                        .field(JcrFeedServiceLevelAgreementRelationship.SLA)
                            .type(FieldDescriptor.Type.WEAK_REFERENCE)
                            .displayName("SLA")
                            .description("The SLA")
                            .required(true)
                            .add()
                        .build();
                        // @formatter:on
                }

                return feedSla.getName();
            }, MetadataAccess.SERVICE);
        }
        return null;
    }


    /**
     * Create/Update the Relationship between a SLA and a Set of Feeds
     */
    @Override
    public FeedServiceLevelAgreementRelationship relate(ServiceLevelAgreement sla, Set<Feed.ID> feedIds) {
        JcrFeedServiceLevelAgreementRelationship relationship = null;
        //find if this relationship already exists
        Set<Node> feedNodes = new HashSet<>();
        Map<String, Object> props = new HashMap<>();
        Set<Feed> feeds = new HashSet<>();
        for (Feed.ID feedId : feedIds) {
            Feed feed = feedProvider.getFeed(feedId);
            if (feed != null) {
                feeds.add(feed);
            }
        }
        return relateFeeds(sla, feeds);
    }

    @Override
    public FeedServiceLevelAgreementRelationship relateFeeds(ServiceLevelAgreement sla, Set<Feed> feeds) {
        JcrFeedServiceLevelAgreementRelationship relationship = null;
        //find if this relationship already exists
        JcrFeedServiceLevelAgreementRelationship feedSla = (JcrFeedServiceLevelAgreementRelationship) findRelationship(sla.getId());
        Set<Node> feedNodes = new HashSet<>();
        Map<String, Object> props = new HashMap<>();
        for (Feed feed : feeds) {
            if (feed != null) {
                JcrFeed jcrFeed = (JcrFeed) feed;
                feedNodes.add(jcrFeed.getNode());
            }
        }
        props.put(JcrFeedServiceLevelAgreementRelationship.FEEDS, feedNodes);
        props.put(JcrFeedServiceLevelAgreementRelationship.SLA, ((JcrServiceLevelAgreement) sla).getNode());

        //remove any existing relationships
        removeFeedRelationships(sla.getId());
        if (feedSla == null) {

            ExtensibleType type = typeProvider.getType(JcrFeedServiceLevelAgreementRelationship.TYPE_NAME);
            JcrExtensibleEntity entity = (JcrExtensibleEntity) entityProvider.createEntity(type, props);

            relationship = new JcrFeedServiceLevelAgreementRelationship(entity.getNode());
        } else {
            JcrExtensibleEntity entity = (JcrExtensibleEntity) entityProvider.updateEntity(feedSla, props);
            relationship = new JcrFeedServiceLevelAgreementRelationship(entity.getNode());
        }
        //update the feed relationships
        for (Feed feed : feeds) {
            feedProvider.updateFeedServiceLevelAgreement(feed.getId(), sla);
        }

        return relationship;
    }


    @Override
    public boolean removeFeedRelationships(ServiceLevelAgreement.ID id) {
        try {
            Session session = getSession();
            JcrFeedServiceLevelAgreementRelationship extensibleEntity = (JcrFeedServiceLevelAgreementRelationship) findRelationship(id);
            if (extensibleEntity != null) {
                JcrMetadataAccess.ensureCheckoutNode(extensibleEntity.getNode());
                for (final Feed feed : extensibleEntity.getFeeds()) {
                    if (feed != null) {
                        final JcrFeed jcrFeed = (JcrFeed) feed;
                        JcrMetadataAccess.ensureCheckoutNode(jcrFeed.getNode());
                    }
                }
                return extensibleEntity.removeFeedRelationships(id);
            }
        } catch (RepositoryException e) {
            throw new MetadataRepositoryException("unable to remove Feed SLA relationships for SLA " + id, e);
        }
        return false;
    }

    public boolean removeAllRelationships(ServiceLevelAgreement.ID id) {
        try {
            Session session = getSession();
            JcrFeedServiceLevelAgreementRelationship extensibleEntity = (JcrFeedServiceLevelAgreementRelationship) findRelationship(id);
            if (extensibleEntity != null) {
                JcrMetadataAccess.ensureCheckoutNode(extensibleEntity.getNode());
                for (final Feed feed : extensibleEntity.getFeeds()) {
                    if (feed != null) {
                        final JcrFeed jcrFeed = (JcrFeed) feed;
                        JcrMetadataAccess.ensureCheckoutNode(jcrFeed.getNode());
                    }
                }
                extensibleEntity.removeFeedRelationships(id);
                extensibleEntity.getNode().remove();
            }
        } catch (RepositoryException e) {
            throw new MetadataRepositoryException("unable to remove Feed SLA relationships for SLA " + id, e);
        }
        return false;
    }


    protected Session getSession() {
        return JcrMetadataAccess.getActiveSession();
    }


}
