/*-
 * ~~~~~~licensing~~~~~~
 * atlasmap-entaxy-services
 * ==========
 * Copyright (C) 2020 - 2025 EmDev LLC
 * ==========
 * You may not use this file except in accordance with the License Terms of the Copyright
 * Holder located at: https://entaxy.ru/eula . All copyrights, all intellectual property
 * rights to the Software and any copies are the property of the Copyright Holder. Unless
 * it is explicitly allowed the Copyright Holder, the User is prohibited from using the
 * Software for commercial purposes to provide services to third parties.
 * 
 * The Copyright Holder hereby declares that the Software is provided on an "AS IS".
 * Under no circumstances does the Copyright Holder guarantee or promise that the
 * Software provided by him will be suitable or not suitable for the specific purposes
 * of the User, that the Software will meet all commercial and personal subjective
 * expectations of the User, that the Software will work properly, without technical
 * errors, quickly and uninterruptedly.
 * 
 * Under no circumstances shall the Copyright Holder or its Affiliates is not liable
 * to the User for any direct or indirect losses of the User, his expenses or actual
 * damage, including, downtime; loss of bussines; lost profit; lost earnings; loss
 * or damage to data, property, etc.
 * ~~~~~~/licensing~~~~~~
 */
/*
 * Copyright (C) 2017 Red Hat, Inc.
 *
 * 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 io.atlasmap.json.core;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;

import io.atlasmap.api.AtlasConversionException;
import io.atlasmap.api.AtlasException;
import io.atlasmap.core.AtlasPath;
import io.atlasmap.core.AtlasPath.SegmentContext;
import io.atlasmap.core.AtlasUtil;
import io.atlasmap.json.v2.AtlasJsonModelFactory;
import io.atlasmap.json.v2.JsonEnumField;
import io.atlasmap.json.v2.JsonField;
import io.atlasmap.spi.AtlasConversionService;
import io.atlasmap.spi.AtlasFieldReader;
import io.atlasmap.spi.AtlasInternalSession;
import io.atlasmap.v2.AtlasModelFactory;
import io.atlasmap.v2.AuditStatus;
import io.atlasmap.v2.CollectionType;
import io.atlasmap.v2.Field;
import io.atlasmap.v2.FieldGroup;
import io.atlasmap.v2.FieldStatus;
import io.atlasmap.v2.FieldType;

public class JsonFieldReader implements AtlasFieldReader {

    private static final Logger LOG = LoggerFactory.getLogger(JsonFieldReader.class);

    private AtlasConversionService conversionService;
    private JsonNode rootNode;

    @SuppressWarnings("unused")
    private JsonFieldReader() {
    }

    public JsonFieldReader(AtlasConversionService conversionService) {
        this.conversionService = conversionService;
    }

    @Override
    public Field read(AtlasInternalSession session) throws AtlasException {
        Field field = session.head().getSourceField();
        if (rootNode == null) {
            AtlasUtil.addAudit(session, field,
                String.format("Cannot read a field '%s' of JSON document '%s', document is null",
                    field.getPath(), field.getDocId()),
                AuditStatus.ERROR, null);
            return field;
        }

        AtlasPath path = new AtlasPath(field.getPath());

        List<Field> fields = getJsonFieldsForPath(session, rootNode, field, path, 0);
        if (path.hasCollection() && !path.isIndexedCollection()) {
            FieldGroup fieldGroup = AtlasModelFactory.createFieldGroupFrom(field, true);
            fieldGroup.getField().addAll(fields);
            if (fields.size() == 0) {
                fieldGroup.setStatus(FieldStatus.NOT_FOUND);
            }
            session.head().setSourceField(fieldGroup);
            return fieldGroup;
        } else if (fields.size() == 1) {
            field.setValue(fields.get(0).getValue());
            return field;
        } else {
            if (fields.size() == 0) {
                field.setStatus(FieldStatus.NOT_FOUND);
            }
            return field;
        }
    }

    private List<Field> getJsonFieldsForPath(AtlasInternalSession session, JsonNode node, Field field, AtlasPath path, int depth) throws AtlasException {
        List<Field> fields = new ArrayList<>();
        List<SegmentContext> segments = path.getSegments(true);
        if (segments.size() < depth) {
            throw new AtlasException(String.format("depth '%s' exceeds segment size '%s'", depth, segments.size()));
        }
        if (segments.size() == depth) {
            //if traversed the entire path and found value
            if (field.getFieldType() == FieldType.COMPLEX && !node.isValueNode()) {
                FieldGroup group = (FieldGroup) field;
                populateChildFields(session, node, group, path);
                fields.add(group);
            } else {
                JsonField jsonField = new JsonField();
                AtlasModelFactory.copyField(field, jsonField, true);
                if (field instanceof JsonEnumField && field.getFieldType() == FieldType.COMPLEX) {
                    jsonField.setFieldType(FieldType.STRING); // enum has COMPLEX by default
                }
                Object value = handleValueNode(session, node, jsonField);
                jsonField.setValue(value);
                jsonField.setIndex(null); //reset index for subfields
                fields.add(jsonField);
            }
            return fields;
        }

        // segments.size() > depth
        SegmentContext segmentContext;
        JsonNode child;
        if (depth == 0 && path.hasCollectionRoot()) {
            child = node; //if root is a collection
            segmentContext = segments.get(depth);
        } else {
            if (depth == 0) {
                if (node.size() == 1 && !path.getSegments(false).get(0).getExpression().startsWith(rootNode.fieldNames().next())) {
                    //peel off a rooted object, i.e. mapping /orderId works for document { source: { orderId: 123 } }
                    node = node.elements().next();
                }

                if (segments.size() > 1) {
                    depth = 1; //skip the root, if not a collection
                }
            }
            segmentContext = segments.get(depth);
            String fieldName = segmentContext.getName();
            child = node.get(fieldName);
        }
        if (child == null) {
            return fields;
        }

        if (segmentContext.getCollectionType() == CollectionType.NONE) {
            List<Field> childFields = getJsonFieldsForPath(session, child, field, path, depth + 1);
            fields.addAll(childFields);
            return fields;
        }

        // collection
        if (segmentContext.getCollectionIndex() != null) {
            if (child.size() <= segmentContext.getCollectionIndex()) {
                //index out of range
                return fields;
            }
            List<Field> arrayFields = getJsonFieldsForPath(session, child.get(segmentContext.getCollectionIndex()), field, path, depth + 1);
            fields.addAll(arrayFields);
        } else {
            //if index not included, iterate over all
            for (int i=0; i<child.size(); i++) {
                Field itemField;
                if (field instanceof FieldGroup) {
                    itemField = AtlasJsonModelFactory.cloneFieldGroup((FieldGroup)field);
                    AtlasPath.setCollectionIndexRecursively((FieldGroup)itemField, depth, i);
                 } else {
                    itemField = AtlasJsonModelFactory.cloneField((JsonField)field, false);
                    AtlasPath itemPath = new AtlasPath(field.getPath());
                    itemPath.setCollectionIndex(depth, i);
                    itemField.setPath(itemPath.toString());
                 }
                List<Field> arrayFields = getJsonFieldsForPath(
                    session, child.get(i), itemField, new AtlasPath(itemField.getPath()), depth + 1);
                fields.addAll(arrayFields);
            }
        }
        return fields;
    }

    private void populateChildFields(AtlasInternalSession session, JsonNode node, FieldGroup fieldGroup, AtlasPath path)
     throws AtlasException {
        List<Field> newChildren = new ArrayList<>();
        for (Field child : fieldGroup.getField()) {
            AtlasPath childPath = new AtlasPath(child.getPath());
            JsonNode childNode = node.get(childPath.getLastSegment().getName());
            if (childPath.getLastSegment().getCollectionType() != CollectionType.NONE) {
                FieldGroup childGroup = populateCollectionItems(session, (ArrayNode)childNode, child);
                newChildren.add(childGroup);
            } else {
                if (child instanceof FieldGroup) {
                    populateChildFields(session, childNode, (FieldGroup)child, childPath);
                } else {
                    Object value = handleValueNode(session, childNode, (JsonField)child);
                    child.setValue(value);
                }
                newChildren.add(child);
            }
        }
        fieldGroup.getField().clear();
        fieldGroup.getField().addAll(newChildren);
    }

    private FieldGroup populateCollectionItems(AtlasInternalSession session, JsonNode node, Field field)
     throws AtlasException {
        if (!node.isArray()) {
            throw new AtlasException(String.format("Couldn't find JSON array for field %s:%s",
                field.getDocId(), field.getPath()));
        }
        FieldGroup group = field instanceof FieldGroup ?
         (FieldGroup)field : AtlasModelFactory.createFieldGroupFrom(field, true);
        ArrayNode arrayNode = (ArrayNode)node;
        for (int i=0; i<arrayNode.size(); i++) {
            AtlasPath itemPath = new AtlasPath(group.getPath());
            List<SegmentContext> segments = itemPath.getSegments(true);
            itemPath.setCollectionIndex(segments.size() - 1, i);
            if (field instanceof FieldGroup) {
                FieldGroup itemGroup = AtlasJsonModelFactory.cloneFieldGroup((FieldGroup)field);
                AtlasPath.setCollectionIndexRecursively(itemGroup, segments.size(), i);
                populateChildFields(session, arrayNode.get(i), itemGroup, itemPath);
                group.getField().add(itemGroup);
            } else {
                JsonField itemField = AtlasJsonModelFactory.cloneField((JsonField)field, false);
                itemField.setPath(itemPath.toString());
                Object value = handleValueNode(session, arrayNode.get(i), itemField);
                itemField.setValue(value);
                group.getField().add(itemField);
            }
        }
        return group;
    }

    private Object handleValueNode(AtlasInternalSession session, JsonNode valueNode, JsonField jsonField) throws AtlasException {
        if (valueNode.isNull()) {
            return null;
            // we can't detect field type if it's null node
        }
        if (valueNode.isObject()) {
            jsonField.setFieldType(FieldType.COMPLEX);
            return null;
        }
        if (valueNode.isArray()) {
            AtlasUtil.addAudit(session, jsonField,
                    String.format("Unexpected array node is detected: '%s'", valueNode.asText()),
                    AuditStatus.ERROR, valueNode.asText());
            return null;
        }

        if (jsonField.getFieldType() != null) { // mapping is overriding the fieldType
            try {
                return conversionService.convertType(valueNode.asText(), jsonField.getFormat(),
                        jsonField.getFieldType(), null);
            } catch (AtlasConversionException e) {
                AtlasUtil.addAudit(session, jsonField,
                        String.format("Failed to convert field value '%s' into type '%s'", valueNode.asText(),
                                jsonField.getFieldType()),
                        AuditStatus.ERROR, valueNode.asText());
                return null;
            }
        }

        if (valueNode.isTextual()) {
            return handleTextualNode(valueNode, jsonField);
        } else if (valueNode.isNumber()) {
            return handleNumberNode(valueNode, jsonField);
        } else if (valueNode.isBoolean()) {
            return handleBooleanNode(valueNode, jsonField);
        } else if (valueNode.isContainerNode()) {
            return handleContainerNode(valueNode, jsonField);
        } else if (valueNode.isNull()) {
            return null;

        } else {
            LOG.warn(String.format("Detected unsupported json type for field p=%s docId=%s",
                    jsonField.getPath(), jsonField.getDocId()));
            jsonField.setFieldType(FieldType.UNSUPPORTED);
            return valueNode.toString();
        }
    }

    private Object handleTextualNode(JsonNode valueNode, JsonField jsonField) {
        if (jsonField.getFieldType() == null || FieldType.STRING.equals(jsonField.getFieldType())) {
            jsonField.setFieldType(FieldType.STRING);
            return valueNode.textValue();
        }
        if (FieldType.CHAR.equals(jsonField.getFieldType())) {
            return valueNode.textValue().charAt(0);
        }
        LOG.warn(String.format("Unsupported FieldType for text data t=%s p=%s docId=%s",
                jsonField.getFieldType().value(), jsonField.getPath(), jsonField.getDocId()));
        return valueNode.textValue();
    }

    private Object handleNumberNode(JsonNode valueNode, JsonField jsonField) {
        if (valueNode.isInt()) {
            jsonField.setFieldType(FieldType.INTEGER);
            return valueNode.intValue();
        } else if (valueNode.isDouble()) {
            jsonField.setFieldType(FieldType.DOUBLE);
            return valueNode.doubleValue();
        } else if (valueNode.isBigDecimal()) {
            jsonField.setFieldType(FieldType.DECIMAL);
            return valueNode.decimalValue();
        } else if (valueNode.isFloat()) {
            jsonField.setFieldType(FieldType.DOUBLE);
            return valueNode.floatValue();
        } else if (valueNode.isLong()) {
            jsonField.setFieldType(FieldType.LONG);
            return valueNode.longValue();
        } else if (valueNode.isShort()) {
            jsonField.setFieldType(FieldType.SHORT);
            return valueNode.shortValue();
        } else if (valueNode.isBigInteger()) {
            jsonField.setFieldType(FieldType.BIG_INTEGER);
            return valueNode.bigIntegerValue();
        } else {
            jsonField.setFieldType(FieldType.NUMBER);
            return valueNode.numberValue();
        }
    }

    private Object handleBooleanNode(JsonNode valueNode, JsonField jsonField) {
        jsonField.setFieldType(FieldType.BOOLEAN);
        return valueNode.booleanValue();
    }

    private Object handleContainerNode(JsonNode valueNode, JsonField jsonField) throws AtlasException {
        if (valueNode.isArray()) {
            if (LOG.isDebugEnabled()) {
                LOG.debug(
                        String.format("Detected json array p=%s docId=%s", jsonField.getPath(), jsonField.getDocId()));
            }
            jsonField.setFieldType(FieldType.COMPLEX);
            jsonField.setCollectionType(CollectionType.ARRAY);
            return valueNode.toString();
        }
        if (valueNode.isObject()) {
            if (LOG.isDebugEnabled()) {
                LOG.debug(String.format("Detected json complex object p=%s docId=%s", jsonField.getPath(),
                        jsonField.getDocId()));
            }
            jsonField.setFieldType(FieldType.COMPLEX);
            return valueNode.toString();
        }
        throw new AtlasException("Unknown error: detected a container JSON node which is not ARRAY nor OBJECT");
    }

    public void setDocument(String document) throws AtlasException {
        if (document == null || document.isEmpty()) {
            this.rootNode = null;
            return;
        }

        try {
            JsonFactory factory = new JsonFactory();
            ObjectMapper mapper = new ObjectMapper();
            //TODO Return as Jackson 2.11 comes into play
            //    .enable(MapperFeature.BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES);
            JsonParser parser = factory.createParser(document);
            this.rootNode = mapper.readTree(parser);
        } catch (Exception e) {
            throw new AtlasException(e);
        }
    }

}
