import { mutate } from "swr";
import {
  Coding,
  DosageInstruction,
  Medication,
  MedicationDispense,
  MedicationDispenseStatus,
  MedicationReferenceOrCodeableConcept,
  MedicationRequest,
  MedicationRequestBundle,
  MedicationRequestCombinedStatus,
  MedicationRequestFulfillerStatus,
  MedicationRequestStatus,
  Quantity,
} from "./types";
import { fhirBaseUrl, parseDate } from "@openmrs/esm-framework";
import {
  OPENMRS_FHIR_EXT_DISPENSE_RECORDED,
  OPENMRS_FHIR_EXT_MEDICINE,
  OPENMRS_FHIR_EXT_REQUEST_FULFILLER_STATUS,
  PRESCRIPTION_DETAILS_ENDPOINT,
  PRESCRIPTIONS_TABLE_ENDPOINT,
} from "./constants";
import dayjs from "dayjs";

const unitsDontMatchErrorMessage =
  "Misconfiguration, please contact your System Administrator:  Can't calculate quantity dispensed if units don't match. Likely issue: allowModifyingPrescription and restrictTotalQuantityDispensed configuration parameters both set to true. Either set restrictTotalQuantityDispensed to false or set allowModifyingPrescription to false and clean up bad data.";

/**
 * Computes the fulfiller status for a bundle
 *
 * @param medicationRequestBundle
 * @param restrictTotalQuantityDispensed
 */
export function computeFulfillerStatus(
  medicationRequestBundle: MedicationRequestBundle,
  restrictTotalQuantityDispensed: boolean
): MedicationRequestFulfillerStatus {
  if (
    restrictTotalQuantityDispensed &&
    computeQuantityRemaining(medicationRequestBundle) <= 0
  ) {
    // if we set to restrict total quantity dispenses and quantity remaining less than 0, set status to completed
    return MedicationRequestFulfillerStatus.completed;
  }

  // otherwise, set based on most recent dispense status as follows
  const mostRecentMedicationDispenseStatus =
    getMostRecentMedicationDispenseStatus(medicationRequestBundle.dispenses);

  if (
    mostRecentMedicationDispenseStatus === MedicationDispenseStatus.declined
  ) {
    return MedicationRequestFulfillerStatus.declined;
  }

  if (mostRecentMedicationDispenseStatus === MedicationDispenseStatus.on_hold) {
    return MedicationRequestFulfillerStatus.on_hold;
  }

  return null;
}
/**
 * Within the UI, the "status" of a request we want to display to the pharmacist is
 * a combination of the status and the fulfiller statuts; given a request
 * this calculates the actual status we want to display to the pharmacist
 *
 * @param medicationRequests
 * @param medicationRequestExpirationPeriodInDays
 */
export function computeMedicationRequestCombinedStatus(
  medicationRequest: MedicationRequest,
  medicationRequestExpirationPeriondInDays: number
): MedicationRequestCombinedStatus {
  const medicationRequestStatus: MedicationRequestStatus =
    computeMedicationRequestStatus(
      medicationRequest,
      medicationRequestExpirationPeriondInDays
    );
  const medicationRequestFulfillerStatus: MedicationRequestFulfillerStatus =
    getFulfillerStatus(medicationRequest);

  // if the request is no longer active, that status takes precedent
  if (medicationRequestStatus !== MedicationRequestStatus.active) {
    if (medicationRequestStatus === MedicationRequestStatus.expired) {
      return MedicationRequestCombinedStatus.expired;
    } else if (medicationRequestStatus === MedicationRequestStatus.completed) {
      return MedicationRequestCombinedStatus.completed;
    } else if (medicationRequestStatus === MedicationRequestStatus.cancelled) {
      return MedicationRequestCombinedStatus.cancelled;
    }
  }
  // otherwise, if the medication dispense status is paused or closed, return that
  if (
    medicationRequestFulfillerStatus ===
    MedicationRequestFulfillerStatus.declined
  ) {
    return MedicationRequestCombinedStatus.declined;
  } else if (
    medicationRequestFulfillerStatus ===
    MedicationRequestFulfillerStatus.on_hold
  ) {
    return MedicationRequestCombinedStatus.on_hold;
  }

  // otherwise, return active
  return MedicationRequestCombinedStatus.active;
}

/**
 * Calculates the status of a medication request given the request and the expiration period in days
 * Necessary to handle the (admittedly confusing) fact that the Dispense ESMs idea of "expired" is different
 * from that of the OpenMRS Backend, see logic below
 *
 * @param medicationRequests
 * @param medicationRequestExpirationPeriodInDays
 */
export function computeMedicationRequestStatus(
  medicationRequest: MedicationRequest,
  medicationRequestExpirationPeriodInDays: number
): MedicationRequestStatus {
  if (
    medicationRequest.status === MedicationRequestStatus.cancelled ||
    medicationRequest.status === MedicationRequestStatus.completed
  ) {
    return medicationRequest.status;
  }

  // expired is not based on based actual medication request expired status, but calculated from our configurable expiration period in days
  // NOTE: the assumption here is that the validityPeriod.start is equal to encounter datetime of the associated encounter, because we use the encounter date when doing backend querying
  if (
    medicationRequest.dispenseRequest?.validityPeriod?.start &&
    dayjs(medicationRequest.dispenseRequest.validityPeriod.start).isBefore(
      dayjs()
        .startOf("day")
        .subtract(medicationRequestExpirationPeriodInDays, "day")
    )
  ) {
    return MedicationRequestStatus.expired;
  }

  return MedicationRequestStatus.active;
}

/**
 * Captures the logic to compute the new fulfiller status after a dispense event, where dispense event = a medication dispense where medication is actually dispensed (as opposed one with status "on_hold" or "declined")
 *
 * @param medicationDispense the medication dispense being added or editing
 * @param medicationRequestBundle the entire existing bundle associated with the dispense being added/edited
 * @param restrictTotalQuantityDispensed value of the "dispenseBehavior.restrictTotalQuantityDispensed"
 */
export function computeNewFulfillerStatusAfterDispenseEvent(
  medicationDispense: MedicationDispense,
  medicationRequestBundle: MedicationRequestBundle,
  restrictTotalQuantityDispensed: boolean
) {
  // add or edit the existing bundle as necessary
  let dispenses = [...medicationRequestBundle.dispenses];

  if (!medicationDispense.id) {
    // new dispense, add to the array
    dispenses = [medicationDispense, ...dispenses];
  } else {
    // edited dispense, swap out
    dispenses = dispenses.map((dispense) =>
      dispense.id === medicationDispense.id ? medicationDispense : dispense
    );
  }

  // then call computeFulfillerStatus to compute status
  return computeFulfillerStatus(
    {
      request: medicationRequestBundle.request,
      dispenses: dispenses,
    },
    restrictTotalQuantityDispensed
  );
}

/**
 * Captures the logic to compute the new fulfiller status after a medication dispense is deleted
 *
 * @param deletedMedicationDispense the medication dispense we are deleting delete
 * @param medicationRequestBundle the entire existing bundle associated with the dispense being delete
 * @param restrictTotalQuantityDispensed value of the "dispenseBehavior.restrictTotalQuantityDispensed"
 */
export function computeNewFulfillerStatusAfterDelete(
  deletedMedicationDispense: MedicationDispense,
  medicationRequestBundle: MedicationRequestBundle,
  restrictTotalQuantityDispensed: boolean
): MedicationRequestFulfillerStatus {
  // filter out the dispense being deleted and call computeFulfillerStatus
  return computeFulfillerStatus(
    {
      request: medicationRequestBundle.request,
      dispenses: medicationRequestBundle.dispenses.filter(
        (dispense) => dispense.id !== deletedMedicationDispense.id
      ),
    },
    restrictTotalQuantityDispensed
  );
}

/**
 * Given a set of medication requests, calculates the "combined" status (see computeMedicationRequestCombinedStatus)
 * of each, and then, from those determines the overall status of the "prescription" (where "prescription"
 * means all medication requests in a single encounter)
 * @param medicationRequests
 * @param medicationRequestExpirationPeriodInDays
 */
export function computePrescriptionStatus(
  medicationRequests: Array<MedicationRequest>,
  medicationRequestExpirationPeriodInDays: number
): MedicationRequestCombinedStatus {
  if (!medicationRequests || medicationRequests.length === 0) {
    return null;
  }

  const medicationRequestCombinedStatuses: Array<MedicationRequestCombinedStatus> =
    medicationRequests.map((medicationRequest) =>
      computeMedicationRequestCombinedStatus(
        medicationRequest,
        medicationRequestExpirationPeriodInDays
      )
    );

  if (
    medicationRequestCombinedStatuses.includes(
      MedicationRequestCombinedStatus.active
    )
  ) {
    return MedicationRequestCombinedStatus.active;
  } else if (
    medicationRequestCombinedStatuses.includes(
      MedicationRequestCombinedStatus.on_hold
    )
  ) {
    return MedicationRequestCombinedStatus.on_hold;
  } else if (
    medicationRequestCombinedStatuses.includes(
      MedicationRequestCombinedStatus.completed
    )
  ) {
    return MedicationRequestCombinedStatus.completed;
  } else if (
    medicationRequestCombinedStatuses.includes(
      MedicationRequestCombinedStatus.declined
    )
  ) {
    return MedicationRequestCombinedStatus.declined;
  } else if (
    medicationRequestCombinedStatuses.includes(
      MedicationRequestCombinedStatus.cancelled
    )
  ) {
    return MedicationRequestCombinedStatus.cancelled;
  } else if (
    medicationRequestCombinedStatuses.includes(
      MedicationRequestCombinedStatus.expired
    )
  ) {
    return MedicationRequestCombinedStatus.expired;
  }

  return null;
}

/**
 * Calculates the prescription status and then returns the actual message code we want to display to the end user
 *
 * @param medicationRequests
 * @param medicationRequestExpirationPeriodInDays
 */
export function computePrescriptionStatusMessageCode(
  medicationRequests: Array<MedicationRequest>,
  medicationRequestExpirationPeriodInDays: number
): string {
  const medicationRequestCombinedStatus: MedicationRequestCombinedStatus =
    computePrescriptionStatus(
      medicationRequests,
      medicationRequestExpirationPeriodInDays
    );

  if (medicationRequestCombinedStatus === null) {
    return null;
  } else if (
    medicationRequestCombinedStatus === MedicationRequestCombinedStatus.active
  ) {
    return "active";
  } else if (
    medicationRequestCombinedStatus === MedicationRequestCombinedStatus.on_hold
  ) {
    return "paused";
  } else if (
    medicationRequestCombinedStatus ===
    MedicationRequestCombinedStatus.completed
  ) {
    return "completed";
  } else if (
    medicationRequestCombinedStatus === MedicationRequestCombinedStatus.declined
  ) {
    return "closed";
  } else if (
    medicationRequestCombinedStatus === MedicationRequestCombinedStatus.expired
  ) {
    return "expired";
  } else if (
    medicationRequestCombinedStatus ===
    MedicationRequestCombinedStatus.cancelled
  ) {
    return "cancelled";
  }
  return null;
}

export function computeQuantityRemaining(medicationRequestBundle): number {
  if (medicationRequestBundle) {
    // hard protect against quantity type mistmatch
    if (
      !getQuantityUnitsMatch([
        medicationRequestBundle.request,
        ...medicationRequestBundle.dispenses,
      ])
    ) {
      throw new Error(unitsDontMatchErrorMessage);
    }

    return (
      computeTotalQuantityOrdered(medicationRequestBundle.request) -
      computeTotalQuantityDispensed(medicationRequestBundle.dispenses)
    );
  }
  return 0;
}

/**
 * Given a set of medication dispenses, calculate the total quantity dispensed
 * @param medicationDispenses
 */
export function computeTotalQuantityDispensed(
  medicationDispenses: Array<MedicationDispense>
): number {
  if (medicationDispenses) {
    if (!getQuantityUnitsMatch(medicationDispenses)) {
      throw new Error(unitsDontMatchErrorMessage);
    }
    const quantity = medicationDispenses
      .map((medicationDispense) =>
        medicationDispense.quantity?.value
          ? medicationDispense.quantity?.value
          : 0
      )
      .reduce((acc, currentValue) => acc + currentValue, 0);
    return quantity;
  } else {
    return 0;
  }
}

/**
 * Given a medication request, calculate the total quantity ordered (including all refills)
 * @param medicationRequest
 */
export function computeTotalQuantityOrdered(
  medicationRequest: MedicationRequest
): number {
  const refillsAllowed = getRefillsAllowed(medicationRequest);
  if (medicationRequest.dispenseRequest?.quantity?.value) {
    return (
      medicationRequest.dispenseRequest.quantity.value *
      (1 + (refillsAllowed ? refillsAllowed : 0))
    );
  } else {
    return null;
  }
}

/**
 * Given a medication request and an array of medication dispenses, fetch all dispenses authorized by that request
 *
 * @param medicationRequest
 * @param medicationDispenses
 */
export function getAssociatedMedicationDispenses(
  medicationRequest: MedicationRequest,
  medicationDispenses: Array<MedicationDispense>
): Array<MedicationDispense> {
  return medicationDispenses?.filter((medicationDispense) =>
    medicationDispense?.authorizingPrescription?.some((prescription) =>
      prescription.reference.endsWith(medicationRequest.id)
    )
  );
}

/**
 * Given a medication dispense and an array of medication requests, fetch request which authorized this request
 *
 * @param medicationDispense
 * @param medicationRequests
 */
export function getAssociatedMedicationRequest(
  medicationDispense: MedicationDispense,
  medicationRequests: Array<MedicationRequest>
): MedicationRequest {
  return medicationRequests.find((medicationRequest) =>
    medicationDispense?.authorizingPrescription?.some((prescription) =>
      prescription.reference.endsWith(medicationRequest.id)
    )
  );
}

/**
 * Given an array of CodeableConcept codings, return the first one without an associated system (which should be the concept-referenced-by-uuid coding)
 * @param codings
 */
export function getConceptCoding(codings: Coding[]): Coding {
  return codings
    ? codings.find((c) => !("system" in c) || c.system === undefined)
    : null;
}

/**
 * Given an array of CodeableConcept codings, return the display for the first one without an associated system (which should be the concept-referenced-by-uuid coding)
 * @param codings
 */
export function getConceptCodingDisplay(codings: Coding[]): string {
  return getConceptCoding(codings)?.display;
}

/**
 * Given an array of CodeableConcept codings, return the code for the first one without an associated system (which should be the concept-referenced-by-uuid coding)
 * @param codings
 */
export function getConceptCodingUuid(codings: Coding[]): string {
  return getConceptCoding(codings)?.code;
}

/**
 * Fetch the "recorded" extension off a medication request
 * @param medicationDispense
 */
export function getDateRecorded(
  medicationDispense: MedicationDispense
): string {
  return medicationDispense?.extension?.find(
    (ext) => ext.url === OPENMRS_FHIR_EXT_DISPENSE_RECORDED
  )?.valueDateTime;
}

export function getDosageInstruction(
  dosageInstructions: Array<DosageInstruction>
): DosageInstruction {
  if (dosageInstructions?.length > 0) {
    return dosageInstructions[0];
  }
  return null;
}

/**
 * Fetch the "fulfiller status" extension off a medication request
 * @param medicationDispense
 */
export function getFulfillerStatus(
  medicationRequest: MedicationRequest
): MedicationRequestFulfillerStatus {
  return medicationRequest?.extension?.find(
    (ext) => ext.url === OPENMRS_FHIR_EXT_REQUEST_FULFILLER_STATUS
  )?.valueCode;
}

export function getMedicationsByConceptEndpoint(conceptUuid: string): string {
  return `${fhirBaseUrl}/Medication?code=${conceptUuid}`;
}

/**
 * Given a medication reference/codeable concept, format for display
 * When we have a medication reference (ie a coded Drug reference in the OpenMRS model) we simply use the display property associated with the medication reference
 * When we do not have medication reference, we display the associated concept and the OpenMRS DrugOrder.drugNonCoded string (which is stored in the codeable concept text field)
 *  (this may be slightly duplicative, but protects against the case when the provider only enters the formulation, not the drug, in the drugNonCoded field)
 * @param medication
 */
export function getMedicationDisplay(
  medication: MedicationReferenceOrCodeableConcept
): string {
  return medication.medicationReference
    ? medication.medicationReference.display
    : getConceptCodingDisplay(medication?.medicationCodeableConcept.coding) +
        ": " +
        medication?.medicationCodeableConcept.text;
}

// TODO does this need to null-check
export function getMedicationReferenceOrCodeableConcept(
  resource: MedicationRequest | MedicationDispense
): MedicationReferenceOrCodeableConcept {
  return {
    medicationReference: resource.medicationReference,
    medicationCodeableConcept: resource.medicationCodeableConcept,
  };
}

/**
 * Given a set of medication requests, return the status of the one with the most recent recorded date
 */
export function getMostRecentMedicationDispenseStatus(
  medicationDispenses: Array<MedicationDispense>
): MedicationDispenseStatus {
  const sorted = medicationDispenses?.sort(
    sortMedicationDispensesByDateRecorded
  );
  return sorted && sorted.length > 0 ? sorted[0].status : null;
}

/**
 * Given a set of medication requests, return the status of the one with the next most recent recorded date
 * (used when deleting the most recent, as we may need to update fulfiller status based on the next recent)
 */
export function getNextMostRecentMedicationDispenseStatus(
  medicationDispenses: Array<MedicationDispense>
): MedicationDispenseStatus {
  const sorted = medicationDispenses?.sort(
    sortMedicationDispensesByDateRecorded
  );
  return sorted && sorted.length > 1 ? sorted[1].status : null;
}

export function getMedicationRequestBundleContainingMedicationDispense(
  medicationRequestBundles: Array<MedicationRequestBundle>,
  medicationDispense: MedicationDispense
) {
  return medicationRequestBundles.find((bundle) =>
    bundle.dispenses.find((dispense) => dispense.id === medicationDispense.id)
  );
}

/**
 * Given a FHIR Medication, returns the string value stored in the "http://fhir.openmrs.org/ext/medicine#drugName" extension
 * @param medication
 */
export function getOpenMRSMedicineDrugName(medication: Medication): string {
  if (!medication || !medication.extension) {
    return null;
  }

  const medicineExtension = medication.extension.find(
    (ext) => ext.url === OPENMRS_FHIR_EXT_MEDICINE
  );

  if (!medicineExtension || !medicineExtension.extension) {
    return null;
  }

  const medicationExtensionDrugName = medicineExtension.extension.find(
    (ext) => ext.url === OPENMRS_FHIR_EXT_MEDICINE + "#drugName"
  );

  return medicationExtensionDrugName
    ? medicationExtensionDrugName.valueString
    : null;
}

export function getPrescriptionDetailsEndpoint(encounterUuid: string): string {
  return `${fhirBaseUrl}/${PRESCRIPTION_DETAILS_ENDPOINT}?encounter=${encounterUuid}&_revinclude=MedicationDispense:prescription&_include=MedicationRequest:encounter`;
}

export function getPrescriptionTableActiveMedicationRequestsEndpoint(
  pageOffset: number,
  pageSize: number,
  date: string,
  patientSearchTerm: string,
  location: string
): string {
  return appendSearchTermAndLocation(
    `${fhirBaseUrl}/${PRESCRIPTIONS_TABLE_ENDPOINT}&_getpagesoffset=${pageOffset}&_count=${pageSize}&date=ge${date}&status=active`,
    patientSearchTerm,
    location
  );
}

export function getPrescriptionTableAllMedicationRequestsEndpoint(
  pageOffset: number,
  pageSize: number,
  patientSearchTerm: string,
  location: string
): string {
  return appendSearchTermAndLocation(
    `${fhirBaseUrl}/${PRESCRIPTIONS_TABLE_ENDPOINT}&_getpagesoffset=${pageOffset}&_count=${pageSize}`,
    patientSearchTerm,
    location
  );
}

function appendSearchTermAndLocation(
  url: string,
  patientSearchTerm: string,
  location: string
): string {
  if (patientSearchTerm) {
    url = `${url}&patientSearchTerm=${patientSearchTerm}`;
  }
  if (location) {
    url = `${url}&location=${location}`;
  }
  return url;
}

export function getQuantity(
  resource: MedicationRequest | MedicationDispense
): Quantity {
  if (resource.resourceType == "MedicationRequest") {
    return (resource as MedicationRequest).dispenseRequest?.quantity;
  }
  if (resource.resourceType == "MedicationDispense") {
    return (resource as MedicationDispense).quantity;
  }
}

/**
 * Returns true/false whether the quantity units on all the resources are identical (or match)
 * @param resources
 */
export function getQuantityUnitsMatch(
  resources: Array<MedicationRequest | MedicationDispense>
): boolean {
  if (resources) {
    const quantityUnitsArray = resources
      .map((resource) => getQuantity(resource)?.code)
      .filter((quantity) => quantity);
    if (quantityUnitsArray.length > 0) {
      return quantityUnitsArray.every(
        (element) => element === quantityUnitsArray[0]
      );
    } else {
      return true; // consider true if empty
    }
  } else {
    return true; // consider true if null
  }
}

export function getRefillsAllowed(
  resource: MedicationRequest | MedicationDispense
): number {
  if (resource.resourceType == "MedicationRequest") {
    return (resource as MedicationRequest).dispenseRequest
      ?.numberOfRepeatsAllowed;
  } else {
    return null; // dispense doesn't have a "refills allowed" component
  }
}

/**
 * Given a refernece in format "MedicationReference/uuid" or just "uuid", returns just the uuid compoennt
 */
export function getUuidFromReference(reference: string): string {
  if (reference?.includes("/")) {
    return reference.split("/")[1];
  } else {
    return reference;
  }
}

/**
 * Returns true/false whether the most passed in medication dispense status is the most recent
 * @param medicationDispenses
 */
export function isMostRecentMedicationDispense(
  medicationDispense: MedicationDispense,
  medicationDispenses: Array<MedicationDispense>
): boolean {
  const sorted = medicationDispenses?.sort(
    sortMedicationDispensesByDateRecorded
  );

  // prettier-ignore
  return medicationDispense &&
    sorted &&
    sorted.length > 0 &&
    sorted[0].id === medicationDispense.id ? true : false;
}

/**
 * Revalidated (reloads) both the prescription associated with the encounter uuid,
 * and the entire prescrption table
 * @param encounterUuid
 */
export function revalidate(encounterUuid: string) {
  mutate(
    (key) =>
      typeof key === "string" &&
      (key.startsWith(`${fhirBaseUrl}/${PRESCRIPTIONS_TABLE_ENDPOINT}`) ||
        key.startsWith(
          `${fhirBaseUrl}/${PRESCRIPTION_DETAILS_ENDPOINT}?encounter=${encounterUuid}`
        ))
  );
}

export function sortMedicationDispensesByDateRecorded(
  a: MedicationDispense,
  b: MedicationDispense
): number {
  if (getDateRecorded(b) === null) {
    return 1;
  } else if (getDateRecorded(a) === null) {
    return -1;
  }
  const dateDiff =
    parseDate(getDateRecorded(b)).getTime() -
    parseDate(getDateRecorded(a)).getTime();
  if (dateDiff !== 0) {
    return dateDiff;
  } else {
    return a.id.localeCompare(b.id); // just to enforce a standard order if two dates are equals
  }
}
