package com.agilex.healthcare.schedulingmanager.resources;

import com.agilex.healthcare.mobilehealthplatform.domain.PatientIdentifier;
import com.agilex.healthcare.mobilehealthplatform.domain.ResourceDirectory;
import com.agilex.healthcare.mobilehealthplatform.domain.filter.datefilter.DateFilter;
import com.agilex.healthcare.mobilehealthplatform.domain.filter.datefilter.DateFilterFactory;
import com.agilex.healthcare.schedulingmanager.datalayer.EmrDataLayer;
import com.agilex.healthcare.schedulingmanager.datalayer.SchedulingDataLayer;
import com.agilex.healthcare.schedulingmanager.datalayer.SMPatientDataLayer;
import agilex.commons.pagination.PaginationDataLayer;
import com.agilex.healthcare.schedulingmanager.dataservice.ConsultDataService;
import com.agilex.healthcare.schedulingmanager.dataservice.PdfDataService;
import com.agilex.healthcare.schedulingmanager.domain.*;
import com.agilex.healthcare.schedulingmanager.linkbuilder.ConsultLinkBuilder;
import com.agilex.healthcare.schedulingmanager.linkbuilder.SchedulingLinkBuilder;
import com.agilex.healthcare.schedulingmanager.linkbuilder.SchedulingServiceResourceDirectoryBuilder;
import com.agilex.healthcare.schedulingmanager.pdf.PdfGenerationContext;
import com.agilex.healthcare.schedulingmanager.utils.ListType;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.StringTokenizer;

@Path("/scheduling-service/site/{site-code}/")
@Component
@Scope("request")
public class SchedulingServiceResource {

    private static final org.apache.commons.logging.Log logger = org.apache.commons.logging.LogFactory.getLog(SchedulingServiceResource.class);

    private static final String GENERIC_BOOKING_ERROR = "An error occurred while booking the appointment.";

    @Resource
    SchedulingServiceResourceDirectoryBuilder schedulingServiceResourceDirectoryBuilder;

    @Resource(name = "schedulingDataLayer")
    SchedulingDataLayer schedulingDataLayer;

    @Resource
    MessageSource messageSource;

    PdfDataService pdfDataService;

    @Resource(name = "smPatientDataLayer")
    SMPatientDataLayer smPatientDataLayer;

    @Resource(name = "emrDataLayer")
    EmrDataLayer emrDataLayer;

    @Resource
    ConsultDataService consultDataService;

    @Resource(name = "paginationDataLayer")
    PaginationDataLayer paginationDataLayer;

    @GET
    @Produces({ "application/xml", "application/json" })
    @Path("resource-directory")
    public ResourceDirectory fetchResourceDirectory(@Context UriInfo uriInfo, @Context HttpServletRequest request, @Context HttpHeaders headers, @PathParam("site-code") String siteCode) {
        ResourceDirectory resourceDirectory = schedulingServiceResourceDirectoryBuilder.getResourceDirectory(uriInfo.getBaseUri(), uriInfo.getRequestUri(), siteCode);
        return resourceDirectory;
    }

    /**
     * This resource returns available appointment slots, for a clinic in a given site, within specified date range as query parameters.
     * @param uriInfo
     * @param request
     * @param headers
     * @param siteCode
     * @param clinicId
     * @return
     */
    @GET
    @Produces({ "application/xml", "application/json" })
    @Path("available-appointment-slots/clinic/{clinic-id}")
    public AppointmentTimeSlots getAppointmentSlots(@Context UriInfo uriInfo, @Context HttpServletRequest request, @Context HttpHeaders headers, @PathParam("site-code") String siteCode, @PathParam("clinic-id") String clinicId){

        AppointmentSlotFilter appointmentSlotFilter = getAvailableAppointmentSlotsCriteria(uriInfo, siteCode, clinicId);
        AppointmentTimeSlots appointmentTimeSlots = schedulingDataLayer.getClinicSchedulingDetail(appointmentSlotFilter);
        return appointmentTimeSlots;
    }

    private AppointmentSlotFilter getAvailableAppointmentSlotsCriteria(UriInfo uriInfo, String siteCode, String clinicId){
        DateFilter dateFilter = DateFilterFactory.createFilterFromUri(uriInfo);

        AppointmentSlotFilter appointmentSlotFilter = new AppointmentSlotFilter();
        appointmentSlotFilter.setClinicId(clinicId);
        appointmentSlotFilter.setSiteCode(siteCode);
        appointmentSlotFilter.setStartDate(dateFilter.getStartDate());
        appointmentSlotFilter.setEndDate(dateFilter.getEndDate());

        return appointmentSlotFilter;
    }

    /**
     * This resource returns future appointments for a patient
     * @param uriInfo
     * @param request
     * @param headers
     * @param siteCode
     * @param patientId
     * @param assigningAuthority
     * @return
     */
    @GET
    @Produces({"application/xml", "application/json"})
    @Path("patient/{assigning-authority}/{patient-id}/booked-appointments")
    public BookedAppointments getPendingAppointmentsForPatient(@Context UriInfo uriInfo, @Context HttpServletRequest request, @Context HttpHeaders headers,
                                                                          @PathParam("site-code") String siteCode, @PathParam("patient-id") String patientId,
                                                                          @PathParam("assigning-authority") String assigningAuthority){

        AppointmentSlotFilter appointmentSlotFilter = getBookedAppointmentSlotsForPatient(uriInfo, siteCode, null, patientId);
        PatientIdentifier patientIdentifier = new PatientIdentifier(assigningAuthority, patientId);
        appointmentSlotFilter.setPatientIdentifier(patientIdentifier);
        BookedAppointments bookedAppointments = schedulingDataLayer.getPatientBookedAppointments(appointmentSlotFilter);
        SchedulingLinkBuilder linkBuilder = new SchedulingLinkBuilder(uriInfo.getBaseUri());
        bookedAppointments = linkBuilder.fillLinks(bookedAppointments, patientIdentifier, siteCode);
        return bookedAppointments;
    }

    /**
     * This resource is used to book new appointment for a patient in selected clinic.
     * @param bookAppointment
     * @param assigningAuthority
     * @param patientId
     * @param uriInfo
     * @return
     */
    @POST
    @Consumes({ "application/xml", "application/json"})
    @Produces({ "application/xml", "application/json"})
    @Path("patient/{assigning-authority}/{patient-id}/book-appointment")
    public Response bookNewAppointment(BookAppointment bookAppointment, @PathParam("assigning-authority") String assigningAuthority, @PathParam("patient-id") String patientId, @Context UriInfo uriInfo, @Context HttpHeaders headers){
        PatientIdentifier patientIdentifier = new PatientIdentifier(assigningAuthority, patientId);
        bookAppointment.setPatientIdentifier(patientIdentifier);
        BookAppointment newBookedAppointment = new BookAppointment();
        try{
            newBookedAppointment = schedulingDataLayer.bookPatientAppointment(bookAppointment);
        }catch(Exception e){
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(GENERIC_BOOKING_ERROR).build();
        }
        if(newBookedAppointment.getError() != null && newBookedAppointment.getError().length() > 0){
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(newBookedAppointment.getError()).build();
        }
        if(isAppointmentSavedCorrectly(bookAppointment, newBookedAppointment)){
            return Response.ok().build();
        }
        return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(GENERIC_BOOKING_ERROR).build();
    }

    /**
     * This resource returns list of Appointment Type (displayed as purpose drop-down) required as a part of appointment booking process.
     * @param siteCode
     * @param uriInfo
     * @param headers
     * @return
     */
    @GET
    @Produces({ "application/json", "application/xml" })
    @Path("purpose-list")
    public AppointmentTypes getPurposeDropDownList(@PathParam("site-code") String siteCode, @Context UriInfo uriInfo, @Context HttpHeaders headers){
        AppointmentTypes purposeList = schedulingDataLayer.getAppointmentTypes(siteCode);
        return purposeList;
    }

    private AppointmentSlotFilter getBookedAppointmentSlotsForPatient(UriInfo uriInfo, String siteCode, String clinicId, String patientId){
        AppointmentSlotFilter appointmentSlotFilter = getAvailableAppointmentSlotsCriteria(uriInfo, siteCode, clinicId);
        appointmentSlotFilter.setPatientId(patientId);
        return appointmentSlotFilter;
    }

    private boolean isAppointmentSavedCorrectly(BookAppointment requestedAppointment, BookAppointment savedAppointment){
        return (requestedAppointment.getDateTime().equals(savedAppointment.getDateTime())
                    && requestedAppointment.getClinicId().equals(savedAppointment.getClinicId())
                    && requestedAppointment.getPurpose().equals(savedAppointment.getPurpose()));
    }


    /**
     * This resource returns list of Cancel Reasons to be displayed
     * in a UI drop-down as a part of appointment booking process.
     * @return A list of Cancel Reasons
     */
    @GET
    @Produces({ "application/json", "application/xml" })
    @Path("cancel-reasons-list")
    public CancelReasons getCancelReasonsDropDownList(@PathParam("site-code") String siteCode, @Context UriInfo uriInfo, @Context HttpHeaders headers){
        CancelReasons cancelReasons = schedulingDataLayer.getCancelReasonList(siteCode);
        return cancelReasons;
    }

    /**
     * This resource is used to cancel Patient's future appointment.
     * @param cancelAppointment
     * @param assigningAuthority
     * @param patientId
     * @param uriInfo
     * @return
     */
    @PUT
    @Produces({ "application/json", "application/xml" })
    @Consumes({ "application/json", "application/xml" })
    @Path("patient/{assigning-authority}/{patient-id}/cancel-appointment")
    public Response cancelPatientAppointment(CancelAppointment cancelAppointment, @PathParam("assigning-authority") String assigningAuthority, @PathParam("patient-id") String patientId, @Context UriInfo uriInfo, @Context HttpHeaders headers){
        PatientIdentifier patientIdentifier = new PatientIdentifier(assigningAuthority, patientId);
        cancelAppointment.setPatientIdentifier(patientIdentifier);
        CancelAppointment canceledAppointment = schedulingDataLayer.cancelAppointmentForPatient(cancelAppointment);
        if(isAppointmentCanceledSuccessfully(cancelAppointment, canceledAppointment)){
            return Response.ok().build();
        }
        return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
    }

    /**
     * This resource is used to remove Recall entry after booking an Appointment for a patient.
     * @param waitListEntry
     * @param waitListType
     * @param siteCode
     * @param assigningAuthority
     * @param patientId
     * @param uriInfo
     * @return
     */
    @Deprecated
    @DELETE
    @Consumes({ "application/xml", "application/json" })
    @Produces({ "application/xml", "application/json" })
    @Path("patient/{assigning-authority}/{patient-id}/remove-waitlist-entry")
    public Response removeRecallEntryforPatient(WaitListEntry waitListEntry, @QueryParam("waitListType") String waitListType, @PathParam("site-code") String siteCode, @PathParam("assigning-authority") String assigningAuthority, @PathParam("patient-id") String patientId, @Context UriInfo uriInfo, @Context HttpHeaders headers){
        /*PatientIdentifier patientIdentifier = new PatientIdentifier(assigningAuthority, patientId);
        waitListEntry.setPatientIdentifier(patientIdentifier);
        waitListEntry.setFacilityId(siteCode);
        if(waitListType.equals("RECALL")){
            WaitListEntry canceledRecallEntry = schedulingDataLayer.removeRecallEntryForPatient(waitListEntry);
            if(canceledRecallEntry != null){
                return Response.ok().build();
            }
        }
        return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();*/
    	return Response.ok().build();
    }
    
    /**
     * This resource updates an entry from the NEAR wait list.
     * @param siteCode
     * @param assigningAuthority
     * @param patientId
     * @param status
     * @return
     */
    @Deprecated
    @PUT
    @Produces({ "application/json", "application/xml" })
    @Consumes({ "application/json", "application/xml" })
    @Path("patient/{assigning-authority}/{patient-id}/update-near-list/{status}")
    public Response updateNearListEntry(@PathParam("site-code") String siteCode, @PathParam("assigning-authority") String assigningAuthority, @PathParam("patient-id") String patientId, @PathParam("status") String status, @Context HttpHeaders headers, @Context UriInfo uriInfo){
        String response = schedulingDataLayer.updateNearEntryForPatient(new PatientIdentifier(assigningAuthority, patientId), siteCode, status);

        if(response != null && response.equals("Success")) {
            return Response.ok().build();
        }
        else {
            return Response.serverError().build();
        }
    }

    @Deprecated
    @PUT
    @Consumes({ "application/xml", "application/json" })
    @Produces({ "application/xml", "application/json" })
    @Path("patient/{assigning-authority}/{patient-id}/update-ewl-entry")
    public Response updateEWLWaitListEntry(EwlUpdateEntry ewlUpdateEntry, @PathParam("site-code") String siteCode, @PathParam("assigning-authority") String assigningAuthority, @PathParam("patient-id") String patientId, @Context UriInfo uriInfo, @Context HttpHeaders headers){
        PatientIdentifier patientIdentifier = new PatientIdentifier(assigningAuthority, patientId);
        ewlUpdateEntry.setPatientIdentifier(patientIdentifier);
        ewlUpdateEntry.setFacilityId(siteCode);
        if(ewlUpdateEntry.getAppointmentTime() == null || ewlUpdateEntry.getClinicId() == null || ewlUpdateEntry.getCode() == null
                                || ewlUpdateEntry.getStopCode() == null || ewlUpdateEntry.getCreditCode() == null){
            return Response.status(Response.Status.BAD_REQUEST).build();
        }
        EwlUpdateEntry updatedEWL = schedulingDataLayer.updateEWLEntry(ewlUpdateEntry);
        if(updatedEWL != null){
            return Response.ok().build();
        }
        return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
    }

    private boolean isAppointmentCanceledSuccessfully(CancelAppointment originalObject, CancelAppointment returnObject){
        return (originalObject.getClinicId().equals(returnObject.getClinicId())
                && originalObject.getAppointmentTime().equals(returnObject.getAppointmentTime())
                && originalObject.getCancelCode().equals(returnObject.getCancelCode())
                && originalObject.getCancelReason().equals(returnObject.getCancelReason()));
    }

    /**
     * This resource retrieves all the consults for a given service/specialty for current facility.
     * @param serviceId
     * @param providerId
     * @param siteCode
     * @param uriInfo
     * @return
     */
    @Deprecated
    @GET
    @Consumes({ "application/xml", "application/json" })
    @Produces({ "application/xml", "application/json" })
    @Path("/consults")
    public SMConsults getConsultsByService(@QueryParam("serviceId") String serviceId,
                                           @QueryParam("providerId") String providerId,
                                           @PathParam("site-code") String siteCode,
                                           @QueryParam("refresh") String refresh,
                                           @QueryParam("max-per-page") String maxPerPageStr,
                                           @QueryParam("go-to-page") String goToPageStr,
                                           @QueryParam("sort-on") String sortOnString,
                                           @Context UriInfo uriInfo,
                                           @Context HttpHeaders headers){
        int                         gotoPage;
        int                         maxPerPage;
        HashMap<String, Boolean>    sortFields = null;
        StringTokenizer             strtok;
        StringTokenizer             strtok2;

        //
        // Set default values for null parameters.
        //
        if(refresh == null) {
            refresh = "true";
        }

        if(maxPerPageStr == null) {
            maxPerPageStr = "20";
        }

        if(goToPageStr == null) {
            goToPageStr = "1";
        }

        maxPerPage = Integer.parseInt(maxPerPageStr);
        gotoPage = Integer.parseInt(goToPageStr);

        //
        // The page number we are passed is NOT zero based, but or code
        // assumes it is, so we need to adjust it.  We also need to make sure it is not negative.
        //
        gotoPage -= 1;
        if(gotoPage < 0) {
            gotoPage = 0;
        }

        if(sortOnString != null) {
            sortFields = new HashMap<String, Boolean>();

            strtok = new StringTokenizer(sortOnString, ";");
            while(strtok.hasMoreTokens()) {
                String      sortData = strtok.nextToken();

                strtok2 = new StringTokenizer(sortData, ",");

                sortFields.put(strtok2.nextToken(), new Boolean(strtok2.nextToken()));
            }
        }

        DateFilter dateFilter = DateFilterFactory.createFilterFromUri(uriInfo);
        if(providerId == null)
            providerId = "";
//        SMConsults smConsults = schedulingDataLayer.getConsultsBySpecialties(serviceId, providerId, dateFilter, siteCode);
        SMConsults smConsults = consultDataService.getListForFacility(schedulingDataLayer, ListType.CONSULT, Boolean.parseBoolean(refresh), siteCode,
                                                                        serviceId, sortFields, gotoPage * maxPerPage,
                                                                        (gotoPage * maxPerPage) + maxPerPage, providerId, dateFilter);

        gotoPage += 1;
        smConsults.setRecordCount(paginationDataLayer.getRecordCount(ListType.CONSULT.getName(), siteCode, providerId, null, serviceId));
        int pgCount = (int)smConsults.getRecordCount()/maxPerPage;
        if((int)smConsults.getRecordCount() % maxPerPage != 0){
            pgCount ++;
        }
        smConsults.setPageCount(pgCount);
        if(gotoPage > smConsults.getPageCount()){
            smConsults.setCurrentPage(smConsults.getPageCount());
        }
        else{
            smConsults.setCurrentPage(gotoPage);
        }
        smConsults.setStartRecord((maxPerPage * (gotoPage - 1)) + 1);
        smConsults.setStopRecord(maxPerPage * gotoPage);
        if(smConsults.getStopRecord() > smConsults.getRecordCount()){
            smConsults.setStopRecord(smConsults.getRecordCount());
        }

        ConsultLinkBuilder consultLinkBuilder = new ConsultLinkBuilder(uriInfo.getBaseUri());
        consultLinkBuilder.fillLinks(smConsults, uriInfo.getRequestUri(), siteCode);
        return smConsults;
    }

    /**
     * This Resource returns list of services for consults.
     * @param siteCode
     * @param uriInfo
     * @return
     */
    @Deprecated
    @GET
    @Consumes({ "application/xml", "application/json" })
    @Produces({ "application/xml", "application/json" })
    @Path("/consult/services")
    public SMConsultServices getListOfServicesForConsult(@PathParam("site-code") String siteCode, @Context UriInfo uriInfo, @Context HttpHeaders headers){
        SMConsultServices smConsultServices = schedulingDataLayer.getConsultServices(siteCode);
        return smConsultServices;
    }

    /**
     * This resource returns detail (text blob) for consult from VistA.
     * @param consultIen
     * @param siteCode
     * @param uriInfo
     * @return
     */
    @Deprecated
    @GET
    @Consumes({ "application/xml", "application/json" })
    @Produces({ "application/xml", "application/json" })
    @Path("/consult/{consult-ien}/detail")
    public SMConsultDetail getConsultDetails(@PathParam("consult-ien") String consultIen, @PathParam("site-code") String siteCode, @Context UriInfo uriInfo, @Context HttpHeaders headers){
        SMConsultDetail smConsultDetail = schedulingDataLayer.getDetailsForConsult(consultIen, siteCode);
        ConsultLinkBuilder consultLinkBuilder = new ConsultLinkBuilder(uriInfo.getBaseUri());
        consultLinkBuilder.fillLinks(smConsultDetail, uriInfo.getRequestUri(), siteCode);
        return smConsultDetail;
    }
    
    @Deprecated
    @GET
    @Consumes({ "application/xml", "application/json" })
    @Produces({ "application/xml", "application/json" })
    @Path("/patient/{assigning-authority}/{patient-id}/consults")
    public SMConsults getConsultsForPatient(@PathParam("assigning-authority") String assigningAuthority, @PathParam("patient-id") String patientId, @PathParam("site-code") String siteCode, @Context UriInfo uriInfo, @Context HttpHeaders headers){
        SMConsults smConsults = emrDataLayer.getPatientConsults(new PatientIdentifier(assigningAuthority, patientId));
        ConsultLinkBuilder consultLinkBuilder = new ConsultLinkBuilder(uriInfo.getBaseUri());
        consultLinkBuilder.fillLinks(smConsults, uriInfo.getRequestUri(), patientId, siteCode);
        return smConsults;
    }
    
    @Deprecated
    @GET
    @Produces({ "application/pdf" })
    @Path("/patient/{assigning-authority}/{patient-id}/consult/{consult-ien}/{service-id}/pdf")
    public Response generatePdfForConsult(@PathParam("assigning-authority") String assigningAuthority,
                                          @PathParam("patient-id") String patientId,
                                          @PathParam("consult-ien") String consultIen,
                                          @PathParam("service-id") String serviceId,
                                          @PathParam("site-code") String siteCode,
                                          @Context UriInfo uriInfo,
                                          @Context HttpHeaders headers,
                                          @Context HttpServletRequest request) {

        PatientIdentifier patientIdentifier = new PatientIdentifier(assigningAuthority, patientId);
        //TODO after fetch consult by patient service is build this needs to be changed.
        SMConsults smConsults = this.getConsultsByService(serviceId, "", siteCode, "true", "1000", "1", "", uriInfo, headers);
        SMConsult smConsult = smConsults.findConsultById(consultIen);
        SMConsultDetail smConsultDetail = this.getConsultDetails(smConsult.getConsultIen(), siteCode, uriInfo, headers);
        smConsult.setText(smConsultDetail.getText());

        if (smConsult != null) {
            final PdfGenerationContext context = buildContext(request, patientIdentifier, smConsult);
            StreamingOutput stream = generatePdfToStreamingOutput(context);
            return Response.ok(stream).build();
        } else {
            return Response.ok().build();
        }
    }

    @Deprecated
    @GET
    @Produces({ "application/pdf" })
    @Path("/patient/{assigning-authority}/{patient-id}/consult/{consult-ien}/pdf")
    public Response generatePdfForPatientConsult(@PathParam("assigning-authority") String assigningAuthority,
                                                 @PathParam("patient-id") String patientId,
                                                 @PathParam("consult-ien") String consultIen,
                                                 @PathParam("site-code") String siteCode,
                                                 @Context UriInfo uriInfo,
                                                 @Context HttpHeaders headers,
                                                 @Context HttpServletRequest request){

        PatientIdentifier patientIdentifier = new PatientIdentifier(assigningAuthority, patientId);
        SMConsults smConsults = this.getConsultsForPatient(assigningAuthority, patientId, siteCode, uriInfo, headers);
        SMConsult smConsult = smConsults.findConsultById(consultIen);
        if(smConsult != null){
            final PdfGenerationContext context = buildContext(request, patientIdentifier, smConsult);
            StreamingOutput stream = generatePdfToStreamingOutput(context);
            return Response.ok(stream).build();
        } else {
            return Response.ok().build();
        }
    }

    private PdfGenerationContext buildContext(HttpServletRequest request, PatientIdentifier patientIdentifier, SMConsult smConsult) {
        pdfDataService = new PdfDataService();
        return pdfDataService.buildContext(request, patientIdentifier, messageSource, smConsult, smPatientDataLayer);
    }

    private StreamingOutput generatePdfToStreamingOutput(final PdfGenerationContext context) {
        StreamingOutput stream = new StreamingOutput() {
            public void write(OutputStream output) throws IOException, WebApplicationException {
                try {
                    pdfDataService = new PdfDataService();
                    pdfDataService.generateReport(output, context);
                } catch (Exception e) {
                    logger.error("Error generating PDF Report", e);
                    throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
                }
            }
        };
        return stream;
    }
}
