module ActiveGraph
  module Core
    class Label
      attr_reader :name
      delegate :version?, to: ActiveGraph::Base

      def initialize(name)
        @name = name
      end

      def create_index(*properties, **options)
        validate_index_options!(options)
        if version?('>=4.4')
          properties = properties.map { |p| "l.#{p}" }
          fragment = "FOR (l:`#{@name}`) ON"
        else
          fragment = "ON :`#{@name}`"
        end
        schema_query("CREATE INDEX #{fragment} (#{properties.join('.')})")
      end

      def drop_index(property, options = {})
        validate_index_options!(options)
        if version?('<4.3')
          schema_query("DROP INDEX ON :`#{@name}`(#{property})")
        else
          schema_query("SHOW INDEXES YIELD * WHERE labelsOrTypes = $labels AND properties = $properties",
                       labels: [@name], properties: [property]).each do |record|
            schema_query("DROP INDEX #{record[:name]}")
          end
        end
      end

      # Creates a neo4j constraint on a property
      # See http://docs.neo4j.org/chunked/stable/query-constraints.html
      # @example
      #   label = ActiveGraph::Label.create(:person)
      #   label.create_constraint(:name, {type: :unique})
      #
      def create_constraint(property, constraints)
        cypher = case constraints[:type]
                 when :unique, :uniqueness
                   _for, _require = version?('>=4.4') ? %w[FOR REQUIRE] : %w[ON ASSERT]
                   "CREATE CONSTRAINT #{_for} (n:`#{name}`) #{_require} n.`#{property}` IS UNIQUE"
                 else
                   fail "Not supported constraint #{constraints.inspect} for property #{property} (expected :type => :unique)"
                 end
        schema_query(cypher)
      end

      def create_uniqueness_constraint(property, options = {})
        create_constraint(property, options.merge(type: :unique))
      end

      # Drops a neo4j constraint on a property
      # See http://docs.neo4j.org/chunked/stable/query-constraints.html
      # @example
      #   label = ActiveGraph::Label.create(:person)
      #   label.create_constraint(:name, {type: :unique})
      #   label.drop_constraint(:name, {type: :unique})
      #
      def drop_constraint(property, constraint)
        return drop_constraint42(property, constraint) if version?('<4.3')
        type = case constraint[:type]
               when :unique, :uniqueness
                 'UNIQUENESS'
               when :exists
                 'NODE_PROPERTY_EXISTENCE'
               else
                 fail "Not supported constraint #{constraint.inspect}"
               end
        schema_query(
          'SHOW CONSTRAINTS YIELD * WHERE type = $type AND labelsOrTypes = $labels AND properties = $properties',
          type: type, labels: [name], properties: [property]).first[:name].tap do |constraint_name|
          schema_query("DROP CONSTRAINT #{constraint_name}")
        end
      end

      def drop_uniqueness_constraint(property, options = {})
        drop_constraint(property, options.merge(type: :unique))
      end

      def indexes
        self.class.indexes.select do |definition|
          definition[:label] == @name.to_sym
        end
      end

      def drop_indexes
        self.class.drop_indexes
      end

      def index?(property)
        indexes.any? { |definition| definition[:properties] == [property.to_sym] }
      end

      def constraints(_options = {})
        ActiveGraph::Base.constraints.select do |definition|
          definition[:label] == @name.to_sym
        end
      end

      def uniqueness_constraints(_options = {})
        constraints.select do |definition|
          definition[:type] == :uniqueness
        end
      end

      def constraint?(property)
        constraints.any? { |definition| definition[:properties] == [property.to_sym] }
      end

      def uniqueness_constraint?(property)
        uniqueness_constraints.include?([property])
      end

      private

      class << self
        def indexes
          ActiveGraph::Base.indexes
        end

        def drop_indexes
          indexes.each do |definition|
            begin
              ActiveGraph::Base.query(
                if definition[:name]
                  "DROP INDEX #{definition[:name]}"
                else
                  "DROP INDEX ON :`#{definition[:label]}`(#{definition[:properties][0]})"
                end)
            rescue Neo4j::Driver::Exceptions::DatabaseException
              # This will error on each constraint. Ignore and continue.
              next
            end
          end
        end

        def drop_constraints
          result = ActiveGraph::Base.read_transaction do |tx|
            tx.run(ActiveGraph::Base.version?('<4.3') ? 'CALL db.constraints' : 'SHOW CONSTRAINTS YIELD *').to_a
          end
          ActiveGraph::Base.write_transaction do |tx|
            result.each do |record|
              tx.run("DROP #{record.keys.include?(:name) ? "CONSTRAINT #{record[:name]}" : record[:description]}")
            end
          end
        end
      end

      def schema_query(cypher, **params)
        ActiveGraph::Base.query(cypher, params)
      end

      def validate_index_options!(options)
        return unless options[:type] && options[:type] != :exact
        fail "Type #{options[:type]} is not supported"
      end

      def drop_constraint42(property, constraint)
        cypher = case constraint[:type]
                 when :unique, :uniqueness
                   "n.`#{property}` IS UNIQUE"
                 when :exists
                   "exists(n.`#{property}`)"
                 else
                   fail "Not supported constraint #{constraint.inspect}"
                 end
        schema_query("DROP CONSTRAINT ON (n:`#{name}`) ASSERT #{cypher}")
      end
    end
  end
end
