/*
 * Copyright 2011 NEHTA
 *
 * Licensed under the NEHTA Open Source (Apache) License; you may not use this
 * file except in compliance with the License. A copy of the License is in the
 * 'license.txt' file, which should be provided with this work.
 *
 * 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 au.gov.nehta.vendorlibrary.hi.hpii;


import au.gov.nehta.common.utils.ArgumentUtils;
import au.gov.nehta.vendorlibrary.hi.handler.message.HIHeaderHandler;
import au.gov.nehta.vendorlibrary.hi.handler.security.HISecurityHandler;
import au.gov.nehta.vendorlibrary.ws.TimeUtility;
import au.gov.nehta.vendorlibrary.ws.WebServiceClientUtil;
import au.gov.nehta.vendorlibrary.ws.handler.LoggingHandler;
import au.net.electronichealth.ns.hi.common.commoncoreelements._3_0.ProductType;
import au.net.electronichealth.ns.hi.common.commoncoreelements._3_0.SignatureContainerType;
import au.net.electronichealth.ns.hi.common.commoncoreelements._3_0.TimestampType;
import au.net.electronichealth.ns.hi.common.qualifiedidentifier._3_0.QualifiedId;
import au.net.electronichealth.ns.hi.providercore.address._3_2_0.AustralianAddressCriteriaType;
import au.net.electronichealth.ns.hi.providercore.address._3_2_0.InternationalAddressCriteriaType;
import au.net.electronichealth.ns.hi.svc.hiproviderdirectoryforindividual._3_2_0.ProviderSearchHIProviderDirectoryForIndividualPortType;
import au.net.electronichealth.ns.hi.svc.hiproviderdirectoryforindividual._3_2_0.ProviderSearchHIProviderDirectoryForIndividualService;
import au.net.electronichealth.ns.hi.svc.hiproviderdirectoryforindividual._3_2_0.SearchHIProviderDirectoryForIndividual;
import au.net.electronichealth.ns.hi.svc.hiproviderdirectoryforindividual._3_2_0.SearchHIProviderDirectoryForIndividualResponse;
import au.net.electronichealth.ns.hi.svc.hiproviderdirectoryforindividual._3_2_0.StandardErrorMsg;

import javax.net.ssl.SSLSocketFactory;
import javax.xml.ws.Binding;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.Holder;
import javax.xml.ws.handler.Handler;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * An implementation of a Healthcare Identifiers (HI) - Healthcare Provider Identifier Individual (HPI-I) search client.
 * This class may be used to connect to the Medicare HI Service to do Provider Search HI Provider Directory For
 * Individual searches.
 */
public class ProviderSearchHIProviderDirectoryForIndividualClient {
  /**
   * Empty String variable.
   */
  public static final String EMPTY = "";

  /**
   * SSL socket factory class name
   */
  private static final String SSL_SOCKET_FACTORY = "com.sun.xml.ws.transport.https.client.SSLSocketFactory";

  /**
   * CSP HPI-O header name.
   */
  private static final String HPIO_CSP_HEADER_ELEMENT_NAME = "hpio";


  /**
   * Validates search parameters are correct for the various search types.
   */
  public static class ArgumentValidator {
    /**
     * The length of an Australian postcode.
     */
    public static final int AUSTRALIAN_POSTCODE_LENGTH = 4;

    /**
     * Checks that only the correct parameters for an identifier search are set.
     *
     * @param request the search request object containing the parameters to be checked.
     */
    public final void identifierSearchCheck(SearchHIProviderDirectoryForIndividual request) {
      ArgumentUtils.checkNotNull(request, "request");
      ArgumentUtils.checkNotNullNorBlank(request.getHpiiNumber(), "HPI-I Number");
      ensureNull(request.getFamilyName(), "Family Name");
      ensureNull(request.getGivenName(), "Given Name");
      ensureNull(request.getSex(), "Sex");
      ensureNull(request.getProviderTypeCode(), "Provider Type Code");
      ensureNull(request.getProviderSpecialty(), "Provider Specialty");
      ensureNull(request.getProviderSpecialisation(), "Provider Specialisation");
      ensureNull(request.getAustralianAddressCriteria(), "Australian Address Criteria");
      ensureNull(request.getInternationalAddressCriteria(), "International Address Criteria");
    }

    /**
     * Checks that only the correct parameters for a demographic search are set.
     *
     * @param request the search request object containing the parameters to be checked.
     */
    public final void demographicSearchCheck(SearchHIProviderDirectoryForIndividual request) {
      ArgumentUtils.checkNotNull(request, "request");
//      ArgumentUtils.checkNotNullNorBlank(request.getFamilyName(), "Family Name");


      ensureNull(request.getHpiiNumber(), "HPI-I Number");
      ensureEitherOneOrBothAreNull(request.getAustralianAddressCriteria(), "Australian Address Criteria",
        request.getInternationalAddressCriteria(), "International Address Criteria");
      if (request.getAustralianAddressCriteria() != null) {
        checkAustralianAddressCriteria(request.getAustralianAddressCriteria());
      } else {
        if (request.getInternationalAddressCriteria() != null) {
          checkInternationalAddressCriteria(request.getInternationalAddressCriteria());
        }
      }
      
    }

    /**
     * Verifies that the Australian Address Criteria provided is valid.
     *
     * @param address the Australian Address Criteria being verified
     */
    private void checkAustralianAddressCriteria(AustralianAddressCriteriaType address) {
      ArgumentUtils.checkNotNullNorBlank(address.getSuburb(), "Australian Address Criteria: Suburb");
      ArgumentUtils.checkNotNull(address.getState(), "Australian Address Criteria: State");
      ArgumentUtils.checkNotNullNorBlank(address.getPostcode(), "Australian Address Criteria: Postcode");
      ensureExactStringLength(address.getPostcode(), AUSTRALIAN_POSTCODE_LENGTH, "Post Code");

      if (isAddressUnstructured(address)) {
        ArgumentUtils.checkNotNullNorBlank(address.getUnstructuredAddressLine(), "Australian Address Criteria: Unstructured Address Line");
      }
    }

    private boolean isAddressUnstructured(final AustralianAddressCriteriaType address) {
      return (address.getLevelGroup() == null
        && address.getUnitGroup() == null
        && address.getAddressSiteName() == null
        && address.getStreetNumber() == null
        && address.getLotNumber() == null
        && address.getStreetName() == null
        && address.getStreetType() == null
        && address.getStreetSuffix() == null
        && address.getPostalDeliveryGroup() == null);
    }

    /**
     * Verifies that the International Address Criteria provided is valid.
     *
     * @param address the International Address Criteria being verified
     */
    private void checkInternationalAddressCriteria(InternationalAddressCriteriaType address) {
      ArgumentUtils.checkNotNullNorBlank(address.getInternationalAddressLine(), "International Address Criteria: Address Line");
      ArgumentUtils.checkNotNullNorBlank(address.getInternationalStateProvince(), "International Address Criteria: State Province");
      ArgumentUtils.checkNotNullNorBlank(address.getInternationalPostcode(), "International Address Criteria: Postcode");
      ArgumentUtils.checkNotNull(address.getCountry(), "International Address Criteria: Country");
    }

    /**
     * Ensures
     *
     * @param obj1     the first object being checked (Mandatory)
     * @param obj1Name the name of the first object variable (for use in Exception messages) (Mandatory)
     * @param obj2     the second object being checked (Mandatory)
     * @param obj2Name the name of the second object variable (for use in Exception messages) (Mandatory)
     */
    private void ensureEitherOneOrBothAreNull(Object obj1, String obj1Name, InternationalAddressCriteriaType obj2, String obj2Name) {
      if (obj1 != null) {
        if (obj2 != null) {
          throw new IllegalArgumentException("Both " + obj1Name + " and " + obj2Name + " are set. Only one of them may be set");
        }
      }
    }

    /**
     * Ensures the passed object is null
     *
     * @param theObject  the object being checked (Mandatory)
     * @param objectName the name of the Object variable (for use in Exception messages) (Mandatory)
     */
    private void ensureNull(Object theObject, String objectName) {
      if (theObject != null) {
        throw new IllegalArgumentException(objectName + " may not be not-null");
      }
    }

    /**
     * Ensures the passed String has a specific length
     *
     * @param theString  the String being checked (Mandatory)
     * @param length     the required length (Mandatory)
     * @param stringName the name of the String variable (for use in Exception messages) (Mandatory)
     */
    private void ensureExactStringLength(String theString, Integer length, String stringName) {
      if (theString.length() != length) {
        throw new IllegalArgumentException(stringName + " must have a length of " + length);
      }
    }
  }

  /**
   * Validates the arguments passed for the various operations
   */
  private ArgumentValidator argumentValidator = new ArgumentValidator();

  /**
   * The SSL Socket Factory to be used for connecting to the Web Service provider
   */
  private SSLSocketFactory sslSocketFactory;

  /**
   * The Private Key to be used for Signing
   */
  private PrivateKey signingPrivateKey;

  /**
   * The Certificate to be used for Signing
   */
  private X509Certificate signingCertificate;

  /**
   * The user Qualified ID associated with this use of the ProviderSearchHIProviderDirectoryForIndividual service
   */
  private final QualifiedId individualQualifiedId;

  /**
   * The organisation Qualified ID associated with this use of the ProviderSearchHIProviderDirectoryForIndividual service
   */
  private final QualifiedId organisationQualifiedId;

  /**
   * The Product details associated with this use of the ProviderSearchHIProviderDirectoryForIndividual service
   */
  private final Holder<ProductType> productHeader;

  /**
   * The Web Services port for the ProviderSearchHIProviderDirectoryForIndividual service
   */
  private ProviderSearchHIProviderDirectoryForIndividualPortType providerSearchHIProviderDirectoryForIndividualPort;

  /**
   * The SOAP request message corresponding to the most recent web service invocation
   */
  private String lastSoapRequest;

  /**
   * The SOAP response message corresponding to the most recent web service invocation
   */
  private String lastSoapResponse;

  /**
   * The Logging handler instance for logging SOAP request and Response messages.
   */
  private LoggingHandler loggingHandler;

  /**
   * Constructor which creates a new ProviderSearchHIProviderDirectoryForIndividual Client with an endpoint and an SSL Socket Factory.
   *
   * @param serviceEndpoint       the Web Service endpoint for the Medicare HI Service interface (Mandatory)
   * @param individualQualifiedId The qualified user ID for connecting to the ProviderSearchHIProviderDirectoryForIndividual service (Mandatory)
   * @param productHeader         The product header data for connecting to the ProviderSearchHIProviderDirectoryForIndividual service (Mandatory)
   * @param signingPrivateKey     The private key to be used for signing (Mandatory)
   * @param signingCertificate    The certificate to be used for signing (Mandatory)
   * @param sslSocketFactory      the SSL Socket Factory to be used when connecting to the Web Service provider
   */
  public ProviderSearchHIProviderDirectoryForIndividualClient(final String serviceEndpoint,
                                                              final QualifiedId individualQualifiedId,
                                                              final Holder<ProductType> productHeader,
                                                              final PrivateKey signingPrivateKey,
                                                              final X509Certificate signingCertificate,
                                                              final SSLSocketFactory sslSocketFactory) {
    this(
      serviceEndpoint,
      individualQualifiedId,
      null,
      productHeader,
      signingPrivateKey,
      signingCertificate,
      sslSocketFactory
    );
  }

  /**
   * Constructor which creates a new ProviderSearchHIProviderDirectoryForIndividual Client with an endpoint and an SSL Socket Factory, with
   * the optional contracted service providers HPI-O organisation qualified ID set.
   *
   * @param serviceEndpoint         the Web Service endpoint for the Medicare HI Service interface (Mandatory)
   * @param individualQualifiedId   The qualified user ID for connecting to the ProviderSearchHIProviderDirectoryForIndividual service (Mandatory)
   * @param organisationQualifiedId The qualified user ID for connecting to the ProviderSearchHIProviderDirectoryForIndividual service (Optional)
   * @param productHeader           The product header data for connecting to the ProviderSearchHIProviderDirectoryForIndividual service (Mandatory)
   * @param signingPrivateKey       The private key to be used for signing (Mandatory)
   * @param signingCertificate      The certificate to be used for signing (Mandatory)
   * @param sslSocketFactory        the SSL Socket Factory to be used when connecting to the Web Service provider
   */
  public ProviderSearchHIProviderDirectoryForIndividualClient(final String serviceEndpoint,
                                                              final QualifiedId individualQualifiedId,
                                                              final QualifiedId organisationQualifiedId,
                                                              final Holder<ProductType> productHeader,
                                                              final PrivateKey signingPrivateKey,
                                                              final X509Certificate signingCertificate,
                                                              final SSLSocketFactory sslSocketFactory) {

    ArgumentUtils.checkNotNullNorBlank(serviceEndpoint, "serviceEndpoint");
    ArgumentUtils.checkNotNull(individualQualifiedId, "individualQualifiedId");
    ArgumentUtils.checkNotNull(productHeader, "productHeader");
    ArgumentUtils.checkNotNull(signingPrivateKey, "signingPrivateKey");
    ArgumentUtils.checkNotNull(signingCertificate, "signingPrivateKey");
    ArgumentUtils.checkNotNull(sslSocketFactory, "sslSocketFactory");

    this.sslSocketFactory = sslSocketFactory;
    this.individualQualifiedId = individualQualifiedId;
    this.organisationQualifiedId = organisationQualifiedId;
    this.productHeader = productHeader;

    this.signingPrivateKey = signingPrivateKey;
    this.signingCertificate = signingCertificate;
    this.loggingHandler = new LoggingHandler(true); //Set to true to dump the SOAP message to the default logger.

    List<Handler> handlerChain = new ArrayList<Handler>();
    handlerChain.add(loggingHandler);
    this.providerSearchHIProviderDirectoryForIndividualPort = WebServiceClientUtil.getPort(
      ProviderSearchHIProviderDirectoryForIndividualPortType.class,
      ProviderSearchHIProviderDirectoryForIndividualService.class,
      sslSocketFactory,
      handlerChain);
    configureEndpoint(this.providerSearchHIProviderDirectoryForIndividualPort, serviceEndpoint);
  }


  /**
   * Returns the current {@link au.net.electronichealth.ns.hi.common.commoncoreelements._3_0.TimestampType}
   *
   * @return {@link au.net.electronichealth.ns.hi.common.commoncoreelements._3_0.TimestampType} instance with current
   *         as created time.
   */
  private TimestampType getTimestampHeader() {
    TimestampType timestampHeader = new TimestampType();
    timestampHeader.setCreated(TimeUtility.nowXMLGregorianCalendar());
    return timestampHeader;
  }

  /**
   * Executes an Identifier HPI-I search.
   *
   * @param request the SearchHIProviderDirectoryForIndividual request object containing the following mandatory fields:
   *                HPI-I
   * @return the response from the SearchHIProviderDirectoryForIndividual service
   * @throws StandardErrorMsg if the Web Service call fails.
   */
  public final SearchHIProviderDirectoryForIndividualResponse identifierSearch(SearchHIProviderDirectoryForIndividual request)
    throws StandardErrorMsg {

    this.argumentValidator.identifierSearchCheck(request);

    Holder<SignatureContainerType> signatureHeader = null;

    return this.providerSearchHIProviderDirectoryForIndividualPort.searchHIProviderDirectoryForIndividual(
      request,
      this.productHeader,
      getTimestampHeader(),
      signatureHeader,
      this.individualQualifiedId,
      this.organisationQualifiedId
    );
  }

  /**
   * Executes a Demographic HPI-I search.
   *
   * @param request the SearchHIProviderDirectoryForIndividual request object containing the following mandatory fields:
   *                Family Name
   *                and the following optional fields:
   *                Either "Australian Address Criteria" OR "International Address Criteria" but not both.
   * @return the response from the SearchHIProviderDirectoryForIndividual service
   * @throws StandardErrorMsg if the Web Service call fails.
   */
  public final SearchHIProviderDirectoryForIndividualResponse demographicSearch(SearchHIProviderDirectoryForIndividual request)
    throws StandardErrorMsg {

    this.argumentValidator.demographicSearchCheck(request);
    Holder<SignatureContainerType> signatureHeader = null;
    return this.providerSearchHIProviderDirectoryForIndividualPort.searchHIProviderDirectoryForIndividual(
      request,
      this.productHeader,
      getTimestampHeader(),
      signatureHeader,
      this.individualQualifiedId,
      this.organisationQualifiedId
    );
  }

  // Helper Methods

  /**
   * Configure the endpoint using the provided configuration information.
   *
   * @param servicePort the service definition. (Mandatory)
   * @param endpoint    the URL for the Web service endpoint. (Mandatory)
   */
  private void configureEndpoint(final Object servicePort, final String endpoint) {
    final BindingProvider bindingProvider = (BindingProvider) servicePort;

    // Set details on request context
    final Map<String, Object> requestContext = bindingProvider.getRequestContext();
    requestContext.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, endpoint.trim());
    requestContext.put(SSL_SOCKET_FACTORY, this.sslSocketFactory);
    if (bindingProvider != null && bindingProvider.getBinding() != null) {
      Binding binding = bindingProvider.getBinding();
      // Get handler chain
      List<Handler> handlerChain = binding.getHandlerChain();

      // Remove hpioHeader if value is not specified.
      if (this.organisationQualifiedId == null) {
        List<String> headerNames = new ArrayList<String>();
        headerNames.add(HPIO_CSP_HEADER_ELEMENT_NAME);
        handlerChain.add(new HIHeaderHandler(headerNames));
      }

      //Add HISecurityHandler to sign the Outoging SOAP message and verify the incoming SOAP message.
      handlerChain.add(new HISecurityHandler(this.signingCertificate, this.signingPrivateKey));

      //Add handler to capture inbound and outbound SOAP messages
      handlerChain.add(this.loggingHandler);
      binding.setHandlerChain(handlerChain);
    }
  }

  /**
   * Getter for lastSoapResponse.
   *
   * @return lastSoapResponse the lastSoapResponse instance variable
   */
  public final String getLastSoapResponse() {
    if (loggingHandler != null) {
      return loggingHandler.getLastSoapResponse();
    } else {
      return EMPTY;
    }
  }

  /**
   * Getter for lastSoapRequest.
   *
   * @return lastSoapRequest the lastSoapRequest instance variable (Mandatory)
   */
  public final String getLastSoapRequest() {
    if (loggingHandler != null) {
      return loggingHandler.getLastSoapRequest();
    } else {
      return EMPTY;
    }
  }
}
