/**
 *
 */
package com.thinkbiganalytics.alerts.spi.defaults;

/*-
 * #%L
 * thinkbig-alerts-default
 * %%
 * 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.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Predicate;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.thinkbiganalytics.alerts.api.Alert;
import com.thinkbiganalytics.alerts.api.Alert.ID;
import com.thinkbiganalytics.alerts.api.Alert.Level;
import com.thinkbiganalytics.alerts.api.Alert.State;
import com.thinkbiganalytics.alerts.api.AlertChangeEvent;
import com.thinkbiganalytics.alerts.api.AlertCriteria;
import com.thinkbiganalytics.alerts.api.AlertNotfoundException;
import com.thinkbiganalytics.alerts.api.AlertResponse;
import com.thinkbiganalytics.alerts.api.core.BaseAlertCriteria;
import com.thinkbiganalytics.alerts.spi.AlertDescriptor;
import com.thinkbiganalytics.alerts.spi.AlertManager;
import com.thinkbiganalytics.alerts.spi.AlertNotifyReceiver;
import com.thinkbiganalytics.metadata.api.MetadataAccess;
import com.thinkbiganalytics.metadata.jpa.alerts.JpaAlert;
import com.thinkbiganalytics.metadata.jpa.alerts.JpaAlert.AlertId;
import com.thinkbiganalytics.metadata.jpa.alerts.JpaAlertChangeEvent;
import com.thinkbiganalytics.metadata.jpa.alerts.JpaAlertRepository;
import com.thinkbiganalytics.metadata.jpa.alerts.QJpaAlert;

import org.joda.time.DateTime;
import org.springframework.data.jpa.repository.support.QueryDslRepositorySupport;
import org.springframework.security.core.context.SecurityContextHolder;

import java.io.Serializable;
import java.net.URI;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import javax.inject.Inject;

/**
 *
 */
public class DefaultAlertManager extends QueryDslRepositorySupport implements AlertManager {

    @Inject
    private JPAQueryFactory queryFactory;

    @Inject
    private MetadataAccess metadataAccess;

    private Set<AlertNotifyReceiver> alertReceivers = Collections.synchronizedSet(new HashSet<>());
    private JpaAlertRepository repository;


    /**
     * @param repo
     */
    public DefaultAlertManager(JpaAlertRepository repo) {
        super(JpaAlert.class);
        this.repository = repo;
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.alerts.spi.AlertSource#resolve(java.io.Serializable)
     */
    @Override
    public ID resolve(Serializable id) {
        if (id instanceof JpaAlert.AlertId) {
            return (JpaAlert.AlertId) id;
        } else {
            return new JpaAlert.AlertId(id);
        }
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.alerts.spi.AlertSource#getAlertDescriptors()
     */
    @Override
    public Set<AlertDescriptor> getAlertDescriptors() {
        // TODO Auto-generated method stub
        return null;
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.alerts.spi.AlertSource#addReceiver(com.thinkbiganalytics.alerts.spi.AlertNotifyReceiver)
     */
    @Override
    public void addReceiver(AlertNotifyReceiver receiver) {
        this.alertReceivers.add(receiver);
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.alerts.spi.AlertSource#removeReceiver(com.thinkbiganalytics.alerts.spi.AlertNotifyReceiver)
     */
    @Override
    public void removeReceiver(AlertNotifyReceiver receiver) {
        this.alertReceivers.remove(receiver);
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.alerts.spi.AlertSource#criteria()
     */
    @Override
    public AlertCriteria criteria() {
        return new Criteria();
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.alerts.spi.AlertSource#getAlert(com.thinkbiganalytics.alerts.api.Alert.ID)
     */
    @Override
    public Optional<Alert> getAlert(ID id) {
        return this.metadataAccess.read(() -> {
            return Optional.of(findAlert(id).map(a -> asValue(a)).orElseThrow(() -> new AlertNotfoundException(id)));
        }, MetadataAccess.SERVICE);
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.alerts.spi.AlertSource#getAlerts()
     */
    @Override
    public Iterator<Alert> getAlerts(AlertCriteria criteria) {
        return this.metadataAccess.read(() -> {
            Criteria critImpl = (Criteria) (criteria == null ? criteria() : criteria);
            return critImpl.createQuery().fetch().stream()
                .map(a -> asValue(a))
                .collect(Collectors.toList()) // Need to terminate the stream while still in a transaction
                .iterator();
        }, MetadataAccess.SERVICE);
    }
//
//    /* (non-Javadoc)
//     * @see com.thinkbiganalytics.alerts.spi.AlertSource#getAlerts(org.joda.time.DateTime)
//     */
//    @Override
//    public Iterator<Alert> getAlerts(DateTime since) {
//        return this.metadataAccess.read(() -> {
//            return repository.findAlertsAfter(since).stream()
//                            .map(a -> asValue(a))
//                            .collect(Collectors.toList()) // Need to terminate the stream while still in a transaction
//                            .iterator();
//        }, MetadataAccess.SERVICE);
//    }
//
//    /* (non-Javadoc)
//     * @see com.thinkbiganalytics.alerts.spi.AlertSource#getAlerts(com.thinkbiganalytics.alerts.api.Alert.ID)
//     */
//    @Override
//    public Iterator<Alert> getAlerts(ID since) {
//        return this.metadataAccess.read(() -> {
//            return getAlert(since)
//                            .map(a -> getAlerts(a.getCreatedTime()))
//                            .orElseThrow(() -> new AlertNotfoundException(since));
//        }, MetadataAccess.SERVICE);
//    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.alerts.spi.AlertManager#addDescriptor(com.thinkbiganalytics.alerts.spi.AlertDescriptor)
     */
    @Override
    public boolean addDescriptor(AlertDescriptor descriptor) {
        // TODO Auto-generated method stub
        return false;
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.alerts.spi.AlertManager#create(java.net.URI, com.thinkbiganalytics.alerts.api.Alert.Level, java.lang.String, java.io.Serializable)
     */
    @Override
    public <C extends Serializable> Alert create(URI type, Level level, String description, C content) {
        final Principal user = SecurityContextHolder.getContext().getAuthentication() != null
                               ? SecurityContextHolder.getContext().getAuthentication()
                               : null;

        Alert created = this.metadataAccess.commit(() -> {
            JpaAlert alert = new JpaAlert(type, level, user, description, content);
            this.repository.save(alert);
            return asValue(alert);
        }, MetadataAccess.SERVICE);

        notifyReceivers(1);
        return created;
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.alerts.spi.AlertManager#getResponse(com.thinkbiganalytics.alerts.api.Alert)
     */
    @Override
    public AlertResponse getResponse(Alert alert) {
        JpaAlert.AlertId idImpl = (JpaAlert.AlertId) resolve(alert.getId());
        return new TransactionalResponse(idImpl);
    }

    /* (non-Javadoc)
     * @see com.thinkbiganalytics.alerts.spi.AlertManager#remove(com.thinkbiganalytics.alerts.api.Alert.ID)
     */
    @Override
    public Alert remove(ID id) {
        JpaAlert.AlertId idImpl = (JpaAlert.AlertId) resolve(id);

        return this.metadataAccess.commit(() -> {
            JpaAlert alert = repository.findOne(idImpl);
            this.repository.delete(id);
            return alert;
        }, MetadataAccess.SERVICE);
    }

    protected Optional<JpaAlert> findAlert(Alert.ID id) {
        JpaAlert.AlertId idImpl = (AlertId) resolve(id);
        return Optional.ofNullable(repository.findOne(idImpl));
    }

    protected Alert asValue(Alert alert) {
        return new ImmutableAlert(alert, this);
    }

    protected JpaAlert clearAlert(JpaAlert.AlertId id) {
        return this.metadataAccess.commit(() -> {
            JpaAlert alert = repository.findOne(id);
            alert.setCleared(true);
            return alert;
        }, MetadataAccess.SERVICE);
    }

    protected <C extends Serializable> Alert changeAlert(JpaAlert.AlertId id, State state, String descr, C content) {
        final Principal user = SecurityContextHolder.getContext().getAuthentication() != null
                               ? SecurityContextHolder.getContext().getAuthentication()
                               : null;

        Alert changed = this.metadataAccess.commit(() -> {
            JpaAlert alert = findAlert(id).orElseThrow(() -> new AlertNotfoundException(id));
            JpaAlertChangeEvent event = new JpaAlertChangeEvent(state, user, descr, content);
            alert.addEvent(event);
            return asValue(alert);
        }, MetadataAccess.SERVICE);

        notifyReceivers(1);
        return changed;
    }

    protected void notifyReceivers(int count) {
        Set<AlertNotifyReceiver> receivers;
        synchronized (this.alertReceivers) {
            receivers = new HashSet<>(this.alertReceivers);
        }
        receivers.forEach(a -> a.alertsAvailable(count));
    }

    private static class ImmutableAlert implements Alert {

        private final AlertManager source;
        private final Alert.ID id;
        private final String description;
        private final Level level;
        private final URI type;
        private final DateTime createdTime;
        private final Serializable content;
        private final boolean cleared;
        private final List<AlertChangeEvent> events;

        public ImmutableAlert(Alert alert, AlertManager mgr) {
            this.source = mgr;
            this.id = alert.getId();
            this.content = alert.getContent();
            this.description = alert.getDescription();
            this.level = alert.getLevel();
            this.type = alert.getType();
            this.cleared = alert.isCleared();
            this.createdTime = alert.getCreatedTime();
            this.events = Collections.unmodifiableList(alert.getEvents().stream()
                                                           .map(a -> new ImmutableAlertChangeEvent(a))
                                                           .collect(Collectors.toList()));
        }

        @Override
        public AlertManager getSource() {
            return source;
        }

        @Override
        public Alert.ID getId() {
            return id;
        }

        @Override
        public String getDescription() {
            return description;
        }

        @Override
        public Level getLevel() {
            return level;
        }

        @Override
        public URI getType() {
            return type;
        }

        @Override
        public State getState() {
            return this.events.get(0).getState();
        }

        @Override
        @SuppressWarnings("unchecked")
        public Serializable getContent() {
            return content;
        }

        @Override
        public List<AlertChangeEvent> getEvents() {
            return events;
        }

        @Override
        public DateTime getCreatedTime() {
            return this.createdTime;
        }

        @Override
        public boolean isCleared() {
            return this.cleared;
        }

        @Override
        public boolean isActionable() {
            return true;
        }
    }

    private static class ImmutableAlertChangeEvent implements AlertChangeEvent {

        private final DateTime changeTime;
        private final State state;
        private final Principal user;
        private final String description;
        private final Serializable content;

        public ImmutableAlertChangeEvent(AlertChangeEvent event) {
            this.changeTime = event.getChangeTime();
            this.state = event.getState();
            this.user = event.getUser();
            this.description = event.getDescription();
            this.content = event.getContent();
        }

        public DateTime getChangeTime() {
            return changeTime;
        }

        public State getState() {
            return state;
        }

        @Override
        public Principal getUser() {
            return user;
        }

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

        public Serializable getContent() {
            return this.content;
        }
    }

    private class TransactionalResponse implements AlertResponse {

        private final JpaAlert.AlertId id;

        public TransactionalResponse(JpaAlert.AlertId id) {
            super();
            this.id = id;
        }

        /* (non-Javadoc)
         * @see com.thinkbiganalytics.alerts.api.AlertResponse#inProgress()
         */
        @Override
        public Alert inProgress(String descr) {
            return inProgress(descr, null);
        }

        /* (non-Javadoc)
         * @see com.thinkbiganalytics.alerts.api.AlertResponse#inProgress(java.io.Serializable)
         */
        @Override
        public <C extends Serializable> Alert inProgress(String descr, C content) {
            return changeAlert(this.id, State.IN_PROGRESS, descr, content);
        }

        /* (non-Javadoc)
         * @see com.thinkbiganalytics.alerts.api.AlertResponse#handle()
         */
        @Override
        public Alert handle(String descr) {
            return handle(descr, null);
        }

        /* (non-Javadoc)
         * @see com.thinkbiganalytics.alerts.api.AlertResponse#handle(java.io.Serializable)
         */
        @Override
        public <C extends Serializable> Alert handle(String descr, C content) {
            return changeAlert(this.id, State.HANDLED, descr, content);
        }

        /* (non-Javadoc)
         * @see com.thinkbiganalytics.alerts.api.AlertResponse#unHandle()
         */
        @Override
        public Alert unhandle(String descr) {
            return unhandle(descr, null);
        }

        /* (non-Javadoc)
         * @see com.thinkbiganalytics.alerts.api.AlertResponse#unhandle(java.io.Serializable)
         */
        @Override
        public <C extends Serializable> Alert unhandle(String descr, C content) {
            return changeAlert(this.id, State.UNHANDLED, descr, content);
        }

        /* (non-Javadoc)
         * @see com.thinkbiganalytics.alerts.api.AlertResponse#clear()
         */
        @Override
        public void clear() {
            clearAlert(this.id);
        }
    }

    private class Criteria extends BaseAlertCriteria {

        public JPAQuery<JpaAlert> createQuery() {
            List<Predicate> preds = new ArrayList<>();
            QJpaAlert alert = QJpaAlert.jpaAlert;

            JPAQuery<JpaAlert> query = queryFactory
                .select(alert)
                .from(alert)
                .limit(getLimit());

            // The "state" criteria now means filter by an alert's current state.
            // To support filtering by any state the alert has transitioned through
            // we can add the commented out code below (the old state behavior)
//            if (getTransitions().size() > 0) {
//                QAlertChangeEvent event = QAlertChangeEvent.alertChangeEvent;
//                query.join(alert.events, event);
//                preds.add(event.state.in(getTransitions()));
//            }

            if (getStates().size() > 0) {
                preds.add(alert.state.in(getStates()));
            }
            if (getLevels().size() > 0) {
                preds.add(alert.level.in(getLevels()));
            }
            if (getAfterTime() != null) {
                preds.add(alert.createdTime.gt(getAfterTime()));
            }
            if (getBeforeTime() != null) {
                preds.add(alert.createdTime.lt(getBeforeTime()));
            }
            if (!isIncludeCleared()) {
                preds.add(alert.cleared.isFalse());
            }

            if (getTypes().size() > 0) {
                BooleanBuilder likes = new BooleanBuilder();
                getTypes().stream()
                    .map(uri -> alert.typeString.like(uri.toASCIIString().concat("%")))
                    .forEach(pred -> likes.or(pred));
                preds.add(likes);
            }

            // When limiting and using "after" criteria only, we need to sort ascending to get the next n values after the given id/time.
            // In all other cases sort descending. The results will be ordered correctly when aggregated by the provider.
            if (getLimit() != Integer.MAX_VALUE && getAfterTime() != null && getBeforeTime() == null) {
                query.orderBy(alert.createdTime.asc());
            } else {
                query.orderBy(alert.createdTime.desc());
            }

            if (preds.isEmpty()) {
                return query;
            } else {
                return query.where(preds.toArray(new Predicate[preds.size()]));
            }
        }
    }

}
