package org.opencds.cqf.cql.data.fhir;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.IGenericClient;
import ca.uhn.fhir.rest.gclient.IQuery;
import org.opencds.cqf.cql.data.DataProvider;
import org.opencds.cqf.cql.runtime.Code;
import org.opencds.cqf.cql.runtime.Interval;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Enumeration;
import org.joda.time.Partial;
import org.opencds.cqf.cql.terminology.TerminologyProvider;
import org.opencds.cqf.cql.terminology.ValueSetInfo;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URLEncoder;

/**
 * Created by Bryn on 4/16/2016.
 */
public class FhirDataProvider extends BaseFhirDataProvider {

    private String endpoint;
    public String getEndpoint() {
        return endpoint;
    }
    public void setEndpoint(String endpoint) {
        this.endpoint = endpoint;
        fhirClient = fhirContext.newRestfulGenericClient(endpoint);
    }
    public FhirDataProvider withEndpoint(String endpoint) {
        setEndpoint(endpoint);
        return this;
    }

    private TerminologyProvider terminologyProvider;
    public TerminologyProvider getTerminologyProvider() {
        return terminologyProvider;
    }
    public void setTerminologyProvider(TerminologyProvider terminologyProvider) {
        this.terminologyProvider = terminologyProvider;
    }

    private boolean expandValueSets;
    public boolean getExpandValueSets() {
        return expandValueSets;
    }
    public void setExpandValueSets(boolean expandValueSets) {
        this.expandValueSets = expandValueSets;
    }

    private IGenericClient fhirClient;

    // TODO: It would be nice not to have to expose this, but I needed it in the MeasureEvaluator....
    public IGenericClient getFhirClient() {
        return fhirClient;
    }

    private String URLEncode(String url) {
        try {
            return URLEncoder.encode(url, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException(e.getMessage());
        }
    }

    public Iterable<Object> retrieve(String context, Object contextValue, String dataType, String templateId,
                                     String codePath, Iterable<Code> codes, String valueSet, String datePath, String dateLowPath,
                                     String dateHighPath, Interval dateRange) {

        // Apply filtering based on
        //  profile (templateId)
        //  codes
        //  dateRange
        IQuery<ca.uhn.fhir.model.api.Bundle> search = null; //fhirClient.search().forResource(dataType);

        // TODO: Would like to be able to use the criteria builders, but it looks like they don't have one for :in with a valueset?
        // So..... I'll just construct a search URL
        //        if (templateId != null && !templateId.equals("")) {
        //            search = search.withProfile(templateId);
        //        }
        //
        //        if (codePath != null && !codePath.equals("")) {
        //            search.where(Patient.ACTIVE.)
        //        }

        // TODO: It's unclear from the FHIR documentation whether we need to use a URLEncoder.encode call on the embedded system and valueset uris here...
        StringBuilder params = new StringBuilder();

        if (templateId != null && !templateId.equals("")) {
            params.append(String.format("_profile=%s", templateId));
        }

        if (codePath == null && (codes != null || valueSet != null)) {
            throw new IllegalArgumentException("A code path must be provided when filtering on codes or a valueset.");
        }

        if (context != null && context.equals("Patient") && contextValue != null) {
            if (params.length() > 0) {
                params.append("&");
            }

            params.append(String.format("%s=%s", getPatientSearchParam(dataType), URLEncode((String)contextValue)));
        }

        if (codePath != null && !codePath.equals("")) {
            if (params.length() > 0) {
                params.append("&");
            }
            if (valueSet != null && !valueSet.equals("")) {
                if (terminologyProvider != null && expandValueSets) {
                    ValueSetInfo valueSetInfo = new ValueSetInfo().withId(valueSet);
                    codes = terminologyProvider.expand(valueSetInfo);
                }
                else {
                    params.append(String.format("%s:in=%s", convertPathToSearchParam(dataType, codePath), URLEncode(valueSet)));
                }
            }

            if (codes != null) {
                StringBuilder codeList = new StringBuilder();
                for (Code code : codes) {
                    if (codeList.length() > 0) {
                        codeList.append(",");
                    }

                    if (code.getSystem() != null) {
                        codeList.append(URLEncode(code.getSystem()));
                        codeList.append("|");
                    }

                    codeList.append(URLEncode(code.getCode()));
                }
                params.append(String.format("%s=%s", convertPathToSearchParam(dataType, codePath), codeList.toString()));
            }
        }

        if (dateRange != null) {
            if (dateRange.getLow() != null) {
              String lowDatePath = convertPathToSearchParam(dataType, dateLowPath != null ? dateLowPath : datePath);
              if (lowDatePath == null || lowDatePath.equals("")) {
                throw new IllegalArgumentException("A date path or low date path must be provided when filtering on a date range.");
              }

              params.append(String.format("%s=%s%s",
                      lowDatePath,
                      dateRange.getLowClosed() ? "ge" : "gt",
                      dateRange.getLow().toString()));
            }

            if (dateRange.getHigh() != null) {
              String highDatePath = convertPathToSearchParam(dataType, dateHighPath != null ? dateHighPath : datePath);
              if (highDatePath == null || highDatePath.equals("")) {
                throw new IllegalArgumentException("A date path or high date path must be provided when filtering on a date range.");
              }

              params.append(String.format("%s=%s%s",
                      highDatePath,
                      dateRange.getHighClosed() ? "le" : "lt",
                      dateRange.getHigh().toString()));
            }
        }

        // TODO: Use compartment search for patient context?
        if (params.length() > 0) {
            search = fhirClient.search().byUrl(String.format("%s?%s", dataType, params.toString()));
        }
        else {
            search = fhirClient.search().byUrl(String.format("%s", dataType));
        }

        Bundle results = search.returnBundle(Bundle.class).execute();
        return new FhirBundleCursor(fhirClient, results);
    }

    private String getPatientSearchParam(String dataType) {
        switch (dataType) {
            case "Patient":
                return "_id";
            case "Observation":
            case "RiskAssessment":
                return "subject";
            default: return "patient";
        }
    }

    private String convertPathToSearchParam(String dataType, String codePath) {
        return codePath.replace('.', '-');
        // TODO: The above won't work in all cases, but is a best guess for now
        // What we really need is something like the below, but that's a lot of specific mapping...
//        switch (dataType) {
//            case "Patient": {
//                switch (codePath) {
//                    case "active": return "active";
//                    case "address": return "address";
//                    case "address.city": return "address-city";
//                    case "address.country": return "address-country";
//                    case "address.postalCode": return "address-postalcode";
//                    case "address.state": return "address-state";
//                    case "address.use": return "adddress-use";
//                    case "animal.breed": return "animal-breed";
//                    case "animal.species": return "animal-species";
//                    case "birthDate": return "birthdate";
//                    // TODO: the rest of the patient search parameters
//                    case "gender": return "gender";
//                }
//            }
//            default: throw new IllegalArgumentException(String.format("Search path map for code path %s.%s is not defined.", dataType, codePath));
//        }
    }
}

@@ -3962,24 +3962,32 @@
 .format(%22%25s=
+Patient/
 %25s%22, getPati

Fix for patient reference in FHIR url.