require_relative 'date_search_validator'
require_relative 'fhir_resource_navigation'
require_relative 'search_test_properties'

module CarinForBlueButtonTestKit
  module CarinSearchTest
    extend Forwardable
    include DateSearchValidation
    include FHIRResourceNavigation

    def_delegators 'self.class', :metadata, :properties
    def_delegators 'properties',
                   :resource_type,
                   :search_param_names,
                   :saves_delayed_references?,
                   :first_search?,
                   :fixed_value_search?,
                   :possible_status_search?,
                   :test_medication_inclusion?,
                   :test_post_search?,
                   :token_search_params,
                   :test_reference_variants?,
                   :params_with_comparators,
                   :multiple_or_search_params,
                   :include_parameters

    def all_scratch_resources
      scratch_resources[:all] ||= []
    end

    def run_search_test(param_value = nil)
      search_params = build_search_params(param_value)
      returned_resources = execute_search(search_params)

      all_scratch_resources.concat(returned_resources).uniq! if first_search?
      save_delayed_references(returned_resources) if saves_delayed_references?
      perform_response_validation(returned_resources, search_params)
    end

    def execute_search(search_params)
      if patient_id_param?(search_param_names[0])
        patient_id_list.inject([]) do |resources, id|
          search = { search_param_names[0] => id }
          perform_search_and_validate(search)
          resources.concat(extract_relevant_resources)
        end
      else
        perform_search_and_validate(search_params)
        extract_relevant_resources
      end
    end

    def perform_search_and_validate(search_params)
      fhir_search(resource_type, params: search_params)

      assert_response_status(200)
      assert_resource_type(:bundle)
    end

    def extract_relevant_resources
      extract_resources_from_bundle(bundle: resource, response:).select do |item|
        item.resourceType == resource_type
      end
    end

    def build_search_params(param_value = nil)
      search_params = {}
      param_name = search_param_names[0]

      if patient_id_param?(param_name)
        search_params[param_name] = patient_id_list.join(',')
      else
        param_value = find_param_value(param_value, param_name) if param_value.blank?
        skip_if param_value.blank?, no_resources_skip_message
        search_params[param_name] = param_value
      end

      search_params
    end

    def find_param_value(param_value, param_name)
      all_scratch_resources.each do |resource|
        break if param_value.present?

        param_value = search_param_value(param_name, resource)
      end

      if param_value.nil? && param_name == '_id' && resource_type != 'ExplanationOfBenefit'
        resources = readable_resources(scratch.dig(:references, resource_type))
        param_value = resource_id(resources.first)
      end

      param_value
    end

    def search_param_value(name, resource, include_system = false)
      paths = determine_paths(name)
      paths.each do |path|
        element = find_a_value_at(resource, path) { |el| element_has_valid_value?(el, include_system) }
        search_value = process_element(name, element, include_system, resource)
        return search_value.gsub(',', '\\,') if search_value.present?
      end
      nil
    end

    # Handles the processing logic for each resource element type.
    def process_element(name, element, include_system, resource)
      case element
      when FHIR::Period
        process_period_element(element)
      when FHIR::Reference
        element.reference
      when FHIR::CodeableConcept
        process_codeable_concept_element(element, include_system)
      when FHIR::Identifier
        include_system ? "#{element.system}|#{element.value}" : element.value
      when FHIR::Coding
        include_system ? "#{element.system}|#{element.code}" : element.code
      when FHIR::HumanName
        element.family || element.given&.first || element.text
      when FHIR::Address
        element.text || element.city || element.state || element.postalCode || element.country
      else
        process_other_element_types(name, element, resource)
      end
    end

    # Handles logic for processing FHIR::Period elements.
    def process_period_element(element)
      if element.start.present?
        "gt#{(DateTime.xmlschema(element.start) - 1).xmlschema}"
      else
        end_datetime = get_fhir_datetime_range(element.end)[:end]
        "lt#{(end_datetime + 1).xmlschema}"
      end
    end

    # Handles logic for processing FHIR::CodeableConcept elements.
    def process_codeable_concept_element(element, include_system)
      if include_system
        coding = find_a_value_at(element, 'coding') { |c| c.code.present? && c.system.present? }
        "#{coding.system}|#{coding.code}"
      else
        find_a_value_at(element, 'coding.code')
      end
    end

    #  Handles the remaining types, including date types
    def process_other_element_types(name, element, resource)
      type = metadata.search_definitions[name.to_sym][:type]
      if type == 'date' && params_with_comparators&.include?(name)
        process_date_element(name, element, resource)
      else
        element
      end
    end

    def process_date_element(element)
      if /^\d{4}(-\d{2})?$/.match?(element) || # YYYY or YYYY-MM
         (/^\d{4}-\d{2}-\d{2}$/.match?(element) && resource_type != 'Goal') # YYYY-MM-DD AND Resource is NOT Goal
        "gt#{(DateTime.xmlschema(element) - 1).xmlschema}"
      else
        element
      end
    end

    def readable_resources(resources)
      return [] if resources.nil?

      resources
        .select { |resource| resource.is_a?(resource_class) || resource.is_a?(FHIR::Reference) }
        .select { |resource| (resource.is_a?(FHIR::Reference) ? resource.reference.split('/').last : resource.id).present? }
        .compact
        .uniq { |resource| resource.is_a?(FHIR::Reference) ? resource.reference.split('/').last : resource.id }
    end

    def resource_id(resource)
      return if resource.blank?

      resource.is_a?(FHIR::Reference) ? resource.reference.split('/').last : resource.id
    end

    def perform_response_validation(returned_resources, search_params)
      skip_if returned_resources.blank?, no_resources_skip_message

      returned_resources.each do |resource|
        check_resource_against_params(resource, search_params)
      end
    end

    def check_resource_against_params(resource, search_params)
      search_params.each do |name, param_value|
        paths = determine_paths(name)
        type = determine_type(name)
        assert check_paths_for_match(resource, paths, type, param_value),
               'Returned resource did not match the search parameter'
      end
    end

    # Determines the path based on the param name
    def determine_paths(name)
      case name
      when '_id'
        ['id']
      when '_lastUpdated'
        ['meta.lastUpdated']
      else
        search_param_paths(name)
      end
    end

    # Determines the param type based on the param name
    def determine_type(name)
      case name
      when '_id'
        'http://hl7.org/fhirpath/System.String'
      when '_lastUpdated'
        'date'
      else
        metadata.search_definitions[name.to_sym][:type]
      end
    end

    # Checks if any of the paths have a matching value
    def check_paths_for_match(resource, paths, type, param_value)
      paths.any? do |path|
        values_found = extract_values(resource, path, type)
        match_found?(values_found, type, param_value)
      end
    end

    # Extracts values from the resource based on the path and type
    def extract_values(resource, path, type)
      resolve_path(resource, path).map do |value|
        type == 'Reference' ? value.try(:reference) : value
      end
    end

    # Determines if a match is found based on the type and value
    def match_found?(values_found, type, param_value)
      case type
      when 'Reference'
        values_found.any? { |val| param_value.split(',').any? { |item| val.include?(item) } }
      when 'CodeableConcept'
        codings = values_found.flat_map { |val| val.coding || nil }.compact
        if param_value.include? '|'
          system, code = param_value.split('|', 2)
          codings.any? { |coding| coding.system == system && coding.code&.casecmp?(code) }
        else
          codings.any? { |coding| coding.code&.casecmp?(param_value) }
        end
      when 'Identifier'
        if param_value.include? '|'
          values_found.any? do |identifier|
            "#{identifier.system}|#{identifier.value}" == param_value
          end
        else
          values_found.any? { |identifier| identifier.value == param_value }
        end
      when 'Period', 'date', 'instant', 'dateTime'
        values_found.any? { |date| validate_date_search(param_value, date) }
      when 'http://hl7.org/fhirpath/System.String'
        values_found.any? { |str| param_value.split(',').include?(str) }
      else
        false
      end
    end

    def extract_resources_from_bundle(
      bundle: nil,
      response: nil,
      reply_handler: nil,
      max_pages: 20
    )
      page_count = 1
      resources = []

      until bundle.nil? || page_count == max_pages
        resources += bundle&.entry&.map { |entry| entry&.resource }
        next_bundle_link = bundle&.link&.find { |link| link.relation == 'next' }&.url
        reply_handler&.call(response)

        break if next_bundle_link.blank?

        page_count += 1
      end

      resources
    end

    def element_has_valid_value?(element, include_system)
      case element
      when FHIR::Reference
        element.reference.present?
      when FHIR::CodeableConcept
        if include_system
          coding =
            find_a_value_at(element, 'coding') { |koding| koding.code.present? && koding.system.present? }
          coding.present?
        else
          find_a_value_at(element, 'coding.code').present?
        end
      when FHIR::Identifier
        include_system ? element.value.present? && element.system.present? : element.value.present?
      when FHIR::Coding
        include_system ? element.code.present? && element.system.present? : element.code.present?
      when FHIR::HumanName
        (element.family || element.given&.first || element.text).present?
      when FHIR::Address
        (element.text || element.city || element.state || element.postalCode || element.country).present?
      else
        true
      end
    end

    def search_param_paths(name)
      paths = metadata.search_definitions[name.to_sym][:paths]
      paths[0] = 'local_class' if paths.first == 'class'

      paths
    end

    def resource_class
      FHIR.const_get(resource_type)
    end

    def no_resources_skip_message(resource_type = self.resource_type)
      msg = "No #{resource_type} resources appear to be available"

      "#{msg}. Please use patients with more information"
    end

    def run_include_search(param_value)
      search_params = initialize_include_search_params(param_value)
      perform_search_and_validate(search_params)
      returned_resources = extract_resources_from_bundle(bundle: resource, response:)
      process_resources(returned_resources, param_value)
    end

    # Initializes seach param for include search
    def initialize_include_search_params(param_value)
      resource_id = all_scratch_resources.first&.id
      { _id: resource_id, '_include': param_value }
    end

    #  Processes the base resources and included resources for include searches
    def process_resources(returned_resources, param_value)
      base_resources = filter_base_resources(returned_resources)
      all_included_resource_types = extract_included_resource_types
      included_refs = included_refs(returned_resources, all_included_resource_types)

      skip_if base_resources.blank?, no_resources_skip_message

      base_resources.each do |resource|
        process_each_base_resource(resource, param_value, returned_resources, included_refs)
      end
    end

    def filter_base_resources(returned_resources)
      returned_resources.select { |item| item.resourceType == resource_type }
    end

    def extract_included_resource_types
      include_parameters.map { |param| param[:target] }.flatten.uniq
    end

    # Handles the processing for each base resource of an include search

    def process_each_base_resource(resource, param_value, returned_resources, included_refs)
      match_found, base_resource_matches = check_for_include_match(resource, param_value, returned_resources)
      assert match_found, 'Returned resource did not match the search parameter'
      validate_included_resources(base_resource_matches, included_refs)
    end

    def check_for_include_match(resource, param_value, returned_resources)
      if param_value != 'ExplanationOfBenefit:*'
        check_normal_include(resource, param_value, returned_resources)
      else
        check_explanation_of_benefit_include(resource, param_value, returned_resources)
      end
    end

    # Processes the base resource for the normal include case.
    def check_normal_include(resource, _param_value, returned_resources)
      base_resource_matches = []
      match_found = include_parameters.any? do |include_param|
        values_found = resolve_path(resource, include_param[:path])
        base_resource_matches.concat(matched_base_resources(resource, include_param[:target], returned_resources,
                                                            values_found))
        values_found.length.positive?
      end
      [match_found, base_resource_matches]
    end

    # Processes the base resource for the 'ExplanationOfBenefit:*' case.
    def check_explanation_of_benefit_include(resource, _param_value, returned_resources)
      values_found = []
      base_resource_matches = []

      include_parameters.each do |include_param|
        paths_found = resolve_path(resource, include_param[:path])
        values_found.concat(paths_found)
        base_resource_matches.concat(matched_base_resources(resource, include_param[:target], returned_resources,
                                                            values_found))
      end

      match_found = (values_found.length >= 5)
      [match_found, base_resource_matches]
    end

    # Validates that the included resources match the search criteria.
    def validate_included_resources(base_resource_matches, included_refs)
      not_matched_included_resources = included_refs.select do |resource_reference|
        base_resource_matches.none? do |base_resource_reference|
          reference_match?(base_resource_reference.reference, resource_reference)
        end
      end
      not_matched_included_resources_string = not_matched_included_resources.join(',')
      assert not_matched_included_resources.empty?,
             "No #{resource_type} references #{not_matched_included_resources_string} in the search result."
    end

    def matched_base_resources(_resource, referenced_resource_types, returned_resources_all, values_found)
      included_refs = included_refs(returned_resources_all, referenced_resource_types)

      values_found.select do |base_resource_references|
        included_refs.any? do |referenced_resource|
          reference_match?(base_resource_references.reference, referenced_resource)
        end
      end
    end

    def included_resources(returned_resources, referenced_resource_types)
      returned_resources.select { |item| referenced_resource_types.include?(item.resourceType) }
    end

    def included_refs(returned_resources, referenced_resource_types)
      included_resources(returned_resources, referenced_resource_types)
        .map { |resource| "#{resource.resourceType}/#{resource.id}" }
    end

    def reference_match?(reference, local_reference)
      regex_pattern = %r{^(#{Regexp.escape(local_reference)}|\S+/#{Regexp.escape(local_reference)}(?:[/|]\S+)*)$}
      reference.match?(regex_pattern)
    end

    def patient_id_param?(name)
      (name == '_id' && resource_type == 'Patient') || name == 'patient'
    end

    def patient_id_list
      return [nil] unless respond_to? :patient_ids

      patient_ids.split(',').map(&:strip)
    end

    def references_to_save
      metadata.delayed_references
    end

    def save_resource_reference(resource_type, reference)
      scratch[:references] ||= {}
      scratch[:references][resource_type] ||= Set.new
      scratch[:references][resource_type] << reference
    end

    def save_delayed_references(resources)
      resources.each do |resource|
        references_to_save.each do |reference_to_save|
          resolve_path(resource, reference_to_save[:path])
            .select { |reference| reference.is_a?(FHIR::Reference) && !reference.contained? }
            .each do |reference|
              resource_type = reference.resource_class.name.demodulize
              need_to_save = reference_to_save[:resources].include?(resource_type)
              next unless need_to_save

              save_resource_reference(resource_type, reference)
            end
        end
      end
    end
  end
end