/*
 * 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.jackrabbit.oak.plugins.index.elastic;

import java.io.IOException;

import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.plugins.index.AsyncIndexInfo;
import org.apache.jackrabbit.oak.plugins.index.AsyncIndexInfoService;
import org.apache.jackrabbit.oak.plugins.index.IndexInfo;
import org.apache.jackrabbit.oak.plugins.index.IndexInfoProvider;
import org.apache.jackrabbit.oak.plugins.index.IndexUtils;
import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.oak.spi.state.NodeStateUtils;
import org.apache.jackrabbit.oak.spi.state.NodeStore;
import org.apache.jackrabbit.util.ISO8601;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import co.elastic.clients.elasticsearch._types.HealthStatus;
import co.elastic.clients.elasticsearch._types.Level;
import co.elastic.clients.elasticsearch.cluster.HealthRequest;
import co.elastic.clients.elasticsearch.cluster.HealthResponse;

class ElasticIndexInfoProvider implements IndexInfoProvider {

    private final NodeStore nodeStore;

    private final ElasticIndexTracker indexTracker;

    private final AsyncIndexInfoService asyncIndexInfoService;

    ElasticIndexInfoProvider(@NotNull NodeStore nodeStore,
                             @NotNull ElasticIndexTracker indexTracker,
                             @NotNull AsyncIndexInfoService asyncIndexInfoService) {
        this.nodeStore = nodeStore;
        this.indexTracker = indexTracker;
        this.asyncIndexInfoService = asyncIndexInfoService;
    }

    @Override
    public String getType() {
        return ElasticIndexDefinition.TYPE_ELASTICSEARCH;
    }

    @Override
    public @Nullable IndexInfo getInfo(String indexPath) {
        ElasticIndexNode node = indexTracker.acquireIndexNode(indexPath);
        try {
            NodeState idxState = NodeStateUtils.getNode(nodeStore.getRoot(), indexPath);
            String asyncName = IndexUtils.getAsyncLaneName(idxState, indexPath);
            AsyncIndexInfo asyncInfo = asyncIndexInfoService.getInfo(asyncName);
            boolean hasHiddenOakMount = false;
            for (String c : idxState.getChildNodeNames()) {
                if (c.startsWith(IndexDefinition.HIDDEN_OAK_MOUNT_PREFIX)) {
                    hasHiddenOakMount = true;
                    break;
                }
            }

            return new ElasticIndexInfo(
                    indexPath,
                    asyncName,
                    asyncInfo != null ? asyncInfo.getLastIndexedTo() : -1L,
                    getStatusTimestamp(idxState, IndexDefinition.STATUS_LAST_UPDATED),
                    node.getIndexStatistics().numDocs(),
                    node.getIndexStatistics().primaryStoreSize(),
                    node.getIndexStatistics().creationDate(),
                    getStatusTimestamp(idxState, IndexDefinition.REINDEX_COMPLETION_TIMESTAMP),
                    hasHiddenOakMount
            );
        } finally {
            node.release();
        }
    }

    @Override
    public boolean isValid(String indexPath) throws IOException {
        ElasticIndexNode node = indexTracker.acquireIndexNode(indexPath);
        try {
            HealthResponse response = node.getConnection().getClient().cluster()
                    .health(HealthRequest.of(hrb -> hrb
                            .index(node.getDefinition().getIndexAlias())
                            .level(Level.Indices)));
            return response.indices().values().stream().map(i -> i.status() == HealthStatus.Green).findFirst().orElse(false);
        } finally {
            node.release();
        }
    }

    private long getStatusTimestamp(NodeState idxState, String propName) {
        NodeState status = idxState.getChildNode(IndexDefinition.STATUS_NODE);
        if (status.exists()) {
            PropertyState updatedTime = status.getProperty(propName);
            if (updatedTime != null) {
                return ISO8601.parse(updatedTime.getValue(Type.DATE)).getTimeInMillis();
            }
        }
        return -1;
    }

    private static class ElasticIndexInfo implements IndexInfo {

        private final String indexPath;
        private final String asyncLane;
        private final long indexedUpToTime;
        private final long lastUpdatedTime;
        private final long estimatedEntryCount;
        private final long sizeInBytes;
        private final long creationTimestamp;
        private final long reindexCompletionTimestamp;
        private final boolean hasHiddenOakLibsMount;

        ElasticIndexInfo(String indexPath, String asyncLane, long indexedUpToTime, long lastUpdatedTime,
                         long estimatedEntryCount, long sizeInBytes, long creationTimestamp, long reindexCompletionTimestamp,
                         boolean hasHiddenOakLibsMount) {
            this.indexPath = indexPath;
            this.asyncLane = asyncLane;
            this.indexedUpToTime = indexedUpToTime;
            this.lastUpdatedTime = lastUpdatedTime;
            this.estimatedEntryCount = estimatedEntryCount;
            this.sizeInBytes = sizeInBytes;
            this.creationTimestamp = creationTimestamp;
            this.reindexCompletionTimestamp = reindexCompletionTimestamp;
            this.hasHiddenOakLibsMount = hasHiddenOakLibsMount;
        }

        @Override
        public String getIndexPath() {
            return indexPath;
        }

        @Override
        public String getType() {
            return ElasticIndexDefinition.TYPE_ELASTICSEARCH;
        }

        @Override
        public @Nullable String getAsyncLaneName() {
            return asyncLane;
        }

        @Override
        public long getLastUpdatedTime() {
            return lastUpdatedTime;
        }

        @Override
        public long getIndexedUpToTime() {
            return indexedUpToTime;
        }

        @Override
        public long getEstimatedEntryCount() {
            return estimatedEntryCount;
        }

        @Override
        public long getSizeInBytes() {
            return sizeInBytes;
        }

        @Override
        public boolean hasIndexDefinitionChangedWithoutReindexing() {
            // not available in elastic, stored index definitions not supported
            return false;
        }

        @Override
        public @Nullable String getIndexDefinitionDiff() {
            // not available in elastic, stored index definitions not supported
            return null;
        }

        @Override
        public boolean hasHiddenOakLibsMount() {
            return hasHiddenOakLibsMount;
        }

        @Override
        public boolean hasPropertyIndexNode() {
            // not available in elastic
            return false;
        }

        @Override
        public long getSuggestSizeInBytes() {
            // not available in elastic
            return -1;
        }

        @Override
        public long getCreationTimestamp() {
            return creationTimestamp;
        }

        @Override
        public long getReindexCompletionTimestamp() {
            return reindexCompletionTimestamp;
        }

        @Override
        public void setActive(boolean value) {
            // ignore
        }

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