/**
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License. See LICENSE in the project root for
 * license information.
 */

package com.microsoft.azure.spring.data.cosmosdb.repository.support;

import com.azure.data.cosmos.ExcludedPath;
import com.azure.data.cosmos.IncludedPath;
import com.azure.data.cosmos.IndexingMode;
import com.azure.data.cosmos.IndexingPolicy;
import com.microsoft.azure.spring.data.cosmosdb.Constants;
import com.microsoft.azure.spring.data.cosmosdb.core.mapping.Document;
import com.microsoft.azure.spring.data.cosmosdb.core.mapping.DocumentIndexingPolicy;
import com.microsoft.azure.spring.data.cosmosdb.core.mapping.PartitionKey;
import org.apache.commons.lang3.reflect.FieldUtils;

import org.json.JSONObject;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Version;
import org.springframework.data.repository.core.support.AbstractEntityInformation;
import org.springframework.lang.NonNull;
import org.springframework.util.ReflectionUtils;

import static com.microsoft.azure.spring.data.cosmosdb.common.ExpressionResolver.resolveExpression;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;


public class CosmosEntityInformation<T, ID> extends AbstractEntityInformation<T, ID> {

    private static final String ETAG = "_etag";
    private Field id;
    private Field partitionKeyField;
    private String containerName;
    private Integer requestUnit;
    private Integer timeToLive;
    private IndexingPolicy indexingPolicy;
    private boolean isVersioned;
    private boolean autoCreateContainer;

    public CosmosEntityInformation(Class<T> domainType) {
        super(domainType);

        this.id = getIdField(domainType);
        ReflectionUtils.makeAccessible(this.id);

        this.containerName = getContainerName(domainType);
        this.partitionKeyField = getPartitionKeyField(domainType);
        if (this.partitionKeyField != null) {
            ReflectionUtils.makeAccessible(this.partitionKeyField);
        }

        this.requestUnit = getRequestUnit(domainType);
        this.timeToLive = getTimeToLive(domainType);
        this.indexingPolicy = getIndexingPolicy(domainType);
        this.isVersioned = getIsVersioned(domainType);
        this.autoCreateContainer = getIsAutoCreateContainer(domainType);
    }

    @SuppressWarnings("unchecked")
    public ID getId(T entity) {
        return (ID) ReflectionUtils.getField(id, entity);
    }

    public Field getIdField() {
        return this.id;
    }

    @SuppressWarnings("unchecked")
    public Class<ID> getIdType() {
        return (Class<ID>) id.getType();
    }

    @Deprecated
    public String getCollectionName() {
        return this.containerName;
    }

    public String getContainerName() {
        return this.containerName;
    }

    public Integer getRequestUnit() {
        return this.requestUnit;
    }

    public Integer getTimeToLive() {
        return this.timeToLive;
    }

    @NonNull
    public IndexingPolicy getIndexingPolicy() {
        return this.indexingPolicy;
    }

    public boolean isVersioned() {
        return isVersioned;
    }

    public String getPartitionKeyFieldName() {
        if (partitionKeyField == null) {
            return null;
        } else {
            final PartitionKey partitionKey = partitionKeyField.getAnnotation(PartitionKey.class);
            return partitionKey.value().equals("") ? partitionKeyField.getName() : partitionKey.value();
        }
    }

    public String getPartitionKeyFieldValue(T entity) {
        return partitionKeyField == null ? null : (String) ReflectionUtils.getField(partitionKeyField, entity);
    }

    @Deprecated
    public boolean isAutoCreateCollection() {
        return autoCreateContainer;
    }

    public boolean isAutoCreateContainer() {
        return autoCreateContainer;
    }

    private IndexingPolicy getIndexingPolicy(Class<?> domainType) {
        final IndexingPolicy policy = new IndexingPolicy();

        policy.automatic(this.getIndexingPolicyAutomatic(domainType));
        policy.indexingMode(this.getIndexingPolicyMode(domainType));
        policy.setIncludedPaths(this.getIndexingPolicyIncludePaths(domainType));
        policy.excludedPaths(this.getIndexingPolicyExcludePaths(domainType));

        return policy;
    }

    private Field getIdField(Class<?> domainType) {
        final Field idField;
        final List<Field> fields = FieldUtils.getFieldsListWithAnnotation(domainType, Id.class);

        if (fields.isEmpty()) {
            idField = ReflectionUtils.findField(getJavaType(), Constants.ID_PROPERTY_NAME);
        } else if (fields.size() == 1) {
            idField = fields.get(0);
        } else {
            throw new IllegalArgumentException("only one field with @Id annotation!");
        }

        if (idField == null) {
            throw new IllegalArgumentException("domain should contain @Id field or field named id");
        } else if (idField.getType() != String.class
                && idField.getType() != Integer.class && idField.getType() != int.class) {
            throw new IllegalArgumentException("type of id field must be String or Integer");
        }

        return idField;
    }

    private String getContainerName(Class<?> domainType) {
        String customContainerName = domainType.getSimpleName();

        final Document annotation = domainType.getAnnotation(Document.class);

        if (annotation != null && annotation.collection() != null && !annotation.collection().isEmpty()) {
            customContainerName = resolveExpression(annotation.collection());
        }

        return customContainerName;
    }

    private Field getPartitionKeyField(Class<?> domainType) {
        Field partitionKey = null;

        final List<Field> fields = FieldUtils.getFieldsListWithAnnotation(domainType, PartitionKey.class);

        if (fields.size() == 1) {
            partitionKey = fields.get(0);
        } else if (fields.size() > 1) {
            throw new IllegalArgumentException("Azure Cosmos DB supports only one partition key, " +
                    "only one field with @PartitionKey annotation!");
        }

        if (partitionKey != null && partitionKey.getType() != String.class) {
            throw new IllegalArgumentException("type of PartitionKey field must be String");
        }
        return partitionKey;
    }

    private Integer getRequestUnit(Class<?> domainType) {
        Integer ru = Integer.parseInt(Constants.DEFAULT_REQUEST_UNIT);
        final Document annotation = domainType.getAnnotation(Document.class);

        if (annotation != null && annotation.ru() != null && !annotation.ru().isEmpty()) {
            ru = Integer.parseInt(annotation.ru());
        }
        return ru;
    }

    private Integer getTimeToLive(Class<T> domainType) {
        Integer ttl = Constants.DEFAULT_TIME_TO_LIVE;
        final Document annotation = domainType.getAnnotation(Document.class);

        if (annotation != null) {
            ttl = annotation.timeToLive();
        }

        return ttl;
    }


    private Boolean getIndexingPolicyAutomatic(Class<?> domainType) {
        Boolean isAutomatic = Boolean.valueOf(Constants.DEFAULT_INDEXINGPOLICY_AUTOMATIC);
        final DocumentIndexingPolicy annotation = domainType.getAnnotation(DocumentIndexingPolicy.class);

        if (annotation != null) {
            isAutomatic = Boolean.valueOf(annotation.automatic());
        }

        return isAutomatic;
    }

    private IndexingMode getIndexingPolicyMode(Class<?> domainType) {
        IndexingMode mode = Constants.DEFAULT_INDEXINGPOLICY_MODE;
        final DocumentIndexingPolicy annotation = domainType.getAnnotation(DocumentIndexingPolicy.class);

        if (annotation != null) {
            mode = annotation.mode();
        }

        return mode;
    }

    private List<IncludedPath> getIndexingPolicyIncludePaths(Class<?> domainType) {
        final List<IncludedPath> pathArrayList = new ArrayList<>();
        final DocumentIndexingPolicy annotation = domainType.getAnnotation(DocumentIndexingPolicy.class);

        if (annotation == null || annotation.includePaths() == null || annotation.includePaths().length == 0) {
            return null; // Align the default value of IndexingPolicy
        }

        final String[] rawPaths = annotation.includePaths();

        for (final String path : rawPaths) {
            pathArrayList.add(new IncludedPath(path));
        }

        return pathArrayList;
    }

    private List<ExcludedPath> getIndexingPolicyExcludePaths(Class<?> domainType) {
        final List<ExcludedPath> pathArrayList = new ArrayList<>();
        final DocumentIndexingPolicy annotation = domainType.getAnnotation(DocumentIndexingPolicy.class);

        if (annotation == null || annotation.excludePaths().length == 0) {
            return null; // Align the default value of IndexingPolicy
        }

        final String[] rawPaths = annotation.excludePaths();
        for (final String path : rawPaths) {
            final JSONObject obj = new JSONObject(path);
            pathArrayList.add(new ExcludedPath().path(obj.get("path").toString()));
        }

        return pathArrayList;
    }

    private boolean getIsVersioned(Class<T> domainType) {
        final Field findField = ReflectionUtils.findField(domainType, ETAG);
        return findField != null 
                && findField.getType() == String.class
                && findField.isAnnotationPresent(Version.class);
    }

    private boolean getIsAutoCreateContainer(Class<T> domainType) {
        final Document annotation = domainType.getAnnotation(Document.class);

        boolean autoCreateContainer = Constants.DEFAULT_AUTO_CREATE_CONTAINER;
        if (annotation != null) {
            autoCreateContainer = annotation.autoCreateCollection();
        }

        return autoCreateContainer;
    }

}