package com.atlassian.stash.internal.nav;

import com.atlassian.event.api.EventListener;
import com.atlassian.plugin.spring.AvailableToPlugins;
import com.atlassian.bitbucket.content.Path;
import com.atlassian.bitbucket.content.SimplePath;
import com.atlassian.bitbucket.event.request.RequestStartedEvent;
import com.atlassian.stash.internal.ApplicationConstants;
import com.atlassian.stash.internal.annotation.NotProfiled;
import com.atlassian.stash.internal.concurrent.StatefulService;
import com.atlassian.stash.internal.concurrent.TransferableState;
import com.atlassian.stash.internal.project.InternalPersonalProject;
import com.atlassian.bitbucket.nav.NavBuilder;
import com.atlassian.bitbucket.pull.PullRequestDirection;
import com.atlassian.bitbucket.pull.PullRequestState;
import com.atlassian.bitbucket.repository.Branch;
import com.atlassian.bitbucket.repository.Ref;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryVisibility;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.UserType;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriUtils;

import javax.annotation.Nonnull;
import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.Arrays.asList;

@AvailableToPlugins(NavBuilder.class)
@Component("navBuilder")
public class NavBuilderImpl implements NavBuilder, StatefulService {

    private static final Function<String, String> ENCODE_PATH = new Function<String, String>() {
        @Override
        public String apply(String component) {
            try {
                return UriUtils.encodePathSegment(component, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            }
        }
    };

    private static final String ABOUT = "about";
    private static final String ACTION = "action";
    private static final String ACTIVITY_ID = "activityId";
    private static final String ADMIN = "admin";
    private static final String API = "api";
    private static final String APPLICATION_LINKS = "applinks";
    private static final String AT = "at";
    private static final String ATTACHMENTS = "attachments";
    private static final String AUTHENTICATION = "authentication";
    private static final String AVATARS = "avatars";
    private static final String AVATAR = "avatar";
    private static final String AVATAR_EXTENSION = ".png";
    private static final String BOTS = "bots";
    private static final String BRANCHES = "branches";
    private static final String BROWSE = "browse";
    private static final String CAPTCHA = "captcha";
    private static final String CLUSTERING = "clustering";
    private static final String COMMENT_ID = "commentId";
    private static final String COMMITS = "commits";
    private static final String COMPARE = "compare";
    private static final String CREATE = "create";
    private static final String DB = "db";
    private static final String DELETE = "delete";
    private static final String DIFF = "diff";
    private static final String DIRECTION = "direction";
    private static final String EDIT = "edit";
    private static final String EMBEDDED = "embedded";
    private static final String FORK = "fork";
    private static final String GROUPS = "groups";
    private static final String HOOKS = "hooks";
    private static final String I18N = "i18n";
    private static final String LICENSE = "license";
    private static final String LIMIT = "limit";
    private static final String LIST_APPLICATION_LINKS = "listApplicationLinks";
    private static final String LOGIN = "login";
    private static final String LOGGING = "logging";
    private static final String MAIL_SERVER = "mail-server";
    private static final String MVC = "mvc";
    private static final String NOTIFICATION = "notification";
    private static final String OAUTH = "oauth";
    private static final String OVERVIEW = "overview";
    private static final String PASSWORDRESET = "passwordreset";
    private static final String PERMISSIONS = "permissions";
    private static final String PLUGINS = "plugins";
    private static final String PROFILE = "profile";
    private static final String PROJECTS = "projects";
    private static final String PULL_REQUESTS = "pull-requests";
    private static final String REPOS = "repos";
    private static final String REST = "rest";
    private static final String RETRIEVE_PASSWORD = "retrievePassword";
    private static final String SECURITY = "security";
    private static final String SERVER_SETTINGS = "server-settings";
    private static final String SERVLET = "servlet";
    private static final String SETTINGS = "settings";
    private static final String SETUP = "setup";
    private static final String SIGNUP = "signup";
    private static final String SSH_SECTION_HASH = "ssh";
    private static final String START = "start";
    private static final String STATE = "state";
    private static final String SYSTEM_INFO = "system-info";
    private static final String TAGS = "tags";
    private static final String TMP = "tmp";
    private static final String TOKENS = "access-tokens";
    private static final String UNWATCH = "unwatch";
    private static final String UPM = "upm";
    private static final String USERS = "users";
    private static final String VIEW = "view";
    private static final String XSRF_NOTIFICATION = "xsrfNotification";
    private static final String GETTING_STARTED = "getting-started";
    private static final String UPGRADE_ONBOARDING = "upgrade-onboarding";
    private static final String ACCOUNT = "account";
    private static final String PASSWORD = "password";
    private static final String DASHBOARD = "dashboard";

    private static final Logger log = LoggerFactory.getLogger(NavBuilderImpl.class);

    private final ApplicationPropertiesService applicationPropertiesService;
    private final ThreadLocal<RequestConfiguration> requestConfiguration;

    @Autowired
    public NavBuilderImpl(ApplicationPropertiesService applicationPropertiesService) {
        this.applicationPropertiesService = applicationPropertiesService;
        this.requestConfiguration = new ThreadLocal<>();
    }

    @EventListener
    public void onRequestStarted(RequestStartedEvent event) {
        if (event.isHttp()) {
            HttpServletRequest request = (HttpServletRequest) event.getRequestContext().getRawRequest();
            URI uri;
            try {
                int port = request.getServerPort();
                if (("http".equals(request.getScheme()) && port == 80) || ("https".equals(request.getScheme()) && port == 443)) {
                    // default port for the scheme, don't make it explicit in the URL
                    port = -1;
                }
                uri = new URI(request.getScheme(), null, request.getServerName(), port, request.getContextPath(), null, null);
            } catch (URISyntaxException e) {
                log.error("Failed to construct a URI for the NavBuilder from the current requests. Defaulting to application properties.", e);
                uri = getBaseUrl();
            }
            String requestUrl = request.getRequestURI();
            String queryString = request.getQueryString();
            requestConfiguration.set(new RequestConfiguration(uri, "true".equals(request.getParameter(USE_BASE_URL_TOKEN)), requestUrl, queryString));
            event.getRequestContext().addCleanupCallback(new Runnable() {
                @Override
                public void run() {
                    requestConfiguration.remove();
                }
            });
        }
    }

    private URI buildBaseUri() {
        RequestConfiguration config = requestConfiguration.get();
        return config != null ? config.getRequestBaseUri() : getBaseUrl();
    }

    @Override
    public String buildBaseUrl() {
        return buildBaseUri().toASCIIString();
    }

    protected String buildAbsoluteUrl(URI defaultBaseUrl) {
        RequestConfiguration config = requestConfiguration.get();
        if (config != null && config.isBaseUriTokenRequested()) {
            return BASE_URL_TOKEN;
        }
        if (defaultBaseUrl == null) {
            return null;
        }
        return defaultBaseUrl.toASCIIString();
    }

    @Override
    public String buildAbsolute() {
        return buildAbsoluteUrl(buildBaseUri());
    }

    @Override
    public String buildConfigured() {
        URI baseUrl = getBaseUrl();
        // BaseUrl can be null during installation (used by nav-links)
        return buildAbsoluteUrl(baseUrl != null ? baseUrl : buildBaseUri());
    }

    @Override
    public String buildRelative() {
        return buildBaseUri().getRawPath();
    }

    @Nonnull
    @Override
    @NotProfiled
    public TransferableState getState() {
        return new TransferableState() {
            private final RequestConfiguration configuration = requestConfiguration.get();

            @Override
            public void apply() {
                requestConfiguration.set(configuration);
            }

            @Override
            public void remove() {
                requestConfiguration.remove();
            }
        };
    }

    @Override
    public Project project(@Nonnull String key) {
        return new ProjectImpl(key);
    }

    @Override
    public Project project(@Nonnull com.atlassian.bitbucket.project.Project project) {
        return new ProjectImpl(project.getKey());
    }

    @Override
    public Repo repo(@Nonnull Repository repository) {
        return new RepoImpl(repository.getProject().getKey(), repository.getSlug());
    }

    @Override
    public Login login() {
        RequestConfiguration config = requestConfiguration.get();
        return new LoginImpl(config);
    }

    @Override
    public Builder<?> logout() {
        return new SimpleBuilder(components(ApplicationConstants.URL_SECURITY_LOGOUT));
    }

    @Override
    public Builder<?> signup() {
        return new SimpleBuilder(components(SIGNUP));
    }

    @Override
    public Builder<?> captcha() {
        //Includes a changing query param to ensure all browsers reload the image when refreshing it;
        //some don't respect the caching HTTP headers
        return new SimpleBuilder(components(CAPTCHA))
                .withParam("ts", Long.toString(System.currentTimeMillis()));
    }

    @Override
    public Builder<?> retrievePassword() {
        return new SimpleBuilder(components(LOGIN, RETRIEVE_PASSWORD));
    }

    @Override
    public Builder<?> passwordReset() {
        return new SimpleBuilder(components(PASSWORDRESET));
    }

    @Override
    public Profile profile() {
        return new CurrentUserProfileImpl();
    }

    @Override
    public Profile userBySlug(String slug, UserType type) {
        return type == UserType.SERVICE ? new ServiceUserProfileImpl(slug) : new UserProfileImpl(slug);
    }

    @Override
    public Profile user(ApplicationUser user) {
        return user.getType() == UserType.SERVICE ?
                new ServiceUserProfileImpl(user.getSlug()) :
                new UserProfileImpl(user.getSlug());
    }

    @Override
    public Builder<?> securityCheck() {
        return new SimpleBuilder(components(ApplicationConstants.URL_SECURITY_CHECK));
    }

    @Override
    public Builder<?> xsrfNotification() {
        return new SimpleBuilder(components(MVC, XSRF_NOTIFICATION));
    }

    @Override
    public ListProjects allProjects() {
        return new ListProjectsImpl();
    }

    @Override
    public GlobalListRepos allRepos() {
        return new GlobalListReposImpl();
    }

    @Override
    public CreateProject createProject() {
        return new CreateProjectImpl();
    }

    @Override
    public UserAccount account() {
        return new UserAccountImpl();
    }

    @Override
    public Admin admin() {
        return new AdminImpl();
    }

    @Override
    public Builder<?> about() {
        return new SimpleBuilder(components(ABOUT));
    }

    @Override
    public PluginServlets pluginServlets() {
        return new PluginServletsImpl();
    }

    @Override
    public Rest rest() {
        return new RestImpl();
    }

    @Override
    public Builder<?> setup() {
        return new SimpleBuilder(components(SETUP));
    }

    @Override
    public Tmp tmp() {
        return new TmpImpl();
    }

    @Nonnull
    public Addons addons() {
        return new AddonsImpl();
    }

    @Override
    public GettingStarted gettingStarted() {
        return new GettingStartedImpl(requestConfiguration.get());
    }

    @Override
    public UpgradeOnboarding upgradeOnboarding() {
        return new UpgradeOnboardingImpl(requestConfiguration.get());
    }

    @Override
    public Dashboard dashboard() {
        return new DashboardImpl();
    }

    abstract class AbstractBuilder<B extends Builder<B>> implements Builder<B> {

        protected Map<String, String> params;
        protected String anchor;

        protected AbstractBuilder() {
            this(Collections.<String, String>emptyMap());
        }

        protected AbstractBuilder(Map<String, String> params) {
            this.params = params;
        }

        public Builder<B> withParam(@Nonnull String name, String value) {
            // copy, so same builder can be used to define multiple links under some common path
            params = Maps.newLinkedHashMap(params);
            params.put(name, value);
            return self();
        }

        public B withAnchor(@Nonnull String anchor) {
            this.anchor = anchor;
            return self();
        }

        public B withAnchor(@Nonnull Path path) {
            return withAnchor(StringUtils.join(asList(path.getComponents()), '/'));
        }

        @Override
        public String buildRelative() {
            return NavBuilderImpl.this.buildRelative() + buildRelNoContext();
        }

        @Override
        public String buildAbsolute() {
            return NavBuilderImpl.this.buildAbsolute() + buildRelNoContext();
        }

        @Override
        public String buildConfigured() {
            return NavBuilderImpl.this.buildConfigured() + buildRelNoContext();
        }

        @Override
        public String buildRelNoContext() {
            String path = StringUtils.join(buildPath(), '/');
            path = path.isEmpty() ? path : "/" + path;
            String queryString = buildQueryString();
            try {
                return path
                        + (StringUtils.isNotEmpty(queryString) ? '?' + queryString : "")
                        + (StringUtils.isNotEmpty(anchor) ? '#' + UriUtils.encodeFragment(anchor, "UTF-8").replace("%23", "#") : "");
            } catch (UnsupportedEncodingException e) {
                throw new IllegalStateException("UTF-8 should be a supported encoding", e);
            }
        }

        public abstract List<String> buildPath();

        protected String buildQueryString() {
            try {
                StringBuilder query = new StringBuilder("");
                boolean firstParam = true;
                for (Map.Entry<String, String> entry : params.entrySet()) {
                    if (entry.getKey().length() > 0) {
                        if (!firstParam) {
                            query.append("&");
                        } else {
                            firstParam = false;
                        }
                        query.append(UriUtils.encodeQuery(entry.getKey(), "UTF-8"));
                        if (entry.getValue() != null && entry.getValue().length() > 0) {
                            query.append("=");
                            query.append(UriUtils.encodeQueryParam(entry.getValue(), "UTF-8"));
                        }
                    }
                }

                return query.length() == 0 ? null : query.toString();
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            }
        }

        protected List<String> encode(List<String> components) {
            return Lists.transform(components, ENCODE_PATH);
        }

        protected List<String> encode(String... components) {
            return encode(Arrays.asList(components));
        }

        protected List<String> parentComponents(AbstractBuilder<?> parent, String... others) {
            return append(parent.buildPath(), others);
        }

        @Nonnull
        @SuppressWarnings("unchecked")
        protected B self() {
            return (B) this;
        }
    }

    private class SimpleBuilder extends AbstractBuilder<SimpleBuilder> {

        private final List<String> components;

        public SimpleBuilder(List<String> components) {

            this.components = components;
        }

        @Override
        public List<String> buildPath() {
            return encode(components);
        }
    }

    abstract class AbstractPathBuilder<B extends PathBuilder<B>> extends AbstractBuilder<B> implements PathBuilder<B> {
        protected SimplePath path = new SimplePath("");

        AbstractPathBuilder(Map<String, String> params) {
            super(params);
        }

        @Override
        public PathBuilder<B> path(@Nonnull String... components) {
            return path(new SimplePath(SimplePath.ROOT, components));
        }

        @Override
        public PathBuilder<B> path(@Nonnull Path path) {
            this.path = new SimplePath(path);
            return this;
        }

        protected List<String> encode(List<String> components) {
            List<String> componentsList = new ArrayList<>(components.size() + path.getNumComponents());
            componentsList.addAll(components);
            componentsList.addAll(asList(path.getComponents()));
            return super.encode(componentsList);
        }
    }

    abstract class AbstractPagedBuilder<B extends PagedBuilder<B>> extends AbstractBuilder<B> implements PagedBuilder<B> {
        protected AbstractPagedBuilder() {
            super();
        }

        protected AbstractPagedBuilder(Map<String, String> params) {
            super(params);
        }

        @Override
        public PagedBuilder<B> withLimit(int limit) {
            return (PagedBuilder<B>) withParam(LIMIT, Integer.toString(limit));
        }

        @Override
        public PagedBuilder<B> startAt(int index) {
            return (PagedBuilder<B>) withParam(START, Integer.toString(index));
        }
    }

    private class SimplePagedBuilder extends AbstractPagedBuilder<SimplePagedBuilder> {

        private final List<String> components;

        public SimplePagedBuilder(List<String> components) {

            this.components = components;
        }

        @Override
        public List<String> buildPath() {
            return encode(components);
        }
    }

    class CurrentUserProfileImpl extends AbstractBuilder<Profile> implements Profile {
        @Override
        public List<String> buildPath() {
            return encode(components(PROFILE));
        }

        @Override
        public Builder<?> avatar(final int size) {
            return new SimpleBuilder(components(PROFILE, AVATAR + AVATAR_EXTENSION))
                    .withParam("s", String.valueOf(size));
        }

        @Override
        public Builder<?> listSshKeys() {
            withAnchor(SSH_SECTION_HASH);
            return this;
        }
    }

    class ServiceUserProfileImpl extends AbstractBuilder<Profile> implements Profile {
        private final String slug;

        ServiceUserProfileImpl(String slug) {
            this.slug = slug;
        }

        @Override
        public Builder<?> avatar(int size) {
            return new SimpleBuilder(components(BOTS, slug, AVATAR + AVATAR_EXTENSION))
                    .withParam("s", String.valueOf(size));
        }

        @Override
        public List<String> buildPath() {
            return encode(components(BOTS, slug));
        }

        @Override
        public Builder<?> listSshKeys() {
            return withAnchor(SSH_SECTION_HASH);
        }
    }

    class UserProfileImpl extends AbstractBuilder<Profile> implements Profile {
        private final String slug;

        UserProfileImpl(String slug) {
            this.slug = slug;
        }

        @Override
        public Builder<?> avatar(final int size) {
            return new SimpleBuilder(components(USERS, slug, AVATAR + AVATAR_EXTENSION))
                    .withParam("s", String.valueOf(size));
        }

        @Override
        public List<String> buildPath() {
            return encode(components(USERS, slug));
        }

        @Override
        public Builder<?> listSshKeys() {
            withAnchor(SSH_SECTION_HASH);
            return this;
        }
    }

    /**
     * Base class of builders for urls anchored off /projects/PROJ/repos/REPO to help DRY up some code
     */
    abstract class RepoBuilderImpl<B extends Builder<B>> extends AbstractBuilder<B> implements Builder<B> {
        protected final String projectKey;
        protected final String repoSlug;

        protected RepoBuilderImpl(String projectKey, String repoSlug, Map<String, String> params) {
            super(params);
            this.projectKey = projectKey;
            this.repoSlug = repoSlug;
        }
    }

    /**
     * Base class of path builders for urls anchored off /projects/PROJ/repos/REPO to help DRY up some code
     */
    abstract class RepoPathBuilderImpl<B extends PathBuilder<B>> extends AbstractPathBuilder<B> implements PathBuilder<B> {
        protected final String projectKey;
        protected final String repoSlug;

        RepoPathBuilderImpl(String projectKey, String repoSlug, Map<String, String> params) {
            super(params);
            this.projectKey = projectKey;
            this.repoSlug = repoSlug;
        }
    }

    /**
     * Base class of paged builders for urls anchored off /projects/PROJ/repos/REPO to help DRY up some code
     */
    abstract class RepoPagedBuilderImpl<B extends PagedBuilder<B>> extends AbstractPagedBuilder<B> implements PagedBuilder<B> {
        protected final String projectKey;
        protected final String repoSlug;

        RepoPagedBuilderImpl(String projectKey, String repoSlug, Map<String, String> params) {
            super(params);
            this.projectKey = projectKey;
            this.repoSlug = repoSlug;
        }
    }

    class AdminImpl extends AbstractBuilder<Admin> implements Admin {
        @Override
        public List<String> buildPath() {
            return encode(adminComponents());
        }

        @Override
        public Users users() {
            return new UsersImpl();
        }

        @Override
        public Groups groups() {
            return new GroupsImpl();
        }

        @Override
        public Builder<?> security() {
            return new SimpleBuilder(adminComponents(SECURITY));
        }

        @Override
        public Builder<?> plugins() {
            return new SimpleBuilder(adminComponents(PLUGINS));
        }

        @Override
        public Builder<?> settings() {
            return new SimpleBuilder(adminComponents(SETTINGS));
        }

        @Override
        public Builder<?> permissions() {
            return new SimpleBuilder(adminComponents(PERMISSIONS));
        }

        @Override
        public Builder<?> avatars() {
            return new SimpleBuilder(adminComponents(AVATARS));
        }

        @Override
        public Builder<?> authentication() {
            return new SimpleBuilder(adminComponents(AUTHENTICATION));
        }

        @Override
        public Builder<?> mailServer() {
            return new SimpleBuilder(adminComponents(MAIL_SERVER));
        }

        @Override
        public Builder<?> i18n() {
            return new SimpleBuilder(adminComponents(I18N));
        }

        @Override
        public Database database() {
            return new DatabaseImpl();
        }

        @Override
        public Licensing licensing() {
            return new LicensingImpl();
        }

        @Override
        public Builder<?> appLinks() {
            return new SimpleBuilder(pluginsServletComponents(APPLICATION_LINKS, LIST_APPLICATION_LINKS));
        }

        @Override
        public Builder<?> logging() {
            return new SimpleBuilder(adminComponents(LOGGING));
        }

        @Override
        public Builder<?> serverSettings() {
            return new SimpleBuilder(adminComponents(SERVER_SETTINGS));
        }

        @Override
        public Builder<?> sysInfo() {
            return new SimpleBuilder(adminComponents(SYSTEM_INFO));
        }

        public Builder<?> clustering() {
            return new SimpleBuilder(adminComponents(CLUSTERING));
        }
    }

    class DatabaseImpl extends AbstractBuilder<Database> implements Database {
        @Override
        public List<String> buildPath() {
            return encode(databaseComponents());
        }

        @Override
        public Builder<?> migrate() {
            return new SimpleBuilder(databaseComponents(EDIT));
        }
    }

    class LicensingImpl extends AbstractBuilder<Licensing> implements Licensing {
        @Override
        public List<String> buildPath() {
            return encode(licensingComponents());
        }

        @Override
        public Builder<?> edit() {
            return new SimpleBuilder(licensingComponents())
                    .withParam(EDIT, null);
        }
    }

    class UsersImpl extends AbstractPagedBuilder<Users> implements Users {

        @Override
        public List<String> buildPath() {
            return encode(usersComponents());
        }

        @Override
        public Builder<?> create() {
            return new SimpleBuilder(usersComponents())
                    .withParam(CREATE, null);
        }

        @Override
        public DeleteUser delete(String username) {
            return new DeleteUserImpl(username);
        }

        @Override
        public ViewUser view(String username) {
            return new ViewUserImpl(username);
        }

        @Override
        public Users filter(String filter) {
            withParam("filter", filter);
            return this;
        }

        @Override
        public Builder<?> captcha(String username) {
            return new SimpleBuilder(usersComponents(CAPTCHA))
                    .withParam("name", username);
        }
    }

    class ViewUserImpl extends AbstractBuilder<ViewUser> implements ViewUser {
        ViewUserImpl(String username) {
            super(Collections.singletonMap("name", username));
        }

        @Override
        public List<String> buildPath() {
            return encode(usersComponents(VIEW));
        }

        @Override
        public Builder<?> listSshKeys() {
            withAnchor(SSH_SECTION_HASH);
            return this;
        }
    }

    class DeleteUserImpl extends AbstractBuilder<DeleteUser> implements DeleteUser {
        DeleteUserImpl(String username) {
            super(Collections.singletonMap("name", username));
        }

        @Override
        public List<String> buildPath() {
            return encode(usersComponents(DELETE));
        }
    }

    class GroupsImpl extends AbstractPagedBuilder<Groups> implements Groups {

        @Override
        public Builder<?> create() {
            return new SimpleBuilder(groupsComponents())
                    .withParam(CREATE, null);
        }

        @Override
        public DeleteGroup delete(String groupName) {
            return new DeleteGroupImpl(groupName);
        }

        @Override
        public List<String> buildPath() {
            return encode(groupsComponents());
        }

        @Override
        public ViewGroup view(String name) {
            return new ViewGroupImpl(name);
        }

        @Override
        public Groups filter(String filter) {
            withParam("filter", filter);
            return this;
        }
    }

    class DeleteGroupImpl extends AbstractBuilder<DeleteGroup> implements DeleteGroup {

        DeleteGroupImpl(String username) {
            super(Collections.singletonMap("name", username));
        }

        @Override
        public List<String> buildPath() {
            return encode(groupsComponents());
        }
    }

    class ViewGroupImpl extends AbstractBuilder<ViewGroup> implements ViewGroup {
        ViewGroupImpl(String username) {
            super(Collections.singletonMap("name", username));
        }

        @Override
        public List<String> buildPath() {
            return encode(groupsComponents(VIEW));
        }
    }

    class PluginServletsImpl extends AbstractPathBuilder<PluginServlets> implements PluginServlets {
        PluginServletsImpl() {
            super(Collections.<String, String>emptyMap());
        }

        @Override
        public List<String> buildPath() {
            return encode(pluginsServletComponents());
        }

        @Override
        public Builder<?> oauthTokens() {
            return new SimpleBuilder(pluginsServletComponents(OAUTH, USERS, TOKENS));
        }

        @Override
        public Builder<?> notificationSettings() {
            return new SimpleBuilder(pluginsServletComponents(NOTIFICATION, SETTINGS));
        }
    }

    class RestImpl extends AbstractBuilder<Rest> implements Rest {

        @Override
        public List<String> buildPath() {
            return encode(restComponents());
        }

        @Override
        public RestAdmin admin() {
            return new RestAdminImpl();
        }

        @Override
        public RestUsers users() {
            return new RestUsersImpl();
        }

        @Override
        public RestUser users(String slug) {
            return new RestUserImpl(checkNotNull(slug, "slug"));
        }

        @Override
        public Builder<?> pullRequest(final long id) {
            return new SimpleBuilder(restComponents(PULL_REQUESTS, Long.toString(id)));
        }

        @Override
        public RestHooks hooks() {
            return new RestHooksImpl(this);
        }
    }

    class RestAdminImpl extends AbstractBuilder<RestAdmin> implements RestAdmin {
        @Override
        public List<String> buildPath() {
            return encode(restComponents(ADMIN));
        }

        @Override
        public Builder<?> users() {
            return new SimpleBuilder(restComponents(ADMIN, USERS));
        }
    }

    class RestHooksImpl extends AbstractBuilder<RestHooks> implements RestHooks {
        private final AbstractBuilder<?> parent;

        public RestHooksImpl(AbstractBuilder<?> parent) {
            this.parent = parent;
        }

        @Override
        public List<String> buildPath() {
            return parentComponents(parent, HOOKS);
        }

        @Override
        public RestHook hook(@Nonnull String hookKey) {
            return new RestHookImpl(this, checkNotNull(hookKey, "Must supply a hookKey"));
        }
    }

    class RestHookImpl extends AbstractBuilder<RestHook> implements RestHook {
        private final AbstractBuilder<?> parent;
        private final String hookKey;

        public RestHookImpl(AbstractBuilder<?> parent, String hookKey) {
            this.parent = parent;
            this.hookKey = hookKey;
        }

        @Override
        public List<String> buildPath() {
            return parentComponents(parent, hookKey);
        }

        @Override
        public Builder<?> avatar(final String version) {
            SimpleBuilder builder = new SimpleBuilder(parentComponents(this, "avatar"));
            if (version != null) {
                builder.withParam("version", version);
            }

            return builder;
        }
    }

    class RestUsersImpl extends AbstractBuilder<RestUsers> implements RestUsers {
        @Override
        public List<String> buildPath() {
            return encode(restComponents(USERS));
        }
    }

    class RestUserImpl extends AbstractBuilder<RestUser> implements RestUser {
        private final String slug;

        public RestUserImpl(String slug) {
            this.slug = slug;
        }

        @Override
        public List<String> buildPath() {
            return encode(restComponents(USERS, slug));
        }

        @Override
        public Builder<?> avatar() {
            return new SimpleBuilder(restComponents(USERS, slug, AVATAR + AVATAR_EXTENSION));
        }
    }

    class ListProjectsImpl extends AbstractPagedBuilder<ListProjects> implements ListProjects {
        public List<String> buildPath() {
            return encode(projectsComponents());
        }
    }

    class GlobalListReposImpl extends AbstractBuilder<GlobalListRepos> implements GlobalListRepos {
        @Override
        public List<String> buildPath() {
            return encode(Collections.singletonList(REPOS));
        }

        @Override
        public GlobalListRepos privateVisibility() {
            return visibility(RepositoryVisibility.PRIVATE);
        }

        @Override
        public GlobalListRepos publicVisibility() {
            return visibility(RepositoryVisibility.PUBLIC);
        }

        @Override
        public GlobalListRepos visibility(RepositoryVisibility visibility) {
            if (visibility != null) {
                withParam("visibility", visibility.name().toLowerCase(Locale.US));
            }
            return this;
        }
    }

    class CompareImpl extends RepoPathBuilderImpl<Compare> implements Compare {
        CompareImpl(String projectKey, String repoSlug, Map<String, String> params) {
            super(projectKey, repoSlug, params);
        }

        @Override
        public List<String> buildPath() {
            // default include /commits
            return encode(append(repoComponents(projectKey, repoSlug, COMPARE), "commits"));
        }

        @Override
        public CommitsBuilder commits() {
            return new CommitsBuilderImpl();
        }

        @Override
        public DiffBuilder diff() {
            return new DiffBuilderImpl();
        }

        // Would be nice to share these with CompareBuilderImpl :(

        @Override
        public CompareImpl sourceBranch(@Nonnull String sourceBranch) {
            withParam("sourceBranch", sourceBranch);
            return this;
        }

        @Override
        public CompareImpl targetBranch(@Nonnull String targetBranch) {
            withParam("targetBranch", targetBranch);
            return this;
        }

        abstract class CompareBuilderImpl<T extends CompareBuilder<T>> extends AbstractBuilder<T> implements CompareBuilder<T> {

            @Override
            public T sourceBranch(@Nonnull String sourceBranch) {
                withParam("sourceBranch", sourceBranch);
                return self();
            }

            @Override
            public T targetBranch(@Nonnull String targetBranch) {
                withParam("targetBranch", targetBranch);
                return self();
            }
        }

        class CommitsBuilderImpl extends CompareBuilderImpl<CommitsBuilder> implements CommitsBuilder {

            @Override
            public List<String> buildPath() {
                return encode(append(repoComponents(projectKey, repoSlug, COMPARE), "commits"));
            }
        }

        class DiffBuilderImpl extends CompareBuilderImpl<DiffBuilder> implements DiffBuilder {

            @Override
            public List<String> buildPath() {
                return encode(append(repoComponents(projectKey, repoSlug, COMPARE), "diff"));
            }
        }
    }

    class CreateProjectImpl extends AbstractBuilder<CreateProject> implements CreateProject {
        @Override
        public List<String> buildPath() {
            return encode(projectsComponents());
        }

        @Override
        protected String buildQueryString() {
            withParam(CREATE, null);
            return super.buildQueryString();
        }
    }

    class CreateRepoImpl extends AbstractBuilder<CreateRepo> implements CreateRepo {
        protected final String key;

        protected CreateRepoImpl(String key, Map<String, String> params) {
            super(params);
            this.key = key;
        }

        @Override
        public List<String> buildPath() {
            return encode(projectComponents(true, key, REPOS));
        }

        @Override
        protected String buildQueryString() {
            withParam(CREATE, null);
            return super.buildQueryString();
        }
    }

    class ProjectImpl extends AbstractBuilder<Project> implements Project {
        protected final String key;

        ProjectImpl(String key) {
            super();
            this.key = key;
        }

        @Override
        public Builder<?> avatar(int size) {
            return new SimpleBuilder(projectComponents(key, AVATAR + AVATAR_EXTENSION))
                    .withParam("s", String.valueOf(size));
        }

        @Override
        public ListRepos repos() {
            return new ListReposImpl(key, params);
        }

        @Override
        public ProjectSettings settings() {
            return new ProjectSettingsImpl(key, params);
        }

        @Override
        public ProjectPermissions permissions() {
            return new ProjectPermissionsImpl(key, params);
        }

        @Override
        public Repo repo(@Nonnull String slug) {
            return new RepoImpl(key, slug, params);
        }

        @Override
        public Repo repo(@Nonnull Repository repository) {
            return new RepoImpl(key, repository.getSlug(), params);
        }

        @Override
        public CreateRepo createRepo() {
            return new CreateRepoImpl(key, params);
        }

        @Override
        public ProjectDelete delete() {
            return new ProjectDeleteImpl(key, params);
        }

        @Override
        public List<String> buildPath() {
            return encode(projectComponents(true, key));
        }
    }

    class ProjectSettingsImpl extends AbstractBuilder<ProjectSettings> implements ProjectSettings {
        protected final String key;

        public ProjectSettingsImpl(String key, Map<String, String> params) {
            super(params);
            this.key = key;
        }

        @Override
        public List<String> buildPath() {
            return encode(projectComponents(key, SETTINGS));
        }
    }

    class ProjectPermissionsImpl extends AbstractBuilder<ProjectPermissions> implements ProjectPermissions {
        protected final String key;

        public ProjectPermissionsImpl(String key, Map<String, String> params) {
            super(params);
            this.key = key;
        }

        @Override
        public List<String> buildPath() {
            return encode(projectComponents(key, PERMISSIONS));
        }
    }

    class ProjectDeleteImpl extends AbstractBuilder<ProjectDelete> implements ProjectDelete {
        protected final String key;

        public ProjectDeleteImpl(String key, Map<String, String> params) {
            super(params);
            this.key = key;
        }

        @Override
        public List<String> buildPath() {
            return encode(projectComponents(key));
        }
    }

    class ListReposImpl extends AbstractPagedBuilder<ListRepos> implements ListRepos {
        private final String key;

        ListReposImpl(String key, Map<String, String> params) {
            super(params);
            this.key = key;
        }

        @Override
        public List<String> buildPath() {
            return encode(projectComponents(true, key));
        }
    }

    class RepoImpl extends AbstractBuilder<Repo> implements Repo {
        private final String projectKey;
        private final String repoSlug;

        RepoImpl(String projectKey, String repoSlug) {
            super();
            this.projectKey = projectKey;
            this.repoSlug = repoSlug.toLowerCase(Locale.US);
        }

        RepoImpl(String projectKey, String repoSlug, Map<String, String> params) {
            super(params);
            this.projectKey = projectKey;
            this.repoSlug = repoSlug.toLowerCase(Locale.US);
        }

        @Override
        public RepoAttachments attachments() {
            return new RepoAttachmentsImpl(projectKey, repoSlug, params);
        }

        @Override
        public BrowseRepoResource browse() {
            return new BrowseRepoResourceImpl(projectKey, repoSlug, params);
        }

        @Override
        public ListBranches branches() {
            return new ListBranchesImpl(projectKey, repoSlug, params);
        }

        @Override
        public ListTags tags() {
            return new ListTagsImpl(projectKey, repoSlug, params);
        }

        @Nonnull
        @Override
        public Commit commit(@Nonnull String commitId) {
            return new CommitBuilder(projectKey, repoSlug, commitId, params);
        }

        @Override
        public ListCommits commits() {
            return new ListCommitsImpl(projectKey, repoSlug, params);
        }

        @Override
        public Compare compare() {
            return new CompareImpl(projectKey, repoSlug, params);
        }

        @Override
        public RepoSettings settings() {
            return new RepoSettingsImpl(projectKey, repoSlug, params);
        }

        @Override
        public RepoHooks hooks() {
            return new RepoHooksImpl(projectKey, repoSlug, params);
        }

        @Override
        public RepoPermissions permissions() {
            return new RepoPermissionsImpl(projectKey, repoSlug, params);
        }

        @Override
        public RepoDelete delete() {
            return new RepoDeleteImpl(projectKey, repoSlug, params);
        }

        @Override
        public RepoFork fork() {
            return new RepoForkImpl(projectKey, repoSlug, params);
        }

        @Override
        public CreatePullRequest createPullRequest() {
            return new CreatePullRequestImpl(projectKey, repoSlug, params);
        }

        @Override
        public PullRequest pullRequest(long id) {
            return new PullRequestImpl(projectKey, repoSlug, id, params);
        }

        @Override
        public ListPullRequests pullRequests() {
            return new ListPullRequestsImpl(projectKey, repoSlug, params);
        }

        @Override
        public List<String> buildPath() {
            return encode(repoComponents(projectKey, repoSlug, BROWSE));
        }
    }

    class UserAccountImpl extends AbstractBuilder<UserAccount> implements UserAccount {
        @Override
        public List<String> buildPath() {
            return encode(components(ACCOUNT));
        }

        @Override
        public Builder<?> settings() {
            return UserAccountImpl.this;
        }

        @Override
        public Builder<?> password() {
            return new SimpleBuilder(components(ACCOUNT, PASSWORD));
        }
    }

    class BrowseRepoResourceImpl extends RepoPathBuilderImpl<BrowseRepoResource> implements BrowseRepoResource {
        BrowseRepoResourceImpl(String projectKey, String repoSlug, Map<String, String> params) {
            super(projectKey, repoSlug, params);
        }

        @Override
        public RevisionBrowse atRevision(@Nonnull Ref ref) {
            return atRevision(ref instanceof Branch && ((Branch) ref).getIsDefault() ? null : ref.getDisplayId());
        }

        @Override
        public RevisionBrowse atRevision(String revision) {
            return new RevisionBrowseImpl(projectKey, repoSlug, revision, params);
        }

        @Override
        public List<String> buildPath() {
            return encode(repoComponents(projectKey, repoSlug, BROWSE));
        }
    }

    class RevisionBrowseImpl extends RepoPathBuilderImpl<RevisionBrowse> implements RevisionBrowse {
        private final String revision;

        RevisionBrowseImpl(String projectKey, String repoSlug, String revision, Map<String, String> params) {
            super(projectKey, repoSlug, params);
            this.revision = revision;
        }

        @Override
        public List<String> buildPath() {
            return encode(repoComponents(projectKey, repoSlug, BROWSE));
        }

        @Override
        protected String buildQueryString() {
            if (revision != null) {
                withParam(AT, revision);
            }
            return super.buildQueryString();
        }
    }

    class ListBranchesImpl extends RepoPagedBuilderImpl<ListBranches> implements ListBranches {
        ListBranchesImpl(String projectKey, String repoSlug, Map<String, String> params) {
            super(projectKey, repoSlug, params);
        }

        @Override
        public List<String> buildPath() {
            return encode(repoComponents(projectKey, repoSlug, BRANCHES));
        }
    }

    class ListTagsImpl extends RepoPagedBuilderImpl<ListTags> implements ListTags {
        ListTagsImpl(String projectKey, String repoSlug, Map<String, String> params) {
            super(projectKey, repoSlug, params);
        }

        @Override
        public List<String> buildPath() {
            return encode(repoComponents(projectKey, repoSlug, TAGS));
        }
    }

    class RepoAttachmentsImpl extends RepoBuilderImpl<RepoAttachments> implements RepoAttachments {
        RepoAttachmentsImpl(String projectKey, String repoSlug, Map<String, String> params) {
            super(projectKey, repoSlug, params);
        }

        @Override
        public Builder<?> attachment(@Nonnull String id) {
            String[] idComponents = checkNotNull(id, "id").split("[\\\\/]");

            return new SimpleBuilder(append(repoComponents(projectKey, repoSlug, ATTACHMENTS), idComponents));
        }

        @Override
        public List<String> buildPath() {
            return encode(repoComponents(projectKey, repoSlug, ATTACHMENTS));
        }
    }

    class RepoSettingsImpl extends RepoBuilderImpl<RepoSettings> implements RepoSettings {
        RepoSettingsImpl(String projectKey, String repoSlug, Map<String, String> params) {
            super(projectKey, repoSlug, params);
        }

        @Override
        public List<String> buildPath() {
            return encode(repoComponents(projectKey, repoSlug, SETTINGS));
        }

        @Override
        public PullRequestRepoSettings pullRequests() {
            return new PullRequestRepoSettingsImpl(projectKey, repoSlug, params);
        }
    }

    class PullRequestRepoSettingsImpl extends RepoBuilderImpl<PullRequestRepoSettings> implements PullRequestRepoSettings {
        protected PullRequestRepoSettingsImpl(String projectKey, String repoSlug, Map<String, String> params) {
            super(projectKey, repoSlug, params);
        }

        @Override
        public List<String> buildPath() {
            return encode(repoComponents(projectKey, repoSlug, SETTINGS, PULL_REQUESTS));
        }
    }

    class RepoHooksImpl extends RepoBuilderImpl<RepoHooks> implements RepoHooks {
        RepoHooksImpl(String projectKey, String repoSlug, Map<String, String> params) {
            super(projectKey, repoSlug, params);
        }

        @Override
        public List<String> buildPath() {
            return encode(repoComponents(projectKey, repoSlug, SETTINGS, HOOKS));
        }
    }

    class RepoPermissionsImpl extends RepoBuilderImpl<RepoPermissions> implements RepoPermissions {
        RepoPermissionsImpl(String projectKey, String repoSlug, Map<String, String> params) {
            super(projectKey, repoSlug, params);
        }

        @Override
        public List<String> buildPath() {
            return encode(repoComponents(projectKey, repoSlug, PERMISSIONS));
        }
    }

    class RepoDeleteImpl extends RepoBuilderImpl<RepoDelete> implements RepoDelete {
        RepoDeleteImpl(String projectKey, String repoSlug, Map<String, String> params) {
            super(projectKey, repoSlug, params);
        }

        @Override
        public List<String> buildPath() {
            return encode(repoComponents(projectKey, repoSlug));
        }
    }

    class RepoForkImpl extends RepoBuilderImpl<RepoFork> implements RepoFork {

        protected RepoForkImpl(String projectKey, String repoSlug, Map<String, String> params) {
            super(projectKey, repoSlug, params);
        }

        @Override
        public List<String> buildPath() {
            return encode(repoComponents(projectKey, repoSlug));
        }

        @Override
        protected String buildQueryString() {
            withParam(FORK, null);
            return super.buildQueryString();
        }
    }

    class CreatePullRequestImpl extends RepoBuilderImpl<CreatePullRequest> implements CreatePullRequest {

        protected final String projectKey;
        protected final String repoSlug;

        protected CreatePullRequestImpl(String projectKey, String repoSlug, Map<String, String> params) {
            super(projectKey, repoSlug, params);
            this.projectKey = projectKey;
            this.repoSlug = repoSlug;

            withParam(CREATE, null); // in the ctor so that the ?create query string param is always first
        }

        @Override
        public List<String> buildPath() {
            return encode(repoComponents(projectKey, repoSlug, PULL_REQUESTS));
        }

        @Override
        public CreatePullRequest sourceBranch(String sourceBranch) {
            withParam("sourceBranch", sourceBranch);
            return this;
        }

        @Override
        public CreatePullRequest targetBranch(String targetBranch) {
            withParam("targetBranch", targetBranch);
            return this;
        }
    }

    class PullRequestImpl extends RepoBuilderImpl<PullRequest> implements PullRequest {

        private final long id;

        PullRequestImpl(String projectKey, String repoSlug, long id, Map<String, String> params) {
            super(projectKey, repoSlug, params);
            this.id = id;
        }

        @Override
        public List<String> buildPath() {
            return encode(repoComponents(projectKey, repoSlug, PULL_REQUESTS, Long.toString(id)));
        }

        @Override
        public PagedBuilder<?> unwatch() {
            PagedBuilder<?> builder = new SimplePagedBuilder(
                    repoComponents(projectKey, repoSlug, PULL_REQUESTS, Long.toString(id)));
            builder.withParam(UNWATCH, "true"); //Yay bad generics!

            return builder;
        }

        @Nonnull
        @Override
        public PullRequestActivity activity(long activityId) {
            return new PullRequestActivityImpl(projectKey, repoSlug, id, activityId, params);
        }

        @Nonnull
        @Override
        public PullRequestComment comment(long commentId) {
            return new PullRequestCommentImpl(projectKey, repoSlug, id, commentId, params);
        }

        @Nonnull
        @Override
        public PullRequestCommit commit(@Nonnull String commitId) {
            return new PullRequestCommitBuilder(projectKey, repoSlug, id, commitId, params);
        }

        @Override
        public PagedBuilder<?> commits() {
            return new SimplePagedBuilder(repoComponents(projectKey, repoSlug, PULL_REQUESTS, Long.toString(id), COMMITS));
        }

        public PullRequestOverview overview() {
            return new PullRequestOverviewImpl(projectKey, repoSlug, id, params);
        }

        @Override
        public Builder diff() {
            return new SimpleBuilder(repoComponents(projectKey, repoSlug, PULL_REQUESTS, Long.toString(id), DIFF));
        }
    }

    class PullRequestActivityImpl extends RepoBuilderImpl<PullRequestActivity> implements PullRequestActivity {

        private final long pullRequestId;

        PullRequestActivityImpl(String projectKey, String repoSlug,
                               long pullRequestId,
                               long activityId,
                               Map<String, String> params) {
            super(projectKey, repoSlug, params);
            this.pullRequestId = pullRequestId;
            withParam(ACTIVITY_ID, String.valueOf(activityId));
        }

        @Override
        public List<String> buildPath() {
            return encode(repoComponents(projectKey, repoSlug, PULL_REQUESTS, Long.toString(pullRequestId), OVERVIEW));
        }
    }

    class PullRequestCommentImpl extends RepoBuilderImpl<PullRequestComment> implements PullRequestComment {

        private final long pullRequestId;

        PullRequestCommentImpl(String projectKey, String repoSlug,
                               long pullRequestId,
                               long commentId,
                               Map<String, String> params) {
            super(projectKey, repoSlug, params);
            this.pullRequestId = pullRequestId;
            withParam(COMMENT_ID, String.valueOf(commentId));
        }

        @Override
        public List<String> buildPath() {
            return encode(repoComponents(projectKey, repoSlug, PULL_REQUESTS, Long.toString(pullRequestId), OVERVIEW));
        }

        @Nonnull
        @Override
        public PullRequestComment like() {
            return (PullRequestComment) withParam(ACTION, "like");
        }

        @Nonnull
        @Override
        public PullRequestComment reply() {
            return (PullRequestComment) withParam(ACTION, "reply");
        }
    }

    class PullRequestOverviewImpl extends RepoPagedBuilderImpl<PullRequestOverview> implements PullRequestOverview {
        private final long id;

        PullRequestOverviewImpl(String projectKey, String repoSlug, long id, Map<String, String> params) {
            super(projectKey, repoSlug, params);
            this.id = id;
        }

        @Override
        public List<String> buildPath() {
            return encode(repoComponents(projectKey, repoSlug, PULL_REQUESTS, Long.toString(id), OVERVIEW));
        }
    }

    class ListPullRequestsImpl extends RepoPagedBuilderImpl<ListPullRequests> implements ListPullRequests {

        ListPullRequestsImpl(String projectKey, String repoSlug, Map<String, String> params) {
            super(projectKey, repoSlug, params);
        }

        @Override
        public List<String> buildPath() {
            return encode(repoComponents(projectKey, repoSlug, PULL_REQUESTS));
        }

        @Override
        public Builder<?> open() {
            return withParam(STATE, PullRequestState.OPEN.name().toLowerCase(Locale.US));
        }

        @Override
        public Builder<?> declined() {
            return withParam(STATE, PullRequestState.DECLINED.name().toLowerCase(Locale.US));
        }

        @Override
        public Builder<?> merged() {
            return withParam(STATE, PullRequestState.MERGED.name().toLowerCase(Locale.US));
        }

        @Override
        public ListPullRequests incoming() {
            withParam(DIRECTION, PullRequestDirection.INCOMING.name().toLowerCase(Locale.US));
            return this;
        }

        @Override
        public ListPullRequests outgoing() {
            withParam(DIRECTION, PullRequestDirection.OUTGOING.name().toLowerCase(Locale.US));
            return this;
        }
    }

    class ListCommitsImpl extends RepoPagedBuilderImpl<ListCommits> implements ListCommits {
        ListCommitsImpl(String projectKey, String repoSlug, Map<String, String> params) {
            super(projectKey, repoSlug, params);
        }

        @Override
        public List<String> buildPath() {
            return encode(repoComponents(projectKey, repoSlug, COMMITS));
        }

        @Override
        public ListCommits until(@Nonnull Ref ref) {
            return ref instanceof Branch && ((Branch) ref).getIsDefault() ?
                    this :
                    until(ref.getDisplayId());
        }

        @Override
        public ListCommits until(@Nonnull String until) {
            withParam("until", until);

            return this;
        }
    }

    /**
     * Extends {@link RepoPathBuilderImpl}, as the commit hash is actually a path-like object. Branch names such as
     * "issue/STASHDEV-123" are valid refs. These URLs are used to see the commit a branch points to.
     */
    abstract class AbstractCommitBuilder<T extends PathBuilder<T>> extends RepoPathBuilderImpl<T> {

        protected final String commitId;

        AbstractCommitBuilder(String projectKey, String repoSlug, String commitId, Map<String, String> params) {
            super(projectKey, repoSlug, params);

            this.commitId = commitId;

            path(new SimplePath(commitId));
        }
    }

    class PullRequestCommitBuilder extends AbstractCommitBuilder<PullRequestCommit> implements PullRequestCommit {

        private final long id;

        PullRequestCommitBuilder(String projectKey, String repoSlug, long id,
                                 String commitId, Map<String, String> params) {
            super(projectKey, repoSlug, commitId, params);

            this.id = id;
        }

        @Override
        public List<String> buildPath() {
            return encode(repoComponents(projectKey, repoSlug, PULL_REQUESTS, Long.toString(id), COMMITS));
        }
    }

    class CommitBuilder extends AbstractCommitBuilder<Commit> implements Commit {

        CommitBuilder(String projectKey, String repoSlug, String commitId, Map<String, String> params) {
            super(projectKey, repoSlug, commitId, params);
        }

        @Override
        public List<String> buildPath() {
            return encode(repoComponents(projectKey, repoSlug, COMMITS));
        }

        @Nonnull
        public Commit change(@Nonnull Path path) {
            return withAnchor(path);
        }

        @Nonnull
        public Commit change(@Nonnull String path) {
            return withAnchor(path);
        }

        @Nonnull
        @Override
        public CommitComment comment(long commentId) {
            return new CommitCommentBuilder(commentId);
        }

        @Nonnull
        @Override
        public Commit unwatch() {
            withParam(UNWATCH, "true");

            return self();
        }

        private class CommitCommentBuilder extends AbstractBuilder<CommitComment> implements CommitComment {

            private CommitCommentBuilder(long commentId) {
                withParam(COMMENT_ID, Long.toString(commentId));
            }

            @Override
            public List<String> buildPath() {
                return CommitBuilder.this.buildPath();
            }
        }
    }

    private class TmpImpl extends AbstractBuilder<Tmp> implements Tmp {

        private TmpImpl() {
        }

        @Override
        public Builder<?> avatar(@Nonnull final String id) {
            checkArgument(!checkNotNull(id, "id").trim().isEmpty(), "An avatar ID is required");

            return new SimpleBuilder(components(TMP, AVATARS, id + AVATAR_EXTENSION));
        }

        @Override
        public List<String> buildPath() {
            return components(TMP);
        }
    }

    private class AddonsImpl extends AbstractBuilder<Addons> implements Addons {
        @Override
        public Builder<?> manage() {
            return this;
        }
        @Override
        public AddonRequests requests() {
            return new AddonRequestsImpl();
        }
        @Override
        public List<String> buildPath() {
            return encode(pluginsServletComponents(UPM));
        }
    }

    private class AddonRequestsImpl extends AbstractBuilder<AddonRequests> implements AddonRequests {
        @Override
        public Builder<?> category(String category) {
            withParam("category", category);
            return this;
        }
        @Override
        public List<String> buildPath() {
            return encode(pluginsServletComponents(UPM, "requests", "popular"));
        }
    }

    private abstract class NavigationStepImpl<B extends NavigationStep<B>> extends AbstractBuilder<B> implements NavigationStep<B> {


        private static final String LOGOUT_URL = "/logout";
        private final RequestConfiguration requestConfiguration;

        public NavigationStepImpl(RequestConfiguration requestConfiguration) {
            this.requestConfiguration = requestConfiguration;
        }

        @Override
        public abstract List<String> buildPath();

        @Override
        @Nonnull
        public Builder<?> next(@Nonnull String url) {
            return withParam("next", checkNotNull(url, "url"));
        }

        @Override
        @Nonnull
        public Builder<?> next() {
            String relativeUrl = getRelativeUrl();
            if (StringUtils.isNotEmpty(relativeUrl) && !isLogoutUrl(relativeUrl)) {
                next(relativeUrl);
            }
            return self();
        }

        /**
         * This method returns the relative Url for the request. This method will remove the
         * context and "/mvc" added by Spring
         *
         * @return Relative Url String
         */
        private String getRelativeUrl() {
            String contextPath = requestConfiguration.getRequestBaseUri().getRawPath();
            String relativeUrl = stripPrefix(contextPath, requestConfiguration.getRequestUrl());
            relativeUrl = stripPrefix("/mvc", relativeUrl);
            String queryString = requestConfiguration.getQueryString();
            return queryString == null ? relativeUrl : relativeUrl + "?" + queryString;
        }

        private boolean isLogoutUrl(String requestUrl) {
            return LOGOUT_URL.equals(requestUrl);
        }

        private String stripPrefix(String prefix, String value) {
            if (StringUtils.isNotEmpty(prefix) && StringUtils.startsWith(value, prefix)) {
                return value.substring(prefix.length());
            }
            return value;
        }
    }

    private class LoginImpl extends NavigationStepImpl<Login> implements Login {

        public LoginImpl(RequestConfiguration requestConfiguration) {
            super(requestConfiguration);
        }

        @Override
        public List<String> buildPath() {
            return encode(LOGIN);
        }

        @Nonnull
        @Override
        public Login embedded() {
            withParam(EMBEDDED, Boolean.TRUE.toString());

            return this;
        }

        @Nonnull
        @Override
        public Login admin() {
            return withPermission(Permission.ADMIN);
        }

        @Nonnull
        @Override
        public Login sysadmin() {
            return withPermission(Permission.SYS_ADMIN);
        }

        private Login withPermission(@Nonnull Permission permission) {
            // TODO fix in Stash 5.0: just return withParam once the return types are fixed in NavBuilder.Builder
            withParam("permission", permission.name());
            return this;
        }
    }

    private class GettingStartedImpl extends NavigationStepImpl<GettingStarted> implements GettingStarted {

        public GettingStartedImpl(RequestConfiguration requestConfiguration) {
            super(requestConfiguration);
        }

        @Override
        public List<String> buildPath() {
            return encode(components(GETTING_STARTED));
        }

    }

    private class UpgradeOnboardingImpl extends NavigationStepImpl<UpgradeOnboarding> implements UpgradeOnboarding {

        public UpgradeOnboardingImpl(RequestConfiguration requestConfiguration) {
            super(requestConfiguration);
        }

        @Override
        public List<String> buildPath() {
            return encode(components(UPGRADE_ONBOARDING));
        }
    }

    private class DashboardImpl extends AbstractBuilder<Dashboard> implements Dashboard {

        @Override
        public List<String> buildPath() {
            return encode(components(DASHBOARD));
        }
    }

    private List<String> append(List<String> components, String... others) {
        return ImmutableList.<String>builder().addAll(components).add(others).build();
    }

    private List<String> components(String... others) {
        return asList(others);
    }

    private List<String> restComponents(String... others) {
        return append(asList(REST, API, "1.0"), others);
    }

    private List<String> adminComponents(String... others) {
        return append(Collections.singletonList(ADMIN), others);
    }

    private List<String> databaseComponents(String... others) {
        return append(asList(ADMIN, DB), others);
    }

    private List<String> licensingComponents(String... others) {
        return append(asList(ADMIN, LICENSE), others);
    }

    private List<String> usersComponents(String... others) {
        return append(asList(ADMIN, USERS), others);
    }

    private List<String> groupsComponents(String... others) {
        return append(asList(ADMIN, GROUPS), others);
    }

    private List<String> pluginsServletComponents(String... others) {
        return append(asList(PLUGINS, SERVLET), others);
    }

    private List<String> projectsComponents() {
        return Collections.singletonList(PROJECTS);
    }

    private List<String> projectComponents(String projectKey, String... others) {
        return projectComponents(false, projectKey, others);
    }

    private List<String> projectComponents(boolean usePersonalProjectPath, String projectKey, String... others) {
        if (InternalPersonalProject.isPersonalProjectKey(projectKey) && usePersonalProjectPath) {
            return ImmutableList.<String>builder().add(USERS, InternalPersonalProject.getPersonalProjectOwner(projectKey)).add(others).build();
        }
        return ImmutableList.<String>builder().addAll(projectsComponents()).add(projectKey).add(others).build();
    }

    private List<String> repoComponents(String projectKey, String repoSlug, String... others) {
        return append(projectComponents(true, projectKey, REPOS, repoSlug), others);
    }

    /**
     * Returns the base URL from the {@link ApplicationPropertiesService}. Ensure the context class loader is set to
     * the host applications class loader when retrieving the app property.
     */
    private URI getBaseUrl() {
        Thread currentThread = Thread.currentThread();
        ClassLoader original = currentThread.getContextClassLoader();
        try {
            currentThread.setContextClassLoader(NavBuilderImpl.class.getClassLoader());
            return applicationPropertiesService.getBaseUrl();
        } finally {
            currentThread.setContextClassLoader(original);
        }
    }

    private static class RequestConfiguration {
        private final URI requestBaseUri;
        private final boolean baseUriTokenRequested;
        private final String requestUrl;
        private final String queryString;

        public RequestConfiguration(URI requestBaseUri, boolean baseUriTokenRequested, String requestUrl, String queryString) {
            this.baseUriTokenRequested = baseUriTokenRequested;
            this.requestBaseUri = requestBaseUri;
            this.requestUrl = requestUrl;
            this.queryString = queryString;
        }

        public URI getRequestBaseUri() {
            return requestBaseUri;
        }

        public boolean isBaseUriTokenRequested() {
            return baseUriTokenRequested;
        }

        public String getRequestUrl() {
            return requestUrl;
        }

        public String getQueryString() {
            return queryString;
        }
    }
}
