package com.atlassian.stash.internal.user;

import com.atlassian.crowd.embedded.api.User;
import com.atlassian.crowd.embedded.impl.IdentifierUtils;
import com.atlassian.security.random.SecureTokenGenerator;
import com.atlassian.stash.internal.ApplicationConstants;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.ApplicationUserEquality;
import com.atlassian.bitbucket.user.UserType;
import com.atlassian.bitbucket.util.BuilderSupport;
import com.google.common.base.Predicate;
import org.hibernate.annotations.CacheConcurrencyStrategy;

import javax.annotation.Nonnull;
import javax.persistence.*;
import java.io.Serializable;
import java.text.Normalizer;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

@Cacheable
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@Table(name = InternalApplicationUser.TABLE)
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public abstract class InternalApplicationUser implements ApplicationUser, Serializable {

    public static final String ID_GEN = "userIdGenerator";
    public static final int MAX_GENERATED_SLUG_LENGTH = MAX_SLUG_LENGTH - 1;
    public static final int MAX_SLUG_RETRY_COUNT = 10;
    public static final String TABLE = "stash_user";
    public static final InternalStashUserVisitor<User> TO_CROWD_USER = new ToCrowdUserVisitor();

    @Id
    @GeneratedValue(generator = ID_GEN, strategy = GenerationType.TABLE)
    @TableGenerator(name = ID_GEN, table = ApplicationConstants.ID_SEQUENCE_TABLE, pkColumnValue = TABLE, allocationSize = 50)
    private final int id;

    // for hibernate
    protected InternalApplicationUser() {
        id = ApplicationConstants.UNSAVED_ENTITY;
    }

    protected InternalApplicationUser(@Nonnull AbstractBuilder<?, ?> builder) {
        id = checkNotNull(builder, "builder").id;
    }

    /**
     * Uses the provided {@link com.atlassian.security.random.SecureTokenGenerator} to generate a user slug.
     *
     * @param tokenGenerator the generator to use
     * @return the generated slug
     */
    public static String generateSlug(SecureTokenGenerator tokenGenerator) {
        String slug = tokenGenerator.generateToken();
        if (slug.length() > MAX_SLUG_LENGTH) { //No retries here, so we can use the full length
            //Generated tokens are currently always 40 characters long but the SecureTokenGenerator contract states
            //they may be up to 255 characters
            slug = slug.substring(0, MAX_SLUG_LENGTH);
        }
        return slug;
    }

    /**
     * Slugifies a user name.
     * <p>
     * First the name is normalised to NFKD form; then any URL delimiters it contains are replaced with underscores;
     * then it is truncated to {@link #MAX_GENERATED_SLUG_LENGTH 126 characters} (if necessary); then it is converted
     * to lowercase.
     *
     * @param name the user name
     * @return the generated user slug
     */
    public static String slugify(String name) {
        String slug = Normalizer.normalize(name, Normalizer.Form.NFKD)
                .replaceAll("[:/?#@!$&'()*+,;=%\\\\\\[\\]]", "_"); //Note: The IDEA Pattern inspection is wrong here
        if (slug.length() > MAX_GENERATED_SLUG_LENGTH) {
            slug = slug.substring(0, MAX_GENERATED_SLUG_LENGTH);
        }
        //username has IdentifierUtils.toLowerCase applied, so we do the same for the slug
        return IdentifierUtils.toLowerCase(slug);
    }

    public abstract <T> T accept(@Nonnull InternalStashUserVisitor<T> visitor);

    public abstract AbstractBuilder copy();

    @Override
    @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
    public boolean equals(Object o) {
        return ApplicationUserEquality.equals(this, o);
    }

    /**
     * @return email address of the user.
     */
    @Override
    public String getEmailAddress() {
        return null;
    }

    @Override
    public int getId() {
        return id;
    }

    @Nonnull
    public abstract UserType getType();

    @Override
    public int hashCode() {
        return ApplicationUserEquality.hashCode(this);
    }

    @Nonnull
    public String getUsername() {
        return getName();
    }

    private static String checkSlug(String slug) {
        checkNotNull(slug, "slug");
        checkArgument(!slug.trim().isEmpty(), "A non-blank slug is required");
        checkArgument(slug.length() <= MAX_SLUG_LENGTH, "The provided slug exceeds the maximum allowed length");

        String check = slugify(slug);
        if (slug.length() < MAX_SLUG_LENGTH) {
            //If the slug is less than the max length, the slugify(String) should produce a value which matches
            //its input value exactly. Otherwise, if any characters were replaced, it means the provided slug
            //does not follow the rules for a slug and cannot be used
            checkArgument(slug.equals(check), "The provided slug is not valid (%s != %s)", slug, check);
        } else if (slug.length() == MAX_SLUG_LENGTH) {
            //If the slug is the maximum length, slugify(String) will truncate the character at the end to ensure
            //there is one character left for a count. All other characters should be the same, however
            checkArgument(slug.startsWith(check) && Character.isDigit(slug.charAt(MAX_GENERATED_SLUG_LENGTH)),
                    "The provided slug is not valid (%s != %s with a trailing digit)", slug, check);
        }
        return slug;
    }

    public abstract static class AbstractBuilder<B extends AbstractBuilder, T extends InternalApplicationUser>
            extends BuilderSupport {

        protected String name;
        protected String slug;

        private int id;

        public AbstractBuilder() {
            id = ApplicationConstants.UNSAVED_ENTITY;
        }

        public AbstractBuilder(@Nonnull InternalApplicationUser user) {
            id = checkNotNull(user, "user").getId();
            name = user.getName();
            slug = user.getSlug();
        }

        @Nonnull
        public abstract T build();

        @Nonnull
        public B id(int value) {
            id = value;

            return self();
        }

        @Nonnull
        public B name(@Nonnull String value) {
            name = checkNotBlank(value, "name");
            slug = slugify(value);

            return self();
        }

        @Nonnull
        public B slug(@Nonnull String value) {
            slug = checkSlug(value);

            return self();
        }

        @Nonnull
        public B regenerateSlug() {
            slug = null;

            return self();
        }

        @Nonnull
        protected abstract B self();
    }

    private static class ToCrowdUserVisitor implements InternalStashUserVisitor<User> {

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

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