/*
 * 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.camel.component.fhir.api;

import java.util.Map;

import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.gclient.IOperationProcessMsg;
import ca.uhn.fhir.rest.gclient.IOperationProcessMsgMode;
import ca.uhn.fhir.rest.gclient.IOperationUnnamed;
import ca.uhn.fhir.rest.gclient.IOperationUntyped;
import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInput;
import org.apache.camel.util.ObjectHelper;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;

/**
 * API for extended FHIR operations https://www.hl7.org/fhir/operations.html
 */
public class FhirOperation {

    private final IGenericClient client;

    public FhirOperation(IGenericClient client) {
        this.client = client;
    }

    /**
     * Perform the operation across all versions of all resources of the given type on the server
     *
     * @param  resourceType        The resource type to operate on
     * @param  name                Operation name
     * @param  parameters          The parameters to use as input. May also be <code>null</code> if the operation does
     *                             not require any input parameters.
     * @param  outputParameterType The type to use for the output parameters (this should be set to
     *                             <code>Parameters.class</code> drawn from the version of the FHIR structures you are
     *                             using), may be NULL
     * @param  useHttpGet          use HTTP GET verb
     * @param  returnType          If this operation returns a single resource body as its return type instead of a
     *                             <code>Parameters</code> resource, use this method to specify that resource type. This
     *                             is useful for certain operations (e.g. <code>Patient/NNN/$everything</code>) which
     *                             return a bundle instead of a Parameters resource, may be NULL
     * @param  extraParameters     see {@link ExtraParameters} for a full list of parameters that can be passed, may be
     *                             NULL
     * @param  <T>                 extends {@link IBaseParameters}
     * @return                     the {@link IBaseResource}
     */
    public <T extends IBaseParameters> IBaseResource onType(
            Class<IBaseResource> resourceType, String name,
            T parameters, Class<T> outputParameterType, boolean useHttpGet, Class<IBaseResource> returnType,
            Map<ExtraParameters, Object> extraParameters) {
        IOperationUnnamed operationUnnamed = client.operation().onType(resourceType);
        IOperationUntypedWithInput<? extends IBaseResource> operationUntypedWithInput = processNamedOperationParameters(
                name, parameters, outputParameterType, useHttpGet, returnType, extraParameters, operationUnnamed);
        return operationUntypedWithInput.execute();
    }

    /**
     * Perform the operation across all versions of all resources of all types on the server
     *
     * @param  name                Operation name
     * @param  parameters          The parameters to use as input. May also be <code>null</code> if the operation does
     *                             not require any input parameters.
     * @param  outputParameterType The type to use for the output parameters (this should be set to
     *                             <code>Parameters.class</code> drawn from the version of the FHIR structures you are
     *                             using), may be NULL
     * @param  useHttpGet          use HTTP GET verb
     * @param  returnType          If this operation returns a single resource body as its return type instead of a
     *                             <code>Parameters</code> resource, use this method to specify that resource type. This
     *                             is useful for certain operations (e.g. <code>Patient/NNN/$everything</code>) which
     *                             return a bundle instead of a Parameters resource, may be NULL
     * @param  extraParameters     see {@link ExtraParameters} for a full list of parameters that can be passed, may be
     *                             NULL
     * @param  <T>                 extends {@link IBaseParameters}
     * @return                     the {@link IBaseResource}
     */
    public <T extends IBaseParameters> IBaseResource onServer(
            String name, T parameters, Class<T> outputParameterType, boolean useHttpGet, Class<IBaseResource> returnType,
            Map<ExtraParameters, Object> extraParameters) {
        IOperationUnnamed operationUnnamed = client.operation().onServer();
        IOperationUntypedWithInput<? extends IBaseResource> operationUntypedWithInput = processNamedOperationParameters(
                name, parameters, outputParameterType, useHttpGet, returnType, extraParameters, operationUnnamed);
        return operationUntypedWithInput.execute();
    }

    /**
     * Perform the operation across all versions of a specific resource (by ID and type) on the server. Note that
     * <code>theId</code> must be populated with both a resource type and a resource ID at a minimum.
     *
     * @param  id                       Resource (version will be stripped)
     * @param  name                     Operation name
     * @param  parameters               The parameters to use as input. May also be <code>null</code> if the operation
     *                                  does not require any input parameters.
     * @param  outputParameterType      The type to use for the output parameters (this should be set to
     *                                  <code>Parameters.class</code> drawn from the version of the FHIR structures you
     *                                  are using), may be NULL
     * @param  useHttpGet               use HTTP GET verb
     * @param  returnType               If this operation returns a single resource body as its return type instead of a
     *                                  <code>Parameters</code> resource, use this method to specify that resource type.
     *                                  This is useful for certain operations (e.g.
     *                                  <code>Patient/NNN/$everything</code>) which return a bundle instead of a
     *                                  Parameters resource, may be NULL
     * @param  extraParameters          see {@link ExtraParameters} for a full list of parameters that can be passed,
     *                                  may be NULL
     * @param  <T>                      extends {@link IBaseParameters}
     *
     * @throws IllegalArgumentException If <code>theId</code> does not contain at least a resource type and ID
     *
     * @return                          the {@link IBaseResource}
     */
    public <T extends IBaseParameters> IBaseResource onInstance(
            IIdType id, String name, T parameters, Class<T> outputParameterType, boolean useHttpGet,
            Class<IBaseResource> returnType, Map<ExtraParameters, Object> extraParameters) {
        IOperationUnnamed operationUnnamed = client.operation().onInstanceVersion(id);
        IOperationUntypedWithInput<? extends IBaseResource> operationUntypedWithInput = processNamedOperationParameters(
                name, parameters, outputParameterType, useHttpGet, returnType, extraParameters, operationUnnamed);
        return operationUntypedWithInput.execute();
    }

    /**
     * This operation operates on a specific version of a resource
     *
     * @param  id                  Resource version
     * @param  name                Operation name
     * @param  parameters          The parameters to use as input. May also be <code>null</code> if the operation does
     *                             not require any input parameters.
     * @param  outputParameterType The type to use for the output parameters (this should be set to
     *                             <code>Parameters.class</code> drawn from the version of the FHIR structures you are
     *                             using), may be NULL
     * @param  useHttpGet          use HTTP GET verb
     * @param  returnType          If this operation returns a single resource body as its return type instead of a
     *                             <code>Parameters</code> resource, use this method to specify that resource type. This
     *                             is useful for certain operations (e.g. <code>Patient/NNN/$everything</code>) which
     *                             return a bundle instead of a Parameters resource, may be NULL
     * @param  extraParameters     see {@link ExtraParameters} for a full list of parameters that can be passed, may be
     *                             NULL
     * @param  <T>                 extends {@link IBaseParameters}
     * @return                     the {@link IBaseResource}
     */
    public <T extends IBaseParameters> IBaseResource onInstanceVersion(
            IIdType id, String name, T parameters, Class<T> outputParameterType, boolean useHttpGet,
            Class<IBaseResource> returnType, Map<ExtraParameters, Object> extraParameters) {
        IOperationUnnamed operationUnnamed = client.operation().onInstanceVersion(id);
        IOperationUntypedWithInput<? extends IBaseResource> operationUntypedWithInput = processNamedOperationParameters(
                name, parameters, outputParameterType, useHttpGet, returnType, extraParameters, operationUnnamed);
        return operationUntypedWithInput.execute();
    }

    /**
     * This operation is called <b><a href="https://www.hl7.org/fhir/messaging.html">$process-message</a></b> as defined
     * by the FHIR specification.<br>
     * <br>
     * 
     * @param  respondToUri    An optional query parameter indicating that responses from the receiving server should be
     *                         sent to this URI, may be NULL
     * @param  msgBundle       Set the Message Bundle to POST to the messaging server
     * @param  asynchronous    Whether to process the message asynchronously or synchronously, defaults to synchronous.
     * @param  responseClass   the response class
     * @param  extraParameters see {@link ExtraParameters} for a full list of parameters that can be passed, may be NULL
     * @param  <T>             extends {@link IBaseBundle}
     * @return                 the {@link IBaseBundle}
     */
    public <T extends IBaseBundle> T processMessage(
            String respondToUri, IBaseBundle msgBundle, boolean asynchronous, Class<T> responseClass,
            Map<ExtraParameters, Object> extraParameters) {
        IOperationProcessMsg operationProcessMsg = client.operation().processMessage();

        if (ObjectHelper.isNotEmpty(respondToUri)) {
            operationProcessMsg.setResponseUrlParam(respondToUri);
        }

        IOperationProcessMsgMode<T> operationProcessMsgMode = operationProcessMsg.setMessageBundle(msgBundle);

        if (asynchronous) {
            operationProcessMsgMode.asynchronous(responseClass);
        } else {
            operationProcessMsgMode.synchronous(responseClass);
        }

        ExtraParameters.process(extraParameters, operationProcessMsgMode);
        return operationProcessMsgMode.execute();
    }

    private <T extends IBaseParameters> IOperationUntypedWithInput<? extends IBaseResource> processNamedOperationParameters(
            String name, T parameters, Class<T> outputParameterType,
            boolean useHttpGet, Class<? extends IBaseResource> returnType, Map<ExtraParameters, Object> extraParameters,
            IOperationUnnamed operationUnnamed) {
        IOperationUntyped named = operationUnnamed.named(name);
        IOperationUntypedWithInput<? extends IBaseResource> operationUntypedWithInput;
        if (outputParameterType != null) {
            operationUntypedWithInput = named.withNoParameters(outputParameterType);
        } else {
            operationUntypedWithInput = named.withParameters(parameters);
        }
        if (useHttpGet) {
            operationUntypedWithInput.useHttpGet();
        }
        if (returnType != null) {
            operationUntypedWithInput.returnResourceType(returnType);
        }
        ExtraParameters.process(extraParameters, operationUntypedWithInput);
        return operationUntypedWithInput;
    }
}
