/*
 * The MIT License
 *
 * Copyright (c) 2017-2020, CloudBees, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.jenkinsci.plugin.gitea.client.impl;

import com.damnhandy.uri.template.UriTemplate;
import com.damnhandy.uri.template.UriTemplateBuilder;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.util.StdDateFormat;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.net.ssl.HttpsURLConnection;
import jenkins.model.Jenkins;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.plugin.gitea.client.api.GiteaAnnotatedTag;
import org.jenkinsci.plugin.gitea.client.api.GiteaAuth;
import org.jenkinsci.plugin.gitea.client.api.GiteaAuthToken;
import org.jenkinsci.plugin.gitea.client.api.GiteaAuthUser;
import org.jenkinsci.plugin.gitea.client.api.GiteaBranch;
import org.jenkinsci.plugin.gitea.client.api.GiteaCommitDetail;
import org.jenkinsci.plugin.gitea.client.api.GiteaCommitStatus;
import org.jenkinsci.plugin.gitea.client.api.GiteaConnection;
import org.jenkinsci.plugin.gitea.client.api.GiteaHook;
import org.jenkinsci.plugin.gitea.client.api.GiteaHttpStatusException;
import org.jenkinsci.plugin.gitea.client.api.GiteaIssue;
import org.jenkinsci.plugin.gitea.client.api.GiteaIssueState;
import org.jenkinsci.plugin.gitea.client.api.GiteaOrganization;
import org.jenkinsci.plugin.gitea.client.api.GiteaOwner;
import org.jenkinsci.plugin.gitea.client.api.GiteaPullRequest;
import org.jenkinsci.plugin.gitea.client.api.GiteaRepository;
import org.jenkinsci.plugin.gitea.client.api.GiteaTag;
import org.jenkinsci.plugin.gitea.client.api.GiteaUser;
import org.jenkinsci.plugin.gitea.client.api.GiteaVersion;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

/**
 * Default implementation of {@link GiteaConnection} that uses the JVM native {@link URLConnection} to communicate
 * with a remote Gitea server. Requires a valid end-point. Package protected to ensure access goes through the
 * API and not direct construction.
 */
class DefaultGiteaConnection implements GiteaConnection {

    private final String serverUrl;

    private final GiteaAuth authentication;
    private final ObjectMapper mapper = new ObjectMapper();

    DefaultGiteaConnection(@NonNull String serverUrl,
                           @NonNull GiteaAuth authentication) {
        this.serverUrl = serverUrl;
        this.authentication = authentication;
    }

    /**
     * Workaround for a bug in {@code HttpURLConnection.setRequestMethod(String)}
     * The implementation of Sun/Oracle is throwing a {@code ProtocolException}
     * when the method is other than the HTTP/1.1 default methods. So to use {@code PROPFIND}
     * and others, we must apply this workaround.
     */
    private static void setRequestMethodViaJreBugWorkaround(final HttpURLConnection httpURLConnection,
                                                            final String method) {
        try {
            httpURLConnection.setRequestMethod(method); // Check whether we are running on a buggy JRE
        } catch (final ProtocolException pe) {
            try {
                AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
                    @Override
                    public Object run() throws NoSuchFieldException, IllegalAccessException {
                        final Object target;
                        if (httpURLConnection instanceof HttpsURLConnection) {
                            final Field delegate = httpURLConnection.getClass().getDeclaredField("delegate");
                            delegate.setAccessible(true);
                            target = delegate.get(httpURLConnection);
                        } else {
                            target = httpURLConnection;
                        }
                        final Field methodField = HttpURLConnection.class.getDeclaredField("method");
                        methodField.setAccessible(true);
                        methodField.set(target, method);
                        return null;
                    }
                });
            } catch (final PrivilegedActionException e) {
                final Throwable cause = e.getCause();
                if (cause instanceof RuntimeException) {
                    throw (RuntimeException) cause;
                } else {
                    throw new RuntimeException(cause);
                }
            }
        }
    }

    @Override
    public GiteaVersion fetchVersion() throws IOException, InterruptedException {
        return getObject(
                api()
                        .literal("/version")
                        .build(),
                GiteaVersion.class
        );
    }

    @Override
    public GiteaUser fetchCurrentUser() throws IOException, InterruptedException {
        return getObject(
                api()
                        .literal("/user")
                        .build(),
                GiteaUser.class
        );
    }

    @Override
    public GiteaOwner fetchOwner(String name) throws IOException, InterruptedException {
        try {
            GiteaOrganization giteaOrganization = fetchOrganization(name);
            if (giteaOrganization != null) {
                return giteaOrganization;
            }
        } catch (GiteaHttpStatusException e) {
            // When it's NotFound, owner might be a user, so only rethrow when not 404
            // Every other non 200 status code should be thrown again by fetchUser()
            if (e.getStatusCode() != 404) {
                throw e;
            }
        }
        return fetchUser(name);
    }

    @Override
    public GiteaUser fetchUser(String name) throws IOException, InterruptedException {
        return getObject(
                api()
                        .literal("/users")
                        .path(UriTemplateBuilder.var("name"))
                        .build()
                        .set("name", name),
                GiteaUser.class
        );
    }

    @Override
    public GiteaOrganization fetchOrganization(String name) throws IOException, InterruptedException {
        return getObject(
                api()
                        .literal("/orgs")
                        .path(UriTemplateBuilder.var("name"))
                        .build()
                        .set("name", name),
                GiteaOrganization.class
        );
    }

    @Override
    public GiteaRepository fetchRepository(String username, String name) throws IOException, InterruptedException {
        return getObject(
                api()
                        .literal("/repos")
                        .path(UriTemplateBuilder.var("username"))
                        .path(UriTemplateBuilder.var("name"))
                        .build()
                        .set("username", username)
                        .set("name", name),
                GiteaRepository.class
        );
    }

    @Override
    public GiteaRepository fetchRepository(GiteaOwner owner, String name) throws IOException, InterruptedException {
        return fetchRepository(owner.getUsername(), name);
    }

    @Override
    public List<GiteaRepository> fetchCurrentUserRepositories() throws IOException, InterruptedException {
        return getList(
                api()
                        .literal("/user")
                        .literal("/repos")
                        .build(),
                GiteaRepository.class
        );
    }

    @Override
    public List<GiteaRepository> fetchRepositories(String username) throws IOException, InterruptedException {
        return getList(
                api()
                        .literal("/users")
                        .path(UriTemplateBuilder.var("username"))
                        .literal("/repos")
                        .build()
                        .set("username", username),
                GiteaRepository.class
        );
    }

    @Override
    public List<GiteaRepository> fetchRepositories(GiteaOwner owner) throws IOException, InterruptedException {
        if(owner instanceof GiteaOrganization) {
            return fetchOrganizationRepositories(owner);
        }
        return fetchRepositories(owner.getUsername());

    }

    @Override
    public List<GiteaRepository> fetchOrganizationRepositories(GiteaOwner owner) throws IOException, InterruptedException {
        return getList(
                api()
                        .literal("/orgs")
                        .path(UriTemplateBuilder.var("org"))
                        .literal("/repos")
                        .build()
                        .set("org", owner.getUsername()),
                GiteaRepository.class
        );
    }

    @Override
    public GiteaBranch fetchBranch(String username, String repository, String name)
            throws IOException, InterruptedException {
        if (name.indexOf('/') != -1) {
            // TODO remove hack once https://github.com/go-gitea/gitea/issues/2088 is fixed
            for (GiteaBranch b : fetchBranches(username, repository)) {
                if (name.equals(b.getName())) {
                    return b;
                }
            }
        }
        return getObject(
                api()
                        .literal("/repos")
                        .path(UriTemplateBuilder.var("username"))
                        .path(UriTemplateBuilder.var("repository"))
                        .literal("/branches")
                        .path(UriTemplateBuilder.var("name", true))
                        .build()
                        .set("username", username)
                        .set("repository", repository)
                        .set("name", StringUtils.split(name, '/')),
                GiteaBranch.class
        );
    }

    @Override
    public GiteaBranch fetchBranch(GiteaRepository repository, String name) throws IOException, InterruptedException {
        return fetchBranch(repository.getOwner().getUsername(), repository.getName(), name);
    }

    @Override
    public List<GiteaBranch> fetchBranches(String username, String name) throws IOException, InterruptedException {
        return getList(
                api()
                        .literal("/repos")
                        .path(UriTemplateBuilder.var("username"))
                        .path(UriTemplateBuilder.var("name"))
                        .literal("/branches")
                        .build()
                        .set("username", username)
                        .set("name", name),
                GiteaBranch.class
        );
    }

    @Override
    public List<GiteaBranch> fetchBranches(GiteaRepository repository) throws IOException, InterruptedException {
        return fetchBranches(repository.getOwner().getUsername(), repository.getName());
    }

    @Override
    public GiteaAnnotatedTag fetchAnnotatedTag(String username, String repository, String sha1)
            throws IOException, InterruptedException {
        return getObject(
                api()
                        .literal("/repos")
                        .path(UriTemplateBuilder.var("username"))
                        .path(UriTemplateBuilder.var("repository"))
                        .literal("/git/tags")
                        .path(UriTemplateBuilder.var("sha1"))
                        .build()
                        .set("username", username)
                        .set("repository", repository)
                        .set("sha1", sha1),
                GiteaAnnotatedTag.class
        );
    }

    @Override
    public GiteaAnnotatedTag fetchAnnotatedTag(GiteaRepository repository, GiteaTag tag) throws IOException, InterruptedException {
        return fetchAnnotatedTag(repository.getOwner().getUsername(), repository.getName(), tag.getId());
    }

    @Override
    public List<GiteaTag> fetchTags(String username, String name) throws IOException, InterruptedException {
        return getList(
                api()
                        .literal("/repos")
                        .path(UriTemplateBuilder.var("username"))
                        .path(UriTemplateBuilder.var("name"))
                        .literal("/tags")
                        .build()
                        .set("username", username)
                        .set("name", name),
                GiteaTag.class
        );
    }

    @Override
    public List<GiteaTag> fetchTags(GiteaRepository repository) throws IOException, InterruptedException {
        return fetchTags(repository.getOwner().getUsername(), repository.getName());
    }

    @Override
    public GiteaCommitDetail fetchCommit(String username, String repository, String sha1)
            throws IOException, InterruptedException {
        return getObject(
                api()
                        .literal("/repos")
                        .path(UriTemplateBuilder.var("username"))
                        .path(UriTemplateBuilder.var("repository"))
                        .literal("/git/commits")
                        .path(UriTemplateBuilder.var("sha1"))
                        .build()
                        .set("username", username)
                        .set("repository", repository)
                        .set("sha1", sha1),
                GiteaCommitDetail.class
        );
    }

    @Override
    public GiteaCommitDetail fetchCommit(GiteaRepository repository, String sha1)
            throws IOException, InterruptedException {
        return fetchCommit(repository.getOwner().getUsername(), repository.getName(), sha1);
    }

    @Override
    public List<GiteaUser> fetchCollaborators(String username, String name) throws IOException, InterruptedException {
        return getList(
                api()
                        .literal("/repos")
                        .path(UriTemplateBuilder.var("username"))
                        .path(UriTemplateBuilder.var("name"))
                        .literal("/collaborators")
                        .build()
                        .set("username", username)
                        .set("name", name),
                GiteaUser.class
        );
    }

    @Override
    public List<GiteaUser> fetchCollaborators(GiteaRepository repository) throws IOException, InterruptedException {
        return fetchCollaborators(repository.getOwner().getUsername(), repository.getName());
    }

    @Override
    public boolean checkCollaborator(String username, String name, String collaboratorName)
            throws IOException, InterruptedException {
        return status(
                api()
                        .literal("/repos")
                        .path(UriTemplateBuilder.var("username"))
                        .path(UriTemplateBuilder.var("name"))
                        .literal("/collaborators")
                        .path(UriTemplateBuilder.var("collaboratorName"))
                        .build()
                        .set("username", username)
                        .set("name", name)
                        .set("collaboratorName", collaboratorName)
        ) / 100 == 2;
    }

    @Override
    public boolean checkCollaborator(GiteaRepository repository, String collaboratorName)
            throws IOException, InterruptedException {
        return checkCollaborator(repository.getOwner().getUsername(), repository.getName(), collaboratorName);
    }

    @Override
    public List<GiteaHook> fetchHooks(String organizationName) throws IOException, InterruptedException {
        return getList(
                api()
                        .literal("/orgs")
                        .path(UriTemplateBuilder.var("name"))
                        .literal("/hooks")
                        .build()
                        .set("name", organizationName),
                GiteaHook.class
        );
    }

    @Override
    public List<GiteaHook> fetchHooks(GiteaOrganization organization) throws IOException, InterruptedException {
        return getList(
                api()
                        .literal("/orgs")
                        .path(UriTemplateBuilder.var("name"))
                        .literal("/hooks")
                        .build()
                        .set("name", organization.getUsername()),
                GiteaHook.class
        );
    }

    @Override
    public GiteaHook createHook(GiteaOrganization organization, GiteaHook hook)
            throws IOException, InterruptedException {
        return post(api()
                        .literal("/orgs")
                        .path(UriTemplateBuilder.var("name"))
                        .literal("/hooks")
                        .build()
                        .set("name", organization.getUsername()),
                hook, GiteaHook.class);
    }

    @Override
    public void deleteHook(GiteaOrganization organization, GiteaHook hook) throws IOException, InterruptedException {
        deleteHook(organization, hook.getId());
    }

    @Override
    public void deleteHook(GiteaOrganization organization, long id) throws IOException, InterruptedException {
        int status = delete(api()
                .literal("/orgs")
                .path(UriTemplateBuilder.var("name"))
                .literal("/hooks")
                .path(UriTemplateBuilder.var("id"))
                .build()
                .set("name", organization.getUsername())
                .set("id", id)
        );
        if (status / 100 != 2) {
            throw new IOException(
                    "Could not delete organization hook " + id + " for " + organization.getUsername() + " HTTP/"
                            + status);
        }
    }

    @Override
    public List<GiteaHook> fetchHooks(String username, String name) throws IOException, InterruptedException {
        return getList(
                api()
                        .literal("/repos")
                        .path(UriTemplateBuilder.var("username"))
                        .path(UriTemplateBuilder.var("name"))
                        .literal("/hooks")
                        .build()
                        .set("username", username)
                        .set("name", name),
                GiteaHook.class
        );
    }

    @Override
    public List<GiteaHook> fetchHooks(GiteaRepository repository) throws IOException, InterruptedException {
        return fetchHooks(repository.getOwner().getUsername(), repository.getName());
    }

    @Override
    public GiteaHook createHook(GiteaRepository repository, GiteaHook hook) throws IOException, InterruptedException {
        return post(api()
                        .literal("/repos")
                        .path(UriTemplateBuilder.var("username"))
                        .path(UriTemplateBuilder.var("name"))
                        .literal("/hooks")
                        .build()
                        .set("username", repository.getOwner().getUsername())
                        .set("name", repository.getName()),
                hook, GiteaHook.class);
    }

    @Override
    public void deleteHook(GiteaRepository repository, GiteaHook hook) throws IOException, InterruptedException {
        deleteHook(repository, hook.getId());
    }

    @Override
    public void deleteHook(GiteaRepository repository, long id) throws IOException, InterruptedException {
        int status = delete(api()
                .literal("/repos")
                .path(UriTemplateBuilder.var("username"))
                .path(UriTemplateBuilder.var("name"))
                .literal("/hooks")
                .path(UriTemplateBuilder.var("id"))
                .build()
                .set("username", repository.getOwner().getUsername())
                .set("name", repository.getName())
                .set("id", id)
        );
        if (status / 100 != 2) {
            throw new IOException(
                    "Could not delete hook " + id + " for " + repository.getOwner().getUsername() + "/" + repository
                            .getName() + " HTTP/" + status);
        }
    }

    @Override
    public void updateHook(GiteaOrganization organization, GiteaHook hook) throws IOException, InterruptedException {
        GiteaHook diff = new GiteaHook();
        diff.setConfig(hook.getConfig());
        diff.setActive(hook.isActive());
        diff.setEvents(hook.getEvents());
        patch(api()
                        .literal("/orgs")
                        .path(UriTemplateBuilder.var("name"))
                        .literal("/hooks")
                        .path(UriTemplateBuilder.var("id"))
                        .build()
                        .set("name", organization.getUsername())
                        .set("id", hook.getId()),
                diff, Void.class);
    }

    @Override
    public void updateHook(GiteaRepository repository, GiteaHook hook) throws IOException, InterruptedException {
        GiteaHook diff = new GiteaHook();
        diff.setConfig(hook.getConfig());
        diff.setActive(hook.isActive());
        diff.setEvents(hook.getEvents());
        patch(api()
                        .literal("/repos")
                        .path(UriTemplateBuilder.var("username"))
                        .path(UriTemplateBuilder.var("name"))
                        .literal("/hooks")
                        .path(UriTemplateBuilder.var("id"))
                        .build()
                        .set("username", repository.getOwner().getUsername())
                        .set("name", repository.getName())
                        .set("id", hook.getId()),
                diff, Void.class);
    }

    @Override
    public List<GiteaCommitStatus> fetchCommitStatuses(GiteaRepository repository, String sha)
            throws IOException, InterruptedException {
        return getList(
                api()
                        .literal("/repos")
                        .path(UriTemplateBuilder.var("username"))
                        .path(UriTemplateBuilder.var("name"))
                        .literal("/statuses")
                        .path(UriTemplateBuilder.var("sha"))
                        .build()
                        .set("username", repository.getOwner().getUsername())
                        .set("name", repository.getName())
                        .set("sha", sha),
                GiteaCommitStatus.class
        );
    }

    @Override
    public GiteaCommitStatus createCommitStatus(String username, String repository, String sha,
                                                GiteaCommitStatus status) throws IOException, InterruptedException {
        return post(api()
                        .literal("/repos")
                        .path(UriTemplateBuilder.var("username"))
                        .path(UriTemplateBuilder.var("name"))
                        .literal("/statuses")
                        .path(UriTemplateBuilder.var("sha"))
                        .build()
                        .set("username", username)
                        .set("name", repository)
                        .set("sha", sha),
                status, GiteaCommitStatus.class);
    }

    @Override
    public GiteaCommitStatus createCommitStatus(GiteaRepository repository, String sha, GiteaCommitStatus status)
            throws IOException, InterruptedException {
        return createCommitStatus(repository.getOwner().getUsername(), repository.getName(), sha, status);
    }

    @Override
    public GiteaPullRequest fetchPullRequest(String username, String name, long id)
            throws IOException, InterruptedException {
        return getObject(
                api()
                        .literal("/repos")
                        .path(UriTemplateBuilder.var("username"))
                        .path(UriTemplateBuilder.var("name"))
                        .literal("/pulls")
                        .path(UriTemplateBuilder.var("id"))
                        .build()
                        .set("username", username)
                        .set("name", name)
                        .set("id", Long.toString(id)),
                GiteaPullRequest.class
        );
    }

    @Override
    public GiteaPullRequest fetchPullRequest(GiteaRepository repository, long id)
            throws IOException, InterruptedException {
        return fetchPullRequest(repository.getOwner().getUsername(), repository.getName(), id);
    }

    @Override
    public List<GiteaPullRequest> fetchPullRequests(String username, String name)
            throws IOException, InterruptedException {
        return fetchPullRequests(username, name, EnumSet.of(GiteaIssueState.OPEN));
    }

    @Override
    public List<GiteaPullRequest> fetchPullRequests(GiteaRepository repository)
            throws IOException, InterruptedException {
        return fetchPullRequests(repository, EnumSet.of(GiteaIssueState.OPEN));
    }

    @Override
    public List<GiteaPullRequest> fetchPullRequests(String username, String name, Set<GiteaIssueState> states)
            throws IOException, InterruptedException {
        String state = null;
        if (states != null && states.size() == 1) {
            // state query only works if there is one state
            for (GiteaIssueState s : GiteaIssueState.values()) {
                if (states.contains(s)) {
                    state = s.getKey();
                }
            }
        }
        try {
            return getList(
                    api()
                            .literal("/repos")
                            .path(UriTemplateBuilder.var("username"))
                            .path(UriTemplateBuilder.var("name"))
                            .literal("/pulls")
                            .query(UriTemplateBuilder.var("state"))
                            .build()
                            .set("username", username)
                            .set("name", name)
                            .set("state", state),
                    GiteaPullRequest.class
            );
        } catch (GiteaHttpStatusException e) {
            // Gitea REST API returns HTTP Code 404 when pull requests or issues are disabled
            // Therefore we need to handle this case and return a empty List
            if (e.getStatusCode() == 404) {
                return Collections.emptyList();
            } else {
                // Else other cause... throw exception again
                throw e;
            }
        }
    }

    @Override
    public List<GiteaPullRequest> fetchPullRequests(GiteaRepository repository, Set<GiteaIssueState> states)
            throws IOException, InterruptedException {
        return fetchPullRequests(repository.getOwner().getUsername(), repository.getName(), states);
    }

    @Override
    public List<GiteaIssue> fetchIssues(String username, String name)
            throws IOException, InterruptedException {
        return fetchIssues(username, name, EnumSet.of(GiteaIssueState.OPEN));
    }

    @Override
    public List<GiteaIssue> fetchIssues(GiteaRepository repository)
            throws IOException, InterruptedException {
        return fetchIssues(repository, EnumSet.of(GiteaIssueState.OPEN));
    }

    @Override
    public List<GiteaIssue> fetchIssues(String username, String name, Set<GiteaIssueState> states)
            throws IOException, InterruptedException {
        String state = null;
        if (states != null && states.size() == 1) {
            // state query only works if there is one state
            for (GiteaIssueState s : GiteaIssueState.values()) {
                if (states.contains(s)) {
                    state = s.getKey();
                }
            }
        }

        try {
            return getList(
                    api()
                            .literal("/repos")
                            .path(UriTemplateBuilder.var("username"))
                            .path(UriTemplateBuilder.var("name"))
                            .literal("/issues")
                            .query(UriTemplateBuilder.var("state"))
                            .build()
                            .set("username", username)
                            .set("name", name)
                            .set("state", state),
                    GiteaIssue.class
            );
        } catch (GiteaHttpStatusException e) {
            // Gitea REST API returns HTTP Code 404 when pull requests or issues are disabled
            // Therefore we need to handle this case and return a empty List
            if (e.getStatusCode() == 404) {
                return Collections.emptyList();
            } else {
                // Else other cause... throw exception again
                throw e;
            }
        }
    }

    @Override
    public List<GiteaIssue> fetchIssues(GiteaRepository repository, Set<GiteaIssueState> states)
            throws IOException, InterruptedException {
        return fetchIssues(repository.getOwner().getUsername(), repository.getName(), states);
    }

    @Override
    public byte[] fetchFile(GiteaRepository repository, String ref, String path)
            throws IOException, InterruptedException {
        HttpURLConnection connection = openConnection(api()
                .literal("/repos")
                .path(UriTemplateBuilder.var("username"))
                .path(UriTemplateBuilder.var("name"))
                .literal("/raw")
                .path(UriTemplateBuilder.var("ref", true))
                .path(UriTemplateBuilder.var("path", true))
                .build()
                .set("username", repository.getOwner().getUsername())
                .set("name", repository.getName())
                .set("ref", StringUtils.split(ref, '/'))
                .set("path", StringUtils.split(path, "/")));
        withAuthentication(connection);
        try {
            connection.connect();
            int status = connection.getResponseCode();
            if (status == 404) {
                throw new FileNotFoundException(path);
            }
            if (status / 100 == 2) {
                try (InputStream is = connection.getInputStream()) {
                    return IOUtils.toByteArray(is);
                }
            }
            throw new IOException("HTTP " + status + "/" + connection.getResponseMessage());
        } finally {
            connection.disconnect();
        }
    }

    @Override
    public boolean checkFile(GiteaRepository repository, String ref, String path)
            throws IOException, InterruptedException {
        HttpURLConnection connection = openConnection(api()
                .literal("/repos")
                .path(UriTemplateBuilder.var("username"))
                .path(UriTemplateBuilder.var("name"))
                .literal("/raw")
                .path(UriTemplateBuilder.var("ref", true))
                .path(UriTemplateBuilder.var("path", true))
                .build()
                .set("username", repository.getOwner().getUsername())
                .set("name", repository.getName())
                .set("ref", StringUtils.split(ref, '/'))
                .set("path", StringUtils.split(path, "/")));
        withAuthentication(connection);
        try {
            connection.connect();
            int status = connection.getResponseCode();
            if (status == 404) {
                return false;
            }
            if (status / 100 == 2) {
                return true;
            }
            throw new IOException("HTTP " + status + "/" + connection.getResponseMessage());
        } finally {
            connection.disconnect();
        }
    }

    @Override
    public void close() throws IOException {
    }

    private UriTemplateBuilder api() {
        return UriTemplate.buildFromTemplate(serverUrl).literal("/api/v1");
    }

    private void withAuthentication(HttpURLConnection connection) {
        if (authentication instanceof GiteaAuthUser) {
            String auth = (((GiteaAuthUser) authentication).getUsername()) + ":" + (((GiteaAuthUser) authentication).
                    getPassword());
            connection.setRequestProperty("Authorization", "Basic " + Base64.encodeBase64String(auth.getBytes(
                    StandardCharsets.UTF_8)));
        } else if (authentication instanceof GiteaAuthToken) {
            connection.setRequestProperty("Authorization", "token " + ((GiteaAuthToken) authentication).getToken());
        }
    }

    private int status(UriTemplate template) throws IOException, InterruptedException {
        HttpURLConnection connection = openConnection(template);
        withAuthentication(connection);
        try {
            connection.connect();
            return connection.getResponseCode();
        } finally {
            connection.disconnect();
        }
    }

    private int delete(UriTemplate template) throws IOException, InterruptedException {
        HttpURLConnection connection = openConnection(template);
        withAuthentication(connection);
        connection.setRequestMethod("DELETE");
        try {
            connection.connect();
            return connection.getResponseCode();
        } finally {
            connection.disconnect();
        }
    }

    private <T> T getObject(UriTemplate template, final Class<T> modelClass) throws IOException, InterruptedException {
        HttpURLConnection connection = openConnection(template);
        withAuthentication(connection);
        try {
            connection.connect();
            int status = connection.getResponseCode();
            if (status == 200) {
                try (InputStream is = connection.getInputStream()) {
                    return mapper.readerFor(modelClass).readValue(is);
                }
            }
            throw new GiteaHttpStatusException(status, connection.getResponseMessage());
        } finally {
            connection.disconnect();
        }
    }

    private <T> T post(UriTemplate template, Object body, final Class<T> modelClass)
            throws IOException, InterruptedException {
        HttpURLConnection connection = openConnection(template);
        withAuthentication(connection);
        connection.setRequestMethod("POST");
        byte[] bytes;
        if (body != null) {
            bytes = mapper.writer(new StdDateFormat()).writeValueAsBytes(body);
            connection.setRequestProperty("Content-Type", "application/json");
            connection.setRequestProperty("Content-Length", Integer.toString(bytes.length));
            connection.setDoOutput(true);
        } else {
            bytes = null;
            connection.setDoOutput(false);
        }
        connection.setDoInput(!Void.class.equals(modelClass));

        try {
            connection.connect();
            if (bytes != null) {
                try (OutputStream os = connection.getOutputStream()) {
                    os.write(bytes);
                }
            }
            int status = connection.getResponseCode();
            if (status / 100 == 2) {
                if (Void.class.equals(modelClass)) {
                    return null;
                }
                try (InputStream is = connection.getInputStream()) {
                    return mapper.readerFor(modelClass).readValue(is);
                }
            }
            throw new GiteaHttpStatusException(
                    status,
                    connection.getResponseMessage(),
                    bytes != null ? new String(bytes, StandardCharsets.UTF_8) : null
            );
        } finally {
            connection.disconnect();
        }
    }

    private <T> T patch(UriTemplate template, Object body, final Class<T> modelClass)
            throws IOException, InterruptedException {
        HttpURLConnection connection = openConnection(template);
        withAuthentication(connection);
        setRequestMethodViaJreBugWorkaround(connection, "PATCH");
        byte[] bytes;
        if (body != null) {
            bytes = mapper.writer(new StdDateFormat()).writeValueAsBytes(body);
            connection.setRequestProperty("Content-Type", "application/json");
            connection.setRequestProperty("Content-Length", Integer.toString(bytes.length));
            connection.setDoOutput(true);
        } else {
            bytes = null;
            connection.setDoOutput(false);
        }
        connection.setDoInput(true);

        try {
            connection.connect();
            if (bytes != null) {
                try (OutputStream os = connection.getOutputStream()) {
                    os.write(bytes);
                }
            }
            int status = connection.getResponseCode();
            if (status / 100 == 2) {
                if (Void.class.equals(modelClass)) {
                    return null;
                }
                try (InputStream is = connection.getInputStream()) {
                    return mapper.readerFor(modelClass).readValue(is);
                }
            }
            throw new GiteaHttpStatusException(
                    status,
                    connection.getResponseMessage(),
                    bytes != null ? new String(bytes, StandardCharsets.UTF_8) : null
            );
        } finally {
            connection.disconnect();
        }
    }

    private <T> List<T> getList(UriTemplate template, final Class<T> modelClass)
            throws IOException, InterruptedException {
        HttpURLConnection connection = openConnection(template);
        withAuthentication(connection);
        try {
            connection.connect();
            int status = connection.getResponseCode();
            if (status / 100 == 2) {
                try (InputStream is = connection.getInputStream()) {
                    List<T> list = mapper.readerFor(mapper.getTypeFactory()
                            .constructCollectionType(List.class, modelClass))
                            .readValue(is);
                    // strip null values from the list
                    for (Iterator<T> iterator = list.iterator(); iterator.hasNext(); ) {
                        if (iterator.next() == null) {
                            iterator.remove();
                        }
                    }
                    return list;
                }
            }
            throw new GiteaHttpStatusException(status, connection.getResponseMessage());
        } finally {
            connection.disconnect();
        }
    }

    @Restricted(NoExternalUse.class)
    protected HttpURLConnection openConnection(UriTemplate template) throws IOException {
        URL url = new URL(template.expand());
        Jenkins jenkins = Jenkins.get();
        if (jenkins.proxy == null) {
            return (HttpURLConnection) url.openConnection();
        }
        return (HttpURLConnection) url.openConnection(jenkins.proxy.createProxy(url.getHost()));
    }

}