require 'neography'
require 'neoid/version'
require 'neoid/config'
require 'neoid/model_config'
require 'neoid/model_additions'
require 'neoid/search_session'
require 'neoid/node'
require 'neoid/relationship'
require 'neoid/batch'
require 'neoid/database_cleaner'
require 'neoid/railtie' if defined?(Rails)

module Neoid
  DEFAULT_FULLTEXT_SEARCH_INDEX_NAME = :neoid_default_search_index
  NODE_AUTO_INDEX_NAME = 'node_auto_index'
  RELATIONSHIP_AUTO_INDEX_NAME = 'relationship_auto_index'
  UNIQUE_ID_KEY = 'neoid_unique_id'

  class << self
    attr_accessor :db
    attr_accessor :logger
    attr_accessor :ref_node
    attr_accessor :env_loaded
    attr_reader :config

    def node_models
      @node_models ||= []
    end

    def relationship_models
      @relationship_models ||= []
    end

    def config
      @config ||= begin
        c = Neoid::Config.new

        # default
        c.enable_subrefs = true
        c.enable_per_model_indexes = false

        c
      end
    end

    def configure
      yield config
    end

    def initialize_all
      @env_loaded = true
      logger.info 'Neoid initialize_all'
      initialize_relationships
      initialize_server
    end

    def initialize_server
      initialize_auto_index
      initialize_subrefs
      initialize_per_model_indexes
      # begin
      #   @initialized_server = true
      #   initialize_auto_index
      #   initialize_subrefs
      #   initialize_per_model_indexes
      # rescue Exception => e
      #   @initialized_server = false
      #   logger.error "Failed to initialize neoid: #{e.message}"
      # end
    end

    def db
      raise 'Must set Neoid.db with a Neography::Rest instance' unless @db
      # initialize_server unless @initialized_server
      @db
    end

    def batch(options={}, &block)
      Neoid::Batch.new(options, &block).run
    end

    def logger
      @logger ||= Logger.new(ENV['NEOID_LOG'] ? ENV['NEOID_LOG_FILE'] || $stdout : '/dev/null')
    end

    def ref_node
      @ref_node ||= Neography::Node.load(Neoid.db.get_root['self'])
    end

    def reset_cached_variables
      initialize_subrefs
    end

    def clean_db(confirm)
      puts 'must call with confirm: Neoid.clean_db(:yes_i_am_sure)' and return unless confirm == :yes_i_am_sure
      Neoid::NeoDatabaseCleaner.clean_db
    end

    def enabled=(flag)
      Thread.current[:neoid_enabled] = flag
    end

    def enabled
      flag = Thread.current[:neoid_enabled]
      # flag should be set by the middleware. in case it wasn't (non-rails app or console), default it to true
      flag.nil? ? true : flag
    end
    alias enabled? enabled

    def use(flag=true)
      old, self.enabled = enabled?, flag
      yield if block_given?
    ensure
      self.enabled = old
    end

    def execute_script_or_add_to_batch(gremlin_query, script_vars)
      if Neoid::Batch.current_batch
        # returns a SingleResultPromiseProxy!
        Neoid::Batch.current_batch << [:execute_script, gremlin_query, script_vars]
      else
        value = Neoid.db.execute_script(gremlin_query, script_vars)

        value = yield(value) if block_given?

        Neoid::BatchPromiseProxy.new(value)
      end
    end

    # create a fulltext index if not exists
    def ensure_default_fulltext_search_index
      Neoid.db.create_node_index(DEFAULT_FULLTEXT_SEARCH_INDEX_NAME, 'fulltext', 'lucene') unless (indexes = Neoid.db.list_node_indexes) && indexes[DEFAULT_FULLTEXT_SEARCH_INDEX_NAME]
    end

    def search(types, term, options = {})
      options = options.reverse_merge(limit: 15, match_type: 'AND')

      types = [*types]

      query = []

      types.each do |type|
        query_for_type = []

        query_for_type << "ar_type:#{type.name}"

        case term
        when String
          search_in_fields = type.neoid_config.search_options.fulltext_fields.keys
          next if search_in_fields.empty?
          query_for_type << search_in_fields.map{ |field| generate_field_query(field, term, true, options[:match_type]) }.join(' OR ')
        when Hash
          term.each do |field, value|
            query_for_type << generate_field_query(field, value, false)
          end
        end

        query << "(#{query_for_type.join(') AND (')})"
      end

      query = "(#{query.join(') OR (')})"

      logger.info "Neoid query #{query}"

      gremlin_query = <<-GREMLIN
        #{options[:before_query]}

        idx = g.getRawGraph().index().forNodes('#{DEFAULT_FULLTEXT_SEARCH_INDEX_NAME}')
        hits = idx.query('#{sanitize_query_for_gremlin(query)}')

        hits = #{options[:limit] ? "hits.take(#{options[:limit]})" : 'hits'}

        #{options[:after_query]}
      GREMLIN

      logger.info "[NEOID] search:\n#{gremlin_query}"

      results = Neoid.db.execute_script(gremlin_query)

      SearchSession.new(results, *types)
    end

    private

    def sanitize_term(term)
      # TODO - case sensitive?
      term.downcase
    end

    def sanitize_query_for_gremlin(query)
      # TODO - case sensitive?
      query.gsub("'", "\\\\'")
    end

    def generate_field_query(field, term, fulltext = false, match_type = 'AND')
      term = term.to_s if term
      return '' if term.nil? || term.empty?

      fulltext = fulltext ? '_fulltext' : nil
      valid_match_types = %w( AND OR )
      match_type = valid_match_types.delete(match_type)
      raise "Invalid match_type option. Valid values are #{valid_match_types.join(',')}" unless match_type

      '(' + term.split(/\s+/).reject(&:empty?).map{ |t| "#{field}#{fulltext}:#{sanitize_term(t)}" }.join(" #{match_type} ") + ')'
    end

    def initialize_relationships
      logger.info 'Neoid initialize_relationships'
      relationship_models.each do |rel_model|
        Relationship.initialize_relationship(rel_model)
      end
    end

    def initialize_auto_index
      logger.info 'Neoid initialize_auto_index'
      Neoid.db.set_node_auto_index_status(true)
      Neoid.db.add_node_auto_index_property(UNIQUE_ID_KEY)

      Neoid.db.set_relationship_auto_index_status(true)
      Neoid.db.add_relationship_auto_index_property(UNIQUE_ID_KEY)
    end

    def initialize_subrefs
      return unless config.enable_subrefs

      node_models.each do |klass|
        klass.reset_neo_subref_node
      end

      logger.info 'Neoid initialize_subrefs'
      batch do
        node_models.each(&:neo_subref_node)
      end.then do |results|
        node_models.zip(results).each do |klass, subref|
          klass.neo_subref_node = subref
        end
      end
    end

    def initialize_per_model_indexes
      return unless config.enable_per_model_indexes

      logger.info 'Neoid initialize_subrefs'
      batch do
        node_models.each(&:neo_model_index)
      end
    end
  end
end
