# frozen_string_literal: true

module Connections
  class CursorConnection < GraphQL::Pagination::Connection
    def initialize(items, deprecated_order: nil, **kwargs)
      super(items, **kwargs)
      @deprecated_order = deprecated_order
    end

    def nodes
      load_nodes
    end

    def has_previous_page
      load_nodes
      @has_previous_page
    end

    def has_next_page
      load_nodes
      @has_next_page
    end

    def cursor_for(item)
      cursor_from_column(item, order_column)
    end

    private

    # [d1, d2, d3, d4, d5, d6]
    #
    # first: 2
    # -> d1, d2
    # first: 2, after: d2
    # -> d3, d4
    # first: 2, before: d3
    # -> d1, d2
    #
    # last: 2
    # -> d5, d6
    # last: 2, before: d5
    # -> d3, d4
    # last: 2, after: d4
    # -> d5, d6
    #
    # si after ou before present, last ou first donne juste limit
    def limit_and_inverted(first: nil, last: nil, after: nil, before: nil)
      limit = [first, last, max_page_size].compact.min + 1
      inverted = last.present? || before.present?

      [limit, inverted]
    end

    def previous_page?(after, result_size, limit, inverted)
      after.present? || (result_size == limit && inverted)
    end

    def next_page?(before, result_size, limit, inverted)
      before.present? || (result_size == limit && !inverted)
    end

    def load_nodes
      @nodes ||= begin
        ensure_valid_params
        limit, inverted = limit_and_inverted(first:, last:, after:, before:)

        return load_nodes_deprecated_order(limit, inverted) if @deprecated_order == :desc

        expected_size = limit - 1

        nodes = resolve_nodes(limit:, before:, after:, inverted:)

        result_size = nodes.size
        @has_previous_page = previous_page?(after, result_size, limit, inverted)
        @has_next_page = next_page?(before, result_size, limit, inverted)

        trimmed_nodes = nodes.first(expected_size)
        trimmed_nodes.reverse! if inverted
        trimmed_nodes
      end
    end

    def load_nodes_deprecated_order(limit, inverted)
      payload = {
        message: "CursorConnection: using deprecated order [#{Current.user.email}]",
        user_id: Current.user.id
      }
      logger = Lograge.logger || Rails.logger
      logger.info payload.to_json

      expected_size = limit - 1

      if before.nil?
        inverted = !inverted
      end

      nodes = resolve_nodes(limit:, before: after, after: before, inverted:)

      result_size = nodes.size
      @has_next_page = previous_page?(before, result_size, limit, inverted)
      @has_previous_page = next_page?(after, result_size, limit, inverted)

      nodes.first(expected_size)
    end

    def ensure_valid_params
      if first.present? && last.present?
        raise GraphQL::ExecutionError.new('Arguments "first" and "last" are exclusive', extensions: { code: :bad_request })
      end

      if before.present? && after.present?
        raise GraphQL::ExecutionError.new('Arguments "before" and "after" are exclusive', extensions: { code: :bad_request })
      end

      if first.present? && first < 0
        raise GraphQL::ExecutionError.new('Argument "first" must be a non-negative integer', extensions: { code: :bad_request })
      end

      if last.present? && last < 0
        raise GraphQL::ExecutionError.new('Argument "last" must be a non-negative integer', extensions: { code: :bad_request })
      end

      if last.present? && @deprecated_order == :desc
        raise GraphQL::ExecutionError.new('Argument "last" is not supported with order "desc"', extensions: { code: :bad_request })
      end
    end

    def timestamp_and_id_from_cursor(cursor)
      timestamp, id = decode(cursor).split(';')
      [Time.zone.parse(timestamp), id.to_i]
    end

    def cursor_from_column(item, column)
      encode([item.read_attribute(column).utc.strftime("%Y-%m-%dT%H:%M:%S.%NZ"), item.id].join(';'))
    end

    def order_column
      :updated_at
    end

    def order_table
      raise StandardError, 'Not implemented'
    end

    def resolve_nodes(before:, after:, limit:, inverted:)
      order = inverted ? :desc : :asc
      nodes = items.order(order_column => order, id: order)
      nodes = nodes.limit(limit)

      if before.present?
        timestamp, id = timestamp_and_id_from_cursor(before)
        nodes.where("(#{order_table}.#{order_column}, #{order_table}.id) < (?, ?)", timestamp, id)
      elsif after.present?
        timestamp, id = timestamp_and_id_from_cursor(after)
        nodes.where("(#{order_table}.#{order_column}, #{order_table}.id) > (?, ?)", timestamp, id)
      else
        nodes
      end
    end
  end
end
