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 static java.util.Objects.requireNonNull;

/**
 * A repository that is fully managed by the application.
 * <p>
 * {@code ManagedRepository} provides a few different types of identifiers:
 * <ul>
 *     <li>{@link #id} - the fully qualified ID, of the form {@code p/0123/h/abcdef123783297374924/r/1237910}.</li>
 *     <li>{@link #hierarchyId} - the ID of the hierarchy this repository is part of; {@code abcdef123783297374924}
 *          in the example above.</li>
 *     <li>{@link #localId} - the ID of the repository relative to the hierarchy; {@code 1237910} in the example
 *          above.</li>
 * </ul>
 */
class DefaultManagedRepository extends AbstractRepository implements ManagedRepository {

    private final String id;
    private final String hierarchyId;
    private final String localId;
    private final String partition;

    private Path path;
    private ManagedShard shard;
    private DefaultSharedObjectsRepository sharedObjects;
    private String slug;

    DefaultManagedRepository(@Nonnull String partition, @Nonnull String hierarchyId, @Nonnull String localId) {
        this.hierarchyId = requireNonNull(hierarchyId, "hierarchyId");
        this.localId = requireNonNull(localId, "localId");
        this.partition = requireNonNull(partition, "partition");

        id = ManagedRepository.formatId(partition, hierarchyId, localId);
    }

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

        partition = matcher.group("partition");
        hierarchyId = matcher.group("hierarchy");
        localId = matcher.group("repo");
    }

    @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 DefaultManagedRepository)) {
            return false;
        }

        DefaultManagedRepository that = (DefaultManagedRepository) o;
        return localId.equals(that.localId) &&
                hierarchyId.equals(that.hierarchyId) &&
                partition.equals(that.partition);
    }

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

    @Nonnull
    @Override
    public Path getDirtyMarker() {
        return getSharedObjects().getDirtyMarker(this);
    }

    /**
     * @return the repository hierarchy ID
     */
    @Override
    @Nonnull
    public String getHierarchyId() {
        return hierarchyId;
    }

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

        return shard.getHooksDir();
    }

    /**
     * @return the full repository ID, which includes the hierarchy ID prefix
     */
    @Nonnull
    @Override
    public String getId() {
        return id;
    }

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

        return shard.getLfsDir(partition, hierarchyId);
    }

    /**
     * @return ID that identifies the repository relative to the hierarchy
     */
    @Override
    @Nonnull
    public String getLocalId() {
        return localId;
    }

    @Nonnull
    @Override
    public String getPartition() {
        return partition;
    }

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

        return path;
    }

    @Override
    @Nonnull
    public DefaultSharedObjectsRepository getSharedObjects() {
        if (sharedObjects == null) {
            checkStorageAttached();

            sharedObjects = shard.getSharedObjects(partition, hierarchyId);
            if (attributeManager != null) {
                sharedObjects.attachAttributes(attributeManager);
            }
        }

        return sharedObjects;
    }

    @Nonnull
    @Override
    public String getSlug() {
        if (slug == null) {
            slug = partition + "-" + hierarchyId.replace('-', '_') + "-" + localId.replace('-', '_');
        }
        return slug;
    }

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

    @Override
    public long getVersion() {
        checkAttributesAttached();
        return attributeManager.getVersion(id);
    }

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

    @Override
    public boolean isDirty() {
        return Files.isRegularFile(getDirtyMarker());
    }

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

    @Nonnull
    static Path getDeletedMarker(@Nonnull Path repositoryDir) {
        return requireNonNull(repositoryDir, "repositoryDir")
                .resolveSibling(repositoryDir.getFileName() + ".deleted");
    }

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

        return null;
    }

    @Override
    void attachAttributes(@Nonnull RepositoryAttributeManager attributeManager) {
        super.attachAttributes(attributeManager);

        if (sharedObjects != null) {
            sharedObjects.attachAttributes(attributeManager);
        }
    }

    void attachTo(@Nonnull ManagedShard shard) {
        this.shard = requireNonNull(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(partition, hierarchyId).resolve(localId);
    }

    @Nonnull
    Path getDeletedMarker() {
        checkStorageAttached();

        return getDeletedMarker(path);
    }

    /**
     * @return path to the file that is used to serialize repository create/delete operations per hierarchy
     */
    @Nonnull
    Path getHierarchyLockFile() {
        checkStorageAttached();

        return path.resolveSibling("hierarchy.crud");
    }

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

        return shard.getTemplateDir();
    }

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