/*
 * 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.client.console.wizards.any;

import java.io.Serializable;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.syncope.client.console.SyncopeConsoleSession;
import org.apache.syncope.client.console.SyncopeWebApplication;
import org.apache.syncope.client.console.layout.UserFormLayoutInfo;
import org.apache.syncope.client.ui.commons.layout.AbstractAnyFormLayout;
import org.apache.syncope.client.ui.commons.wizards.AjaxWizard;
import org.apache.syncope.client.ui.commons.wizards.any.AbstractAnyWizardBuilder;
import org.apache.syncope.client.ui.commons.wizards.any.AnyForm;
import org.apache.syncope.client.ui.commons.wizards.any.AnyWrapper;
import org.apache.syncope.client.ui.commons.wizards.any.UserWrapper;
import org.apache.syncope.common.lib.Attr;
import org.apache.syncope.common.lib.to.AnyObjectTO;
import org.apache.syncope.common.lib.to.AnyTO;
import org.apache.syncope.common.lib.to.GroupTO;
import org.apache.syncope.common.lib.to.GroupableRelatableTO;
import org.apache.syncope.common.lib.to.UserTO;
import org.apache.wicket.PageReference;
import org.apache.wicket.extensions.wizard.WizardModel;

public abstract class AnyWizardBuilder<A extends AnyTO> extends AbstractAnyWizardBuilder<A> {

    private static final long serialVersionUID = -2480279868319546243L;

    private static final List<String> NO_VALUES = List.of(StringUtils.EMPTY);

    @SuppressWarnings("unchecked")
    protected static <T extends AnyTO> AnyWrapper<T> wrapper(final T anyTO) {
        return (AnyWrapper<T>) (anyTO instanceof UserTO userTO
                ? new UserWrapper(userTO)
                : anyTO instanceof GroupTO groupTO
                        ? new GroupWrapper(groupTO)
                        : new AnyObjectWrapper((AnyObjectTO) anyTO));
    }

    protected final List<String> anyTypeClasses;

    protected AbstractAnyFormLayout<A, ? extends AnyForm<A>> formLayoutInfo;

    /**
     * Construct.
     *
     * @param anyTO any
     * @param anyTypeClasses any type classes
     * @param formLayoutInfo form layout info
     * @param pageRef caller page reference.
     */
    public AnyWizardBuilder(
            final A anyTO,
            final List<String> anyTypeClasses,
            final AbstractAnyFormLayout<A, ? extends AnyForm<A>> formLayoutInfo,
            final PageReference pageRef) {

        super(wrapper(anyTO), pageRef);
        this.anyTypeClasses = anyTypeClasses;
        this.formLayoutInfo = formLayoutInfo;
    }

    /**
     * Construct.
     *
     * @param wrapper any wrapper
     * @param anyTypeClasses any type classes
     * @param formLayoutInfo form layout info
     * @param pageRef caller page reference.
     */
    public AnyWizardBuilder(
            final AnyWrapper<A> wrapper,
            final List<String> anyTypeClasses,
            final AbstractAnyFormLayout<A, ? extends AnyForm<A>> formLayoutInfo,
            final PageReference pageRef) {

        super(wrapper, pageRef);
        this.anyTypeClasses = anyTypeClasses;
        this.formLayoutInfo = formLayoutInfo;
    }

    @Override
    protected WizardModel buildModelSteps(final AnyWrapper<A> modelObject, final WizardModel wizardModel) {
        // optional details panel step
        addOptionalDetailsPanel(modelObject).ifPresent(wizardModel::add);

        if (formLayoutInfo.isAuxClasses()) {
            wizardModel.add(new ConsoleAuxClasses(modelObject, anyTypeClasses));
        }

        if (formLayoutInfo.isGroups()) {
            wizardModel.add(new Groups(modelObject, mode == AjaxWizard.Mode.TEMPLATE));
        }

        if (formLayoutInfo.isRelationships()) {
            wizardModel.add(new Relationships(modelObject, pageRef));
        }

        // attributes panel steps
        if (formLayoutInfo.isPlainAttrs()) {
            wizardModel.add(new PlainAttrs(modelObject, mode, anyTypeClasses, formLayoutInfo.getWhichPlainAttrs()) {

                private static final long serialVersionUID = 8167894751609598306L;

                @Override
                public PageReference getPageReference() {
                    return pageRef;
                }
            });
        }
        if (formLayoutInfo.isDerAttrs() && mode != AjaxWizard.Mode.TEMPLATE) {
            wizardModel.add(new DerAttrs(modelObject, anyTypeClasses, formLayoutInfo.getWhichDerAttrs()));
        }

        if (formLayoutInfo.isManager()) {
            wizardModel.add(new Management(modelObject, pageRef));
        }

        // role panel step (just available for users)
        if (this instanceof UserWizardBuilder
                && modelObject instanceof final UserWrapper userWrapper
                && formLayoutInfo instanceof final UserFormLayoutInfo ufli
                && ufli.isRoles()) {

            wizardModel.add(new Roles(userWrapper));
        }

        SyncopeWebApplication.get().getAnyWizardBuilderAdditionalSteps().
                buildModelSteps(modelObject, wizardModel, formLayoutInfo);

        return wizardModel;
    }

    protected Optional<Details<A>> addOptionalDetailsPanel(final AnyWrapper<A> modelObject) {
        if (modelObject.getInnerObject().getKey() == null) {
            return Optional.empty();
        } else {
            return Optional.of(new Details<>(modelObject, mode == AjaxWizard.Mode.TEMPLATE, true, pageRef));
        }
    }

    @Override
    protected void fixPlainAttrs(final AnyTO updated, final AnyTO original) {
        // re-add to the updated object any missing plain attribute (compared to original): this to cope with
        // form layout, which might have not included some plain attributes
        for (Attr plainAttr : original.getPlainAttrs()) {
            if (updated.getPlainAttr(plainAttr.getSchema()).isEmpty()) {
                updated.getPlainAttrs().add(plainAttr);
            }
        }

        if (updated instanceof GroupableRelatableTO updatedTO && original instanceof GroupableRelatableTO originalTO) {
            originalTO.getMemberships().
                    forEach(oMemb -> updatedTO.getMembership(oMemb.getGroupKey()).
                    ifPresent(uMemb -> oMemb.getPlainAttrs().stream().
                    filter(attr -> uMemb.getPlainAttr(attr.getSchema()).isEmpty()).
                    forEach(attr -> uMemb.getPlainAttrs().add(attr))));
            originalTO.getRelationships().
                    forEach(oRel -> updatedTO.getRelationship(oRel.getType(), oRel.getOtherEndKey()).
                    ifPresent(uRel -> oRel.getPlainAttrs().stream().
                    filter(attr -> uRel.getPlainAttr(attr.getSchema()).isEmpty()).
                    forEach(attr -> uRel.getPlainAttrs().add(attr))));
        }

        // remove from the updated object any plain attribute without values, thus triggering for removal in
        // the generated patch
        updated.getPlainAttrs().removeIf(attr -> attr.getValues().isEmpty() || NO_VALUES.equals(attr.getValues()));
        if (updated instanceof GroupableRelatableTO updatedTO) {
            updatedTO.getMemberships().
                    forEach(memb -> memb.getPlainAttrs().
                    removeIf(attr -> attr.getValues().isEmpty() || NO_VALUES.equals(attr.getValues())));
            updatedTO.getRelationships().
                    forEach(rel -> rel.getPlainAttrs().
                    removeIf(attr -> attr.getValues().isEmpty() || NO_VALUES.equals(attr.getValues())));
        }
    }

    @Override
    protected long getMaxWaitTimeInSeconds() {
        return SyncopeWebApplication.get().getMaxWaitTimeInSeconds();
    }

    @Override
    protected void sendError(final Exception exception) {
        SyncopeConsoleSession.get().onException(exception);
    }

    @Override
    protected void sendWarning(final String message) {
        SyncopeConsoleSession.get().warn(message);
    }

    @Override
    protected Future<Pair<Serializable, Serializable>> execute(
            final Callable<Pair<Serializable, Serializable>> future) {

        return SyncopeConsoleSession.get().execute(future);
    }
}
