/*
 * 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.syncope.core.provisioning.java.data;

import java.util.stream.Collectors;
import org.apache.syncope.core.provisioning.api.data.SchemaDataBinder;
import org.apache.commons.lang3.StringUtils;
import org.apache.syncope.common.lib.SyncopeClientCompositeException;
import org.apache.syncope.common.lib.SyncopeClientException;
import org.apache.syncope.common.lib.to.DerSchemaTO;
import org.apache.syncope.common.lib.to.PlainSchemaTO;
import org.apache.syncope.common.lib.to.SchemaTO;
import org.apache.syncope.common.lib.to.VirSchemaTO;
import org.apache.syncope.common.lib.types.AnyTypeKind;
import org.apache.syncope.common.lib.types.ClientExceptionType;
import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
import org.apache.syncope.core.persistence.api.entity.DerSchema;
import org.apache.syncope.core.persistence.api.entity.PlainSchema;
import org.apache.syncope.core.persistence.api.entity.VirSchema;
import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
import org.apache.syncope.core.persistence.api.dao.AnyTypeClassDAO;
import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO;
import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
import org.apache.syncope.core.persistence.api.dao.ImplementationDAO;
import org.apache.syncope.core.persistence.api.dao.NotFoundException;
import org.apache.syncope.core.persistence.api.dao.VirSchemaDAO;
import org.apache.syncope.core.persistence.api.entity.AnyType;
import org.apache.syncope.core.persistence.api.entity.AnyTypeClass;
import org.apache.syncope.core.persistence.api.entity.AnyUtils;
import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
import org.apache.syncope.core.persistence.api.entity.EntityFactory;
import org.apache.syncope.core.persistence.api.entity.Implementation;
import org.apache.syncope.core.persistence.api.entity.Schema;
import org.apache.syncope.core.persistence.api.entity.SchemaLabel;
import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
import org.apache.syncope.core.persistence.api.entity.resource.Provision;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class SchemaDataBinderImpl implements SchemaDataBinder {

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

    @Autowired
    private AnyTypeClassDAO anyTypeClassDAO;

    @Autowired
    private PlainSchemaDAO plainSchemaDAO;

    @Autowired
    private DerSchemaDAO derSchemaDAO;

    @Autowired
    private VirSchemaDAO virSchemaDAO;

    @Autowired
    private ExternalResourceDAO resourceDAO;

    @Autowired
    private AnyTypeDAO anyTypeDAO;

    @Autowired
    private ImplementationDAO implementationDAO;

    @Autowired
    private EntityFactory entityFactory;

    @Autowired
    private AnyUtilsFactory anyUtilsFactory;

    private <S extends Schema, T extends SchemaTO> void labels(final T src, final S dst) {
        src.getLabels().forEach((locale, display) -> {
            SchemaLabel label = dst.getLabel(SchemaTO.toLocale(locale)).orElse(null);
            if (label == null) {
                label = entityFactory.newEntity(SchemaLabel.class);
                label.setLocale(SchemaTO.toLocale(locale));
                label.setSchema(dst);
                dst.add(label);
            }
            label.setDisplay(display);
        });

        dst.getLabels().removeIf(label -> !src.getLabels().containsKey(SchemaTO.toString(label.getLocale())));
    }

    private <S extends Schema, T extends SchemaTO> void labels(final S src, final T dst) {
        dst.getLabels().putAll(src.getLabels().stream().
                collect(Collectors.toMap(label -> SchemaTO.toString(label.getLocale()), SchemaLabel::getDisplay)));
    }

    // --------------- PLAIN -----------------
    private PlainSchema fill(final PlainSchema schema, final PlainSchemaTO schemaTO) {
        if (!JexlUtils.isExpressionValid(schemaTO.getMandatoryCondition())) {
            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidValues);
            sce.getElements().add(schemaTO.getMandatoryCondition());
            throw sce;
        }

        schema.setKey(schemaTO.getKey());
        schema.setType(schemaTO.getType());
        schema.setCipherAlgorithm(schemaTO.getCipherAlgorithm());
        schema.setConversionPattern(schemaTO.getConversionPattern());
        schema.setEnumerationKeys(schemaTO.getEnumerationKeys());
        schema.setEnumerationValues(schemaTO.getEnumerationValues());
        schema.setMandatoryCondition(schemaTO.getMandatoryCondition());
        schema.setMimeType(schemaTO.getMimeType());
        schema.setMultivalue(schemaTO.isMultivalue());
        schema.setReadonly(schemaTO.isReadonly());
        schema.setSecretKey(schemaTO.getSecretKey());
        schema.setUniqueConstraint(schemaTO.isUniqueConstraint());

        labels(schemaTO, schema);

        if (schemaTO.getValidator() == null) {
            schema.setValidator(null);
        } else {
            Implementation validator = implementationDAO.find(schemaTO.getValidator());
            if (validator == null) {
                LOG.debug("Invalid " + Implementation.class.getSimpleName() + " {}, ignoring...",
                        schemaTO.getValidator());
            } else {
                schema.setValidator(validator);
            }
        }

        PlainSchema merged = plainSchemaDAO.save(schema);

        if (schemaTO.getAnyTypeClass() != null
                && (merged.getAnyTypeClass() == null
                || !schemaTO.getAnyTypeClass().equals(merged.getAnyTypeClass().getKey()))) {

            AnyTypeClass anyTypeClass = anyTypeClassDAO.find(schemaTO.getAnyTypeClass());
            if (anyTypeClass == null) {
                LOG.debug("Invalid " + AnyTypeClass.class.getSimpleName()
                        + "{}, ignoring...", schemaTO.getAnyTypeClass());
            } else {
                anyTypeClass.add(merged);
                merged.setAnyTypeClass(anyTypeClass);
            }
        } else if (schemaTO.getAnyTypeClass() == null && merged.getAnyTypeClass() != null) {
            merged.getAnyTypeClass().getPlainSchemas().remove(merged);
            merged.setAnyTypeClass(null);
        }

        return merged;
    }

    @Override
    public PlainSchema create(final PlainSchemaTO schemaTO) {
        return fill(entityFactory.newEntity(PlainSchema.class), schemaTO);
    }

    @Override
    public PlainSchema update(final PlainSchemaTO schemaTO, final PlainSchema schema) {
        SyncopeClientCompositeException scce = SyncopeClientException.buildComposite();

        boolean hasAttrs = false;
        for (AnyTypeKind anyTypeKind : AnyTypeKind.values()) {
            AnyUtils anyUtils = anyUtilsFactory.getInstance(anyTypeKind);
            hasAttrs |= plainSchemaDAO.hasAttrs(schema, anyUtils.plainAttrClass());
        }

        if (hasAttrs) {
            if (schema.getType() != schemaTO.getType()) {
                SyncopeClientException e = SyncopeClientException.build(ClientExceptionType.InvalidPlainSchema);
                e.getElements().add("Cannot change type since " + schema.getKey() + " has attributes");

                scce.addException(e);
            }
            if (schema.isUniqueConstraint() != schemaTO.isUniqueConstraint()) {
                SyncopeClientException e = SyncopeClientException.build(ClientExceptionType.InvalidPlainSchema);
                e.getElements().add("Cannot alter unique contraint since " + schema.getKey() + " has attributes");

                scce.addException(e);
            }
        }

        if (scce.hasExceptions()) {
            throw scce;
        }

        return fill(schema, schemaTO);
    }

    @Override
    public PlainSchemaTO getPlainSchemaTO(final String key) {
        PlainSchema schema = plainSchemaDAO.find(key);
        if (schema == null) {
            throw new NotFoundException("Schema '" + key + "'");
        }

        PlainSchemaTO schemaTO = new PlainSchemaTO();
        schemaTO.setKey(schema.getKey());
        schemaTO.setType(schema.getType());
        schemaTO.setCipherAlgorithm(schema.getCipherAlgorithm());
        schemaTO.setConversionPattern(schema.getConversionPattern());
        schemaTO.setEnumerationKeys(schema.getEnumerationKeys());
        schemaTO.setEnumerationValues(schema.getEnumerationValues());
        schemaTO.setMandatoryCondition(schema.getMandatoryCondition());
        schemaTO.setMimeType(schema.getMimeType());
        schemaTO.setMultivalue(schema.isMultivalue());
        schemaTO.setReadonly(schema.isReadonly());
        schemaTO.setSecretKey(schema.getSecretKey());
        schemaTO.setUniqueConstraint(schema.isUniqueConstraint());

        labels(schema, schemaTO);

        schemaTO.setAnyTypeClass(schema.getAnyTypeClass() == null ? null : schema.getAnyTypeClass().getKey());
        if (schema.getValidator() != null) {
            schemaTO.setValidator(schema.getValidator().getKey());
        }

        return schemaTO;
    }

    // --------------- DERIVED -----------------
    private DerSchema fill(final DerSchema schema, final DerSchemaTO schemaTO) {
        SyncopeClientCompositeException scce = SyncopeClientException.buildComposite();

        if (StringUtils.isBlank(schemaTO.getExpression())) {
            SyncopeClientException requiredValuesMissing =
                    SyncopeClientException.build(ClientExceptionType.RequiredValuesMissing);
            requiredValuesMissing.getElements().add("expression");

            scce.addException(requiredValuesMissing);
        } else if (!JexlUtils.isExpressionValid(schemaTO.getExpression())) {
            SyncopeClientException e = SyncopeClientException.build(ClientExceptionType.InvalidValues);
            e.getElements().add(schemaTO.getExpression());

            scce.addException(e);
        }

        if (scce.hasExceptions()) {
            throw scce;
        }

        schema.setKey(schemaTO.getKey());
        schema.setExpression(schemaTO.getExpression());

        labels(schemaTO, schema);

        DerSchema merged = derSchemaDAO.save(schema);

        if (schemaTO.getAnyTypeClass() != null
                && (merged.getAnyTypeClass() == null
                || !schemaTO.getAnyTypeClass().equals(merged.getAnyTypeClass().getKey()))) {

            AnyTypeClass anyTypeClass = anyTypeClassDAO.find(schemaTO.getAnyTypeClass());
            if (anyTypeClass == null) {
                LOG.debug("Invalid " + AnyTypeClass.class.getSimpleName()
                        + "{}, ignoring...", schemaTO.getAnyTypeClass());
            } else {
                anyTypeClass.add(merged);
                merged.setAnyTypeClass(anyTypeClass);
            }
        } else if (schemaTO.getAnyTypeClass() == null && merged.getAnyTypeClass() != null) {
            merged.getAnyTypeClass().getDerSchemas().remove(merged);
            merged.setAnyTypeClass(null);
        }

        return merged;
    }

    @Override
    public DerSchema create(final DerSchemaTO schemaTO) {
        return fill(entityFactory.newEntity(DerSchema.class), schemaTO);
    }

    @Override
    public DerSchema update(final DerSchemaTO schemaTO, final DerSchema schema) {
        return fill(schema, schemaTO);
    }

    @Override
    public DerSchemaTO getDerSchemaTO(final String key) {
        DerSchema schema = derSchemaDAO.find(key);
        if (schema == null) {
            throw new NotFoundException("Derived schema '" + key + "'");
        }

        DerSchemaTO schemaTO = new DerSchemaTO();
        schemaTO.setKey(schema.getKey());
        schemaTO.setExpression(schema.getExpression());

        labels(schema, schemaTO);

        schemaTO.setAnyTypeClass(schema.getAnyTypeClass() == null ? null : schema.getAnyTypeClass().getKey());

        return schemaTO;
    }

    // --------------- VIRTUAL -----------------
    private VirSchema fill(final VirSchema schema, final VirSchemaTO schemaTO) {
        schema.setKey(schemaTO.getKey());
        schema.setExtAttrName(schemaTO.getExtAttrName());
        schema.setReadonly(schemaTO.isReadonly());

        labels(schemaTO, schema);

        if (schemaTO.getAnyTypeClass() != null
                && (schema.getAnyTypeClass() == null
                || !schemaTO.getAnyTypeClass().equals(schema.getAnyTypeClass().getKey()))) {

            AnyTypeClass anyTypeClass = anyTypeClassDAO.find(schemaTO.getAnyTypeClass());
            if (anyTypeClass == null) {
                LOG.debug("Invalid " + AnyTypeClass.class.getSimpleName()
                        + "{}, ignoring...", schemaTO.getAnyTypeClass());
            } else {
                anyTypeClass.add(schema);
                schema.setAnyTypeClass(anyTypeClass);
            }
        } else if (schemaTO.getAnyTypeClass() == null && schema.getAnyTypeClass() != null) {
            schema.getAnyTypeClass().getVirSchemas().remove(schema);
            schema.setAnyTypeClass(null);
        }

        ExternalResource resource = resourceDAO.find(schemaTO.getResource());
        if (resource == null) {
            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidSchemaDefinition);
            sce.getElements().add("Resource " + schemaTO.getResource() + " not found");
            throw sce;
        }
        AnyType anyType = anyTypeDAO.find(schemaTO.getAnyType());
        if (anyType == null) {
            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidSchemaDefinition);
            sce.getElements().add("AnyType " + schemaTO.getAnyType() + " not found");
            throw sce;
        }
        Provision provision = resource.getProvision(anyType).orElse(null);
        if (provision == null) {
            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidSchemaDefinition);
            sce.getElements().add("Provision for AnyType" + schemaTO.getAnyType()
                    + " not found in " + schemaTO.getResource());
            throw sce;
        }
        schema.setProvision(provision);

        return virSchemaDAO.save(schema);
    }

    @Override
    public VirSchema create(final VirSchemaTO schemaTO) {
        return fill(entityFactory.newEntity(VirSchema.class), schemaTO);
    }

    @Override
    public VirSchema update(final VirSchemaTO schemaTO, final VirSchema schema) {
        return fill(schema, schemaTO);
    }

    @Override
    public VirSchemaTO getVirSchemaTO(final String key) {
        VirSchema schema = virSchemaDAO.find(key);
        if (schema == null) {
            throw new NotFoundException("Virtual Schema '" + key + "'");
        }

        VirSchemaTO schemaTO = new VirSchemaTO();
        schemaTO.setKey(schema.getKey());
        schemaTO.setExtAttrName(schema.getExtAttrName());
        schemaTO.setReadonly(schema.isReadonly());

        labels(schema, schemaTO);

        schemaTO.setAnyTypeClass(schema.getAnyTypeClass() == null ? null : schema.getAnyTypeClass().getKey());
        schemaTO.setResource(schema.getProvision().getResource().getKey());
        schemaTO.setAnyType(schema.getProvision().getAnyType().getKey());

        return schemaTO;
    }
}
