/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.nifi.provenance.store;

import org.apache.lucene.util.NamedThreadFactory;
import org.apache.nifi.events.EventReporter;
import org.apache.nifi.provenance.ProvenanceEventRecord;
import org.apache.nifi.provenance.RepositoryConfiguration;
import org.apache.nifi.provenance.authorization.EventAuthorizer;
import org.apache.nifi.provenance.authorization.EventTransformer;
import org.apache.nifi.provenance.store.iterator.AuthorizingEventIterator;
import org.apache.nifi.provenance.store.iterator.EventIterator;
import org.apache.nifi.provenance.util.DirectoryUtils;
import org.apache.nifi.reporting.Severity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;

public abstract class PartitionedEventStore implements EventStore {
    private static final Logger logger = LoggerFactory.getLogger(PartitionedEventStore.class);
    private static final String EVENT_CATEGORY = "Provenance Repository";

    private final AtomicLong partitionIndex = new AtomicLong(0L);
    private final RepositoryConfiguration repoConfig;
    private final EventReporter eventReporter;
    private ScheduledExecutorService maintenanceExecutor;

    public PartitionedEventStore(final RepositoryConfiguration config, final EventReporter eventReporter) {
        this.repoConfig = config;
        this.eventReporter = eventReporter;
    }


    @Override
    public void initialize() throws IOException {
        maintenanceExecutor = Executors.newScheduledThreadPool(1, new NamedThreadFactory("Provenance Repository Maintenance"));
        final long maintenanceMillis = repoConfig.getMaintenanceFrequency(TimeUnit.MILLISECONDS);
        maintenanceExecutor.scheduleWithFixedDelay(this::performMaintenance, maintenanceMillis, maintenanceMillis, TimeUnit.MILLISECONDS);

        for (final EventStorePartition partition : getPartitions()) {
            partition.initialize();
        }
    }

    @Override
    public void close() throws IOException {
        if (maintenanceExecutor != null) {
            maintenanceExecutor.shutdownNow();
        }

        IOException thrown = null;

        for (final EventStorePartition partition : getPartitions()) {
            try {
               partition.close();
            } catch (final IOException ioe) {
                if (thrown == null) {
                    thrown = ioe;
                } else {
                    thrown.addSuppressed(ioe);
                }
            }
        }

        if (thrown != null) {
            throw thrown;
        }
    }


    @Override
    public StorageResult addEvents(final Iterable<ProvenanceEventRecord> events) throws IOException {
        final List<? extends EventStorePartition> partitions = getPartitions();
        final int index = (int) (partitionIndex.getAndIncrement() % partitions.size());
        final EventStorePartition partition = partitions.get(index);
        return partition.addEvents(events);
    }

    @Override
    public long getSize() {
        long size = 0;
        for (final EventStorePartition partition : getPartitions()) {
            size += partition.getSize();
        }

        return size;
    }

    private long getRepoSize() {
        long total = 0L;

        for (final File storageDir : repoConfig.getStorageDirectories().values()) {
            total += DirectoryUtils.getSize(storageDir);
        }

        return total;
    }

    @Override
    public long getMaxEventId() {
        return getPartitions().stream()
            .mapToLong(EventStorePartition::getMaxEventId)
            .max()
            .orElse(-1L);
    }

    @Override
    public Optional<ProvenanceEventRecord> getEvent(final long id) throws IOException {
        for (final EventStorePartition partition : getPartitions()) {
            final Optional<ProvenanceEventRecord> option = partition.getEvent(id);
            if (option.isPresent()) {
                return option;
            }
        }

        return Optional.empty();
    }

    @Override
    public List<ProvenanceEventRecord> getEvents(final long firstRecordId, final int maxRecords) throws IOException {
        return getEvents(firstRecordId, maxRecords, EventAuthorizer.GRANT_ALL, EventTransformer.EMPTY_TRANSFORMER);
    }

    @Override
    public List<ProvenanceEventRecord> getEvents(final long firstRecordId, final int maxRecords, final EventAuthorizer authorizer,
        final EventTransformer transformer) throws IOException {
        if (firstRecordId + maxRecords < 1 || maxRecords < 1 || firstRecordId > getMaxEventId()) {
            return Collections.emptyList();
        }

        return getEvents(maxRecords, authorizer, part -> part.createEventIterator(firstRecordId), transformer);
    }

    @Override
    public List<ProvenanceEventRecord> getEvents(final List<Long> eventIds, final EventAuthorizer authorizer, final EventTransformer transformer) throws IOException {
        if (eventIds == null || eventIds.isEmpty()) {
            return Collections.emptyList();
        }

        return getEvents(eventIds.size(), authorizer, part -> part.createEventIterator(eventIds), transformer);
    }

    private List<ProvenanceEventRecord> getEvents(final int maxRecords, final EventAuthorizer authorizer,
        final Function<EventStorePartition, EventIterator> eventIteratorFactory, final EventTransformer transformer) throws IOException {

        if (maxRecords < 1) {
            return Collections.emptyList();
        }

        final List<ProvenanceEventRecord> selectedEvents = new ArrayList<>();

        // Create a Map so that the key is the next record available from a partition and the value is the EventIterator from which
        // the record came. This sorted map is then used so that we are able to always get the first entry, which is the next
        // lowest record id among all partitions.
        final SortedMap<ProvenanceEventRecord, EventIterator> recordToIteratorMap = new TreeMap<>(
            (o1, o2) -> Long.compare(o1.getEventId(), o2.getEventId()));

        final Collection<EventIterator> createdIterators = new ArrayList<>();
        try {
            // Seed our map with the first event in each Partition.
            for (final EventStorePartition partition : getPartitions()) {
                final EventAuthorizer nonNullAuthorizer = authorizer == null ? EventAuthorizer.GRANT_ALL : authorizer;
                final EventIterator partitionIterator = eventIteratorFactory.apply(partition);
                final EventIterator iterator = new AuthorizingEventIterator(partitionIterator, nonNullAuthorizer, transformer);
                createdIterators.add(iterator);

                final Optional<ProvenanceEventRecord> option = iterator.nextEvent();
                if (option.isPresent()) {
                    recordToIteratorMap.put(option.get(), iterator);
                }
            }

            // If no records found, just return the empty list.
            if (recordToIteratorMap.isEmpty()) {
                return selectedEvents;
            }

            // Get the event with the next-lowest ID. Add it to the list of selected events,
            // then read the next event from the same EventIterator that this event came from.
            // This ensures that our map is always populated with the next event for each
            // EventIterator, which also ensures that the first key in our map is the event
            // with the lowest ID (since all events from a given EventIterator have monotonically
            // increasing Event ID's).
            ProvenanceEventRecord nextEvent = recordToIteratorMap.firstKey();
            while (nextEvent != null && selectedEvents.size() < maxRecords) {
                selectedEvents.add(nextEvent);

                final EventIterator iterator = recordToIteratorMap.remove(nextEvent);
                final Optional<ProvenanceEventRecord> nextRecordFromIterator = iterator.nextEvent();
                if (nextRecordFromIterator.isPresent()) {
                    recordToIteratorMap.put(nextRecordFromIterator.get(), iterator);
                }

                nextEvent = recordToIteratorMap.isEmpty() ? null : recordToIteratorMap.firstKey();
            }

            return selectedEvents;
        } finally {
            // Ensure that we close all record readers that have been created
            for (final EventIterator iterator : createdIterators) {
                try {
                    iterator.close();
                } catch (final Exception e) {
                    if (logger.isDebugEnabled()) {
                        logger.warn("Failed to close Record Reader {}", iterator, e);
                    } else {
                        logger.warn("Failed to close Record Reader {}", iterator);
                    }
                }
            }
        }
    }


    void performMaintenance() {
        try {
            final long maxFileLife = repoConfig.getMaxRecordLife(TimeUnit.MILLISECONDS);
            for (final EventStorePartition partition : getPartitions()) {
                try {
                    partition.purgeOldEvents(maxFileLife, TimeUnit.MILLISECONDS);
                } catch (final Exception e) {
                    logger.error("Failed to purge expired events from {}", partition, e);
                    eventReporter.reportEvent(Severity.WARNING, EVENT_CATEGORY,
                        "Failed to purge expired events from Provenance Repository. See logs for more information.");
                }
            }

            final long maxStorageCapacity = repoConfig.getMaxStorageCapacity();
            long currentSize;
            try {
                currentSize = getRepoSize();
            } catch (final Exception e) {
                logger.error("Could not determine size of Provenance Repository. Will not expire any data due to storage limits", e);
                eventReporter.reportEvent(Severity.WARNING, EVENT_CATEGORY, "Failed to determine size of Provenance Repository. "
                    + "No data will be expired due to storage limits at this time. See logs for more information.");
                return;
            }

            while (currentSize > maxStorageCapacity) {
                for (final EventStorePartition partition : getPartitions()) {
                    try {
                        final long removed = partition.purgeOldestEvents();
                        currentSize -= removed;
                    } catch (final Exception e) {
                        logger.error("Failed to purge oldest events from {}", partition, e);
                        eventReporter.reportEvent(Severity.WARNING, EVENT_CATEGORY,
                            "Failed to purge oldest events from Provenance Repository. See logs for more information.");
                    }
                }
            }
        } catch (final Exception e) {
            logger.error("Failed to perform periodic maintenance", e);
            eventReporter.reportEvent(Severity.ERROR, EVENT_CATEGORY,
                "Failed to perform periodic maintenace for Provenance Repository. See logs for more information.");
        }
    }

    protected abstract List<? extends EventStorePartition> getPartitions();
}
