require 'nokogiri'

module AllscriptsUnityClient

  # Providers an interface to access Unity endpoints.
  #
  # Build using a dependency injection pattern. A Client instances takes an instance of
  # ClientDriver and delegates Unity endpoint methods to the ClientDriver.
  class Client
    attr_accessor :client_driver

    # Constructor.
    #
    # client_driver:: An instance of a ClientDriver. Currently only SoapClientDriver and JsonClientDriver
    # are supported.
    def initialize(client_driver)
      raise ArgumentError, 'client_driver can not be nil' if client_driver.nil?

      @client_driver = client_driver
    end

    # Access client's options. See ClientOptions.
    def options
      @client_driver.options
    end

    # Implement Unity's Magic endpoint
    #
    # parameters:: A Hash of Unity parameters. Takes this form:
    #
    #   {
    #     :action => ...,
    #     :userid => ...,
    #     :appname => ...,
    #     :patientid => ...,
    #     :token => ...,
    #     :parameter1 => ...,
    #     :parameter2 => ...,
    #     :parameter3 => ...,
    #     :parameter4 => ...,
    #     :parameter5 => ...,
    #     :parameter6 => ...,
    #     :data => ...
    #   }
    #
    # Returns the result of the Magic endpoint as a Hash.
    def magic(parameters = {})
      @client_driver.magic(parameters)
    end

    # Implement Unity's GetSecurityToken endpoint.
    #
    # Stores the results in @security_token.
    #
    # parameters:: A hash of Unity parameters for GetSecurityToken:
    #
    #   {
    #     :username => ...,
    #     :password => ...,
    #     :appname => ...
    #   }
    #
    # Returns the security token.
    def get_security_token!(parameters = {})
      @client_driver.get_security_token!(parameters)
    end

    # Implement Unity's RetireSecurityToken endpoint using Savon.
    #
    # parameters:: A hash of Unity parameters for RetireSecurityToken. If not given then defaults to
    # @security_token:
    #
    #   {
    #     :token => ...,
    #     :appname => ...
    #   }
    def retire_security_token!(parameters = {})
      @client_driver.retire_security_token!(parameters)
    end

    # Return true if a Unity security token has been fetched and saved.
    def security_token?
      @client_driver.security_token?
    end

    # Return the client type, either :json or :soap.
    def client_type
      @client_driver.client_type
    end

    def commit_charges
      raise NotImplementedError, 'CommitCharges magic action not implemented'
    end

    def echo(echo_text)
      magic_parameters = {
        action: 'Echo',
        userid: echo_text,
        appname: echo_text,
        patientid: echo_text,
        parameter1: echo_text,
        parameter2: echo_text,
        parameter3: echo_text,
        parameter4: echo_text,
        parameter5: echo_text,
        parameter6: echo_text
      }
      response = magic(magic_parameters)
      response[:userid]
    end

    def get_account
      magic_parameters = {
          action: 'GetAccount'
      }
      magic(magic_parameters)
    end

    def get_changed_patients(since = nil)
      magic_parameters = {
        action: 'GetChangedPatients',
        parameter1: since
      }
      magic(magic_parameters)
    end

    def get_charge_info_by_username
      raise NotImplementedError, 'GetChargeInfoByUsername magic action not implemented'
    end

    def get_charges
      raise NotImplementedError, 'GetCharges magic action not implemented'
    end

    def get_chart_item_details(userid, patientid, section)
      magic_parameters = {
        action: 'GetChartItemDetails',
        userid: userid,
        patientid: patientid,
        parameter1: section
      }
      magic(magic_parameters)
    end

    def get_clinical_summary(userid, patientid, extra_med_data = false)
      magic_parameters = {
        action: 'GetClinicalSummary',
        userid: userid,
        patientid: patientid,
        parameter3: unity_true_or_blank_parameter(extra_med_data)
      }
      response = magic(magic_parameters)

      unless response.is_a?(Array)
        response = [ response ]
      end

      response
    end

    def get_delegates
      raise NotImplementedError, 'GetDelegates magic action not implemented'
    end

    def get_dictionary(dictionary_name, userid = nil, site = nil)
      magic_parameters = {
        action: 'GetDictionary',
        userid: userid,
        parameter1: dictionary_name,
        parameter2: site
      }
      response = magic(magic_parameters)

      unless response.is_a?(Array)
        response = [ response ]
      end

      response
    end

    def get_dictionary_sets
      raise NotImplementedError, 'GetDictionarySets magic action not implemented'
    end

    def get_doc_template
      raise NotImplementedError, 'GetDocTemplate magic action not implemented'
    end

    def get_document_by_accession
      raise NotImplementedError, 'GetDocumentByAccession magic action not implemented'
    end

    def get_document_image
      raise NotImplementedError, 'GetDocumentImage magic action not implemented'
    end

    def get_documents
      raise NotImplementedError, 'GetDocuments magic action not implemented'
    end

    def get_document_type
      raise NotImplementedError, 'GetDocumentType magic action not implemented'
    end

    def get_dur
      raise NotImplementedError, 'GetDUR magic action not implemented'
    end

    def get_encounter
      raise NotImplementedError, 'GetEncounter magic action not implemented'
    end

    def get_encounter_date
      raise NotImplementedError, 'GetEncounterDate magic action not implemented'
    end

    # GetEncounterList helper method.
    # @param [Object] userid
    # @param [Object] patientid
    # @param [String, nil] encounter_type encounter type to filter
    #   on. A value of `nil` filters nothing. Defaults to `nil`.
    # @param [Object] when_param
    # @param [Fixnum, nil] nostradamus how many days to look into the
    #   future. Defaults to `0`.
    # @param [Object] show_past_flag whether to show previous
    #   encounters. All truthy values aside from the string `"N"` are
    #   considered to be true (or `"Y"`) all other values are
    #   considered to be false (or `"N"`). Defaults to `true`.
    # @param [Object] billing_provider_user_name filter by user
    #   name. Defaults to `nil`.
    # @param [Object] show_all
    # @return [Array<Hash>] the filtered encounter list.
    def get_encounter_list(
        userid,
        patientid,
        encounter_type = nil,
        when_param = nil,
        nostradamus = 0,
        show_past_flag = true,
        billing_provider_user_name = nil,
        show_all = false)
      magic_parameters = {
        action: 'GetEncounterList',
        userid: userid,
        patientid: patientid,
        parameter1: encounter_type,
        parameter2: when_param,
        parameter3: nostradamus,
        parameter4: unity_boolean_parameter(show_past_flag),
        parameter5: billing_provider_user_name,
        # According to the developer guide this parameter is no longer
        # used.
        parameter6: show_all ? 'all' : nil
      }

      response = magic(magic_parameters)

      unless response.is_a?(Array)
        response = [ response ]
      end

      # Remove nil encounters
      response.delete_if do |value|
        value[:id] == '0' && value[:patientid] == '0'
      end
    end

    def get_hie_document
      raise NotImplementedError, 'GetHIEDocument magic action not implemented'
    end

    def get_last_patient
      raise NotImplementedError, 'GetLastPatient magic action not implemented'
    end

    def get_list_of_dictionaries
      raise NotImplementedError, 'GetListOfDictionaries magic action not implemented'
    end

    # @param [String] userid the Allscripts EHR username.
    #
    # @param [Numeric,String] patient_id the Allscripts internal patient ID.
    #
    # @param [Numeric,String] transaction_id the transaction ID of the
    #   RX in question. M prefix=medication, H prefix=historic
    #   medication.
    #
    # @return [Array] if {transaction_id} is either 0 or "0".
    #
    # @return [Hash] if {transaction_id} is not 0 or "0".
    def get_medication_by_trans_id(userid, patientid, transaction_id)
      magic_parameters = {
        action: 'GetMedicationByTransID',
        userid: userid,
        patientid: patientid,
        parameter1: transaction_id
      }
      result = magic(magic_parameters)

      if transaction_id == 0 || transaction_id == '0'
        # When transaction_id is 0 all medications should be
        # returned and the result should always be an array.
        if !result.is_a?(Array) && !result.empty?
          result = [ result ]
        elsif result.empty?
          result = []
        end
      end

      result
    end

    # @param [String] userid the Allscripts EHR username.
    #
    # @param [String] ddid the numeric drug identifier.
    #
    # @param [String] patientid the Allscripts internal patient ID.
    #
    # @return [Object]
    def get_medication_info(userid, ddid, patientid = nil)
      magic_parameters = {
        action: 'GetMedicationInfo',
        userid: userid,
        patientid: patientid,
        parameter1: ddid
      }
      magic(magic_parameters)
    end

    def get_order_history
      raise NotImplementedError, 'GetOrderHistory magic action not implemented'
    end

    def get_organization_id
      raise NotImplementedError, 'GetOrganizationID magic action not implemented'
    end

    def get_packages
      raise NotImplementedError, 'GetPackages magic action not implemented'
    end

    def get_patient(userid, patientid, includepix = nil)
      magic_parameters = {
        action: 'GetPatient',
        userid: userid,
        patientid: patientid,
        parameter1: includepix
      }
      magic(magic_parameters)
    end

    def get_patient_activity(userid, patientid)
      magic_parameters = {
        action: 'GetPatientActivity',
        userid: userid,
        patientid: patientid
      }
      magic(magic_parameters)
    end

    def get_patient_by_mrn(userid, mrn)
      magic_parameters = {
        action: 'GetPatientByMRN',
        userid: userid,
        parameter1: mrn
      }
      magic(magic_parameters)
    end

    def get_patient_cda
      raise NotImplementedError, 'GetPatientCDA magic action not implemented'
    end

    def get_patient_diagnosis(userid, patientid, encounter_date = nil, encounter_type = nil, encounter_date_range = nil, encounter_id = nil)
      magic_params = {
        action: 'GetPatientDiagnosis',
        userid: userid,
        patientid: patientid,
        parameter1: encounter_date,
        parameter2: encounter_type,
        parameter3: encounter_date_range,
        parameter4: encounter_id
      }

      results = magic(magic_params)

      if !results.is_a? Array
        if results.empty?
          results = []
        else
          results = [results]
        end
      end

      results
    end

    def get_patient_full
      raise NotImplementedError, 'GetPatientFull magic action not implemented'
    end

    def get_patient_ids
      raise NotImplementedError, 'GetPatientIDs magic action not implemented'
    end

    def get_patient_list
      raise NotImplementedError, 'GetPatientList magic action not implemented'
    end

    def get_patient_locations
      raise NotImplementedError, 'GetPatientLocations magic action not implemented'
    end

    def get_patient_pharmacies(patient_id, limit_to_favorites = false)
      response = magic(
        action: 'GetPatientPharmacies',
        patientid: patient_id,
        parameter1: unity_boolean_parameter(limit_to_favorites)
      )

      # When the patient has only ever used one pharmacy, Unity does
      # not return a collection of one. Rather, it returns the sole
      # pharmacy as a Hash.
      if response.is_a?(Array)
        response
      else
        [response]
      end
    end

    def get_patient_problems(patientid, show_by_encounter_flag = nil, assessed = nil, encounter_id = nil, medcin_id = nil)
      magic_parameters = {
        action: 'GetPatientProblems',
        patientid: patientid,
        parameter1: show_by_encounter_flag,
        parameter2: assessed,
        parameter3: encounter_id,
        parameter4: medcin_id
      }
      response = magic(magic_parameters)

      unless response.is_a?(Array)
        response = [ response ]
      end

      response
    end

    def get_patients_by_icd9(icd9, start = nil, end_param = nil)
      magic_parameters = {
        action: 'GetPatientsByICD9',
        parameter1: icd9,
        parameter2: start,
        parameter3: end_param
      }
      magic(magic_parameters)
    end

    def get_patient_sections
      raise NotImplementedError, 'GetPatientSections magic action not implemented'
    end

    def get_procedures
      raise NotImplementedError, 'GetProcedures magic action not implemented'
    end

    def get_provider(provider_id = nil, user_name = nil)
      if provider_id.nil? && user_name.nil?
        raise ArgumentError, 'provider_id or user_name must be given'
      end

      magic_parameters = {
        action: 'GetProvider',
        parameter1: provider_id,
        parameter2: user_name
      }
      magic(magic_parameters)
    end

    def get_providers(security_filter = nil, name_filter = nil)
      magic_parameters = {
        action: 'GetProviders',
        parameter1: security_filter,
        parameter2: name_filter
      }
      response = magic(magic_parameters)

      unless response.is_a?(Array)
        response = [ response ]
      end

      response
    end

    def get_ref_providers_by_specialty
      raise NotImplementedError, 'GetRefProvidersBySpecialty magic action not implemented'
    end

    def get_rounding_list_entries
      raise NotImplementedError, 'GetRoundingListEntries magic action not implemented'
    end

    def get_rounding_lists
      raise NotImplementedError, 'GetRoundingLists magic action not implemented'
    end

    def get_rx_favs
      raise NotImplementedError, 'GetRXFavs magic action not implemented'
    end

    def get_schedule
      raise NotImplementedError, 'GetSchedule magic action not implemented'
    end

    def get_server_info
      magic_parameters = {
        action: 'GetServerInfo'
      }
      magic(magic_parameters)
    end

    def get_sigs
      raise NotImplementedError, 'GetSigs magic action not implemented'
    end

    def get_task(userid, transaction_id)
      magic_parameters = {
        action: 'GetTask',
        userid: userid,
        parameter1: transaction_id
      }
      magic(magic_parameters)
    end

    # delegated is an undocumented parameter
    def get_task_list(userid = nil, since = nil, delegated = nil, task_types = nil, task_statuses = nil)
      magic_parameters = {
        action: 'GetTaskList',
        userid: userid,
        parameter1: since,
        parameter2: task_types,
        parameter3: task_statuses,
        parameter4: delegated,
        parameter5: 0
      }
      response = magic(magic_parameters)

      unless response.is_a?(Array)
        response = [ response ]
      end

      response
    end

    def get_user_authentication
      @client_driver.get_user_authentication
    end

    def get_user_id
      raise NotImplementedError, 'GetUserID magic action not implemented'
    end

    def get_user_security
      raise NotImplementedError, 'GetUserSecurity magic action not implemented'
    end

    def get_vaccine_manufacturers
      raise NotImplementedError, 'GetVaccineManufacturers magic action not implemented'
    end

    def get_vitals
      raise NotImplementedError, 'GetVitals magic action not implemented'
    end

    def last_logs
      magic_parameters = {
        action: 'LastLogs'
      }
      magic(magic_parameters)
    end

    def make_task
      raise NotImplementedError, 'MakeTask magic action not implemented'
    end

    def save_admin_task
      raise NotImplementedError, 'SaveAdminTask magic action not implemented'
    end

    def save_allergy
      raise NotImplementedError, 'SaveAllergy magic action not implemented'
    end

    def save_ced
      raise NotImplementedError, 'SaveCED magic action not implemented'
    end

    def save_charge
      raise NotImplementedError, 'SaveCharge magic action not implemented'
    end

    def save_chart_view_audit
      raise NotImplementedError, 'SaveChartViewAudit magic action not implemented'
    end

    def save_diagnosis
      raise NotImplementedError, 'SaveDiagnosis magic action not implemented'
    end

    def save_document_image
      raise NotImplementedError, 'SaveDocumentImage magic action not implemented'
    end

    def save_er_note
      raise NotImplementedError, 'SaveERNote magic action not implemented'
    end

    def save_hie_document
      raise NotImplementedError, 'SaveHIEDocument magic action not implemented'
    end

    def save_history
      raise NotImplementedError, 'SaveHistory magic action not implemented'
    end

    def save_immunization
      raise NotImplementedError, 'SaveImmunization magic action not implemented'
    end

    def save_note
      raise NotImplementedError, 'SaveNote magic action not implemented'
    end

    def save_patient
      raise NotImplementedError, 'SavePatient magic action not implemented'
    end

    def save_patient_location
      raise NotImplementedError, 'SavePatientLocation magic action not implemented'
    end

    def save_problem
      raise NotImplementedError, 'SaveProblem magic action not implemented'
    end

    def save_problems_data
      raise NotImplementedError, 'SaveProblemsData magic action not implemented'
    end

    def save_ref_provider
      raise NotImplementedError, 'SaveRefProvider magic action not implemented'
    end

    def save_result
      raise NotImplementedError, 'SaveResult magic action not implemented'
    end

    def save_rx(userid, patientid, rxxml)
      # Generate XML structure for rxxml
      builder = Nokogiri::XML::Builder.new do |xml|
        xml.saverx {
          xml.field('name' => 'transid', 'value' => rxxml[:transid]) unless rxxml[:transid]
          xml.field('name' => 'PharmID', 'value' => rxxml[:pharmid]) unless rxxml[:pharmid]
          xml.field('name' => 'DDI', 'value' => rxxml[:ddi]) unless rxxml[:ddi]
          xml.field('name' => 'GPPCCode', 'value' => rxxml[:gppccode]) unless rxxml[:gppccode]
          xml.field('name' => 'GPPCText', 'value' => rxxml[:gppctext]) unless rxxml[:gppctext]
          xml.field('name' => 'GPPCCustom', 'value' => rxxml[:gppccustom]) unless rxxml[:gppccustom]
          xml.field('name' => 'Sig', 'value' => rxxml[:sig]) unless rxxml[:sig]
          xml.field('name' => 'QuanPresc', 'value' => rxxml[:quanpresc]) unless rxxml[:quanpresc]
          xml.field('name' => 'Refills', 'value' => rxxml[:refills]) unless rxxml[:refills]
          xml.field('name' => 'DAW', 'value' => rxxml[:daw]) unless rxxml[:daw]
          xml.field('name' => 'DaysSupply', 'value' => rxxml[:dayssupply]) unless rxxml[:dayssupply]
          xml.field('name' => 'startdate', 'value' => utc_to_local(Date.parse(rxxml[:startdate].to_s))) unless rxxml[:startdate]
          xml.field('name' => 'historicalflag', 'value' => rxxml[:historicalflag]) unless rxxml[:historicalflag]
          xml.field('name' => 'rxaction', 'value' => rxxml[:rxaction]) unless rxxml[:rxaction]
          xml.field('name' => 'delivery', 'value' => rxxml[:delivery]) unless rxxml[:delivery]
          xml.field('name' => 'ignorepharmzero', 'value' => rxxml[:ignorepharmzero]) unless rxxml[:ignorepharmzero]
          xml.field('name' => 'orderedbyid', 'value' => rxxml[:orderedbyid]) unless rxxml[:orderedbyid]
          xml.field('name' => 'newqty', 'value' => rxxml[:newqty]) unless rxxml[:newqty]
          xml.field('name' => 'newrefills', 'value' => rxxml[:newrefills]) unless rxxml[:newrefills]
          xml.field('name' => 'comments', 'value' => rxxml[:comments]) unless rxxml[:comments]
          xml.field('name' => 'orderstatus', 'value' => rxxml[:order_status]) unless rxxml[:order_status]

          if rxxml[:problems]
            rxxml[:problems].each do |problem|
              xml.field('name' => 'Problem', 'value' => problem)
            end
          end
        }
      end

      magic_parameters = {
        action: 'SaveRX',
        userid: userid,
        patientid: patientid,
        parameter1: nokogiri_to_string(builder)
      }
      magic(magic_parameters)
    end

    def save_simple_encounter
      raise NotImplementedError, 'SaveSimpleEncounter magic action not implemented'
    end

    def save_simple_rx
      raise NotImplementedError, 'SaveSimpleRX magic action not implemented'
    end

    def save_specialist
      raise NotImplementedError, 'SaveSpecialist magic action not implemented'
    end

    def save_task(userid, patientid, task_type = nil, target_user = nil, work_object_id = nil, comments = nil)
      if task_type.nil? && target_user.nil? && work_object_id.nil? && comments.nil?
        raise ArgumentError, 'task_type, target_user, work_object_id, and comments can not all be nil'
      end

      magic_parameters = {
        action: 'SaveTask',
        userid: userid,
        patientid: patientid,
        parameter1: task_type,
        parameter2: target_user,
        parameter3: work_object_id,
        parameter4: comments
      }
      magic(magic_parameters)
    end

    def save_task_status(userid, transaction_id = nil, status = nil, delegate_id = nil, comment = nil, taskchanges = nil, patient_id = nil)
      if transaction_id.nil? && delegate_id.nil? && comment.nil?
        raise ArgumentError, 'transaction_id, delegate_id, and comment can not all be nil'
      end

      # Generate XML structure for rxxml
      builder = Nokogiri::XML::Builder.new do |xml|
        xml.taskchanges {
          xml.refills('value' => taskchanges[:refills]) unless taskchanges.nil? || taskchanges[:refills].nil?
          xml.days('value' => taskchanges[:days]) unless taskchanges.nil? || taskchanges[:days].nil?
          xml.qty('value' => taskchanges[:qty]) unless taskchanges.nil? || taskchanges[:qty].nil?
          xml.tasktype('value' => taskchanges[:tasktype]) unless taskchanges.nil? || taskchanges[:tasktype].nil?
          xml.delegated('value' => taskchanges[:delegated]) unless taskchanges.nil? || taskchanges[:delegated].nil?
          xml.taskstatus('value' => taskchanges[:taskstatus]) unless taskchanges.nil? || taskchanges[:taskstatus].nil?
          xml.removereason('value' => taskchanges[:removereason]) unless taskchanges.nil? || taskchanges[:removereason].nil?
          xml.denyreason('value' => taskchanges[:denyreason]) unless taskchanges.nil? || taskchanges[:denyreason].nil?
        }
      end

      new_status =
        if taskchanges.nil?
          ""
        else
          nokogiri_to_string(builder)
        end

      magic_parameters = {
        action: 'SaveTaskStatus',
        userid: userid,
        parameter1: transaction_id,
        parameter2: status,
        parameter3: delegate_id,
        parameter4: comment,
        parameter6: new_status
      }

      if patient_id
        magic_parameters.update(patientid: patient_id)
      end

      magic(magic_parameters)
    end

    def save_tiff
      raise NotImplementedError, 'SaveTiff magic action not implemented'
    end

    def save_unstructured_document
      raise NotImplementedError, 'SaveUnstructuredDocument magic action not implemented'
    end

    def save_v10_doc_signature
      raise NotImplementedError, 'SaveV10DocSignature magic action not implemented'
    end

    def save_v11_note
      raise NotImplementedError, 'SaveV11Note magic action not implemented'
    end

    def save_vitals
      raise NotImplementedError, 'SaveVitals magic action not implemented'
    end

    def save_vitals_data
      raise NotImplementedError, 'SaveVitalsData magic action not implemented'
    end

    def search_charge_codes
      raise NotImplementedError, 'SearchChargeCodes magic action not implemented'
    end

    def search_diagnosis_codes
      raise NotImplementedError, 'SearchDiagnosisCodes magic action not implemented'
    end

    def search_meds(userid, patientid, search = nil)
      magic_parameters = {
        action: 'SearchMeds',
        userid: userid,
        patientid: patientid,
        parameter1: search
      }

      response = magic(magic_parameters)

      unless response.is_a?(Array)
        response = [ response ]
      end

      response
    end

    def search_patients(search)
      magic_parameters = {
        action: 'SearchPatients',
        parameter1: search
      }
      magic(magic_parameters)
    end

    def search_patients_rxhub5
      raise NotImplementedError, 'SearchPatientsRXHub5 magic action not implemented'
    end

    def search_pharmacies(search)
      magic_parameters = {
        action: 'SearchPharmacies',
        parameter1: search
      }
      magic(magic_parameters)
    end

    def search_problem_codes
      raise NotImplementedError, 'SearchProblemCodes magic action not implemented'
    end

    def update_encounter
      raise NotImplementedError, 'UpdateEncounter magic action not implemented'
    end

    def update_order
      raise NotImplementedError, 'UpdateOrder magic action not implemented'
    end

    def update_referral_order_status
      raise NotImplementedError, 'UpdateReferralOrderStatus magic action not implemented'
    end

    private

    # Truthy values, with the exception to the string "N", will be
    # converted to the string "Y". Falsy values and the string "N"
    # will be converted to the string "N".
    def unity_boolean_parameter(native_value)
      if native_value && native_value != 'N'
        'Y'
      else
        'N'
      end
    end

    # Truthy values will converted to the string "Y". Falsy values
    # will be converted to nil.
    def unity_true_or_blank_parameter(native_value)
      native_value ? 'Y' : nil
    end

    def nokogiri_to_string(builder)
      builder.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML | Nokogiri::XML::Node::SaveOptions::NO_DECLARATION).strip
    end
  end
end