/*
 * Copyright 2000-2013 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.buildTriggers.url;

import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.io.StreamUtil;
import com.intellij.util.containers.ConcurrentWeakHashMap;
import jetbrains.buildServer.util.StringUtil;
import org.apache.http.NameValuePair;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.jetbrains.annotations.NotNull;

import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class OAuthCredentialsProvider {
  @NotNull
  private final HttpClientProvider httpClientProvider;
  @NotNull
  private final Gson gson = new Gson();
  @NotNull
  private final Map<String, CredentialsDTO> cachedCredentialsMap = new ConcurrentWeakHashMap<>();

  private final static Logger LOG = LoggerFactory.getLogger(OAuthCredentialsProvider.class);

  public OAuthCredentialsProvider(@NotNull final HttpClientProvider httpClientProvider) {
    this.httpClientProvider = httpClientProvider;
  }

  public Credentials getCredentials(@NotNull final String oauthUrl, @NotNull final String clientId, @NotNull final String clientSecret, final int connectionTimeout) throws Exception {
    CredentialsDTO cachedCredentials = cachedCredentialsMap.get(oauthUrl);
    if (cachedCredentials != null) {
      if (cachedCredentials.isExpired()) {
        LOG.warn("OAuth credentials expired for " + oauthUrl + ": retrieved at " + cachedCredentials.retrievedAt + ", ttl " + cachedCredentials.expiresIn + ", now " + System.nanoTime());
        cachedCredentialsMap.remove(oauthUrl);
      } else {
        return new BearerTokenCredentials(cachedCredentials.accessToken);
      }
    }

    try (CloseableHttpClient client = httpClientProvider.createClient(new URL(oauthUrl).toURI(), new UsernamePasswordCredentials(clientId, clientSecret), connectionTimeout)) {
      HttpPost post = new HttpPost(new URL(oauthUrl).toURI());
      post.addHeader("Content-Type", "application/x-www-form-urlencoded");

      List<NameValuePair> nvps = new ArrayList<>();
      nvps.add(new BasicNameValuePair("client_id", clientId));
      nvps.add(new BasicNameValuePair("client_secret", clientSecret));
      nvps.add(new BasicNameValuePair("grant_type", "client_credentials"));

      post.setEntity(new UrlEncodedFormEntity(nvps, "UTF-8"));

      CloseableHttpResponse response = client.execute(post);

      if (response.getStatusLine().getStatusCode() != 200) {
        throw new IOException("Got unexpected code " + response.getStatusLine().getStatusCode() + "; " + StringUtil.truncateStringValue(StreamUtil.readText(response.getEntity().getContent()), 100));
      }

      CredentialsDTO credentialsDTO = gson.fromJson(new InputStreamReader(response.getEntity().getContent()), CredentialsDTO.class);
      credentialsDTO.retrievedAt = System.nanoTime(); // gson creates the object through some dirty reflection

      cachedCredentialsMap.put(oauthUrl, credentialsDTO);

      return new BearerTokenCredentials(credentialsDTO.accessToken);
    }
  }

  private static class CredentialsDTO {
    @SerializedName("access_token")
    public final String accessToken;

    @SerializedName("token_type")
    public final String type;

    @SerializedName("expires_in")
    public final int expiresIn;

    transient public long retrievedAt;

    private CredentialsDTO(String accessToken, String type, int expiresIn) {
      this.accessToken = accessToken;
      this.type = type;
      this.expiresIn = expiresIn;
    }

    public boolean isExpired() {
      return expiresIn > 0 && retrievedAt + TimeUnit.SECONDS.toNanos(expiresIn / 2) < System.nanoTime();
    }

    @Override
    public String toString() {
      return "CredentialsDTO{" +
        "accessToken='" + accessToken + '\'' +
        ", type='" + type + '\'' +
        ", expiresIn=" + expiresIn +
        '}';
    }
  }
}
