/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.jclouds.chef.filters;

import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.hash.Hashing.sha1;
import static com.google.common.io.BaseEncoding.base64;

import java.io.IOException;
import java.security.PrivateKey;
import java.util.NoSuchElementException;

import javax.annotation.Resource;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;

import org.jclouds.Constants;
import org.jclouds.crypto.Crypto;
import org.jclouds.date.TimeStamp;
import org.jclouds.domain.Credentials;
import org.jclouds.http.HttpException;
import org.jclouds.http.HttpRequest;
import org.jclouds.http.HttpRequestFilter;
import org.jclouds.http.HttpUtils;
import org.jclouds.http.internal.SignatureWire;
import org.jclouds.io.ByteStreams2;
import org.jclouds.io.Payload;
import org.jclouds.io.Payloads;
import org.jclouds.io.payloads.MultipartForm;
import org.jclouds.io.payloads.Part;
import org.jclouds.io.payloads.RSAEncryptingPayload;
import org.jclouds.logging.Logger;
import org.jclouds.util.Strings2;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Predicate;
import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.base.Throwables;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.google.common.io.ByteSource;

/**
 * Ported from mixlib-authentication in order to sign Chef requests.
 * 
 * @see <a href= "https://github.com/chef/mixlib-authentication" />
 */
@Singleton
public class SignedHeaderAuth implements HttpRequestFilter {
   public static final String SIGNING_DESCRIPTION = "version=1.0";

   private final SignatureWire signatureWire;
   private final Supplier<Credentials> creds;
   private final Supplier<PrivateKey> supplyKey;
   private final Provider<String> timeStampProvider;
   private final String emptyStringHash;
   private final HttpUtils utils;
   private final Crypto crypto;

   @Resource
   @Named(Constants.LOGGER_SIGNATURE)
   Logger signatureLog = Logger.NULL;

   @Inject
   public SignedHeaderAuth(SignatureWire signatureWire, @org.jclouds.location.Provider Supplier<Credentials> creds,
         Supplier<PrivateKey> supplyKey, @TimeStamp Provider<String> timeStampProvider, HttpUtils utils, Crypto crypto) {
      this.signatureWire = checkNotNull(signatureWire, "signatureWire");
      this.creds = checkNotNull(creds, "creds");
      this.supplyKey = checkNotNull(supplyKey, "supplyKey");
      this.timeStampProvider = checkNotNull(timeStampProvider, "timeStampProvider");
      this.emptyStringHash = hashBody(Payloads.newStringPayload(""));
      this.utils = checkNotNull(utils, "utils");
      this.crypto = checkNotNull(crypto, "crypto");
   }

   public HttpRequest filter(HttpRequest input) throws HttpException {
      HttpRequest request = input.toBuilder().endpoint(input.getEndpoint().toString().replace("%3F", "?")).build();
      String contentHash = hashBody(request.getPayload());
      Multimap<String, String> headers = ArrayListMultimap.create();
      headers.put("X-Ops-Content-Hash", contentHash);
      String timestamp = timeStampProvider.get();
      String toSign = createStringToSign(request.getMethod(), hashPath(request.getEndpoint().getPath()), contentHash,
            timestamp);
      headers.put("X-Ops-Userid", creds.get().identity);
      headers.put("X-Ops-Sign", SIGNING_DESCRIPTION);
      request = calculateAndReplaceAuthorizationHeaders(request, toSign);
      headers.put("X-Ops-Timestamp", timestamp);
      utils.logRequest(signatureLog, request, "<<");

      return request.toBuilder().replaceHeaders(headers).build();
   }

   @VisibleForTesting
   HttpRequest calculateAndReplaceAuthorizationHeaders(HttpRequest request, String toSign) throws HttpException {
      String signature = sign(toSign);
      if (signatureWire.enabled())
         signatureWire.input(Strings2.toInputStream(signature));
      String[] signatureLines = Iterables.toArray(Splitter.fixedLength(60).split(signature), String.class);

      Multimap<String, String> headers = ArrayListMultimap.create();
      for (int i = 0; i < signatureLines.length; i++) {
         headers.put("X-Ops-Authorization-" + (i + 1), signatureLines[i]);
      }
      return request.toBuilder().replaceHeaders(headers).build();
   }

   public String createStringToSign(String request, String hashedPath, String contentHash, String timestamp) {

      return new StringBuilder().append("Method:").append(request).append("\n").append("Hashed Path:")
            .append(hashedPath).append("\n").append("X-Ops-Content-Hash:").append(contentHash).append("\n")
            .append("X-Ops-Timestamp:").append(timestamp).append("\n").append("X-Ops-UserId:")
            .append(creds.get().identity).toString();

   }

   @VisibleForTesting
   String hashPath(String path) {
      try {
         return base64().encode(ByteSource.wrap(canonicalPath(path).getBytes(UTF_8)).hash(sha1()).asBytes());
      } catch (Exception e) {
         Throwables.propagateIfPossible(e);
         throw new HttpException("error creating sigature for path: " + path, e);
      }
   }

   /**
    * Build the canonicalized path, which collapses multiple slashes (/) and
    * removes a trailing slash unless the path is only "/"
    */
   @VisibleForTesting
   String canonicalPath(String path) {
      path = path.replaceAll("\\/+", "/");
      return path.endsWith("/") && path.length() > 1 ? path.substring(0, path.length() - 1) : path;
   }

   @VisibleForTesting
   String hashBody(Payload payload) {
      if (payload == null)
         return emptyStringHash;
      payload = useTheFilePartIfForm(payload);
      checkArgument(payload != null, "payload was null");
      checkArgument(payload.isRepeatable(), "payload must be repeatable: " + payload);
      try {
         return base64().encode(ByteStreams2.hashAndClose(payload.getInput(), sha1()).asBytes());
      } catch (Exception e) {
         Throwables.propagateIfPossible(e);
         throw new HttpException("error creating sigature for payload: " + payload, e);
      }
   }

   private Payload useTheFilePartIfForm(Payload payload) {
      if (payload instanceof MultipartForm) {
         Iterable<? extends Part> parts = MultipartForm.class.cast(payload).getRawContent();
         try {
            payload = Iterables.find(parts, new Predicate<Part>() {

               @Override
               public boolean apply(Part input) {
                  return "file".equals(input.getName());
               }

            });
         } catch (NoSuchElementException e) {

         }
      }
      return payload;
   }

   public String sign(String toSign) {
      try {
         byte[] encrypted = ByteStreams2.toByteArrayAndClose(new RSAEncryptingPayload(crypto, Payloads.newStringPayload(toSign), supplyKey.get()).openStream());
         return base64().encode(encrypted);
      } catch (IOException e) {
         throw new HttpException("error signing request", e);
      }
   }

}
