package in.cdac.medinfo.csnotk.csnoserv.rest;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static  in.cdac.medinfo.csnotk.csnoserv.CSnoServResource.*;
import java.util.Set;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import in.cdac.medinfo.csnotk.csnolib.commons.EnumAcceptability;
import in.cdac.medinfo.csnotk.csnolib.commons.EnumSemanticTag;
import in.cdac.medinfo.csnotk.csnolib.commons.EnumState;
import in.cdac.medinfo.csnotk.csnolib.model.CompositeDescription;

//Logs
import in.cdac.medinfo.csnotk.csnolib.util.LoggerUtility;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;


@Path("/search")
public class SearchController{
    /**
     * Gets most matching suggestions for a given term/text in the SNOMED CT
     * repository with specified state(ACTIVE, INACTIVE, BOTH) in specified Semantic
     * Tag by providing acceptability(preferred,preferredExcludingFSN,synonyms,all).
     *
     * @param term                 {@link String} term/text to be searched
     * @param state                {@link String} State of a concept.<br>
     *                             If state is,<br>
     *                             active - It will return all active Concepts with
     *                             all active Description.<br>
     *                             inactive - It will return all inactive Concepts
     *                             with all active and inactive Description.<br>
     *                             both - It will return all active and inactive
     *                             Concepts with all active and inactive
     *                             Description.<br>
     * @param semantictag          {@link String} semantictag in which SNOMED CT
     *                             term/text will be searched. <br>
     *                             If semantictag is, all - It will search SNOMED CT
     *                             term in all Semantic Tag.<br>
     *                             A list of {@link EnumSemanticTag} can also be
     *                             provide in which search/suggest is to be perform
     *                             by appending semantic Tags with each other using
     *                             ++ Example: disorder++finding.<br>
     * @param acceptability        {@link String} Represents whether only FSN and
     *                             Preferred Terms are to be fetched or all matching
     *                             terms. <br>
     *                             If acceptability is,<br>
     *                             preferred - refers to all preferred description
     *                             for a concept (including FSN). <br>
     *                             preferredexcludingfsn - refers to SNOMEDCT
     *                             description for preferred terms excluding
     *                             FSN.<br>
     *                             synonyms - refers to all description for a
     *                             concept (excluding FSN).<br>
     *                             acceptable- refers to all description for a
     *                             concept excluding preferred.<br>
     *                             all - refers to all description for a concept
     *                             (both preferred and acceptable).<br>
     * @param returnLimit          {@link String} Maximum number of matching terms
     *                             to be fetched <br>
     * @param refsetId             {@link String} Refset Identifier from which
     *                             search is to be done and don't provide refsetid
     *                             parameter if search is to be done from whole
     *                             SNOMED CT Terminology. <br>
     * @param excludeparentconcept {@link boolean}Boolean value, if true, then
     *                             method will return set of CompositeDescription
     *                             which will not contain descriptions from given
     *                             parent id. <br>
     *                             The default value is false, which will contain
     *                             descriptions from given parent id.
     * @param callback             {@link String} name of the callback function in a
     *                             json request</br>
     *                             (for allowing cross domain request)
     */
    // @SuppressWarnings("static-access")
    @GET
    @Produces({ MediaType.APPLICATION_JSON + ";charset=utf-8" })
    @Consumes({ MediaType.APPLICATION_FORM_URLENCODED })
    @Path("/suggest")
    public Response suggestTerm(@QueryParam("term") String term,
                                @DefaultValue("both") @QueryParam("state") String state,
                                @DefaultValue("all") @QueryParam("semantictag") String semanticTag,
                                @DefaultValue("-1") @QueryParam("returnlimit") String returnLimit,
                                @DefaultValue("all") @QueryParam("acceptability") String acceptability,
                                @QueryParam("refsetid") String refsetId, @QueryParam("parentid") String parentId,
                                @QueryParam("excludeparentconcept") boolean excludeparentconcept, @QueryParam("callback") String callback) {

        initProperties();
        Logger logger = LogManager.getLogger(SearchController.class);

        if (term == null || term.equalsIgnoreCase("")) {
            logger.error(LoggerUtility.Exception, "Invalid Query Parameter Value: term: {}" , term);
            return CORSHeaderSupport.addCORSHeader(Response.Status.BAD_REQUEST,Response.Status.BAD_REQUEST.toString());
        }

        String jsonString;
        try {
            String encodedTerm = java.net.URLEncoder.encode(term, UTF_8);
            term = java.net.URLDecoder.decode(encodedTerm, UTF_8);
            if (state != null) {
                state = java.net.URLDecoder.decode(state, UTF_8);
                EnumState enumState = EnumState.getEnum(state);
                if (enumState == null) {
                    logger.error(LoggerUtility.Exception, "Invalid Query Parameter Value: state: {}" , state);
                    return CORSHeaderSupport.addCORSHeader(Response.Status.BAD_REQUEST,Response.Status.BAD_REQUEST.toString());
                }
            }
            List<String> semanticTagList = new ArrayList<>();
            if (semanticTag != null) {
                semanticTag = java.net.URLDecoder.decode(semanticTag, UTF_8);
                semanticTagList = Arrays.asList(semanticTag.split("\\s{2}"));
                for (int i = 0; i < semanticTagList.size(); i++)
                    semanticTagList.set(i, semanticTagList.get(i).toLowerCase());
                semanticTagList = new ArrayList<>(semanticTagList);
            }

            if (acceptability != null) {
                acceptability = java.net.URLDecoder.decode(acceptability, UTF_8);
                EnumAcceptability enumAcceptability = EnumAcceptability.getEnum(acceptability);
                if (enumAcceptability == null) {
                    logger.error(LoggerUtility.Exception, "Invalid Query Parameter Value: acceptability: {}" , acceptability);
                    return CORSHeaderSupport.addCORSHeader(Response.Status.BAD_REQUEST,Response.Status.BAD_REQUEST.toString());
                }
            }

            List<String> parentIdList = new ArrayList<>();
            if (parentId != null) {
                parentId = java.net.URLDecoder.decode(parentId, UTF_8);
                parentIdList = Arrays.asList(parentId.split("\\s{2}"));

                if (parentId.equalsIgnoreCase("null")) {
                    parentIdList = null;
                    parentId = null;
                }

                if (parentId != null) {
                    for (String pId : parentIdList) {
                        if (!pId.matches(_0_9)) {
                            logger.error(LoggerUtility.Exception, "Invalid Query Parameter Value: parentId: {}" , parentId);
                            return CORSHeaderSupport.addCORSHeader(Response.Status.BAD_REQUEST,
                                    Response.Status.BAD_REQUEST.toString());
                        }
                    }
                }
            }

            returnLimit = java.net.URLDecoder.decode(returnLimit, UTF_8);
            Integer iReturnLimit = -1; // -1 will show all fetched records
            try {
                iReturnLimit = Integer.parseInt(returnLimit);
            } catch (NumberFormatException e) {
                logger.error(LoggerUtility.Exception, "Invalid Query Parameter Value: returnLimit: {}" , returnLimit);
                return CORSHeaderSupport.addCORSHeader(Response.Status.BAD_REQUEST,Response.Status.BAD_REQUEST.toString());
            }
            if (refsetId != null) {
                refsetId = java.net.URLDecoder.decode(refsetId, UTF_8);
                if (refsetId.equalsIgnoreCase("null"))
                    refsetId = null;
                if (refsetId != null && refsetId.equalsIgnoreCase("")) {
                    logger.error(LoggerUtility.Exception, "Invalid Query Parameter Value: refsetId: {}" , refsetId);
                    return CORSHeaderSupport.addCORSHeader(Response.Status.BAD_REQUEST,Response.Status.BAD_REQUEST.toString());
                }
            }

            // get indexed semantic tag from sem index dir
            Set<String> indexedSemanticTags = agent.getSemanticTags();
            if (indexedSemanticTags == null) {
                throw new Exception("Sematic tag indexed not present, please verify..!!!");
            }
            // replace snomed rt ctv3 to snomed rt+ctv3
            if (semanticTagList.contains("snomed rt ctv3")) {
                semanticTagList.remove("snomed rt ctv3");
                semanticTagList.add("snomed rt+ctv3");
            }
            // validate semantic tags
            for (String strSemanticTag : semanticTagList) {
                if (!indexedSemanticTags.contains(strSemanticTag) && !strSemanticTag.equalsIgnoreCase("all")) {
                  
                        logger.error(LoggerUtility.Exception, "Invalid Query Parameter Value: semantictag: {}" , strSemanticTag);
                        return CORSHeaderSupport.addCORSHeader(Response.Status.BAD_REQUEST,
                                Response.Status.BAD_REQUEST.toString());
                    
                }

            }

            if (parentId != null && refsetId != null) {
                logger.error(LoggerUtility.Exception, "Invalid Query Parameter Value: refsetid: {} and parentid: {}. Only one parameter should be passed for refsetid and parentid at a time" , refsetId , parentId);
                return CORSHeaderSupport.addCORSHeader(Response.Status.BAD_REQUEST,
                        "Logical Error : Only one parameter should be passed for refsetid and parentid at a time");
            }

            Set<String> suggestedterms = agent.getSuggestions(term, EnumState.getEnum(state), semanticTagList,
                    EnumAcceptability.getEnum(acceptability), iReturnLimit, refsetId, parentIdList,
                    excludeparentconcept);
            jsonString = gson.toJson(suggestedterms);
        } catch (Exception e) // caches any of the service exceptions
        {
            // Log the Exception
            logger.error(LoggerUtility.Exception, LoggerUtility.createErrorMessage(e));
            return CORSHeaderSupport.addCORSHeader(Response.Status.BAD_REQUEST,Response.Status.BAD_REQUEST.toString());
        }
        // if callback in URL and is not just the "?" (e.g. from localhost)
        if (callback != null && callback.length() > 1) {
            return Response.ok(callback + "(" + jsonString + ");").build();
        } else {
            return CORSHeaderSupport.addCORSHeader(Response.Status.OK,jsonString);
        }
    }



    /**
     * Return LangRefset from LangRefsetEnum
     *
     * @param callback
     * @return
     */
    @GET
    @Produces({ MediaType.APPLICATION_JSON + ";charset=utf-8" })
    @Consumes({ MediaType.APPLICATION_FORM_URLENCODED })
    @Path("/langrefset")
    public Response getLangRefset(@QueryParam("callback") String callback) {
        initProperties();
        List<List<String>> langRefset = agent.getLangRefset();
        String jsonString = "";
        if (langRefset.isEmpty())
            return CORSHeaderSupport.addCORSHeader(Response.Status.INTERNAL_SERVER_ERROR,
                    Response.Status.INTERNAL_SERVER_ERROR.toString() + " : " + "Semantic index is not created");

        jsonString = gson.toJson(langRefset);
        // if callback in URL and is not just the "?" (e.g. from localhost)
        if (callback != null && callback.length() > 1) {
            return CORSHeaderSupport.addCORSHeader(Response.Status.OK,callback + "(" + jsonString + ");");
        } else {
            return CORSHeaderSupport.addCORSHeader(Response.Status.OK,jsonString);
        }
    }




    /**
     * Searches for a given term/text in the SNOMED CT repository with specified
     * state(ACTIVE, INACTIVE, BOTH) in specified Semantic Tag by providing
     * acceptability(preferred,preferredExcludingFSN,synonyms,acceptable,all).
     *
     * @param term           {@link String} term/text to be searched
     * @param state          {@link String} State of a concept.<br>
     *                       If state is,<br>
     *                       active - It will return all active Concepts with all
     *                       active Description.<br>
     *                       inactive - It will return all inactive Concepts with
     *                       all active and inactive Description.<br>
     *                       both - It will return all active and inactive Concepts
     *                       with all active and inactive Description.<br>
     * @param semantictag    {@link String} semantictag in which SNOMED CT term/text
     *                       will be searched. <br>
     *                       If semantictag is, all - It will search SNOMED CT term
     *                       in all Semantic Tag. A list of {@link EnumSemanticTag}
     *                       can also be provide in which search/suggest is to be
     *                       perform by appending semantic Tags with each other
     *                       using ++ Example: disorder++finding <br>
     * @param acceptability  {@link String} Represents whether only FSN and
     *                       Preferred Terms are to be fetched or all matching
     *                       terms. <br>
     *                       If acceptability is,<br>
     *                       preferred - refers to all preferred description for a
     *                       concept (including FSN). <br>
     *                       preferredexcludingfsn - refers to SNOMEDCT description
     *                       for preferred terms excluding FSN.<br>
     *                       synonyms - refers to all description for a concept
     *                       (excluding FSN).<br>
     *                       acceptable- refers to all description for a concept
     *                       excluding preferred.<br>
     *                       all - refers to all description for a concept (both
     *                       preferred and acceptable).<br>
     * @param returnLimit    {@link String} Maximum number of matching terms to be
     *                       fetched <br>
     * @param groupByConcept {@link String} boolean value, if true, then method will
     *                       return set of {@link CompositeDescription} which will
     *                       contain one description from one concept.<br>
     * @param refsetId       {@link String} Refset Identifier from which search is
     *                       to be done and don't provide refsetid parameter if
     *                       search is to be done from whole SNOMED CT Terminology.
     *                       <br>
     * @param fullConcept    {@link String} Boolean value, if true, then method will
     *                       return set of CompositeDescription which contains full
     *                       concept details object. if false, then method returns
     *                       set of CompositeDescription which contains only
     *                       conceptId, term, id (descriptionId), FSN of concept.
     *                       The default value is true, which will return all
     *                       concept detail. Any value other than 'true' and 'false'
     *                       will internally considered as 'true'.<br>
     * @param excludeconcept {@link boolean}Boolean value, if true, then method will
     *                       return set of CompositeDescription which will not
     *                       contain descriptions from given parent id. <br>
     *                       The default value is false, which will contain
     *                       descriptions from given parent id.
     * @param callback       {@link String} name of the callback function in a json
     *                       request</br>
     *                       (for allowing cross domain request)
     */
    @GET
    @Produces({ MediaType.APPLICATION_JSON + ";charset=utf-8" })
    @Consumes({ MediaType.APPLICATION_FORM_URLENCODED })
    @Path("/search")
    public Response searchTerm(@QueryParam("term") String term, @DefaultValue("both") @QueryParam("state") String state,
                               @DefaultValue("all") @QueryParam("semantictag") String semanticTag,
                               @DefaultValue("-1") @QueryParam("returnlimit") String returnLimit,
                               @DefaultValue("all") @QueryParam("acceptability") String acceptability,
                               @DefaultValue("false") @QueryParam("groupbyconcept") String groupbyconcepts,
                               @QueryParam("refsetid") String refsetId, @QueryParam("parentid") String parentId,
                               @QueryParam("callback") String callback,
                               @DefaultValue("false")@QueryParam("excludeparentconcept") boolean excludeparentconcept,
                               @DefaultValue("true") @QueryParam("fullconcept") String fullConcept) {

        initProperties();
        Logger logger = LogManager.getLogger(SearchController.class);

        Boolean groupbyconcept;
        if (groupbyconcepts.equalsIgnoreCase("true") || groupbyconcepts.equalsIgnoreCase("false")) {
            groupbyconcept = Boolean.valueOf(groupbyconcepts);
        } else {
            logger.error(LoggerUtility.Exception, "Invalid Query Parameter Value: groupbyconcept: {}" , groupbyconcepts);
            return CORSHeaderSupport.addCORSHeader(Response.Status.BAD_REQUEST,Response.Status.BAD_REQUEST.toString());
        }

        if (fullConcept == null)
            fullConcept = "true";
        if (!(fullConcept.equalsIgnoreCase("true") || fullConcept.equalsIgnoreCase("false"))) {
            fullConcept = "true";
        }

        if (term == null || term.equalsIgnoreCase("")) {
            logger.error(LoggerUtility.Exception, "Invalid Query Parameter Value: term: {}" , term);
            return CORSHeaderSupport.addCORSHeader(Response.Status.BAD_REQUEST,Response.Status.BAD_REQUEST.toString());
        }
        Set<CompositeDescription> descriptions = null;
        try {
            String encodedTerm = java.net.URLEncoder.encode(term, UTF_8);
            term = java.net.URLDecoder.decode(encodedTerm, UTF_8);

            if (state != null) {
                state = java.net.URLDecoder.decode(state, UTF_8);
                EnumState enumState = EnumState.getEnum(state);
                if (enumState == null) {
                    logger.error(LoggerUtility.Exception, "Invalid Query Parameter Value: state: {}" , state);
                    return CORSHeaderSupport.addCORSHeader(Response.Status.BAD_REQUEST,Response.Status.BAD_REQUEST.toString());
                }
            }

            List<String> semanticTagList = null;

            if (semanticTag != null) {
                try {
                    semanticTag = java.net.URLDecoder.decode(semanticTag, UTF_8);
                    semanticTagList = Arrays.asList(semanticTag.split("\\s{2}"));
                    for (int i = 0; i < semanticTagList.size(); i++)
                        semanticTagList.set(i, semanticTagList.get(i).toLowerCase());
                } catch (IllegalArgumentException e) {
                    logger.error(LoggerUtility.Exception, "Invalid Query Parameter Value: semanticTagList: {}" , semanticTagList);
                    return CORSHeaderSupport.addCORSHeader(Response.Status.BAD_REQUEST,Response.Status.BAD_REQUEST.toString());
                }
                semanticTagList = new ArrayList<>(semanticTagList);
            }

            if (acceptability != null) {
                acceptability = java.net.URLDecoder.decode(acceptability, UTF_8);
                EnumAcceptability enumAcceptability = EnumAcceptability.getEnum(acceptability);
                if (enumAcceptability == null) {
                    logger.error(LoggerUtility.Exception, "Invalid Query Parameter Value: acceptability: {}" , acceptability);
                    return CORSHeaderSupport.addCORSHeader(Response.Status.BAD_REQUEST,Response.Status.BAD_REQUEST.toString());
                }
            }

            List<String> parentIdList = new ArrayList<>();
            if (parentId != null) {
                parentId = java.net.URLDecoder.decode(parentId, UTF_8);
                parentIdList = Arrays.asList(parentId.split("\\s{2}"));

                
                if (parentId.equalsIgnoreCase("null")) {
                    parentIdList = null;
                    parentId = null;
                }

                if (parentId != null) {
                    for (String pId : parentIdList) {
                        if (!pId.matches(_0_9)) {
                            logger.error(LoggerUtility.Exception, "Invalid Query Parameter Value: parentId: {}" , parentId);
                            return CORSHeaderSupport.addCORSHeader(Response.Status.BAD_REQUEST,
                                    Response.Status.BAD_REQUEST.toString());
                        }
                    }
                }

            }

            returnLimit = java.net.URLDecoder.decode(returnLimit, UTF_8);
            Integer iReturnLimit = -1; // -1 will show all fetched records
            try {
                iReturnLimit = Integer.parseInt(returnLimit);
            } catch (NumberFormatException e) {
                logger.error(LoggerUtility.Exception, "Invalid Query Parameter Value: returnLimit: {}" , returnLimit);
                return CORSHeaderSupport.addCORSHeader(Response.Status.BAD_REQUEST,Response.Status.BAD_REQUEST.toString());
            }
            if (refsetId != null) {
                refsetId = java.net.URLDecoder.decode(refsetId, UTF_8);
                if (refsetId.equalsIgnoreCase("null"))
                    refsetId = null;
                if (refsetId != null && refsetId.equalsIgnoreCase("")) {
                    logger.error(LoggerUtility.Exception, "Invalid Query Parameter Value: refsetId: {}" , refsetId);
                    return CORSHeaderSupport.addCORSHeader(Response.Status.BAD_REQUEST,Response.Status.BAD_REQUEST.toString());
                }
            }

            // get indexed semantic tag from sem index dir
            Set<String> indexedSemanticTags = agent.getSemanticTags();
            if (indexedSemanticTags == null) {
                throw new Exception("Sematic tag indexed not present, please verify..!!!");
            }

            // replace snomed rt ctv3 to snomed rt+ctv3
            if (semanticTagList.contains("snomed rt ctv3")) {
                semanticTagList.remove("snomed rt ctv3");
                semanticTagList.add("snomed rt+ctv3");
            }
            // validate semantic tags
            for (String strSemanticTag : semanticTagList) {
                
            	if (!indexedSemanticTags.contains(strSemanticTag) && !strSemanticTag.equalsIgnoreCase("all")) {
                    
                        logger.error(LoggerUtility.Exception, "Invalid Query Parameter Value: semantictag: {}" , strSemanticTag);
                        return CORSHeaderSupport.addCORSHeader(Response.Status.BAD_REQUEST
                                ,Response.Status.BAD_REQUEST.toString());
                    
                }

            }
            if (parentId != null && refsetId != null) {
                logger.error(LoggerUtility.Exception, "Invalid Query Parameter Value: refsetid: {} and parentid: {}. Only one parameter should be passed for refsetid and parentid at a time" , refsetId , parentId);
                return CORSHeaderSupport.addCORSHeader(Response.Status.BAD_REQUEST,
                        "Logical Error : Only one parameter should be passed for refsetid and parentid at a time");
            }
            descriptions = agent.search(term, EnumState.getEnum(state), semanticTagList,
                    EnumAcceptability.getEnum(acceptability), iReturnLimit, groupbyconcept, refsetId, parentIdList,
                    Boolean.parseBoolean(fullConcept), excludeparentconcept);
        } catch (Exception e) // caches any of the service exception
        {
            // Log the Exception
            logger.error(LoggerUtility.Exception, LoggerUtility.createErrorMessage(e));

            return CORSHeaderSupport.addCORSHeader(Response.Status.BAD_REQUEST,Response.Status.BAD_REQUEST.toString());
        }

        String jsonString = gson.toJson(descriptions);
        // if callback in URL and is not just the "?" (e.g. from local-host)
        if (callback != null && callback.length() > 1) {
            return CORSHeaderSupport.addCORSHeader(Response.Status.OK,callback + "(" + jsonString + ");");
        } else {
            return CORSHeaderSupport.addCORSHeader(Response.Status.OK,jsonString);
        }
    }


    /**
     * Return semantic tags from SemIndex directory
     *
     * @param callback
     * @return
     */
    @GET
    @Produces({ MediaType.APPLICATION_JSON + ";charset=utf-8" })
    @Consumes({ MediaType.APPLICATION_FORM_URLENCODED })
    @Path("/semantic")
    public Response getSemanticTags(@QueryParam("callback") String callback) {
        initProperties();
        Set<String> semanticTags = agent.getSemanticTags();
        String jsonString = "";
        if (semanticTags == null)
            return CORSHeaderSupport.addCORSHeader(Response.Status.INTERNAL_SERVER_ERROR,
                    Response.Status.INTERNAL_SERVER_ERROR.toString() + " : " + "Semantic index is not created");

        jsonString = gson.toJson(semanticTags);
        // if callback in URL and is not just the "?" (e.g. from local-host)
        if (callback != null && callback.length() > 1) {
            return CORSHeaderSupport.addCORSHeader(Response.Status.OK, callback + "(" + jsonString + ");");
        } else {
            return CORSHeaderSupport.addCORSHeader(Response.Status.OK, jsonString);
        }
    }

    /**
     * Return relationship type from RElType directory
     *
     * @param callback
     * @return
     */
    @GET
    @Produces({ MediaType.APPLICATION_JSON + ";charset=utf-8" })
    @Consumes({ MediaType.APPLICATION_FORM_URLENCODED })
    @Path("/relationshiptype")
    public Response getRelationshipType(@QueryParam("callback") String callback) {
        initProperties();
        List<String> relationshiptype = agent.getRelationShipType();
        String jsonString = "";
        if (relationshiptype == null)
            return CORSHeaderSupport.addCORSHeader(Response.Status.INTERNAL_SERVER_ERROR,
                    Response.Status.INTERNAL_SERVER_ERROR.toString() + " : " + "RelType index is not created");

        jsonString = gson.toJson(relationshiptype);
        // if callback in URL and is not just the "?" (e.g. from localhost)
        if (callback != null && callback.length() > 1) {
            return CORSHeaderSupport.addCORSHeader(Response.Status.OK,callback + "(" + jsonString + ");");
        } else {
            return CORSHeaderSupport.addCORSHeader(Response.Status.OK,jsonString);
        }
    }

}