/*
 * Copyright 2016 The Simple File Server Authors
 *
 * 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.
 */

package org.sfs.elasticsearch;

import com.google.common.base.Optional;
import io.vertx.core.json.JsonObject;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import org.sfs.Server;
import org.sfs.VertxContext;
import org.sfs.jobs.VerifyRepairAllContainerObjects;
import org.sfs.nodes.ClusterInfo;
import org.sfs.nodes.all.blobreference.AcknowledgeBlobReference;
import org.sfs.nodes.all.blobreference.DeleteBlobReference;
import org.sfs.nodes.all.blobreference.VerifyBlobReference;
import org.sfs.nodes.all.segment.RebalanceSegment;
import org.sfs.nodes.compute.object.PruneObject;
import org.sfs.rx.Defer;
import org.sfs.rx.RxHelper;
import org.sfs.rx.ToType;
import org.sfs.vo.PersistentAccount;
import org.sfs.vo.PersistentContainer;
import org.sfs.vo.PersistentObject;
import org.sfs.vo.TransientBlobReference;
import org.sfs.vo.TransientSegment;
import org.sfs.vo.TransientServiceDef;
import org.sfs.vo.TransientVersion;
import rx.Observable;
import rx.Scheduler;

import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import static com.google.common.base.Optional.absent;
import static com.google.common.base.Optional.of;
import static com.google.common.collect.FluentIterable.from;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static java.lang.System.currentTimeMillis;
import static org.sfs.rx.Defer.aVoid;
import static org.sfs.rx.Defer.just;
import static rx.Observable.defer;

public class SearchHitMaintainObjectEndableWrite extends AbstractBulkUpdateEndableWriteStream {

    private static final Logger LOGGER = LoggerFactory.getLogger(SearchHitMaintainObjectEndableWrite.class);
    private final ClusterInfo clusterInfo;

    private final CachedAccount cachedAccount;
    private final CachedContainer cachedContainer;

    private final List<TransientServiceDef> dataNodes;
    private final Set<String> forceRemoveVolumes;


    public SearchHitMaintainObjectEndableWrite(VertxContext<Server> vertxContext, Set<String> forceRemoveVolumes) {
        super(vertxContext);
        this.clusterInfo = vertxContext.verticle().getClusterInfo();
        this.dataNodes =
                from(clusterInfo.getDataNodes())
                        .transform(TransientServiceDef::copy)
                        .toList();
        this.cachedAccount = new CachedAccount(vertxContext);
        this.cachedContainer = new CachedContainer(vertxContext);
        this.forceRemoveVolumes = forceRemoveVolumes;
        if (!forceRemoveVolumes.isEmpty()) {
            LOGGER.info("forceRemoveVolumes: " + forceRemoveVolumes);
        }
    }

    @Override
    protected Observable<Optional<JsonObject>> transform(final JsonObject data, String id, long version) {
        return toPersistentObject(data, id, version)
                .map(this::forceRemoveVolumes)
                // delete versions that are to old to attempt verification,ack and rebalance
                .flatMap(this::deleteOldUnAckdVersions)
                .timeout(3, TimeUnit.MINUTES, Observable.error(new RuntimeException(String.format("Timeout on deleteOldUnAckdVersions %s %s", id, data.encodePrettily()))))
                // attempt to delete versions that need deleting
                .flatMap(this::pruneObject)
                .timeout(3, TimeUnit.MINUTES, Observable.error(new RuntimeException(String.format("Timeout on pruneObject %s %s", id, data.encodePrettily()))))
                // verifyAck before rebalance
                // so that in cases where the verify/ack
                // failed to persist to the index
                // we're able to recreate the state
                // needed for rebalancing
                .flatMap(this::verifyAck)
                .timeout(3, TimeUnit.MINUTES, Observable.error(new RuntimeException(String.format("Timeout on verifyAck %s %s", id, data.encodePrettily()))))
                // rebalance the objects, including the ones that were just re-verified
                // disable rebalance because more testing is needed
                .flatMap(this::reBalance)
                .timeout(3, TimeUnit.MINUTES, Observable.error(new RuntimeException(String.format("Timeout on reBalance %s %s", id, data.encodePrettily()))))
                .map(persistentObject -> {
                    if (persistentObject.getVersions().isEmpty()) {
                        return absent();
                    } else {
                        JsonObject jsonObject = persistentObject.toJsonObject();
                        return of(jsonObject);
                    }
                });
    }

    protected PersistentObject forceRemoveVolumes(PersistentObject persistentObject) {
        if (!forceRemoveVolumes.isEmpty()) {
            for (TransientVersion version : persistentObject.getVersions()) {
                for (TransientSegment segment : version.getSegments()) {
                    if (!segment.isTinyData()) {
                        Iterator<TransientBlobReference> iterator = segment.getBlobs().iterator();
                        while (iterator.hasNext()) {
                            TransientBlobReference blob = iterator.next();
                            String volumeId = blob.getVolumeId().orNull();
                            if (volumeId != null && forceRemoveVolumes.contains(volumeId)) {
                                iterator.remove();
                            }
                        }
                    }
                }
            }
        }
        return persistentObject;
    }

    protected Observable<PersistentObject> verifyAck(PersistentObject persistentObject) {
        return just(persistentObject)
                .flatMap(persistentObject1 -> Observable.from(persistentObject1.getVersions()))
                .filter(version -> !version.isDeleted())
                .flatMap(transientVersion -> Observable.from(transientVersion.getSegments()))
                .filter(transientSegment -> !transientSegment.isTinyData())
                .flatMap(transientSegment -> Observable.from(transientSegment.getBlobs()))
                .filter(transientBlobReference -> !transientBlobReference.isDeleted())
                .flatMap(transientBlobReference -> {
                            boolean alreadyAckd = transientBlobReference.isAcknowledged();
                            Optional<Integer> oVerifyFailCount = transientBlobReference.getVerifyFailCount();
                            int verifyFailCount = oVerifyFailCount.isPresent() ? oVerifyFailCount.get() : 0;
                            return just(transientBlobReference)
                                    .flatMap(new VerifyBlobReference(vertxContext))
                                    .map(verified -> {
                                        // we do this here to unAck blob refs
                                        // in cases where the referenced volume has somehow
                                        // become corrupted
                                        if (!verified) {
                                            if (verifyFailCount >= VerifyRepairAllContainerObjects.VERIFY_RETRY_COUNT) {
                                                transientBlobReference.setAcknowledged(FALSE);
                                            } else {
                                                transientBlobReference.setVerifyFailCount(verifyFailCount + 1);
                                            }
                                        } else {
                                            transientBlobReference.setVerifyFailCount(0);
                                            transientBlobReference.setAcknowledged(TRUE);
                                        }
                                        return verified;
                                    })
                                    // only call ack on the volume if this blob reference
                                    // was successfully verified and not already ackd since
                                    // there's not sense in re-ack'ing
                                    .filter(verified -> verified && !alreadyAckd)
                                    .map(aVoid -> transientBlobReference)
                                    .flatMap(new AcknowledgeBlobReference(vertxContext));
                        }
                )
                .count()
                .map(new ToType<>(persistentObject));
    }

    protected Observable<PersistentObject> reBalance(PersistentObject persistentObject) {
        return just(persistentObject)
                .flatMap(persistentObject1 -> Observable.from(persistentObject1.getVersions()))
                .filter(version -> !version.isDeleted())
                .flatMap(transientVersion -> Observable.from(transientVersion.getSegments()))
                .flatMap(transientSegment ->
                        just(transientSegment)
                                .flatMap(new RebalanceSegment(vertxContext, dataNodes))
                                .map(reBalanced -> (Void) null))
                .count()
                .map(new ToType<>(persistentObject));
    }

    protected Observable<PersistentObject> pruneObject(PersistentObject persistentObject) {
        return just(persistentObject)
                .flatMap(new PruneObject(vertxContext))
                .map(modified -> persistentObject);
    }

    protected Observable<PersistentObject> deleteOldUnAckdVersions(PersistentObject persistentObject) {
        return defer(() -> {
            long now = currentTimeMillis();
            return aVoid()
                    .filter(aVoid -> now - persistentObject.getUpdateTs().getTimeInMillis() >= VerifyRepairAllContainerObjects.CONSISTENCY_THRESHOLD)
                    .map(aVoid -> persistentObject)
                    .flatMap(persistentObject1 -> Observable.from(persistentObject1.getVersions()))
                    // don't bother trying to delete old versions that are marked as
                    // deleted since we have another method that will take care of that
                    // at some point in the future
                    .filter(version -> !version.isDeleted())
                    .flatMap(transientVersion -> Observable.from(transientVersion.getSegments()))
                    .filter(transientSegment -> !transientSegment.isTinyData())
                    .flatMap(transientSegment -> Observable.from(transientSegment.getBlobs()))
                    .filter(transientBlobReference -> !transientBlobReference.isAcknowledged())
                    .filter(transientBlobReference -> {
                        Optional<Integer> oVerifyFailCount = transientBlobReference.getVerifyFailCount();
                        return oVerifyFailCount.isPresent() && oVerifyFailCount.get() >= VerifyRepairAllContainerObjects.VERIFY_RETRY_COUNT;
                    })
                    .flatMap(transientBlobReference ->
                            just(transientBlobReference)
                                    .flatMap(new DeleteBlobReference(vertxContext))
                                    .filter(deleted -> deleted)
                                    .map(deleted -> {
                                        if (Boolean.TRUE.equals(deleted)) {
                                            transientBlobReference.setDeleted(deleted);
                                        }
                                        return (Void) null;
                                    }))
                    .onErrorResumeNext(throwable -> {
                        LOGGER.warn("Handling Error", throwable);
                        return Defer.aVoid();
                    })
                    .count()
                    .map(new ToType<>(persistentObject));
        });
    }

    protected Observable<PersistentObject> toPersistentObject(JsonObject jsonObject, String id, long version) {
        return defer(() -> {

            final String accountId = jsonObject.getString("account_id");
            final String containerId = jsonObject.getString("container_id");
            return just(jsonObject)
                    .filter(jsonObject1 -> accountId != null && containerId != null)
                    .flatMap(document -> getAccount(accountId))
                    .flatMap(persistentAccount -> getContainer(persistentAccount, containerId))
                    .map(persistentContainer -> new PersistentObject(persistentContainer, id, version).merge(jsonObject));
        });
    }

    protected Observable<PersistentAccount> getAccount(String accountId) {
        return cachedAccount.get(accountId);
    }

    protected Observable<PersistentContainer> getContainer(PersistentAccount persistentAccount, String containerId) {
        return cachedContainer.get(persistentAccount, containerId);
    }
}