package com.atlassian.bitbucket.mesh.repository;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.util.Objects.requireNonNull;

class DefaultBitbucketServerRepository extends AbstractRepository implements BitbucketServerRepository {

    /**
     * Repository IDs are hierarchical resource identifiers that use
     * <ul>
     *   <li>{@code ds} to identify shard (Bitbucket Server {@code DataStore}) ID</li>
     *   <li>{@code h} to identify the repository hierarchy</li>
     *   <li>{@code r} to identify the repository within the hierarchy</li>
     * </ul>
     * Those core properties can be augmented with other suffixes:
     * <ul>
     *   <li>{@code q} to identify a quarantine environment of the repository (optional)</li>
     * </ul>
     * As an example, {@code ds/0/h/3a1561bdf8c5e349ae03/r/2416} corresponds to repository {@code 2416}
     * in hierarchy {@code 3a1561bdf8c5e349ae03}, which is located on the shared home (shard ID = {@code 0}).
     */
    private static final Pattern PATTERN_REPOSITORY_ID = Pattern.compile(
            "ds/(?<shardId>[0-9a-z]+)" +
                    "/h/(?<hierarchyId>[a-z0-9]{20,})" +
                    "/r/(?<bitbucketId>[a-z0-9]+)" +
                    "(?:\\?.*)?"); //Allow query parameters, but no further path entries

    private final String bitbucketId;
    private final String hierarchyId;
    private final String shardId;
    private final String id;

    private Path path;
    private BitbucketServerShard shard;

    DefaultBitbucketServerRepository(@Nonnull String shardId, @Nonnull String hierarchyId, @Nonnull String bitbucketId) {
        this.bitbucketId = requireNonNull(bitbucketId, "bitbucketId");
        this.hierarchyId = requireNonNull(hierarchyId, "hierarchyId");
        this.shardId = requireNonNull(shardId, "shardId");

        id = BitbucketServerRepository.formatId(shardId, hierarchyId, bitbucketId);
    }

    private DefaultBitbucketServerRepository(String id, Matcher matcher) {
        this.id = id;

        bitbucketId = matcher.group("bitbucketId");
        hierarchyId = matcher.group("hierarchyId");
        shardId = matcher.group("shardId");
    }

    @Override
    public <T> T accept(@Nonnull RepositoryVisitor<T> visitor) {
        return requireNonNull(visitor, "visitor").visit(this);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof DefaultBitbucketServerRepository)) {
            return false;
        }
        DefaultBitbucketServerRepository that = (DefaultBitbucketServerRepository) o;
        return bitbucketId.equals(that.bitbucketId) &&
                hierarchyId.equals(that.hierarchyId) &&
                shardId.equals(that.shardId);
    }

    @Override
    public boolean exists() {
        return Files.isDirectory(getPath()) && !Files.exists(getDeletedMarker());
    }

    @Nonnull
    @Override
    public String getBitbucketId() {
        return bitbucketId;
    }

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

    @Nonnull
    @Override
    public Path getHooksDir() {
        checkStorageAttached();

        return shard.getHooksDir();
    }

    @Nonnull
    @Override
    public String getId() {
        return id;
    }

    @Nonnull
    @Override
    public Path getLfsDir() {
        checkStorageAttached();

        return shard.getLfsDir(hierarchyId);
    }

    @Nonnull
    @Override
    public Path getPath() {
        checkStorageAttached();

        return path;
    }

    @Nonnull
    @Override
    public String getSlug() {
        return hierarchyId + "-" + bitbucketId;
    }

    @Nonnull
    @Override
    public Path getWorkTreesDir() {
        return shard.getWorkTreesDir();
    }

    @Override
    public int hashCode() {
        return Objects.hash(bitbucketId, hierarchyId, shardId);
    }

    @Override
    public String toString() {
        return getSlug();
    }

    @Nonnull
    static Path getDeletedMarker(@Nonnull Path repositoryDir) {
        return requireNonNull(repositoryDir, "repositoryDir").resolve("DELETED");
    }

    @Nullable
    static DefaultBitbucketServerRepository parseId(@Nonnull String repositoryId) {
        Matcher matcher = PATTERN_REPOSITORY_ID.matcher(repositoryId);
        if (matcher.matches()) {
            return new DefaultBitbucketServerRepository(repositoryId, matcher);
        }

        return null;
    }

    void attachTo(@Nonnull BitbucketServerShard shard) {
        this.shard = requireNonNull(shard, "shard");

        //The repository path is frequently accessed, so it's resolved up front. Other directories
        //are less likely to be used, so they're resolved lazily
        path = shard.getRepositoriesDir(hierarchyId).resolve(bitbucketId);
    }

    @Nonnull
    Path getDeletedMarker() {
        return getDeletedMarker(path);
    }

    @Nonnull
    String getShardId() {
        return shardId;
    }

    @Nonnull
    @Override
    Path getTemplateDir() {
        checkStorageAttached();

        return shard.getTemplateDir();
    }

    private void checkStorageAttached() {
        if (shard == null) {
            throw new IllegalStateException("Storage is not attached");
        }
    }
}
