package com.thinkbiganalytics.metadata.modeshape.feed;

import com.thinkbiganalytics.metadata.api.category.Category;
import com.thinkbiganalytics.metadata.api.datasource.Datasource;
import com.thinkbiganalytics.metadata.api.extension.UserFieldDescriptor;
import com.thinkbiganalytics.metadata.api.feed.Feed;
import com.thinkbiganalytics.metadata.api.feed.FeedDestination;
import com.thinkbiganalytics.metadata.api.feed.FeedPrecondition;
import com.thinkbiganalytics.metadata.api.feed.FeedSource;
import com.thinkbiganalytics.metadata.api.feed.InitializationStatus;
import com.thinkbiganalytics.metadata.api.feed.security.FeedOpsAccessControlProvider;
import com.thinkbiganalytics.metadata.api.security.HadoopSecurityGroup;
import com.thinkbiganalytics.metadata.api.template.FeedManagerTemplate;
import com.thinkbiganalytics.metadata.modeshape.MetadataRepositoryException;
import com.thinkbiganalytics.metadata.modeshape.category.JcrCategory;
import com.thinkbiganalytics.metadata.modeshape.common.AbstractJcrAuditableSystemEntity;
import com.thinkbiganalytics.metadata.modeshape.common.JcrEntity;
import com.thinkbiganalytics.metadata.modeshape.datasource.JcrDatasource;
import com.thinkbiganalytics.metadata.modeshape.feed.security.JcrFeedAllowedActions;
import com.thinkbiganalytics.metadata.modeshape.security.action.JcrAllowedActions;
import com.thinkbiganalytics.metadata.modeshape.security.mixin.AccessControlledMixin;
import com.thinkbiganalytics.metadata.modeshape.sla.JcrServiceLevelAgreement;
import com.thinkbiganalytics.metadata.modeshape.support.JcrPropertyUtil;
import com.thinkbiganalytics.metadata.modeshape.support.JcrUtil;
import com.thinkbiganalytics.metadata.sla.api.ServiceLevelAgreement;

import org.joda.time.DateTime;

import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import javax.annotation.Nonnull;
import javax.jcr.Node;
import javax.jcr.RepositoryException;

/*-
 * #%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%
 */

/**
 * An implementation of {@link Feed} backed by a JCR repository.
 */
public class JcrFeed extends AbstractJcrAuditableSystemEntity implements Feed, AccessControlledMixin {

    public static final String PRECONDITION_TYPE = "tba:feedPrecondition";

    public static final String NODE_TYPE = "tba:feed";

    public static final String SUMMARY = "tba:summary";
    public static final String DATA = "tba:data";

    private FeedSummary summary;
    private FeedData data;

    // TODO: Referencing the ops access provider is kind of ugly but is needed so that 
    // a change to the feed's allowed accessions for ops access get's propagated to the JPA table.
    private volatile Optional<FeedOpsAccessControlProvider> opsAccessProvider = Optional.empty();

    public JcrFeed(Node node) {
        super(node);
    }

    public JcrFeed(Node node, FeedOpsAccessControlProvider opsAccessProvider) {
        super(node);
        setOpsAccessProvider(opsAccessProvider);
    }

    public JcrFeed(Node node, JcrCategory category) {
        this(node, (FeedOpsAccessControlProvider) null);
        if (category != null) {
            getFeedSummary().ifPresent(s -> s.setProperty(FeedSummary.CATEGORY, category));
        }
    }

    public JcrFeed(Node node, JcrCategory category, FeedOpsAccessControlProvider opsAccessProvider) {
        this(node, opsAccessProvider);
        if (category != null) {
            getFeedSummary().ifPresent(s -> s.setProperty(FeedSummary.CATEGORY, category));
        }
    }


    /**
     * This should be set after an instance of this type is created to allow the change
     * of a feed's operations access control.
     *
     * @param opsAccessProvider the opsAccessProvider to set
     */
    public void setOpsAccessProvider(FeedOpsAccessControlProvider opsAccessProvider) {
        this.opsAccessProvider = Optional.ofNullable(opsAccessProvider);
    }

    public Optional<FeedOpsAccessControlProvider> getOpsAccessProvider() {
        return this.opsAccessProvider;
    }

    // -=-=--=-=- Delegate Propertied methods to data -=-=-=-=-=-

    @Override
    public Map<String, Object> getProperties() {
        return getFeedData().map(d -> d.getProperties()).orElse(Collections.emptyMap());
    }

    @Override
    public void setProperties(Map<String, Object> properties) {
        getFeedData().ifPresent(d -> d.setProperties(properties));
    }

    @Override
    public void setProperty(String name, Object value) {
        getFeedData().ifPresent(d -> d.setProperty(name, value));
    }

    @Override
    public void removeProperty(String key) {
        getFeedData().ifPresent(d -> d.removeProperty(key));
    }

    @Override
    public Map<String, Object> mergeProperties(Map<String, Object> props) {
        return getFeedData().map(d -> d.mergeProperties(props)).orElse(Collections.emptyMap());
    }

    @Override
    public Map<String, Object> replaceProperties(Map<String, Object> props) {
        return getFeedData().map(d -> d.replaceProperties(props)).orElse(Collections.emptyMap());
    }

    // -=-=--=-=- Delegate taggable methods to summary -=-=-=-=-=-

    @Override
    public Set<String> addTag(String tag) {
        return getFeedSummary().map(s -> s.addTag(tag)).orElse(Collections.emptySet());
    }

    @Override
    public Set<String> getTags() {
        return getFeedSummary().map(s -> s.getTags()).orElse(Collections.emptySet());
    }

    @Override
    public void setTags(Set<String> tags) {
        getFeedSummary().ifPresent(s -> s.setTags(tags));
    }

    @Override
    public boolean hasTag(String tag) {
        return getFeedSummary().map(s -> s.hasTag(tag)).orElse(false);
    }

    // -=-=--=-=- Delegate AbstractJcrAuditableSystemEntity methods to summary -=-=-=-=-=-

    @Override
    public DateTime getCreatedTime() {
        return getFeedSummary().map(s -> s.getCreatedTime()).orElse(null);
    }

    @Override
    public void setCreatedTime(DateTime createdTime) {
        getFeedSummary().ifPresent(s -> s.setCreatedTime(createdTime));
    }

    @Override
    public DateTime getModifiedTime() {
        return getFeedSummary().map(s -> s.getModifiedTime()).orElse(null);
    }

    @Override
    public void setModifiedTime(DateTime modifiedTime) {
        getFeedSummary().ifPresent(s -> s.setModifiedTime(modifiedTime));
    }

    @Override
    public String getCreatedBy() {
        return getFeedSummary().map(s -> s.getCreatedBy()).orElse(null);
    }

    @Override
    public String getModifiedBy() {
        return getFeedSummary().map(s -> s.getModifiedBy()).orElse(null);
    }

    // -=-=--=-=- Delegate AbstractJcrSystemEntity methods to summary -=-=-=-=-=-

    @Override
    public String getDescription() {
        return getFeedSummary().map(s -> s.getDescription()).orElse(null);
    }

    @Override
    public void setDescription(String description) {
        getFeedSummary().ifPresent(s -> s.setDescription(description));
    }

    @Override
    public String getSystemName() {
        return getFeedSummary().map(s -> s.getSystemName()).orElse(null);
    }

    @Override
    public void setSystemName(String systemName) {
        getFeedSummary().ifPresent(s -> s.setSystemName(systemName));
    }

    @Override
    public String getTitle() {
        return getFeedSummary().map(s -> s.getTitle()).orElse(null);
    }

    @Override
    public void setTitle(String title) {
        getFeedSummary().ifPresent(s -> s.setTitle(title));
    }


    public Category getCategory() {
        return getFeedSummary().map(s -> s.getCategory(JcrCategory.class)).orElse(null);
    }

    public FeedManagerTemplate getTemplate() {
        return getFeedDetails().map(d -> d.getTemplate()).orElse(null);
    }

    public void setTemplate(FeedManagerTemplate template) {
        getFeedDetails().ifPresent(d -> d.setTemplate(template));
    }

    public List<? extends FeedSource> getSources() {
        return getFeedDetails().map(d -> d.getSources()).orElse(Collections.emptyList());
    }

    public List<? extends FeedDestination> getDestinations() {
        return getFeedDetails().map(d -> d.getDestinations()).orElse(Collections.emptyList());
    }

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

    @Override
    public String getQualifiedName() {
        return getCategory().getName() + "." + getName();
    }

    @Override
    public String getDisplayName() {
        return getTitle();
    }

    @Override
    public void setDisplayName(String name) {
        setTitle(name);
    }

    @Override
    public Feed.State getState() {
        return getFeedData().map(d -> d.getState()).orElse(null);
    }

    @Override
    public void setState(State state) {
        getFeedData().ifPresent(d -> d.setState(state));
    }

    @Override
    public boolean isInitialized() {
        return getFeedData().map(d -> d.isInitialized()).orElse(null);
    }

    @Override
    public InitializationStatus getCurrentInitStatus() {
        return getFeedData().map(d -> d.getCurrentInitStatus()).orElse(null);
    }

    @Override
    public void updateInitStatus(InitializationStatus status) {
        getFeedData().ifPresent(d -> d.updateInitStatus(status));
    }

    @Override
    public List<InitializationStatus> getInitHistory() {
        return getFeedData().map(d -> d.getInitHistory()).orElse(Collections.emptyList());
    }

    @Override
    public FeedPrecondition getPrecondition() {
        return getFeedDetails().map(d -> d.getPrecondition()).orElse(null);
    }

    public void setPrecondition(JcrServiceLevelAgreement sla) {
//        Node precondNode
    }

    @Override
    public Set<String> getWaterMarkNames() {
        return getFeedData().map(d -> d.getWaterMarkNames()).orElse(Collections.emptySet());
    }

    @Override
    public Optional<String> getWaterMarkValue(String waterMarkName) {
        return getFeedData().flatMap(d -> d.getWaterMarkValue(waterMarkName));
    }

    @Override
    public void setWaterMarkValue(String waterMarkName, String value) {
        getFeedData().ifPresent(d -> d.setWaterMarkValue(waterMarkName, value));
    }

    @Override
    public List<Feed> getDependentFeeds() {
        return getFeedDetails().map(d -> d.getDependentFeeds()).orElse(Collections.emptyList());
    }

    @Override
    public boolean addDependentFeed(Feed feed) {
        return getFeedDetails().map(d -> d.addDependentFeed(feed)).orElse(false);
    }

    @Override
    public boolean removeDependentFeed(Feed feed) {
        return getFeedDetails().map(d -> d.removeDependentFeed(feed)).orElse(false);
    }

    @Override
    public List<Feed> getUsedByFeeds() {
        return getFeedDetails().map(d -> d.getUsedByFeeds()).orElse(Collections.emptyList());
    }

    @Override
    public boolean addUsedByFeed(Feed feed) {
        return getFeedDetails().map(d -> d.addUsedByFeed(feed)).orElse(false);
    }

    @Override
    public boolean removeUsedByFeed(Feed feed) {
        return getFeedDetails().map(d -> d.removeUsedByFeed(feed)).orElse(false);
    }

    @Override
    public FeedSource getSource(final Datasource.ID id) {
        return getFeedDetails().map(d -> d.getSource(id)).orElse(null);
    }

    @Override
    public FeedDestination getDestination(final Datasource.ID id) {
        return getFeedDetails().map(d -> d.getDestination(id)).orElse(null);
    }

    public String getSchedulePeriod() {
        return getFeedData().map(d -> d.getSchedulePeriod()).orElse(null);
    }

    public void setSchedulePeriod(String schedulePeriod) {
        getFeedData().ifPresent(d -> d.setSchedulePeriod(schedulePeriod));
    }

    public String getScheduleStrategy() {
        return getFeedData().map(d -> d.getScheduleStrategy()).orElse(null);
    }

    public void setScheduleStrategy(String scheduleStrategy) {
        getFeedData().ifPresent(d -> d.setScheduleStrategy(scheduleStrategy));
    }

    public List<ServiceLevelAgreement> getServiceLevelAgreements() {
        return getFeedDetails().map(d -> d.getServiceLevelAgreements()).orElse(Collections.emptyList());
    }

    public void setServiceLevelAgreements(List<? extends ServiceLevelAgreement> serviceLevelAgreements) {
        getFeedDetails().ifPresent(d -> d.setServiceLevelAgreements(serviceLevelAgreements));
    }

    public List<? extends HadoopSecurityGroup> getSecurityGroups() {
        return getFeedData().map(d -> d.getSecurityGroups()).orElse(Collections.emptyList());
    }

    public void setSecurityGroups(List<? extends HadoopSecurityGroup> hadoopSecurityGroups) {
        getFeedData().ifPresent(d -> d.setSecurityGroups(hadoopSecurityGroups));
    }

    public void removeServiceLevelAgreement(ServiceLevelAgreement.ID id) {
        getFeedDetails().ifPresent(d -> d.removeServiceLevelAgreement(id));
    }

    public boolean addServiceLevelAgreement(ServiceLevelAgreement sla) {
        return getFeedDetails().map(d -> d.addServiceLevelAgreement(sla)).orElse(false);
    }

    @Nonnull
    @Override
    public Map<String, String> getUserProperties() {
        return JcrPropertyUtil.getUserProperties(node);
    }

    @Override
    public void setUserProperties(@Nonnull final Map<String, String> userProperties, @Nonnull final Set<UserFieldDescriptor> userFields) {
        JcrPropertyUtil.setUserProperties(node, userFields, userProperties);
    }

    @Override
    public String getJson() {
        return getFeedDetails().map(d -> d.getJson()).orElse(null);
    }

    @Override
    public void setJson(String json) {
        getFeedDetails().ifPresent(d -> d.setJson(json));

    }

    @Override
    public String getNifiProcessGroupId() {
        return getFeedDetails().map(d -> d.getNifiProcessGroupId()).orElse(null);
    }

    @Override
    public void setNifiProcessGroupId(String id) {
        getFeedDetails().ifPresent(d -> d.setNifiProcessGroupId(id));
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.metadata.modeshape.security.mixin.AccessControlledMixin#getJcrAllowedActions()
     */
    @Override
    public JcrAllowedActions getJcrAllowedActions() {
        Node allowedNode = JcrUtil.getNode(getNode(), JcrAllowedActions.NODE_NAME);
        return JcrUtil.createJcrObject(allowedNode, getJcrAllowedActionsType(), this.opsAccessProvider.orElse(null));
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.metadata.modeshape.security.mixin.AccessControlledMixin#getJcrAllowedActionsType()
     */
    @Override
    public Class<JcrFeedAllowedActions> getJcrAllowedActionsType() {
        return JcrFeedAllowedActions.class;
    }

    public Optional<FeedSummary> getFeedSummary() {
        if (this.summary == null) {
            if (JcrUtil.hasNode(getNode(), SUMMARY)) {
                this.summary = JcrUtil.getJcrObject(getNode(), SUMMARY, FeedSummary.class, this);
                return Optional.of(this.summary);
            } else {
                return Optional.empty();
            }
        } else {
            return Optional.of(this.summary);
        }
    }

    public Optional<FeedDetails> getFeedDetails() {
        Optional<FeedSummary> summary = getFeedSummary();

        if (summary.isPresent()) {
            return summary.get().getFeedDetails();
        } else {
            return Optional.empty();
        }
    }

    public Optional<FeedData> getFeedData() {
        if (this.data == null) {
            if (JcrUtil.hasNode(getNode(), DATA)) {
                this.data = JcrUtil.getJcrObject(getNode(), DATA, FeedData.class);
                return Optional.of(this.data);
            } else {
                return Optional.empty();
            }
        } else {
            return Optional.of(this.data);
        }
    }

    @Override
    public FeedId getId() {
        try {
            return new JcrFeed.FeedId(getObjectId());
        } catch (RepositoryException e) {
            throw new MetadataRepositoryException("Failed to retrieve the entity id", e);
        }
    }

    protected JcrFeedSource ensureFeedSource(JcrDatasource datasource) {
        return getFeedDetails().map(d -> d.ensureFeedSource(datasource)).orElse(null);
    }

    protected JcrFeedDestination ensureFeedDestination(JcrDatasource datasource) {
        return getFeedDetails().map(d -> d.ensureFeedDestination(datasource)).orElse(null);
    }

    protected void removeFeedSource(JcrFeedSource source) {
        getFeedDetails().ifPresent(d -> d.removeFeedSource(source));
    }

    protected void removeFeedDestination(JcrFeedDestination dest) {
        getFeedDetails().ifPresent(d -> d.removeFeedDestination(dest));
    }

    protected void removeFeedSources() {
        getFeedDetails().ifPresent(d -> d.removeFeedSources());
    }

    protected void removeFeedDestinations() {
        getFeedDetails().ifPresent(d -> d.removeFeedDestinations());
    }

    protected Node createNewPrecondition() {
        return getFeedDetails().map(d -> d.createNewPrecondition()).orElse(null);
    }

    public static class FeedId extends JcrEntity.EntityId implements Feed.ID {

        public FeedId(Serializable ser) {
            super(ser);
        }
    }
}
