package com.atlassian.stash.internal.repository;

import com.atlassian.stash.internal.ApplicationConstants;
import com.atlassian.stash.internal.HibernateUtils;
import com.atlassian.stash.internal.Initializable;
import com.atlassian.stash.internal.project.InternalProject;
import com.atlassian.stash.internal.validation.RepositoryUniqueness;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.validation.ConstraintNature;
import com.atlassian.bitbucket.validation.annotation.RequiredString;
import com.atlassian.bitbucket.validation.groups.Create;
import com.atlassian.bitbucket.validation.groups.Update;
import org.hibernate.annotations.CacheConcurrencyStrategy;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.persistence.*;
import java.text.Normalizer;
import java.util.Locale;
import java.util.Objects;

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

@Cacheable
@Entity
@RepositoryUniqueness(groups = {Create.class, Update.class}, payload = ConstraintNature.Conflict.class)
@Table(name = InternalRepository.TABLE, indexes = {
        @Index(name = "idx_repository_hierarchy_id", columnList = "hierarchy_id"),
        @Index(name = "idx_repository_project_id", columnList = "project_id"),
        @Index(name = "idx_repository_state", columnList = "state")
}, uniqueConstraints = {
        @UniqueConstraint(name = "uk_slug_project_id", columnNames = {"slug", "project_id"})
})
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class InternalRepository implements Initializable, Repository {

    public static final String TABLE = "repository";
    public static final int TOKEN_LENGTH = 20;

    private static final String ID_GEN = "repoIdGenerator";

    @Column(name = "is_forkable", nullable = false)
    private final boolean forkable;

    @Column(name = "hierarchy_id", length = TOKEN_LENGTH, nullable = false, updatable = false)
    @RequiredString(size = TOKEN_LENGTH)
    private final String hierarchyId;

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

    @Column(name = "name")
    private final String name;

    @OneToOne(fetch = FetchType.LAZY, targetEntity = InternalRepository.class)
    @JoinTable(name = "sta_repo_origin",
            joinColumns = @JoinColumn(name = "repository_id", nullable = false),
            inverseJoinColumns = @JoinColumn(name="origin_id", nullable = false))
    private final InternalRepository origin;

    @ManyToOne(fetch = FetchType.LAZY, optional = false, targetEntity = InternalProject.class)
    @JoinColumn(name="project_id", nullable = false)
    private InternalProject project; //Yes, not final. See initialize()

    @Column(name = "is_public", nullable = false)
    private final boolean publiclyAccessible;

    @Column(name = "scm_id", nullable = false, updatable = false)
    private final String scmId;

    @Column(name = "slug")
    private final String slug;

    @Column(name = "state", nullable = false)
    @org.hibernate.annotations.Type(type = "com.atlassian.hibernate.extras.type.GenericEnumUserType", parameters =
            @org.hibernate.annotations.Parameter(name = "enumClass", value = "com.atlassian.bitbucket.repository.Repository$State"))
    private final State state;

    // for hibernate
    protected InternalRepository() {
        forkable = true;
        hierarchyId = null;
        id = ApplicationConstants.UNSAVED_ENTITY;
        name = null;
        origin = null;
        publiclyAccessible = false;
        scmId = null;
        slug = null;
        state = null;
    }

    private InternalRepository(Integer id, InternalProject project, String slug, String name, String scmId,
                               String hierarchyId, InternalRepository origin, State state, boolean forkable, boolean publiclyAccessible) {
        this.forkable = forkable;
        this.hierarchyId = hierarchyId;
        this.id = id;
        this.name = name;
        this.origin = origin;
        this.project = project;
        this.publiclyAccessible = publiclyAccessible;
        this.scmId = scmId;
        this.slug = slug == null ? slugify(name) : slug;
        this.state = state == null ? State.INITIALISING : state;
    }

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

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o instanceof InternalRepository) {
            InternalRepository that = (InternalRepository) o;

            return Objects.equals(getId(), that.getId());
        }

        return false;
    }

    @Nonnull
    @Override
    public String getHierarchyId() {
        return hierarchyId;
    }

    public int getId() {
        return id;
    }

    @Nonnull
    @Override
    public String getName() {
        return name;
    }

    @Override
    public InternalRepository getOrigin() {
        return origin;
    }

    @Nonnull
    @Override
    public InternalProject getProject() {
        return project;
    }

    @Nonnull
    @Override
    public String getScmId() {
        return scmId;
    }

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

    @Nonnull
    @Override
    public State getState() {
        return state;
    }

    @Nonnull
    @Override
    public String getStatusMessage() {
        return getState().getStatusMessage();
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(getId());
    }

    @Override
    public void initialize() {
        //Project may be an InternalPersonalProject _proxied as an InternalProject_. This causes multiple issues:
        //- JsonRenderer/JsonSurrogate will marshal a RestProject instead of a RestPersonalProject
        //- Plugin coders are bound to try and cast the project to PersonalProject, which will fail
        //To get around all of these issues, if the project is a proxy it is unwrapped to the real class and the
        //field is _replaced_ with the unwrapped value
        //
        //Note: This is a _hack_, not a design paradigm. It works around a specific problem in this instance. After
        //      2.4 a better, more generic, approach will be implemented.
        project = HibernateUtils.initialize(getProject());

        //Recursively initialize origins. This needs to be done using our own initialize method, not Hibernate's, in
        //order to recursively initialize up the origin chain and pick up projects along the way
        HibernateUtils.initialize(getOrigin());
    }

    @Override
    public boolean isFork() {
        return getOrigin() != null;
    }

    @Override
    public boolean isForkable() {
        return forkable;
    }

    @Override
    public boolean isPublic() {
        return publiclyAccessible;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        if (getProject().getNamespace() != null) {
            builder.append(getProject().getNamespace()).append('/');
        }
        return builder.append(getProject().getKey()).append('/')
                .append(getSlug())
                .append('[').append(getId()).append(']').toString();
    }

    /**
     * Slugifies a repository name.
     *
     * First the name is normalised to NFKD form; then any character not in the basic latin character set is removed;
     * then any character other than alpha numerics, '-', '_'; and '.' are mapped to '-'; and finally the string is
     * made lower case.
     *
     * Note that this slugification process may result in an empty string if, for instance, the supplied name
     * contains all characters outside the basic latin character set.
     *
     * @param name the repository name
     * @return the slugified version of the name
     */
    public static String slugify(String name) {
        return Normalizer.normalize(name, Normalizer.Form.NFKD)
                .replaceAll("[^\\p{InBasicLatin}]+", "")
                .replaceAll("[^a-zA-Z\\-_0-9\\.]+", "-")
                .toLowerCase(Locale.US);
    }

    public static class Builder {

        private boolean forkable;
        private String hierarchyId;
        private int id;
        private String name;
        private InternalRepository origin;
        private InternalProject project;
        private boolean publiclyAccessible;
        private String scmId;
        private String slug;
        private State state;

        public Builder() {
            forkable = true; //By default, all repositories are forkable
            id = ApplicationConstants.UNSAVED_ENTITY;
        }

        public Builder(@Nonnull InternalRepository repository) {
            forkable = checkNotNull(repository, "repository").isForkable();
            hierarchyId = repository.getHierarchyId();
            id = repository.getId();
            name = repository.getName();
            origin = repository.getOrigin();
            project = repository.getProject();
            publiclyAccessible = repository.isPublic();
            scmId = repository.getScmId();
            slug = repository.getSlug();
            state = repository.getState();
        }

        @Nonnull
        public InternalRepository build() {
            return new InternalRepository(id, project, slug, name, scmId, hierarchyId, origin, state, forkable, publiclyAccessible);
        }

        @Nonnull
        public Builder forkable(boolean value) {
            forkable = value;

            return this;
        }

        @Nonnull
        public Builder hierarchyId(@Nonnull String value) {
            checkArgument(!checkNotNull(value, "value").trim().isEmpty(), "A non-blank hierarchy ID is required");

            hierarchyId = value;

            return this;
        }

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

            return this;
        }

        @Nonnull
        public Builder name(@Nonnull String value) {
            checkArgument(!checkNotNull(value, "value").trim().isEmpty(), "A non-blank name is required");

            name = value;
            slug = slugify(value);

            return this;
        }

        @Nonnull
        public Builder origin(@Nullable InternalRepository value) {
            origin = value;

            return this;
        }

        @Nonnull
        public Builder project(@Nonnull InternalProject value) {
            project = checkNotNull(value, "value");

            return this;
        }

        @Nonnull
        public Builder publiclyAccessible(boolean value) {
            publiclyAccessible = value;

            return this;
        }

        @Nonnull
        public Builder scmId(@Nonnull String value) {
            checkArgument(!checkNotNull(value, "value").trim().isEmpty(), "A non-blank SCM ID is required");

            scmId = value;

            return this;
        }

        @Nonnull
        public Builder state(@Nonnull State value) {
            state = checkNotNull(value, "value");

            return this;
        }
    }
}
