package com.atlassian.stash.internal.user;

import com.atlassian.bitbucket.user.*;
import com.atlassian.bitbucket.validation.annotation.RequiredString;
import com.atlassian.crowd.embedded.api.User;
import com.atlassian.crowd.embedded.impl.IdentifierUtils;
import com.atlassian.stash.internal.jpa.ZoneIdConverter;
import com.google.common.base.Function;
import com.google.common.base.MoreObjects;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.persistence.*;
import java.time.ZoneId;
import java.util.Date;
import java.util.Locale;
import java.util.Optional;

import static com.google.common.base.Preconditions.checkState;

@Entity
@Table(name = InternalNormalUser.TABLE)
@PrimaryKeyJoinColumn(name = "user_id", referencedColumnName = "id",
        foreignKey = @ForeignKey(name = "fk_sta_normal_user_id"))
public class InternalNormalUser extends InternalApplicationUser {

    public static final String TABLE = "sta_normal_user";
    public static final ToInternalNormalUserVisitor TO_NORMAL_USER = new ToInternalNormalUserVisitor();

    /**
     * Convert the passed user into an {@code InternalNormalUser} or {@link Optional#empty()} if its not an
     * {@code InternalNormalUser}.
     *
     * <p>This is not a simple cast because Hibernate may be injecting lazy proxies and we need to force the
     * realization of the user. We do this using the visitors.</p>
     *
     * @param applicationUser the user to convert.
     * @return the passed user as an {@code InternalNormalUser} or {@link Optional#empty()} if its not an
     * {@code InternalNormalUser}.
     *
     * @since 4.6
     */
    public static Optional<InternalNormalUser> fromUser(@Nullable ApplicationUser applicationUser) {
        return Optional.ofNullable(applicationUser)
                .map(user -> user.accept(ToInternalApplicationUser.INSTANCE))
                .map(internalApplicationUser -> internalApplicationUser.accept(TO_NORMAL_USER));
    }

    @Transient
    private transient volatile User backingCrowdUser;

    public static final int MAX_NAME_LENGTH = 255;
    @Column(name = "name", nullable = false, unique = true)
    @RequiredString(size = InternalNormalUser.MAX_NAME_LENGTH)
    private final String username;

    @Column(name = "slug", nullable = false, unique = true)
    private final String slug;

    @Column(name = "locale", nullable = true)
    private final Locale locale;

    @Column(name = "deleted_timestamp", nullable = true)
    @Temporal(TemporalType.TIMESTAMP)
    private final Date deletedDate;

    @Column(name = "time_zone", nullable = true, length = 64)
    @Convert(converter = ZoneIdConverter.class)
    private final ZoneId timeZone;

    // for Hibernate
    protected InternalNormalUser() {
        backingCrowdUser = null;
        slug = null;
        username = null;
        locale = null;
        deletedDate = null;
        timeZone = null;
    }

    protected InternalNormalUser(@Nonnull Builder builder) {
        super(builder);

        this.slug = builder.slug == null ? slugify(builder.name) : builder.slug;
        this.username = IdentifierUtils.toLowerCase(builder.name);
        this.backingCrowdUser = builder.crowdUser;
        this.locale = builder.locale;
        this.deletedDate = builder.deletedDate != null ? new Date(builder.deletedDate.getTime()) : null;
        this.timeZone = builder.timeZone;
    }

    @Override
    public <T> T accept(@Nonnull InternalStashUserVisitor<T> visitor) {
        return visitor.visit(this);
    }

    @Override
    public <T> T accept(@Nonnull ApplicationUserVisitor<T> visitor) {
        return visitor.visit(this);
    }

    @Override
    public Builder copy() {
        return new Builder(this);
    }

    public User getBackingCrowdUser() {
        return backingCrowdUser;
    }

    /**
     * @return the configured locale for this user, or {@code null}
     */
    public Locale getLocale() {
        return locale;
    }

    /**
     * @return the timestamp when the user was deleted (from Crowd), or {@code null}. Note that this is only set after
     * deletion and before the user has been cleaned up. In case the user gets "undeleted", it's cleared again.
     */
    public Date getDeletedDate() {
        return deletedDate != null ? new Date(deletedDate.getTime()) : null;
    }

    @Nonnull
    @Override
    public String getDisplayName() {
        return backingCrowdUser != null ? backingCrowdUser.getDisplayName() : username;
    }

    @Override
    public String getEmailAddress() {
        return backingCrowdUser != null ? backingCrowdUser.getEmailAddress() : super.getEmailAddress();
    }

    /**
     * @return the lower-cased version of the crowd user name. Crowd is case-insensitive, but case-preserving. Stash user
     * shadows the crowd user, so it should be case-insensitive and preserve the case that Crowd uses. Because
     * of the shadowing, the casing in Crowd can change as users from different directories become visible.
     * Therefore, InternalNormalUser.getName should always delegate to the backingCrowdUser.
     */
    @Nonnull
    @Override
    public String getName() {
        return backingCrowdUser != null ? backingCrowdUser.getName() : username;
    }

    @Nonnull
    @Override
    public String getSlug() {
        return slug;
    }

    @Nullable
    public ZoneId getTimeZone() {
        return timeZone;
    }

    @Nonnull
    @Override
    public UserType getType() {
        return UserType.NORMAL;
    }

    public boolean isActive() {
        return backingCrowdUser != null && backingCrowdUser.isActive();
    }

    public boolean isCrowdBacked() {
        return backingCrowdUser != null;
    }

    public void setBackingCrowdUser(User crowdUser) {
        checkState(backingCrowdUser == null, "You cannot set the backing Crowd user after it's been initialized");
        backingCrowdUser = crowdUser;
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
                .add("id", getId())
                .add("username", username)
                .toString();
    }

    public static class Builder extends AbstractBuilder<Builder, InternalNormalUser> {

        private User crowdUser;
        private Locale locale;
        private Date deletedDate;
        private ZoneId timeZone;

        public Builder() {
        }

        public Builder(InternalNormalUser user) {
            super(user);
            crowdUser = user.getBackingCrowdUser();
            locale = user.locale;
            deletedDate = user.deletedDate;
            timeZone = user.timeZone;
        }

        @Nonnull
        public InternalNormalUser build() {
            return new InternalNormalUser(this);
        }

        public Builder crowdUser(User value) {
            crowdUser = value;
            return this;
        }

        public Builder locale(Locale value) {
            locale = value;
            return this;
        }

        public Builder deletedDate(Date value) {
            deletedDate = value;
            return this;
        }

        public Builder timeZone(@Nullable ZoneId value) {
            timeZone = value;
            return this;
        }

        @Nonnull
        @Override
        protected Builder self() {
            return this;
        }

    }

    // Public due to the fact that it implements both Predicate and ApplicationUserVisitor
    public static class ToInternalNormalUserVisitor implements InternalStashUserVisitor<InternalNormalUser>, Function<InternalApplicationUser, InternalNormalUser> {

        private ToInternalNormalUserVisitor() {
        }

        @Override
        public InternalNormalUser apply(InternalApplicationUser user) {
            return user.accept(this);
        }

        @Override
        public InternalNormalUser visit(@Nonnull InternalNormalUser user) {
            return user;
        }

        @Override
        public InternalNormalUser visit(@Nonnull InternalServiceUser user) {
            return null;
        }

    }

    private static class ToInternalApplicationUser extends AbstractApplicationUserVisitor<InternalApplicationUser> {
        private static final ToInternalApplicationUser INSTANCE = new ToInternalApplicationUser();

        @Override
        public InternalApplicationUser visit(@Nonnull DetailedUser user) {
            return ((InternalDetailedUser) user).getDelegate();
        }

        @Override
        public InternalApplicationUser visit(@Nonnull ApplicationUser user) {
            return user instanceof InternalApplicationUser ? (InternalApplicationUser) user : null;
        }
    }
}
