// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.spring.data.cosmos.repository.query;

import com.azure.spring.data.cosmos.core.CosmosOperations;
import com.azure.spring.data.cosmos.core.query.CosmosPageRequest;
import com.azure.spring.data.cosmos.core.query.CosmosQuery;
import com.azure.spring.data.cosmos.exception.CosmosAccessException;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.query.ReturnedType;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
 * Interface to execute cosmos query operations
 */
public interface CosmosQueryExecution {

    /**
     * Declare an execute function for different operations to call
     *
     * @param query document query operation
     * @param type domain type
     * @param container container to conduct query
     * @return Object according to execution result
     */
    Object execute(CosmosQuery query, Class<?> type, String container);

    /**
     * Container operation implementation to execute a container name query
     */
    final class ContainerExecution implements CosmosQueryExecution {

        private final CosmosOperations operations;

        /**
         * Creates a new instance of container execution.
         *
         * @param operations the Cosmos operations
         */
        public ContainerExecution(CosmosOperations operations) {
            this.operations = operations;
        }

        @Override
        public Object execute(CosmosQuery query, Class<?> type, String container) {
            return operations.getContainerName(type);
        }
    }

    /**
     * Find operation implementation to execute a find query for multiple items
     */
    final class MultiEntityExecution implements CosmosQueryExecution {

        private final CosmosOperations operations;

        /**
         * Creates a new instance of multi-entity execution.
         *
         * @param operations the Cosmos operations
         */
        public MultiEntityExecution(CosmosOperations operations) {
            this.operations = operations;
        }

        @Override
        public Object execute(CosmosQuery query, Class<?> type, String container) {
            return operations.find(query, type, container);
        }
    }

    /**
     * Find operation implementation to execute a find query for a single item
     */
    final class SingleEntityExecution implements CosmosQueryExecution {

        private final CosmosOperations operations;
        private final ReturnedType returnedType;

        /**
         * Creates a new instance of single-entity execution.
         *
         * @param operations the Cosmos operations
         * @param returnedType the returned type
         */
        public SingleEntityExecution(CosmosOperations operations, ReturnedType returnedType) {
            this.operations = operations;
            this.returnedType = returnedType;
        }

        @Override
        public Object execute(CosmosQuery query, Class<?> type, String collection) {
            Iterable<?> resultsIterable = operations.find(query, type, collection);
            final List<Object> results = new ArrayList<>();
            resultsIterable.forEach(results::add);
            final Object result;
            if (results.isEmpty()) {
                result = null;
            } else if (results.size() == 1) {
                result = results.get(0);
            } else {
                throw new CosmosAccessException("Too many results - return type "
                    + returnedType.getReturnedType()
                    + " is not of type Iterable but find returned "
                    + results.size()
                    + " results");
            }

            if (returnedType.getReturnedType() == Optional.class) {
                return result == null ? Optional.empty() : Optional.of(result);
            } else {
                return result;
            }
        }
    }

    /**
     * exist operation implementation to execute a exists query
     */
    final class ExistsExecution implements CosmosQueryExecution {

        private final CosmosOperations operations;

        /**
         * Creates a new instance of exists execution.
         *
         * @param operations the Cosmos operations
         */
        public ExistsExecution(CosmosOperations operations) {
            this.operations = operations;
        }

        @Override
        public Object execute(CosmosQuery query, Class<?> type, String container) {
            return operations.exists(query, type, container);
        }
    }

    /**
     * delete operation implementation to execute a delete query
     */
    final class DeleteExecution implements CosmosQueryExecution {

        private final CosmosOperations operations;

        /**
         * Creates a new instance of delete execution.
         *
         * @param operations the Cosmos operations
         */
        public DeleteExecution(CosmosOperations operations) {
            this.operations = operations;
        }

        @Override
        public Object execute(CosmosQuery query, Class<?> type, String container) {
            return operations.delete(query, type, container);
        }
    }

    /**
     * paginationQuery operation implementation to execute a paginationQuery query
     */
    final class PagedExecution implements CosmosQueryExecution {
        private final CosmosOperations operations;
        private final Pageable pageable;

        /**
         * Creates a new instance of paged execution.
         *
         * @param operations the Cosmos operations
         * @param pageable the pageable
         */
        public PagedExecution(CosmosOperations operations, Pageable pageable) {
            this.operations = operations;
            this.pageable = pageable;
        }

        @Override
        public Object execute(CosmosQuery query, Class<?> type, String container) {
            if (pageable.getPageNumber() != 0
                && !(pageable instanceof CosmosPageRequest)) {
                throw new IllegalStateException("Not the first page but Pageable is not a valid "
                    + "CosmosPageRequest, requestContinuation is required for non first page request");
            }

            query.with(pageable);

            return operations.paginationQuery(query, type, container);
        }
    }

    /**
     * sliceQuery operation implementation to execute a sliceQuery query
     */
    final class SliceExecution implements CosmosQueryExecution {
        private final CosmosOperations operations;
        private final Pageable pageable;

        /**
         * Creates a new instance of slice execution.
         *
         * @param operations the Cosmos operations
         * @param pageable the pageable
         */
        public SliceExecution(CosmosOperations operations, Pageable pageable) {
            this.operations = operations;
            this.pageable = pageable;
        }

        @Override
        public Object execute(CosmosQuery query, Class<?> type, String container) {
            if (pageable.getPageNumber() != 0
                && !(pageable instanceof CosmosPageRequest)) {
                throw new IllegalStateException("Not the first page but Pageable is not a valid "
                    + "CosmosPageRequest, requestContinuation is required for non first page request");
            }

            query.with(pageable);

            return operations.sliceQuery(query, type, container);
        }
    }

    /**
     * count operation implementation to execute a count query
     */
    final class CountExecution implements CosmosQueryExecution {

        private final CosmosOperations operations;

        /**
         * Creates a new instance of count execution.
         *
         * @param operations the Cosmos operations
         */
        public CountExecution(CosmosOperations operations) {
            this.operations = operations;
        }

        @Override
        public Object execute(CosmosQuery query, Class<?> type, String container) {
            return operations.count(query, container);
        }
    }

}
