/**
 * 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.apache.manifoldcf.authorities.authorities.amazons3;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.apache.manifoldcf.authorities.interfaces.AuthorizationResponse;
import org.apache.manifoldcf.authorities.system.Logging;
import org.apache.manifoldcf.core.interfaces.ConfigNode;
import org.apache.manifoldcf.core.interfaces.ConfigParams;
import org.apache.manifoldcf.core.interfaces.IHTTPOutput;
import org.apache.manifoldcf.core.interfaces.IPasswordMapperActivity;
import org.apache.manifoldcf.core.interfaces.IPostParameters;
import org.apache.manifoldcf.core.interfaces.IThreadContext;
import org.apache.manifoldcf.core.interfaces.ManifoldCFException;

import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.AccessControlList;
import com.amazonaws.services.s3.model.Bucket;
import com.amazonaws.services.s3.model.CanonicalGrantee;
import com.amazonaws.services.s3.model.Grant;
import com.amazonaws.services.s3.model.Grantee;
import com.amazonaws.services.s3.model.Owner;

/**
 * Authority connector for Amazons3
 * @author Kuhajeyan
 *
 */
public class AmazonS3Authority extends org.apache.manifoldcf.authorities.authorities.BaseAuthorityConnector {
  private static final String TAB_NAME = "TabName";

  protected long lastSessionFetch = -1L;

  protected static final long timeToRelease = 300000L;

  protected AmazonS3 amazonS3;

  protected boolean connected = false;

  protected String amazons3ProxyHost = null;

  protected String amazons3ProxyPort = null;

  protected String amazons3ProxyDomain = null;

  protected String amazons3ProxyUserName = null;

  protected String amazons3ProxyPassword = null;

  protected String amazons3AwsAccessKey = null;

  protected String amazons3AwsSecretKey = null;

  public AmazonS3Authority() {

  }

  @Override
  public void disconnect() throws ManifoldCFException {
    amazons3AwsAccessKey = null;
    amazons3AwsSecretKey = null;

    amazons3ProxyHost = null;
    amazons3ProxyPort = null;
    amazons3ProxyDomain = null;
    amazons3ProxyUserName = null;
    amazons3ProxyPassword = null;
  }

  @Override
  public void connect(ConfigParams configParams) {
    super.connect(configParams);
    // aws access and secret keys
    amazons3AwsAccessKey = configParams
        .getParameter(AmazonS3Config.AWS_ACCESS_KEY);
    amazons3AwsSecretKey = configParams
        .getObfuscatedParameter(AmazonS3Config.AWS_SECRET_KEY);

    // proxy values
    amazons3ProxyHost = configParams
        .getParameter(AmazonS3Config.AMAZONS3_PROXY_HOST);
    amazons3ProxyPort = configParams
        .getParameter(AmazonS3Config.AMAZONS3_PROXY_PORT);
    amazons3ProxyDomain = configParams
        .getParameter(AmazonS3Config.AMAZONS3_PROXY_DOMAIN);
    amazons3ProxyUserName = configParams
        .getParameter(AmazonS3Config.AMAZONS3_PROXY_USERNAME);
    amazons3ProxyPassword = configParams
        .getObfuscatedParameter(AmazonS3Config.AMAZONS3_PROXY_PASSWORD);
  }

  /**
   * Test the connection. Returns a string describing the connection
   * integrity.
   *
   * @return the connection's status as a displayable string.
   */
  @Override
  public String check() throws ManifoldCFException {
    // connect with amazons3 client
    Logging.authorityConnectors.info("Checking connection");

    try {
      // invokes the check thread
      CheckThread checkThread = new CheckThread(getClient());
      checkThread.start();
      checkThread.join();
      if (checkThread.getException() != null) {
        Throwable thr = checkThread.getException();
        return "Check exception: " + thr.getMessage();
      }
      return checkThread.getResult();
    }
    catch (InterruptedException ex) {
      Logging.authorityConnectors.error(
          "Error while checking connection", ex);
      throw new ManifoldCFException(ex.getMessage(), ex,
          ManifoldCFException.INTERRUPTED);
    }

  }

  /**
   * Get the Amazons3 client, relevant access keys should have been posted
   * already
   * @return
   */
  protected AmazonS3 getClient() {
    if (amazonS3 == null) {
      try {
        BasicAWSCredentials awsCreds = new BasicAWSCredentials(
            amazons3AwsAccessKey, amazons3AwsSecretKey);
        amazonS3 = new AmazonS3Client(awsCreds);
      }
      catch (Exception e) {
        Logging.authorityConnectors.error(
            "Error while amazon s3 connectionr", e);

      }
    }
    lastSessionFetch = System.currentTimeMillis();
    return amazonS3;
  }

  @Override
  public boolean isConnected() {
    return amazonS3 != null && amazonS3.getS3AccountOwner() != null;
  }

  @Override
  public void poll() throws ManifoldCFException {
    if (lastSessionFetch == -1L) {
      return;
    }

    long currentTime = System.currentTimeMillis();
    if (currentTime >= lastSessionFetch + timeToRelease) {
      amazonS3 = null;
      lastSessionFetch = -1L;
    }
  }

  private void fillInServerConfigurationMap(Map<String, Object> out,
      IPasswordMapperActivity mapper, ConfigParams parameters) {

    String amazons3AccessKey = parameters
        .getParameter(AmazonS3Config.AWS_ACCESS_KEY);
    String amazons3SecretKey = parameters
        .getObfuscatedParameter(AmazonS3Config.AWS_SECRET_KEY);

    // default values
    if (amazons3AccessKey == null)
      amazons3AccessKey = AmazonS3Config.AMAZONS3_AWS_ACCESS_KEY_DEFAULT;
    if (amazons3SecretKey == null)
      amazons3SecretKey = AmazonS3Config.AMAZONS3_AWS_SECRET_KEY_DEFAULT;
    else
      amazons3SecretKey = mapper.mapPasswordToKey(amazons3SecretKey);

    // fill the map
    out.put("AMAZONS3_AWS_ACCESS_KEY", amazons3AccessKey);
    out.put("AMAZONS3_AWS_SECRET_KEY", amazons3SecretKey);
  }

  private void fillInProxyConfigurationMap(Map<String, Object> out,
      IPasswordMapperActivity mapper, ConfigParams parameters) {
    String amazons3ProxyHost = parameters
        .getParameter(AmazonS3Config.AMAZONS3_PROXY_HOST);
    String amazons3ProxyPort = parameters
        .getParameter(AmazonS3Config.AMAZONS3_PROXY_PORT);
    String amazons3ProxyDomain = parameters
        .getParameter(AmazonS3Config.AMAZONS3_PROXY_DOMAIN);
    String amazons3ProxyUserName = parameters
        .getParameter(AmazonS3Config.AMAZONS3_PROXY_USERNAME);
    String amazons3ProxyPassword = parameters
        .getObfuscatedParameter(AmazonS3Config.AMAZONS3_PROXY_PASSWORD);

    if (amazons3ProxyHost == null)
      amazons3ProxyHost = AmazonS3Config.AMAZONS3_PROXY_HOST_DEFAULT;
    if (amazons3ProxyPort == null)
      amazons3ProxyPort = AmazonS3Config.AMAZONS3_PROXY_PORT_DEFAULT;
    if (amazons3ProxyDomain == null)
      amazons3ProxyDomain = AmazonS3Config.AMAZONS3_PROXY_DOMAIN_DEFAULT;
    if (amazons3ProxyUserName == null)
      amazons3ProxyUserName = AmazonS3Config.AMAZONS3_PROXY_USERNAME_DEFAULT;
    if (amazons3ProxyPassword == null)
      amazons3ProxyPassword = AmazonS3Config.AMAZONS3_PROXY_PASSWORD_DEFAULT;
    else
      amazons3ProxyPassword = mapper
          .mapPasswordToKey(amazons3ProxyPassword);

    // fill the map
    out.put("AMAZONS3_PROXY_HOST", amazons3ProxyHost);
    out.put("AMAZONS3_PROXY_PORT", amazons3ProxyPort);
    out.put("AMAZONS3_PROXY_DOMAIN", amazons3ProxyDomain);
    out.put("AMAZONS3_PROXY_USERNAME", amazons3ProxyUserName);
    out.put("AMAZONS3_PROXY_PWD", amazons3ProxyPassword);
  }

  /**
   * View configuration. This method is called in the body section of the
   * connector's view configuration page. Its purpose is to present the
   * connection information to the user. The coder can presume that the HTML
   * that is output from this configuration will be within appropriate &lt;html&gt;
   * and &lt;body&gt; tags.
   * 
   * */
  public void viewConfiguration(IThreadContext threadContext,
      IHTTPOutput out, Locale locale, ConfigParams parameters)
      throws ManifoldCFException, IOException {
    Map<String, Object> paramMap = new HashMap<String, Object>();

    // Fill in map from each tab
    fillInServerConfigurationMap(paramMap, out, parameters);
    fillInProxyConfigurationMap(paramMap, out, parameters);

    Messages.outputResourceWithVelocity(out, locale, AmazonS3Config.VIEW_CONFIG_FORWARD, paramMap);
  }

  /**
   * Output the configuration header section. This method is called in the
   * head section of the connector's configuration page. Its purpose is to add
   * the required tabs to the list, and to output any javascript methods that
   * might be needed by the configuration editing HTML.
   * */
  @Override
  public void outputConfigurationHeader(IThreadContext threadContext,
      IHTTPOutput out, Locale locale, ConfigParams parameters,
      List<String> tabsArray) throws ManifoldCFException, IOException {
    // Add the Server tab
    tabsArray.add(Messages.getString(locale,
        AmazonS3Config.AMAZONS3_SERVER_TAB_PROPERTY));
    // Add the Proxy tab
    tabsArray.add(Messages.getString(locale,
        AmazonS3Config.AMAZONS3_PROXY_TAB_PROPERTY));
    // Map the parameters
    Map<String, Object> paramMap = new HashMap<String, Object>();

    // Fill in the parameters from each tab
    fillInServerConfigurationMap(paramMap, out, parameters);
    fillInProxyConfigurationMap(paramMap, out, parameters);

    // Output the Javascript - only one Velocity template for all tabs
    Messages.outputResourceWithVelocity(out, locale,
        AmazonS3Config.EDIT_CONFIG_HEADER_FORWARD, paramMap);
  }

  @Override
  public void outputConfigurationBody(IThreadContext threadContext,
      IHTTPOutput out, Locale locale, ConfigParams parameters,
      String tabName) throws ManifoldCFException, IOException {

    // Call the Velocity templates for each tab
    Map<String, Object> paramMap = new HashMap<String, Object>();
    // Set the tab name
    paramMap.put(TAB_NAME, tabName);

    // Fill in the parameters
    fillInServerConfigurationMap(paramMap, out, parameters);
    fillInProxyConfigurationMap(paramMap, out, parameters);

    // Server tab
    Messages.outputResourceWithVelocity(out, locale,
        AmazonS3Config.EDIT_CONFIG_FORWARD_SERVER, paramMap);
    // Proxy tab
    Messages.outputResourceWithVelocity(out, locale,
        AmazonS3Config.EDIT_CONFIG_FORWARD_PROXY, paramMap);

  }

  /**
   * Process a configuration post. This method is called at the start of the
   * connector's configuration page, whenever there is a possibility that form
   * data for a connection has been posted. Its purpose is to gather form
   * information and modify the configuration parameters accordingly. The name
   * of the posted form is "editconnection".
   * */
  @Override
  public String processConfigurationPost(IThreadContext threadContext,
      IPostParameters variableContext, Locale locale,
      ConfigParams parameters) throws ManifoldCFException {
    // server tab
    String awsAccessKey = variableContext.getParameter("aws_access_key");

    if (awsAccessKey != null) {
      parameters
          .setParameter(AmazonS3Config.AWS_ACCESS_KEY, awsAccessKey);
    }
    String awsSecretKey = variableContext.getParameter("aws_secret_key");
    if (awsSecretKey != null) {
      // set as obfuscated parameter
      parameters.setObfuscatedParameter(AmazonS3Config.AWS_SECRET_KEY,
          variableContext.mapKeyToPassword(awsSecretKey));
    }
    Logging.authorityConnectors.info("Saved values for aws keys");

    int i = 0;
    while (i < parameters.getChildCount()) {
      ConfigNode cn = parameters.getChild(i);
      if (cn.getType().equals(AmazonS3Config.AWS_ACCESS_KEY)
          || cn.getType().equals(AmazonS3Config.AWS_SECRET_KEY))
        parameters.removeChild(i);
      else
        i++;
    }

    // proxy tab
    String amazons3ProxyHost = variableContext
        .getParameter("amazons3_proxy_host");
    if (amazons3ProxyHost != null) {
      parameters.setParameter(AmazonS3Config.AMAZONS3_PROXY_HOST,
          amazons3ProxyHost);
    }
    String amazons3ProxyPort = variableContext
        .getParameter("amazons3_proxy_port");
    if (amazons3ProxyPort != null) {
      parameters.setParameter(AmazonS3Config.AMAZONS3_PROXY_PORT,
          amazons3ProxyPort);
    }
    String amazons3ProxyDomain = variableContext
        .getParameter("amazons3_proxy_domain");
    if (amazons3ProxyDomain != null) {
      parameters.setParameter(AmazonS3Config.AMAZONS3_PROXY_DOMAIN,
          amazons3ProxyDomain);
    }
    String amazons3ProxyUserName = variableContext
        .getParameter("amazons3_proxy_username");
    if (amazons3ProxyUserName != null) {
      parameters.setParameter(AmazonS3Config.AMAZONS3_PROXY_USERNAME,
          amazons3ProxyUserName);
    }
    String amazons3ProxyPassword = variableContext
        .getParameter("amazons3_proxy_pwd");
    if (amazons3ProxyPassword != null) {
      // set as obfuscated parameter
      parameters.setObfuscatedParameter(
          AmazonS3Config.AMAZONS3_PROXY_PASSWORD,
          variableContext.mapKeyToPassword(amazons3ProxyPassword));
    }

    return null;
  }

  @Override
  public AuthorizationResponse getAuthorizationResponse(String userName)
      throws ManifoldCFException {

    try {
      HashMap<String, Set<Grant>> checkUserExists = checkUserExists(userName);
      if (isUserAvailable(userName, checkUserExists.values())) {
        return new AuthorizationResponse(new String[] { userName },
          AuthorizationResponse.RESPONSE_OK);
      }
		
    }
    catch (Exception e) {
      Logging.authorityConnectors.error("Error while getting authorization response",e);
      return RESPONSE_UNREACHABLE;
    }
    return RESPONSE_USERNOTFOUND;
  }

  private boolean isUserAvailable(String userName,
      Collection<Set<Grant>> collection) {
    String[] users = getUsers(collection);
    return Arrays.asList(users).contains(userName);
  }

  private String[] getUsers(Collection<Set<Grant>> collection) {
    Set<String> users = new HashSet<String>();// no duplicates
    for (Collection c : collection) {
      Set<Grant> c1 = (Set<Grant>) c;
      for (Grant grant : c1) {
        if (grant != null && grant.getGrantee() != null) {
          Grantee grantee = grant.getGrantee();

          if (grantee instanceof CanonicalGrantee) {
            users.add(((CanonicalGrantee) grantee).getDisplayName());
          }
          else {
            users.add(grantee.getIdentifier());
          }
        }
      }
    }

    return users.toArray(new String[users.size()]);
  }

  private HashMap<String, Set<Grant>> checkUserExists(String userName)
      throws ManifoldCFException {
    GrantsThread t = new GrantsThread(getClient());
    try {
      t.start();
      t.finishUp();
      return t.getResult();
    }
    catch (InterruptedException e) {
      t.interrupt();
      throw new ManifoldCFException("Interrupted: " + e.getMessage(), e,
          ManifoldCFException.INTERRUPTED);
    }
    catch (java.net.SocketTimeoutException e) {
      handleIOException(e);
    }
    catch (InterruptedIOException e) {
      t.interrupt();
      handleIOException(e);
    }
    catch (IOException e) {
      handleIOException(e);
    }
    catch (ResponseException e) {
      handleResponseException(e);
    }
    return null;
  }

  /**
   * Obtain the default access tokens for a given user name.
   * @param userName is the user name or identifier.
   * @return the default response tokens, presuming that the connect method
   * fails.
   */
  @Override
  public AuthorizationResponse getDefaultAuthorizationResponse(String userName) {
    return RESPONSE_UNREACHABLE;
  }

  private static void handleIOException(IOException e)
      throws ManifoldCFException {
    if (!(e instanceof java.net.SocketTimeoutException)
        && (e instanceof InterruptedIOException)) {
      throw new ManifoldCFException("Interrupted: " + e.getMessage(), e,
          ManifoldCFException.INTERRUPTED);
    }
    Logging.authorityConnectors.warn(
        "JIRA: IO exception: " + e.getMessage(), e);
    throw new ManifoldCFException("IO exception: " + e.getMessage(), e);
  }

  private static void handleResponseException(ResponseException e)
      throws ManifoldCFException {
    throw new ManifoldCFException("Response exception: " + e.getMessage(),
        e);
  }

  protected static class GrantsThread extends Thread {

    protected Throwable exception = null;

    protected boolean result = false;

    protected AmazonS3 amazonS3 = null;

    private HashMap<String, Set<Grant>> grants;

    public GrantsThread(AmazonS3 amazonS3) {
      super();
      this.amazonS3 = amazonS3;
      setDaemon(true);
      grants = new HashMap<String, Set<Grant>>();
    }

    public void finishUp() throws InterruptedException, IOException,
        ResponseException {
      join();
      Throwable thr = exception;
      if (thr != null) {
        if (thr instanceof IOException) {
          throw (IOException) thr;
        }
        else if (thr instanceof ResponseException) {
          throw (ResponseException) thr;
        }
        else if (thr instanceof RuntimeException) {
          throw (RuntimeException) thr;
        }
        else {
          throw (Error) thr;
        }
      }
    }

    @Override
    public void run() {

      List<Bucket> listBuckets = amazonS3.listBuckets();
      for (Bucket bucket : listBuckets) {
        AccessControlList bucketAcl = amazonS3.getBucketAcl(bucket
            .getName());

        if (bucketAcl != null)
          grants.put(bucket.getName(), bucketAcl.getGrants());
      }
    }

    public HashMap<String, Set<Grant>> getResult() {
      return grants;
    }

  }

  protected static class CheckThread extends Thread {
    protected String result = "Unknown";

    protected AmazonS3 s3 = null;

    protected Throwable exception = null;

    public CheckThread(AmazonS3 s3) {
      this.s3 = s3;
    }

    public String getResult() {
      return result;
    }

    public Throwable getException() {
      return exception;
    }

    @Override
    public void run() {
      try {
        if (s3 != null) {
          Owner s3AccountOwner = s3.getS3AccountOwner();
          if (s3AccountOwner != null) {
            result = StringUtils.isNotEmpty(s3AccountOwner
                .getDisplayName()) ? "Connection OK"
                : "Connection Failed";
          }

        }
      }
      catch (AmazonServiceException e) {
        result = "Connection Failed : " + e.getMessage();
        exception = e;

        Logging.authorityConnectors.error(e);
      }
      catch (AmazonClientException e) {
        result = "Connection Failed : " + e.getMessage();
        exception = e;

        Logging.authorityConnectors.error(e);
      }
    }
  }

}
