/*
 * Copyright 2000-2012 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jetbrains.buildServer.commitPublisher.github.api.impl;

import com.google.gson.Gson;
import com.intellij.openapi.diagnostic.Logger;
import jetbrains.buildServer.commitPublisher.PublisherException;
import jetbrains.buildServer.commitPublisher.github.api.GitHubApi;
import jetbrains.buildServer.commitPublisher.github.api.GitHubChangeState;
import jetbrains.buildServer.commitPublisher.github.api.impl.data.*;
import jetbrains.buildServer.util.FileUtil;
import org.apache.http.*;
import org.apache.http.auth.AuthenticationException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.message.BasicHeader;
import org.apache.http.util.EntityUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author Eugene Petrenko (eugene.petrenko@gmail.com)
 * @author Tomaz Cerar
 *         Date: 05.09.12 23:39
 */
public abstract class GitHubApiImpl implements GitHubApi {
  private static final Logger LOG = Logger.getInstance(GitHubApiImpl.class.getName());
  private static final Pattern PULL_REQUEST_BRANCH = Pattern.compile("/?refs/pull/(\\d+)/(.*)");
  private static final String MSG_PROXY_OR_PERMISSIONS = "Please check if the error is not returned by a proxy or caused by the lack of permissions.";

  private final HttpClientWrapper myClient;
  private final GitHubApiPaths myUrls;
  private final Gson myGson = new Gson();

  public GitHubApiImpl(@NotNull final HttpClientWrapper client,
                       @NotNull final GitHubApiPaths urls
  ) {
    myClient = client;
    myUrls = urls;
  }

  @Nullable
  private static String getPullRequestId(@NotNull String repoName,
                                         @NotNull String branchName) {
    final Matcher matcher = PULL_REQUEST_BRANCH.matcher(branchName);
    if (!matcher.matches()) {
      LOG.debug("Branch " + branchName + " for repo " + repoName + " does not look like pull request");
      return null;
    }

    final String pullRequestId = matcher.group(1);
    if (pullRequestId == null) {
      LOG.debug("Branch " + branchName + " for repo " + repoName + " does not contain pull request id");
      return null;
    }
    return pullRequestId;
  }

  public void testConnection(@NotNull final String repoOwner,
                             @NotNull final String repoName) throws PublisherException {
    final HttpGet get = new HttpGet(myUrls.getRepoInfo(repoOwner, repoName));
    RepoInfo repoInfo;
    try {
      includeAuthentication(get);
      setDefaultHeaders(get);
      logRequest(get, null);
      repoInfo = processResponse(get, RepoInfo.class, true);
    } catch (Throwable ex) {
      throw new PublisherException(String.format("Error while retrieving %s/%s repository information", repoOwner, repoName), ex);
    } finally {
      get.abort();
    }
    if (null == repoInfo.name || null == repoInfo.permissions) {
      throw new PublisherException(String.format("Repository %s/%s is inaccessible", repoOwner, repoName));
    }
    if (!repoInfo.permissions.push) {
      throw new PublisherException(String.format("There is no push access to the repository %s/%s", repoOwner, repoName));
    }
  }

  public String readChangeStatus(@NotNull final String repoOwner,
                                 @NotNull final String repoName,
                                 @NotNull final String hash) throws IOException {
    final HttpGet post = new HttpGet(myUrls.getStatusUrl(repoOwner, repoName, hash));
    includeAuthentication(post);
    setDefaultHeaders(post);

    try {
      logRequest(post, null);

      final HttpResponse execute = myClient.execute(post);
      if (execute.getStatusLine().getStatusCode() != HttpURLConnection.HTTP_OK) {
        logFailedResponse(post, null, execute);
        throw new IOException(getErrorMessage(execute.getStatusLine(), null));
      }
      return "TBD";
    } finally {
      post.abort();
    }
  }

  private void setDefaultHeaders(@NotNull HttpUriRequest request) {
    request.setHeader(new BasicHeader(HttpHeaders.ACCEPT_ENCODING, "UTF-8"));
    request.setHeader(new BasicHeader(HttpHeaders.ACCEPT, "application/json"));
  }

  public void setChangeStatus(@NotNull final String repoOwner,
                              @NotNull final String repoName,
                              @NotNull final String hash,
                              @NotNull final GitHubChangeState status,
                              @NotNull final String targetUrl,
                              @NotNull final String description,
                              @Nullable final String context) throws IOException {
    final GSonEntity requestEntity = new GSonEntity(myGson, new CommitStatus(status.getState(), targetUrl, description, context));
    final HttpPost post = new HttpPost(myUrls.getStatusUrl(repoOwner, repoName, hash));
    try {
      post.setEntity(requestEntity);
      includeAuthentication(post);
      setDefaultHeaders(post);

      logRequest(post, requestEntity.getText());
      final HttpResponse execute = myClient.execute(post);
      if (execute.getStatusLine().getStatusCode() != HttpURLConnection.HTTP_CREATED) {
        logFailedResponse(post, requestEntity.getText(), execute);
        throw new IOException(getErrorMessage(execute.getStatusLine(), MSG_PROXY_OR_PERMISSIONS));
      }
    } finally {
      post.abort();
    }
  }

  public boolean isPullRequestMergeBranch(@NotNull String branchName) {
    final Matcher match = PULL_REQUEST_BRANCH.matcher(branchName);
    return match.matches() && "merge".equals(match.group(2));
  }

  @Nullable
  public String findPullRequestCommit(@NotNull String repoOwner,
                                      @NotNull String repoName,
                                      @NotNull String branchName) throws IOException, PublisherException {

    final String pullRequestId = getPullRequestId(repoName, branchName);
    if (pullRequestId == null) return null;

    //  /repos/:owner/:repo/pulls/:number

    final String requestUrl = myUrls.getPullRequestInfo(repoOwner, repoName, pullRequestId);
    final HttpGet get = new HttpGet(requestUrl);
    includeAuthentication(get);
    setDefaultHeaders(get);

    final PullRequestInfo pullRequestInfo = processResponse(get, PullRequestInfo.class, false);

    final RepoRefInfo head = pullRequestInfo.head;
    if (head != null) {
      return head.sha;
    }
    return null;
  }

  @NotNull
  public Collection<String> getCommitParents(@NotNull String repoOwner, @NotNull String repoName, @NotNull String hash) throws IOException, PublisherException {

    final String requestUrl = myUrls.getCommitInfo(repoOwner, repoName, hash);
    final HttpGet get = new HttpGet(requestUrl);

    final CommitInfo infos = processResponse(get, CommitInfo.class, false);
    if (infos.parents != null) {
      final Set<String> parents = new HashSet<String>();
      for (CommitInfo p : infos.parents) {
        String sha = p.sha;
        if (sha != null) {
          parents.add(sha);
        }
      }
      return parents;
    }
    return Collections.emptyList();
  }

  @NotNull
  private <T> T processResponse(@NotNull HttpUriRequest request, @NotNull final Class<T> clazz, boolean logErrorsDebugOnly) throws IOException, PublisherException {
    setDefaultHeaders(request);
    try {
      logRequest(request, null);

      final HttpResponse execute = myClient.execute(request);
      if (execute.getStatusLine().getStatusCode() != HttpURLConnection.HTTP_OK) {
        logFailedResponse(request, null, execute, logErrorsDebugOnly);
        throw new IOException(getErrorMessage(execute.getStatusLine(), MSG_PROXY_OR_PERMISSIONS));
      }

      final HttpEntity entity = execute.getEntity();
      if (entity == null) {
        logFailedResponse(request, null, execute, logErrorsDebugOnly);
        throw new IOException(getErrorMessage(execute.getStatusLine(), "Empty response."));
      }

      try {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        entity.writeTo(bos);
        final String json = bos.toString("utf-8");
        LOG.debug("Parsing json for " + request.getURI().toString() + ": " + json);
        T result = myGson.fromJson(json, clazz);
        if (null == result) {
          throw new PublisherException("GitHub publisher fails to parse a response");
        }
        return result;
      } finally {
        EntityUtils.consume(entity);
      }
    } finally {
      request.abort();
    }
  }

  @NotNull
  private static String getErrorMessage(@NotNull StatusLine statusLine, @Nullable String additionalComment) {
    String err = "";
    if (null != additionalComment) {
      err = additionalComment + " ";
    }
    return String.format("Failed to complete request to GitHub. %sStatus: %s", err, statusLine.toString());
  }

  private void includeAuthentication(@NotNull HttpRequest request) throws IOException {
    try {
      setAuthentication(request);
    } catch (AuthenticationException e) {
      throw new IOException("Failed to set authentication for request. " + e.getMessage(), e);
    }
  }

  protected abstract void setAuthentication(@NotNull final HttpRequest request) throws AuthenticationException;


  private void logFailedResponse(@NotNull HttpUriRequest request,
                                 @Nullable String requestEntity,
                                 @NotNull HttpResponse execute) throws IOException {
    logFailedResponse(request, requestEntity, execute, false);
  }


  private void logFailedResponse(@NotNull HttpUriRequest request,
                                 @Nullable String requestEntity,
                                 @NotNull HttpResponse execute,
                                 boolean debugOnly) throws IOException {
    String responseText = extractResponseEntity(execute);
    if (responseText == null) {
      responseText = "<none>";
    }
    if (requestEntity == null) {
      requestEntity = "<none>";
    }

    final String logEntry = "Failed to complete query to GitHub with:\n" +
            "  requestURL: " + request.getURI().toString() + "\n" +
            "  requestMethod: " + request.getMethod() + "\n" +
            "  requestEntity: " + requestEntity + "\n" +
            "  response: " + execute.getStatusLine() + "\n" +
            "  responseEntity: " + responseText;
    if (debugOnly) {
      LOG.debug(logEntry);
    } else {
      LOG.warn(logEntry);
    }
  }

  private void logRequest(@NotNull HttpUriRequest request,
                          @Nullable String requestEntity) {
    if (!LOG.isDebugEnabled()) return;

    if (requestEntity == null) {
      requestEntity = "<none>";
    }

    LOG.debug("Calling GitHub with:\n" +
            "  requestURL: " + request.getURI().toString() + "\n" +
            "  requestMethod: " + request.getMethod() + "\n" +
            "  requestEntity: " + requestEntity
    );
  }

  @Nullable
  private String extractResponseEntity(@NotNull final HttpResponse execute) throws IOException {
    final HttpEntity responseEntity = execute.getEntity();
    if (responseEntity == null) return null;
    try {
      final byte[] dataSlice = new byte[256 * 1024]; //limit buffer with 256K
      final InputStream content = responseEntity.getContent();
      try {
        int sz = content.read(dataSlice, 0, dataSlice.length);
        return new String(dataSlice, 0, sz, "utf-8");
      } finally {
        FileUtil.close(content);
      }
    } finally {
      EntityUtils.consume(responseEntity);
    }
  }

  public void postComment(@NotNull final String ownerName,
                          @NotNull final String repoName,
                          @NotNull final String hash,
                          @NotNull final String comment) throws IOException {

    final String requestUrl = myUrls.getAddCommentUrl(ownerName, repoName, hash);
    final GSonEntity requestEntity = new GSonEntity(myGson, new IssueComment(comment));
    final HttpPost post = new HttpPost(requestUrl);
    try {
      post.setEntity(requestEntity);
      includeAuthentication(post);
      setDefaultHeaders(post);

      logRequest(post, requestEntity.getText());

      final HttpResponse execute = myClient.execute(post);
      if (execute.getStatusLine().getStatusCode() != HttpURLConnection.HTTP_CREATED) {
        logFailedResponse(post, requestEntity.getText(), execute);
        throw new IOException(getErrorMessage(execute.getStatusLine(), null));
      }
    } finally {
      post.abort();
    }
  }
}
