/*
 *  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.commandlog.applib.dom;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.math.BigDecimal;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Consumer;

import jakarta.annotation.Priority;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.validation.constraints.Digits;

import org.springframework.stereotype.Service;

import org.apache.causeway.applib.annotation.Domain;
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.MemberSupport;
import org.apache.causeway.applib.annotation.ObjectSupport;
import org.apache.causeway.applib.annotation.Optionality;
import org.apache.causeway.applib.annotation.Parameter;
import org.apache.causeway.applib.annotation.PriorityPrecedence;
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.Publishing;
import org.apache.causeway.applib.annotation.Where;
import org.apache.causeway.applib.jaxb.JavaSqlXMLGregorianCalendarMarshalling;
import org.apache.causeway.applib.mixins.system.DomainChangeRecord;
import org.apache.causeway.applib.mixins.system.HasInteractionId;
import org.apache.causeway.applib.services.bookmark.Bookmark;
import org.apache.causeway.applib.services.command.Command;
import org.apache.causeway.applib.services.commanddto.HasCommandDto;
import org.apache.causeway.applib.services.commanddto.conmap.UserDataKeys;
import org.apache.causeway.applib.services.tablecol.TableColumnOrderForCollectionTypeAbstract;
import org.apache.causeway.applib.util.ObjectContracts;
import org.apache.causeway.applib.util.TitleBuffer;
import org.apache.causeway.applib.util.ToString;
import org.apache.causeway.commons.internal.base._Casts;
import org.apache.causeway.commons.internal.base._Strings;
import org.apache.causeway.commons.internal.base._Temporals;
import org.apache.causeway.commons.internal.exceptions._Exceptions;
import org.apache.causeway.extensions.commandlog.applib.CausewayModuleExtCommandLogApplib;
import org.apache.causeway.schema.cmd.v2.CommandDto;
import org.apache.causeway.schema.cmd.v2.MapDto;

import lombok.NoArgsConstructor;
import lombok.experimental.UtilityClass;

/**
 * A persistent representation of a {@link Command}, being the intention to edit a property or invoke an action.
 *
 * <p>
 *     Use cases requiring persistence including auditing, and for replay of
 *     commands for regression testing purposes.
 * </p>
 *
 * Note that this class doesn't subclass from {@link Command} ({@link Command}
 * is not an interface), but it does implement {@link HasCommandDto}, providing access to
 * {@link CommandDto}, a serialized representation of the {@link Command}.
 *
 * @since 2.x {@index}
 */
@Named(CommandLogEntry.LOGICAL_TYPE_NAME)
@DomainObject(
        editing = Editing.DISABLED,
        entityChangePublishing = Publishing.DISABLED
)
@DomainObjectLayout(
        titleUiEvent = CommandLogEntry.TitleUiEvent.class,
        iconUiEvent = CommandLogEntry.IconUiEvent.class,
        cssClassUiEvent = CommandLogEntry.CssClassUiEvent.class,
        layoutUiEvent = CommandLogEntry.LayoutUiEvent.class
)
@NoArgsConstructor
public abstract class CommandLogEntry
implements Comparable<CommandLogEntry>, DomainChangeRecord, HasCommandDto {

    public final static String LOGICAL_TYPE_NAME = CausewayModuleExtCommandLogApplib.NAMESPACE + ".CommandLogEntry";
    public static final String SCHEMA = CausewayModuleExtCommandLogApplib.SCHEMA;
    public static final String TABLE = "CommandLogEntry";

    public static class TitleUiEvent extends CausewayModuleExtCommandLogApplib.TitleUiEvent<CommandLogEntry> { }
    public static class IconUiEvent extends CausewayModuleExtCommandLogApplib.IconUiEvent<CommandLogEntry> { }
    public static class CssClassUiEvent extends CausewayModuleExtCommandLogApplib.CssClassUiEvent<CommandLogEntry> { }
    public static class LayoutUiEvent extends CausewayModuleExtCommandLogApplib.LayoutUiEvent<CommandLogEntry> { }

    public static abstract class PropertyDomainEvent<T> extends CausewayModuleExtCommandLogApplib.PropertyDomainEvent<CommandLogEntry, T> { }
    public static abstract class CollectionDomainEvent<T> extends CausewayModuleExtCommandLogApplib.CollectionDomainEvent<CommandLogEntry, T> { }
    public static abstract class ActionDomainEvent extends CausewayModuleExtCommandLogApplib.ActionDomainEvent<CommandLogEntry> { }

    @UtilityClass
    public static class Nq {
        public static final String FIND_BY_INTERACTION_ID               = LOGICAL_TYPE_NAME + ".findByInteractionId";
        public static final String FIND_BY_PARENT_INTERACTION_ID        = LOGICAL_TYPE_NAME + ".findByParentInteractionId";
        public static final String FIND_CURRENT                         = LOGICAL_TYPE_NAME + ".findCurrent";
        public static final String FIND_COMPLETED                       = LOGICAL_TYPE_NAME + ".findCompleted";
        public static final String FIND_RECENT_BY_TARGET                = LOGICAL_TYPE_NAME + ".findRecentByTarget";
        public static final String FIND_RECENT_BY_TARGET_OR_RESULT      = LOGICAL_TYPE_NAME + ".findRecentByTargetOrResult";
        public static final String FIND_BY_TARGET_AND_TIMESTAMP_BETWEEN = LOGICAL_TYPE_NAME + ".findByTargetAndTimestampBetween";
        public static final String FIND_BY_TARGET_AND_TIMESTAMP_AFTER   = LOGICAL_TYPE_NAME + ".findByTargetAndTimestampAfter";
        public static final String FIND_BY_TARGET_AND_TIMESTAMP_BEFORE  = LOGICAL_TYPE_NAME + ".findByTargetAndTimestampBefore";
        public static final String FIND_BY_TARGET                       = LOGICAL_TYPE_NAME + ".findByTarget";
        public static final String FIND_BY_TIMESTAMP_BETWEEN            = LOGICAL_TYPE_NAME + ".findByTimestampBetween";
        public static final String FIND_BY_TIMESTAMP_AFTER              = LOGICAL_TYPE_NAME + ".findByTimestampAfter";
        public static final String FIND_BY_TIMESTAMP_BEFORE             = LOGICAL_TYPE_NAME + ".findByTimestampBefore";
        public static final String FIND                                 = LOGICAL_TYPE_NAME + ".find";
        public static final String FIND_MOST_RECENT                     = LOGICAL_TYPE_NAME + ".findMostRecent";
        public static final String FIND_RECENT_BY_USERNAME              = LOGICAL_TYPE_NAME + ".findRecentByUsername";
        public static final String FIND_FIRST                           = LOGICAL_TYPE_NAME + ".findFirst";
        public static final String FIND_SINCE                           = LOGICAL_TYPE_NAME + ".findSince";
        /**
         * The most recent (replayed) command previously replicated from primary to secondary.
         *
         * <p>
         *     This should always exist except for the very first times (after restored the prod DB to secondary).
         * </p>
         */
        public static final String FIND_MOST_RECENT_REPLAYED            = LOGICAL_TYPE_NAME + ".findMostRecentReplayed";
        /**
         * The most recent completed command, as queried on the secondary, corresponding to the last command run on
         * primary before the production database was restored to the secondary.
         */
        public static final String FIND_MOST_RECENT_COMPLETED           = LOGICAL_TYPE_NAME + ".findMostRecentCompleted";
        public static final String FIND_BY_REPLAY_STATE                 = LOGICAL_TYPE_NAME + ".findNotYetReplayed";
        public static final String FIND_BACKGROUND_AND_NOT_YET_STARTED  = LOGICAL_TYPE_NAME + ".findBackgroundAndNotYetStarted";
        public static final String FIND_RECENT_BACKGROUND_BY_TARGET     = LOGICAL_TYPE_NAME + ".findRecentBackgroundByTarget";
    }

    @Programmatic
    public void sync(final Command command) {

        setInteractionId(command.getInteractionId());
        setUsername(command.getUsername());
        setTimestamp(command.getTimestamp());

        setCommandDto(command.getCommandDto());
        setTarget(command.getTarget());
        setLogicalMemberIdentifier(command.getLogicalMemberIdentifier());

        setStartedAt(command.getStartedAt());
        setCompletedAt(command.getCompletedAt());

        setResult(command.getResult());
        setException(command.getException());

        setReplayState(org.apache.causeway.extensions.commandlog.applib.dom.ReplayState.UNDEFINED);
    }

    /**
     * Intended for use on secondary (replay) system.
     *
     * @param commandDto - obtained from the primary system as a representation of a command invocation
     * @param replayState - controls whether this is to be replayed
     * @param targetIndex - if the command represents a bulk action, then it is flattened out when replayed; this indicates which target to execute against.
     */
    public CommandLogEntry(
            final CommandDto commandDto,
            final org.apache.causeway.extensions.commandlog.applib.dom.ReplayState replayState,
            final int targetIndex) {

        setInteractionId(UUID.fromString(commandDto.getInteractionId()));
        setUsername(commandDto.getUsername());
        setTimestamp(JavaSqlXMLGregorianCalendarMarshalling.toTimestamp(commandDto.getTimestamp()));

        setCommandDto(commandDto);
        setTarget(Bookmark.forOidDto(commandDto.getTargets().getOid().get(targetIndex)));
        setLogicalMemberIdentifier(commandDto.getMember().getLogicalMemberIdentifier());

        // the hierarchy of commands calling other commands is only available on the primary system.
        setParentInteractionId(null);

        setStartedAt(JavaSqlXMLGregorianCalendarMarshalling.toTimestamp(commandDto.getTimings().getStartedAt()));
        setCompletedAt(JavaSqlXMLGregorianCalendarMarshalling.toTimestamp(commandDto.getTimings().getCompletedAt()));

        copyOver(commandDto, UserDataKeys.RESULT, value -> this.setResult(Bookmark.parse(value).orElse(null)));
        copyOver(commandDto, UserDataKeys.EXCEPTION, this::setException);

        setReplayState(replayState);
    }

    static void copyOver(
            final CommandDto commandDto,
            final String key, final Consumer<String> consume) {
        commandDto.getUserData().getEntry()
                .stream()
                .filter(x -> Objects.equals(x.getKey(), key))
                .map(MapDto.Entry::getValue)
                .filter(Objects::nonNull)
                .filter(x -> x.length() > 0)
                .findFirst()
                .ifPresent(consume);
    }

    private static final DateTimeFormatter formatter =
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");

    @ObjectSupport public String title() {
        return new TitleBuffer()
                .append(formatter.format(getTimestamp().toLocalDateTime()))
                .append(" ")
                .append(getLogicalMemberIdentifier())
                .toString();
    }

    @DomainChangeRecord.Type
    @Override
    public ChangeType getType() {
        return ChangeType.COMMAND;
    }

    @Property(
            domainEvent = InteractionId.DomainEvent.class
    )
    @DomainChangeRecord.InteractionId
    @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface InteractionId {
        class DomainEvent extends PropertyDomainEvent<UUID> {}
        String NAME = "interactionId";
        int MAX_LENGTH = HasInteractionId.InteractionId.MAX_LENGTH;
        boolean NULLABLE = HasInteractionId.InteractionId.NULLABLE;
        String ALLOWS_NULL = HasInteractionId.InteractionId.ALLOWS_NULL;
    }
    @Override
    @InteractionId
    public abstract UUID getInteractionId();
    public abstract void setInteractionId(UUID interactionId);

    @Property(
            domainEvent = Username.DomainEvent.class
    )
    @DomainChangeRecord.Username
    @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Username {
        class DomainEvent extends PropertyDomainEvent<String> {}
        int MAX_LENGTH = DomainChangeRecord.Username.MAX_LENGTH;
        boolean NULLABLE = DomainChangeRecord.Username.NULLABLE;
        String ALLOWS_NULL = DomainChangeRecord.Username.ALLOWS_NULL;
    }
    @Override
    @Username
    public abstract String getUsername();
    public abstract void setUsername(String userName);

    @Property(
            domainEvent = Timestamp.DomainEvent.class
    )
    @DomainChangeRecord.Timestamp
    @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Timestamp {
        class DomainEvent extends PropertyDomainEvent<java.sql.Timestamp> {}
        boolean NULLABLE = DomainChangeRecord.Timestamp.NULLABLE;
        String ALLOWS_NULL = DomainChangeRecord.Timestamp.ALLOWS_NULL;
    }
    @Timestamp
    @Override
    public abstract java.sql.Timestamp getTimestamp();
    public abstract void setTimestamp(java.sql.Timestamp timestamp);

    @Property(
            domainEvent = Target.DomainEvent.class
    )
    @DomainChangeRecord.Target
    @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Target {
        class DomainEvent extends PropertyDomainEvent<Bookmark> {}
        int MAX_LENGTH = DomainChangeRecord.Target.MAX_LENGTH;
        boolean NULLABLE = DomainChangeRecord.Target.NULLABLE;
        String ALLOWS_NULL = DomainChangeRecord.Target.ALLOWS_NULL;
    }
    @Override
    @Target
    public abstract Bookmark getTarget();
    public abstract void setTarget(Bookmark target);

    @Property(
            domainEvent = ExecuteIn.DomainEvent.class
    )
    @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ExecuteIn {
        class DomainEvent extends PropertyDomainEvent<org.apache.causeway.extensions.commandlog.applib.dom.ExecuteIn> {}
        int MAX_LENGTH = 10;
        boolean NULLABLE = true;
        String ALLOWS_NULL = "true";
    }
    /**
     * Whether the command was executed immediately in the current thread of execution, or scheduled to be
     * executed at some time later in a &quot;background&quot; thread of execution.
     */
    @ExecuteIn
    public abstract org.apache.causeway.extensions.commandlog.applib.dom.ExecuteIn getExecuteIn();
    public abstract void setExecuteIn(org.apache.causeway.extensions.commandlog.applib.dom.ExecuteIn replayState);

    /**
     * The interactionId of the parent command, if any.
     *
     * <p>
     *     We store only the id rather than a reference to the parent, because the
     *     {@link org.apache.causeway.extensions.commandlog.applib.subscriber.CommandSubscriberForCommandLog}'s
     *     callback is only called at the end of the transaction, meaning that the {@link CommandLogEntry} of the
     *     &quot;parent&quot; will be persisted only after any of its child background {@link CommandLogEntry}s are
     *     to be persisted (within the body of the underlying action).
     * </p>
     *
     * @see #getParent()
     */
    @Domain.Exclude
    public abstract UUID getParentInteractionId();
    public abstract void setParentInteractionId(UUID parentInteractionId);

    @Property(
            domainEvent = Parent.DomainEvent.class,
            optionality = Optionality.OPTIONAL
    )
    @PropertyLayout(
            hidden = Where.ALL_TABLES
    )
    @Parameter(
            optionality = Optionality.OPTIONAL
    )
    @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Parent {
        class DomainEvent extends PropertyDomainEvent<CommandLogEntry> {}
        String NAME = "parentInteractionId";
        boolean NULLABLE = true;
        String ALLOWS_NULL = "true";
    }
    @Parent
    public <C extends CommandLogEntry> C getParent() {
        if (getParentInteractionId() == null) {
            return null;
        }
        var parentCommandLogEntryIfAny = commandLogEntryRepository.findByInteractionId(getParentInteractionId());
        var commandLogEntry = parentCommandLogEntryIfAny.orElse(null);
        return _Casts.uncheckedCast(commandLogEntry);
    }

    @Inject CommandLogEntryRepository commandLogEntryRepository;

    @Property(
            domainEvent = LogicalMemberIdentifier.DomainEvent.class,
            editing = Editing.DISABLED
    )
    @DomainChangeRecord.LogicalMemberIdentifier
    @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface LogicalMemberIdentifier {
        class DomainEvent extends PropertyDomainEvent<String> {}
        int MAX_LENGTH = DomainChangeRecord.LogicalMemberIdentifier.MAX_LENGTH;
        boolean NULLABLE = DomainChangeRecord.LogicalMemberIdentifier.NULLABLE;
        String ALLOWS_NULL = DomainChangeRecord.LogicalMemberIdentifier.ALLOWS_NULL;

    }
    @Override
    @LogicalMemberIdentifier
    public abstract String getLogicalMemberIdentifier();
    public abstract void setLogicalMemberIdentifier(String logicalMemberIdentifier);

    @Property(
            domainEvent = CommandDtoAnnot.DomainEvent.class
    )
    @HasCommandDto.CommandDtoAnnot
    @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface CommandDtoAnnot {
        class DomainEvent extends PropertyDomainEvent<CommandDto> {}
        boolean NULLABLE = HasCommandDto.CommandDtoAnnot.NULLABLE;
        String ALLOWS_NULL = HasCommandDto.CommandDtoAnnot.ALLOWS_NULL;
    }
    @CommandDtoAnnot
    @Override
    public abstract CommandDto getCommandDto();
    public abstract void setCommandDto(CommandDto commandDto);

    @Property(
            domainEvent = StartedAt.DomainEvent.class,
            editing = Editing.DISABLED,
            optionality = Optionality.OPTIONAL
    )
    @Parameter(
            optionality = Optionality.OPTIONAL
    )
    @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface StartedAt {
        class DomainEvent extends PropertyDomainEvent<java.sql.Timestamp> {}
        boolean NULLABLE = true;
        String ALLOWS_NULL = "true";
    }
    @StartedAt
    public abstract java.sql.Timestamp getStartedAt();
    public abstract void setStartedAt(java.sql.Timestamp startedAt);

    @Property(
            domainEvent = CompletedAt.DomainEvent.class,
            editing = Editing.DISABLED,
            optionality = Optionality.OPTIONAL
    )
    @Parameter(
            optionality = Optionality.OPTIONAL
    )
    @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface CompletedAt {
        class DomainEvent extends PropertyDomainEvent<java.sql.Timestamp> {}
        boolean NULLABLE = true;
        String ALLOWS_NULL = "true";
    }
    @CompletedAt
    public abstract java.sql.Timestamp getCompletedAt();
    public abstract void setCompletedAt(java.sql.Timestamp completedAt);

    @Property(
            domainEvent = Duration.DomainEvent.class,
            editing = Editing.DISABLED
    )
    @Digits(integer=Duration.DIGITS_INTEGER, fraction=Duration.DIGITS_FRACTION)
    @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Duration {
        class DomainEvent extends PropertyDomainEvent<BigDecimal> {}
        int DIGITS_INTEGER = 5;
        int DIGITS_FRACTION = 3;
    }
    /**
     * The number of seconds (to 3 decimal places) that this command lasted, derived from
     *      * {@link #getStartedAt()} and {@link #getCompletedAt()}.
     *
     * <p>
     * Populated only if it has {@link #getCompletedAt() completed}.
     */
    @Digits(integer=5, fraction=3)
    @Duration
    public BigDecimal getDuration() {
        return _Temporals.secondsBetweenAsDecimal(getStartedAt(), getCompletedAt())
                .orElse(null);
    }

    @Property(
            domainEvent = IsComplete.DomainEvent.class,
            editing = Editing.DISABLED
    )
    @PropertyLayout(
            hidden = Where.OBJECT_FORMS
    )
    @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface IsComplete {
        class DomainEvent extends PropertyDomainEvent<Boolean> {}
    }
    @IsComplete
    public boolean isComplete() {
        return getCompletedAt() != null;
    }

    @Property(
            domainEvent = Result.DomainEvent.class
    )
    @PropertyLayout(
            hidden = Where.ALL_TABLES,
            named = "Result Bookmark"
    )
    @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Result {
        class DomainEvent extends PropertyDomainEvent<Bookmark> {}
        int MAX_LENGTH = 2000;
        boolean NULLABLE = true;
        String ALLOWS_NULL = "true";
    }
    @Result
    public abstract Bookmark getResult();
    public abstract void setResult(Bookmark result);

    @Property(
            domainEvent = Exception.DomainEvent.class
    )
    @PropertyLayout(
            hidden = Where.ALL_TABLES,
            multiLine = 3,
            named = "Exception (if any)"
    )
    @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Exception {
        class DomainEvent extends PropertyDomainEvent<String> {}
        boolean NULLABLE = true;
        String ALLOWS_NULL = "true";
    }
    /**
     * Stack trace of any exception that might have occurred if this interaction/transaction aborted.
     *
     * <p>
     * Not part of the applib API, because the default implementation is not persistent
     * and so there's no object that can be accessed to be annotated.
     */
    @Exception
    public abstract String getException();
    public abstract void setException(final String exception);

    public void setException(final Throwable exception) {
        setException(_Exceptions.asStacktrace(exception));
    }

    @Property(
            domainEvent = ResultSummary.DomainEvent.class,
            editing = Editing.DISABLED
    )
    @PropertyLayout(
            hidden = Where.OBJECT_FORMS,
            named = "Result"
    )
    @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ResultSummary {
        class DomainEvent extends PropertyDomainEvent<String> {}
    }
    @ResultSummary
    public String getResultSummary() {
        if(getCompletedAt() == null) {
            return "";
        }
        if(!_Strings.isNullOrEmpty(getException())) {
            return "EXCEPTION";
        }
        if(getResult() != null) {
            return "OK";
        } else {
            return "OK (VOID)";
        }
    }

    @Property(
            domainEvent = IsCaused.DomainEvent.class,
            editing = Editing.DISABLED
    )
    @PropertyLayout(
            hidden = Where.OBJECT_FORMS
    )
    @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface IsCaused {
        class DomainEvent extends PropertyDomainEvent<Boolean> {}
    }
    @IsCaused
    public boolean isCausedException() {
        return getException() != null;
    }

    @Property(
            domainEvent = ReplayState.DomainEvent.class
    )
    @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ReplayState {
        class DomainEvent extends PropertyDomainEvent<org.apache.causeway.extensions.commandlog.applib.dom.ReplayState> {}
        int MAX_LENGTH = 10;
        boolean NULLABLE = true;
        String ALLOWS_NULL = "true";
    }
    /**
     * For a replayed command, what the outcome was.
     */
    @ReplayState
    public abstract org.apache.causeway.extensions.commandlog.applib.dom.ReplayState getReplayState();
    public abstract void setReplayState(org.apache.causeway.extensions.commandlog.applib.dom.ReplayState replayState);

    @Property(
            domainEvent = ReplayStateFailureReason.DomainEvent.class,
            optionality = Optionality.OPTIONAL,
            maxLength = ReplayStateFailureReason.MAX_LENGTH
    )
    @PropertyLayout(hidden = Where.ALL_TABLES, multiLine = 5)
    @Parameter(
            optionality = Optionality.OPTIONAL,
            maxLength = ReplayStateFailureReason.MAX_LENGTH
    )
    @java.lang.annotation.Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ReplayStateFailureReason {
        class DomainEvent extends PropertyDomainEvent<String> {}
        int MAX_LENGTH = 255;
        boolean NULLABLE = true;
        String ALLOWS_NULL = "true";
    }
    /**
     * For a {@link org.apache.causeway.extensions.commandlog.applib.dom.ReplayState#FAILED failed} replayed command, what the reason was for the failure.
     */
    @ReplayStateFailureReason
    public abstract String getReplayStateFailureReason();
    public abstract void setReplayStateFailureReason(String replayStateFailureReason);
    @MemberSupport public boolean hideReplayStateFailureReason() {
        return getReplayState() == null || !getReplayState().isFailed();
    }

    @Programmatic
    public void saveAnalysis(final String analysis) {
        if (analysis == null) {
            setReplayState(org.apache.causeway.extensions.commandlog.applib.dom.ReplayState.OK);
        } else {
            setReplayState(org.apache.causeway.extensions.commandlog.applib.dom.ReplayState.FAILED);
            setReplayStateFailureReason(_Strings.trimmed(analysis, 255));
        }

    }

    @Override
    public int compareTo(final CommandLogEntry other) {
        return this.getTimestamp().compareTo(other.getTimestamp());
    }

    static final ToString<CommandLogEntry> stringifier = ObjectContracts
            .toString("interactionId", CommandLogEntry::getInteractionId)
            .thenToString("username", CommandLogEntry::getUsername)
            .thenToString("timestamp", CommandLogEntry::getTimestamp)
            .thenToString("target", CommandLogEntry::getTarget)
            .thenToString("logicalMemberIdentifier", CommandLogEntry::getLogicalMemberIdentifier)
            .thenToStringOmitIfAbsent("startedAt", CommandLogEntry::getStartedAt)
            .thenToStringOmitIfAbsent("completedAt", CommandLogEntry::getCompletedAt);

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

    @Service
    @Priority(PriorityPrecedence.LATE - 10) // before the framework's own default.
    public static class TableColumnOrderDefault extends TableColumnOrderForCollectionTypeAbstract<CommandLogEntry> {

        public TableColumnOrderDefault() { super(CommandLogEntry.class); }

        @Override
        protected List<String> orderParented(final Object parent, final String collectionId, final List<String> propertyIds) {
            return ordered(propertyIds);
        }

        @Override
        protected List<String> orderStandalone(final List<String> propertyIds) {
            return ordered(propertyIds);
        }

        private List<String> ordered(final List<String> propertyIds) {
            return Arrays.asList(
                "timestamp", "target", "logicalMemberIdentifier", "username", "complete", "resultSummary", "duration", "interactionId"
            );
        }
    }

}
