/*
 *  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.causeway.extensions.secman.applib.user.dom.mixins.perms;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Iterator;
import java.util.function.Function;

import jakarta.inject.Inject;
import jakarta.inject.Named;

import org.apache.causeway.applib.ViewModel;
import org.apache.causeway.applib.annotation.BookmarkPolicy;
import org.apache.causeway.applib.annotation.DomainObject;
import org.apache.causeway.applib.annotation.DomainObjectLayout;
import org.apache.causeway.applib.annotation.Editing;
import org.apache.causeway.applib.annotation.Nature;
import org.apache.causeway.applib.annotation.ObjectSupport;
import org.apache.causeway.applib.annotation.Programmatic;
import org.apache.causeway.applib.annotation.Property;
import org.apache.causeway.applib.annotation.PropertyLayout;
import org.apache.causeway.applib.annotation.Where;
import org.apache.causeway.applib.services.appfeat.ApplicationFeature;
import org.apache.causeway.applib.services.appfeat.ApplicationFeatureId;
import org.apache.causeway.applib.services.appfeat.ApplicationFeatureRepository;
import org.apache.causeway.applib.services.appfeat.ApplicationFeatureSort;
import org.apache.causeway.applib.services.appfeatui.ApplicationFeatureViewModel;
import org.apache.causeway.applib.services.factory.FactoryService;
import org.apache.causeway.applib.util.ObjectContracts;
import org.apache.causeway.applib.util.ToString;
import org.apache.causeway.commons.internal.base._Bytes;
import org.apache.causeway.commons.internal.resources._Serializables;
import org.apache.causeway.extensions.secman.applib.CausewayModuleExtSecmanApplib;
import org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermission;
import org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermissionMode;
import org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermissionRepository;
import org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermissionRule;
import org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermissionValue;
import org.apache.causeway.extensions.secman.applib.permission.dom.ApplicationPermissionValueSet;
import org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUser;
import org.apache.causeway.extensions.secman.applib.user.dom.ApplicationUserRepository;

import lombok.Getter;
import lombok.Setter;

/**
 * View model identified by {@link ApplicationFeatureId} and backed by an
 * {@link ApplicationFeature}.
 *
 * @since 2.0 {@index}
 */
@Named(UserPermissionViewModel.LOGICAL_TYPE_NAME)
@DomainObject(
        nature = Nature.VIEW_MODEL)
@DomainObjectLayout(
        bookmarking = BookmarkPolicy.AS_ROOT
)
public class UserPermissionViewModel implements ViewModel {

    public static final String LOGICAL_TYPE_NAME = CausewayModuleExtSecmanApplib.NAMESPACE + ".UserPermissionViewModel";

    public static abstract class PropertyDomainEvent<T> extends CausewayModuleExtSecmanApplib.PropertyDomainEvent<UserPermissionViewModel, T> {}

    @Inject ApplicationUserRepository applicationUserRepository;
    @Inject FactoryService factory;
    @Inject ApplicationFeatureRepository featureRepository;
    @Inject ApplicationPermissionRepository applicationPermissionRepository;

    // -- constructors, factory methods

    public static String memento(
            final ApplicationFeatureId featureId,
            final ApplicationUser user,
            final ApplicationPermissionValueSet.Evaluation viewingEvaluation,
            final ApplicationPermissionValueSet.Evaluation changingEvaluation) {
        return asEncodedString(featureId, user.getUsername(), viewingEvaluation, changingEvaluation);
    }

    public UserPermissionViewModel() {
        setFeatureId(ApplicationFeatureId.NAMESPACE_DEFAULT);
    }

    // -- identification

    @ObjectSupport public String title() {
        return getVerb() + " " + getFeatureId().getFullyQualifiedName();
    }

    @ObjectSupport public String iconName() {
        return "userPermission";
    }

    // -- VIEWMODEL CONTRACT

    @Inject
    public UserPermissionViewModel(final String memento) {
        var payload = _Serializables.read(String[].class,
                _Bytes.ofUrlBase64.apply(memento.getBytes(StandardCharsets.US_ASCII)));
        parse(payload);
    }

    @Override
    public String viewModelMemento() {
        return asEncodedString(getFeatureId(), getUsername(),
                newEvaluation(viewingGranted, viewingFeatureId, viewingRule, viewingMode),
                newEvaluation(changingGranted, changingFeatureId, changingRule, changingMode));
    }

    private static String asEncodedString(
            final ApplicationFeatureId featureId,
            final String username,
            final ApplicationPermissionValueSet.Evaluation viewingEvaluation,
            final ApplicationPermissionValueSet.Evaluation changingEvaluation) {

        final ApplicationPermissionValue viewingEvaluationCause = viewingEvaluation.getCause();
        final ApplicationFeatureId viewingEvaluationCauseFeatureId = viewingEvaluationCause != null
                ? viewingEvaluationCause.getFeatureId()
                : null;

        final ApplicationPermissionValue changingEvaluationCause = changingEvaluation.getCause();
        final ApplicationFeatureId changingEvaluationCauseFeatureId = changingEvaluationCause != null
                ? changingEvaluationCause.getFeatureId()
                : null;

        var payload = new String[] {
            username,

            ""+viewingEvaluation.isGranted(),
            viewingEvaluationCauseFeatureId != null? ""+viewingEvaluationCauseFeatureId.getSort(): "",
            viewingEvaluationCauseFeatureId != null? ""+viewingEvaluationCauseFeatureId.getFullyQualifiedName(): "",
            viewingEvaluationCause != null? ""+viewingEvaluationCause.getRule(): "",
            viewingEvaluationCause != null? ""+viewingEvaluationCause.getMode(): "",

            ""+changingEvaluation.isGranted(),
            changingEvaluationCauseFeatureId != null? ""+changingEvaluationCauseFeatureId.getSort(): "",
            changingEvaluationCauseFeatureId != null? ""+changingEvaluationCauseFeatureId.getFullyQualifiedName(): "",
            changingEvaluationCause != null? ""+changingEvaluationCause.getRule(): "",
            changingEvaluationCause != null? ""+changingEvaluationCause.getMode(): "",

            ""+featureId.getSort(),
            featureId.getFullyQualifiedName()
        };
        return new String(_Bytes.asUrlBase64.apply(_Serializables.write(payload)), StandardCharsets.US_ASCII);
    }

    private void parse(final String[] payload) {
        final Iterator<String> iterator = Arrays.asList(payload).iterator();

        this.username = iterator.next();

        this.viewingGranted = Boolean.parseBoolean(iterator.next());
        final String viewingEvaluationCauseFeatureIdType = iterator.next();

        final ApplicationFeatureSort viewingEvaluationFeatureIdType = !viewingEvaluationCauseFeatureIdType.isEmpty()
                ? ApplicationFeatureSort.valueOf(viewingEvaluationCauseFeatureIdType)
                : null;
        final String viewingEvaluationFeatureFqn = iterator.next();
        this.viewingFeatureId = viewingEvaluationFeatureIdType != null
                ? ApplicationFeatureId.newFeature(viewingEvaluationFeatureIdType, viewingEvaluationFeatureFqn)
                : null;

        final String viewingEvaluationCauseRule = iterator.next();
        this.viewingRule = !viewingEvaluationCauseRule.isEmpty()? ApplicationPermissionRule.valueOf(viewingEvaluationCauseRule): null;
        final String viewingEvaluationCauseMode = iterator.next();
        this.viewingMode = !viewingEvaluationCauseMode.isEmpty()? ApplicationPermissionMode.valueOf(viewingEvaluationCauseMode): null;

        this.changingGranted = Boolean.parseBoolean(iterator.next());
        final String changingEvaluationCauseFeatureIdType = iterator.next();
        final ApplicationFeatureSort changingEvaluationFeatureIdType =  !changingEvaluationCauseFeatureIdType.isEmpty() ? ApplicationFeatureSort.valueOf(changingEvaluationCauseFeatureIdType) : null;
        final String changingEvaluationFeatureFqn = iterator.next();
        this.changingFeatureId = changingEvaluationFeatureIdType != null
                ? ApplicationFeatureId.newFeature(changingEvaluationFeatureIdType, changingEvaluationFeatureFqn)
                : null;

        final String changingEvaluationCauseRule = iterator.next();
        this.changingRule = !changingEvaluationCauseRule.isEmpty()? ApplicationPermissionRule.valueOf(changingEvaluationCauseRule): null;
        final String changingEvaluationCauseMode = iterator.next();
        this.changingMode = !changingEvaluationCauseMode.isEmpty()? ApplicationPermissionMode.valueOf(changingEvaluationCauseMode): null;

        final ApplicationFeatureSort type = ApplicationFeatureSort.valueOf(iterator.next());
        this.featureId = ApplicationFeatureId.newFeature(type, iterator.next());
    }

    private static ApplicationPermissionValueSet.Evaluation newEvaluation(final boolean granted, final ApplicationFeatureId featureId, final ApplicationPermissionRule rule, final ApplicationPermissionMode mode) {
        return new ApplicationPermissionValueSet.Evaluation(newPermissionValue(featureId, rule, mode), granted);
    }

    private static ApplicationPermissionValue newPermissionValue(final ApplicationFeatureId featureId, final ApplicationPermissionRule rule, final ApplicationPermissionMode mode) {
        if(featureId == null || mode == null || rule == null) {
            return null;
        } else {
            return new ApplicationPermissionValue(featureId, rule, mode);
        }
    }

    // -- user (derived property, hidden in parented tables)

    @Property(
            domainEvent = User.DomainEvent.class
    )
    @PropertyLayout(
            fieldSetId = "identity",
            hidden=Where.PARENTED_TABLES,
            sequence = "1"
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @interface User {
        class DomainEvent extends PropertyDomainEvent<ApplicationUser> {}
    }

    @User
    public ApplicationUser getUser() {
        return applicationUserRepository.findOrCreateUserByUsername(getUsername());
    }

    @Getter(onMethod_ = {@Programmatic})
    private String username;

    // -- verb (derived property)

    private boolean viewingGranted;
    private boolean changingGranted;

    @Property (
            domainEvent = Verb.DomainEvent.class
    )
    @PropertyLayout(
            fieldSetId = "identity",
            sequence = "2",
            typicalLength= Verb.TYPICAL_LENGTH
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @interface Verb {
        int TYPICAL_LENGTH = 12;
        class DomainEvent extends PropertyDomainEvent<String> {}
    }

    @Verb
    public String getVerb() {
        return changingGranted
                ? "Can change"
                        : viewingGranted
                        ? "Can view"
                                : "No access to";
    }

    // -- feature (derived property)

    @Property(
            domainEvent = Feature.DomainEvent.class,
            editing = Editing.DISABLED
    )
    @PropertyLayout(
            fieldSetId = "identity",
            hidden=Where.REFERENCES_PARENT,
            sequence = "4"
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @interface Feature {
        class DomainEvent extends PropertyDomainEvent<ApplicationFeatureViewModel> {}
    }

    @Feature
    public ApplicationFeatureViewModel getFeature() {
        if(getFeatureId() == null) {
            return null;
        }
        return ApplicationFeatureViewModel.newViewModel(getFeatureId(), featureRepository, factory);
    }

    @Getter(onMethod_ = {@Programmatic})
    @Setter
    private ApplicationFeatureId featureId;

    // -- viewingPermission (derived property)

    private ApplicationFeatureId viewingFeatureId;
    private ApplicationPermissionMode viewingMode;
    private ApplicationPermissionRule viewingRule;

    @Property(
            domainEvent = ViewingPermission.DomainEvent.class,
            editing = Editing.DISABLED
    )
    @PropertyLayout(
            fieldSetId = "cause",
            hidden = Where.REFERENCES_PARENT,
            sequence = "1"
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @interface ViewingPermission {
        class DomainEvent extends PropertyDomainEvent<ApplicationPermission> {}
    }

    @ViewingPermission
    public ApplicationPermission getViewingPermission() {
        return getViewingPermissionValue() == null
                ? null
                : applicationPermissionRepository.findByUserAndPermissionValue(username, getViewingPermissionValue())
                  .orElse(null);
    }

    private ApplicationPermissionValue getViewingPermissionValue() {
        return viewingFeatureId == null
                ? null
                : new ApplicationPermissionValue(viewingFeatureId, viewingRule, viewingMode);
    }

    // -- changingPermission (derived property)

    private ApplicationFeatureId changingFeatureId;
    private ApplicationPermissionMode changingMode;
    private ApplicationPermissionRule changingRule;

    @Property(
            domainEvent = ChangingPermission.DomainEvent.class,
            editing = Editing.DISABLED
    )
    @PropertyLayout(
            fieldSetId = "cause",
            hidden = Where.REFERENCES_PARENT,
            sequence = "2"
    )
    @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @interface ChangingPermission {
        class DomainEvent extends PropertyDomainEvent<ApplicationPermission> {}
    }

    @ChangingPermission
    public ApplicationPermission getChangingPermission() {
        return getChangingPermissionValue() == null
                ? null
                : applicationPermissionRepository.findByUserAndPermissionValue(username, getChangingPermissionValue())
                    .orElse(null);
    }

    private ApplicationPermissionValue getChangingPermissionValue() {
        return changingFeatureId == null
                ? null
                : new ApplicationPermissionValue(changingFeatureId, changingRule, changingMode);
    }

    // -- toString

    private static final ToString<UserPermissionViewModel> toString =
            ObjectContracts
            .toString("user", UserPermissionViewModel::getUser)
            .thenToString("featureId", UserPermissionViewModel::getFeatureId);

    @Override
    public String toString() {
        return toString.toString(this);
    }

    // -- Factory

    public static Function<ApplicationFeatureId, UserPermissionViewModel> asViewModel(
            final ApplicationUser user,
            final FactoryService factoryService) {

        return (final ApplicationFeatureId featureId) -> {
            var permissionSet = user.getPermissionSet();
            var changingEvaluation = permissionSet.evaluate(featureId, ApplicationPermissionMode.CHANGING);
            var viewingEvaluation = permissionSet.evaluate(featureId, ApplicationPermissionMode.VIEWING);
            return factoryService.viewModel(new UserPermissionViewModel(memento(
                    featureId,
                    user,
                    viewingEvaluation,
                    changingEvaluation)));
        };
    }

}
