/**
 * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2006-2014.
 *
 * 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 org.italiangrid.voms.request.impl;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.x509.AttributeCertificate;
import org.italiangrid.voms.VOMSError;
import org.italiangrid.voms.request.VOMSACRequest;
import org.italiangrid.voms.request.VOMSACService;
import org.italiangrid.voms.request.VOMSESLookupStrategy;
import org.italiangrid.voms.request.VOMSProtocol;
import org.italiangrid.voms.request.VOMSProtocolError;
import org.italiangrid.voms.request.VOMSProtocolListener;
import org.italiangrid.voms.request.VOMSRequestListener;
import org.italiangrid.voms.request.VOMSResponse;
import org.italiangrid.voms.request.VOMSServerInfo;
import org.italiangrid.voms.request.VOMSServerInfoStore;
import org.italiangrid.voms.request.VOMSServerInfoStoreListener;
import org.italiangrid.voms.util.NullListener;

import eu.emi.security.authn.x509.X509CertChainValidatorExt;
import eu.emi.security.authn.x509.X509Credential;

/**
 * The default implementation of the {@link VOMSACService}.
 * 
 * 
 * @author Valerio Venturi
 * @author Andrea Ceccanti
 * 
 */
public class DefaultVOMSACService implements VOMSACService {

  /**
   * The listener that will be informed about request events
   */
  protected VOMSRequestListener requestListener;

  /**
   * The listener that will be informed about low-level protocol details
   */
  protected VOMSProtocolListener protocolListener;

  /**
   * The validator used for the SSL handshake
   */
  protected X509CertChainValidatorExt validator;

  /**
   * The store used to keep VOMS server contact information.
   */
  protected VOMSServerInfoStore serverInfoStore;

  /**
   * The http protocol implementation
   */
  protected VOMSProtocol httpProtocol;

  /**
   * The voms legacy protocol implementation
   */
  protected VOMSProtocol legacyProtocol;

  /**
   * Constructor which builds a {@link DefaultVOMSACService} from a
   * {@link Builder}
   * 
   * @param builder
   *          the builder object that provides the settings for this
   *          {@link VOMSACService}
   */
  protected DefaultVOMSACService(Builder builder) {

    this.validator = builder.validator;
    this.requestListener = builder.requestListener;
    this.protocolListener = builder.protocolListener;
    this.serverInfoStore = builder.serverInfoStore;
    this.httpProtocol = builder.httpProtocol;
    this.legacyProtocol = builder.legacyProtocol;
  }

  /**
   * Extracts an AC from a VOMS response
   * 
   * @param request
   *          the request
   * @param response
   *          the received response
   * @return a possibly <code>null</code> {@link AttributeCertificate} object
   */
  protected AttributeCertificate getACFromResponse(VOMSACRequest request,
    VOMSResponse response) {

    byte[] acBytes = response.getAC();

    if (acBytes == null)
      return null;

    ASN1InputStream asn1InputStream = new ASN1InputStream(acBytes);

    AttributeCertificate attributeCertificate = null;

    try {

      attributeCertificate = AttributeCertificate.getInstance(asn1InputStream
        .readObject());

      asn1InputStream.close();
      return attributeCertificate;

    } catch (Throwable e) {

      requestListener.notifyVOMSRequestFailure(request, null, new VOMSError(
        "Error unmarshalling VOMS AC. Cause: " + e.getMessage(), e));

      return null;
    }
  }

  private VOMSResponse doRequest(VOMSProtocol protocol,
    VOMSServerInfo endpoint, X509Credential cred, VOMSACRequest req) {

    VOMSResponse response = null;

    try {

      response = protocol.doRequest(endpoint, cred, req);

    } catch (VOMSProtocolError e) {
      requestListener.notifyVOMSRequestFailure(req, endpoint, e);
    }

    return response;

  }

  /**
   * Handles errors included in the VOMS response
   * 
   * @param request
   *          the request
   * @param si
   *          the VOMS server endpoint information
   * @param response
   *          the received {@link VOMSResponse}
   */
  protected void handleErrorsInResponse(VOMSACRequest request,
    VOMSServerInfo si, VOMSResponse response) {

    if (response.hasErrors())
      requestListener.notifyErrorsInVOMSReponse(request, si,
        response.errorMessages());

  }

  /**
   * Handles warnings included in the VOMS response
   * 
   * @param request
   *          the request
   * @param si
   *          the VOMS server endpoint information
   * @param response
   *          the received {@link VOMSResponse}
   */
  protected void handleWarningsInResponse(VOMSACRequest request,
    VOMSServerInfo si, VOMSResponse response) {

    if (response.hasWarnings())
      requestListener.notifyWarningsInVOMSResponse(request, si,
        response.warningMessages());
  }

  public AttributeCertificate getVOMSAttributeCertificate(
    X509Credential credential, VOMSACRequest request) {

    List<VOMSServerInfo> vomsServerInfos = getVOMSServerInfos(request);

    if (vomsServerInfos.isEmpty())
      throw new VOMSError("VOMS server for VO " + request.getVoName() + " "
        + "is not known! Check your vomses configuration.");

    VOMSResponse response = null;

    AttributeCertificate vomsAC = null;

    for (VOMSServerInfo vomsServerInfo : vomsServerInfos) {

      requestListener.notifyVOMSRequestStart(request, vomsServerInfo);

      // Try HTTP request first
      response = doRequest(httpProtocol, vomsServerInfo, credential, request);

      // If failed, try legacy request
      if (response == null) {
        response = doRequest(legacyProtocol, vomsServerInfo, credential,
          request);
      }

      // We had failures with both requests
      if (response == null) {
        requestListener.notifyVOMSRequestFailure(request, vomsServerInfo,
          new VOMSError("REST and legacy VOMS endpoints failed."));

        // continue to next server
        continue;
      }

      // Notify that the server was contacted successfully
      requestListener.notifyVOMSRequestSuccess(request, vomsServerInfo);

      // Notify errors
      handleErrorsInResponse(request, vomsServerInfo, response);

      // Notify warnings
      handleWarningsInResponse(request, vomsServerInfo, response);

      vomsAC = getACFromResponse(request, response);

      // Exit the loop only when succesfully get an AC
      // out of the VOMS server
      if (!response.hasErrors() && vomsAC != null) {
        return vomsAC;
      }

    }

    // if we reach this point we had failures in contacting
    // all known voms server for the VO
    requestListener.notifyVOMSRequestFailure(request, null, null);
    return null;
    
  }

  /**
   * Get VOMS server endpoint information that matches with the
   * {@link VOMSACRequest} passed as argument.
   * 
   * This method returns a random shuffle of the {@link VOMSServerInfo} objects
   * that match the input request.
   * 
   * @param request
   *          the request
   * @return a possibly empty {@link List} of {@link VOMSServerInfo} objects
   */
  protected List<VOMSServerInfo> getVOMSServerInfos(VOMSACRequest request) {

    List<VOMSServerInfo> vomsServerInfos = new ArrayList<VOMSServerInfo>(
      serverInfoStore.getVOMSServerInfo(request.getVoName()));

    if (!vomsServerInfos.isEmpty()) {
      Collections.shuffle(vomsServerInfos);
    }
    return vomsServerInfos;
  }

  /**
   * Creates a {@link DefaultVOMSACService} object. The
   * {@link DefaultVOMSACService} parameters can be set with the appropriate
   * methods. Example:
   * 
   * <pre>
   * 
   * 
   * 
   * 
   * 
   * 
   * 
   * 
   * 
   * 
   * {
   *   &#064;code
   *   VOMSACService acService = new DefaultVOMSACService.Builder(certChainValidator)
   *     .requestListener(requestListener)
   *     .serverInfoStoreListener(serverInfoStoreListener)
   *     .protocolListener(protocolListener).build();
   * }
   * </pre>
   * 
   * 
   */
  public static class Builder {

    /**
     * The listener that will be informed about request events
     */
    private VOMSRequestListener requestListener = NullListener.INSTANCE;

    /**
     * The listener that will be informed about low-level protocol details
     */
    private VOMSProtocolListener protocolListener = NullListener.INSTANCE;

    /**
     * The listener that will be informed about server info store events
     */
    private VOMSServerInfoStoreListener storeListener = NullListener.INSTANCE;

    /**
     * The validator used for the SSL handshake
     */
    private X509CertChainValidatorExt validator;

    /**
     * The store used to keep VOMS server contact information.
     */
    private VOMSServerInfoStore serverInfoStore;

    /**
     * The provided strategy to lookup vomses information.
     */
    private VOMSESLookupStrategy vomsesLookupStrategy;

    /**
     * A list of paths where vomses information will be looked for, used to
     * create the server info store.
     */
    private List<String> vomsesLocations;

    /**
     * The connect timeout value
     */
    private int connectTimeout = AbstractVOMSProtocol.DEFAULT_CONNECT_TIMEOUT;

    /**
     * The read timeout used
     */
    private int readTimeout = AbstractVOMSProtocol.DEFAULT_READ_TIMEOUT;

    /**
     * Whether the client should skip hostname checking
     */
    private boolean skipHostnameChecks = true;

    /**
     * The http protocol implementation
     */
    protected VOMSProtocol httpProtocol;

    /**
     * The voms legacy protocol implementation
     */
    protected VOMSProtocol legacyProtocol;

    /**
     * Creates a Builder for a {@link DefaultVOMSACService}.
     * 
     * @param certChainValidator
     *          the validator to use to setup the SSL connection and validate
     *          the certificates
     */
    public Builder(X509CertChainValidatorExt certChainValidator) {

      if (certChainValidator == null)
        throw new NullPointerException(
          "Please provide a non-null certificate chain validator");

      this.validator = certChainValidator;
    }

    /**
     * Sets the request listener for the {@link DefaultVOMSACService} that this
     * builder is creating
     * 
     * @param l
     *          the request listener that will receive notifications about
     *          request events
     * @return this {@link Builder} instance
     */
    public Builder requestListener(VOMSRequestListener l) {

      this.requestListener = l;
      return this;
    }

    /**
     * Sets the {@link VOMSServerInfoStoreListener} for the
     * {@link DefaultVOMSACService} that this builder is creating
     * 
     * @param sl
     *          the store listener that will receive notifications about store
     *          events
     * @return this {@link Builder} instance
     */
    public Builder serverInfoStoreListener(VOMSServerInfoStoreListener sl) {

      this.storeListener = sl;
      return this;
    }

    /**
     * Sets the {@link VOMSServerInfoStore} for the {@link DefaultVOMSACService}
     * that this builder is creating
     * 
     * @param sis
     *          a {@link VOMSServerInfoStore} object
     * @return this {@link Builder} instance
     */
    public Builder serverInfoStore(VOMSServerInfoStore sis) {

      this.serverInfoStore = sis;
      return this;
    }

    /**
     * Sets the {@link VOMSProtocolListener} for the
     * {@link DefaultVOMSACService} that this builder is creating
     * 
     * @param pl
     *          the {@link VOMSProtocolListener} that will receive notifications
     *          about protocol events
     * @return this {@link Builder} instance
     */
    public Builder protocolListener(VOMSProtocolListener pl) {

      this.protocolListener = pl;
      return this;
    }

    /**
     * Sets the connect timeout (in millisecods) for the
     * {@link DefaultVOMSACService} that this builder is creating
     * 
     * @param timeout
     *          the timeout value in milliseconds
     * @return this {@link Builder} instance
     */
    public Builder connectTimeout(int timeout) {

      this.connectTimeout = timeout;
      return this;
    }

    /**
     * Sets the read timeout (in milliseconds) for the
     * {@link DefaultVOMSACService} that this builder is creating
     * 
     * @param timeout
     *          the timeout value in milliseconds
     * @return this {@link Builder} instance
     */
    public Builder readTimeout(int timeout) {

      this.readTimeout = timeout;
      return this;
    }

    /**
     * Sets a flag to skip VOMS hostname checking. Allows for creative VOMS
     * server side certificate configuration.
     * 
     * @param s
     *          <code>true</code> to skip the checks, <code>false</code>
     *          otherwise
     * 
     * @return this {@link Builder} instance
     */
    public Builder skipHostnameChecks(boolean s) {

      this.skipHostnameChecks = s;
      return this;
    }

    /**
     * Sets the vomses lookup strategy for the {@link DefaultVOMSACService} that
     * this builder is creating
     * 
     * @param strategy
     *          the {@link VOMSESLookupStrategy} object
     * @return this {@link Builder} instance
     */
    public Builder vomsesLookupStrategy(VOMSESLookupStrategy strategy) {

      this.vomsesLookupStrategy = strategy;
      return this;
    }

    /**
     * Sets a list of locations that will be used to build a
     * {@link VOMSESLookupStrategy} for the {@link DefaultVOMSACService} that
     * this builder is creating
     * 
     * @param vomsesLocations
     *          a list of paths where vomses information will be looked for
     * @return this {@link Builder} instance
     */
    public Builder vomsesLocations(List<String> vomsesLocations) {

      this.vomsesLocations = vomsesLocations;
      return this;
    }

    /**
     * Sets the http protocol implementation
     * 
     * @param httpProtocol
     *          the http protocol implementatino
     * @return this {@link Builder} instance
     */
    public Builder httpProtocol(VOMSProtocol httpProtocol) {

      this.httpProtocol = httpProtocol;
      return this;
    }

    /**
     * Sets the legacy protocol implementation
     * 
     * @param legacyProtocol
     *          the legacy protocol implementation
     *          
     *  @return 
     *        the {@link Builder}
     */
    public Builder legacyProtocol(VOMSProtocol legacyProtocol) {

      this.legacyProtocol = legacyProtocol;
      return this;
    }

    /**
     * Builds the server info store
     */
    protected void buildServerInfoStore() {

      if (serverInfoStore != null)
        return;

      serverInfoStore = new DefaultVOMSServerInfoStore.Builder()
        .lookupStrategy(vomsesLookupStrategy).storeListener(storeListener)
        .vomsesPaths(vomsesLocations).build();
    }

    /**
     * Builds default protocols if needed
     */
    protected void buildProtocols() {

      if (httpProtocol == null) {

        RESTProtocol p = new RESTProtocol(validator, protocolListener,
          connectTimeout, readTimeout);

        p.setSkipHostnameChecks(skipHostnameChecks);

        httpProtocol = p;

      }

      if (legacyProtocol == null) {

        LegacyProtocol p = new LegacyProtocol(validator, protocolListener,
          connectTimeout, readTimeout);

        p.setSkipHostnameChecks(skipHostnameChecks);

        legacyProtocol = p;

      }
    }

    /**
     * Builds the {@link DefaultVOMSACService}
     * 
     * @return a {@link DefaultVOMSACService} configured as required by this
     *         builder
     */
    public DefaultVOMSACService build() {

      buildServerInfoStore();
      buildProtocols();
      return new DefaultVOMSACService(this);
    }
  }
}
