require 'grpc'
require 'uri'

require 'etcdv3/etcdrpc/rpc_services_pb'
require 'etcdv3/etcdrpc/v3lock_services_pb'
require 'etcdv3/auth'
require 'etcdv3/kv/requests'
require 'etcdv3/kv/transaction'
require 'etcdv3/kv'

require 'etcdv3/namespace/utilities'
require 'etcdv3/namespace/kv/requests'
require 'etcdv3/namespace/kv/transaction'
require 'etcdv3/namespace/lock'
require 'etcdv3/namespace/kv'
require 'etcdv3/namespace/watch'

require 'etcdv3/maintenance'
require 'etcdv3/lease'
require 'etcdv3/watch'
require 'etcdv3/lock'
require 'etcdv3/connection'
require 'etcdv3/connection_wrapper'

class Etcdv3
  extend Forwardable
  def_delegators :@conn, :user, :password, :token, :endpoints, :authenticate

  attr_reader :conn, :credentials, :options
  DEFAULT_TIMEOUT = 120

  def initialize(**options)
    @options = options
    @timeout = options[:command_timeout] || DEFAULT_TIMEOUT
    @namespace = options[:namespace]
    @conn = ConnectionWrapper.new(
      @timeout,
      *sanitized_endpoints,
      @namespace,
      @options.fetch(:allow_reconnect, true),
    )
    warn "WARNING: `url` is deprecated. Please use `endpoints` instead." if @options.key?(:url)
    authenticate(@options[:user], @options[:password]) if @options.key?(:user)
  end

  # Version of Etcd running on member
  def version
    @conn.handle(:maintenance, 'member_status').version
  end

  # Store size in bytes.
  def db_size
    @conn.handle(:maintenance, 'member_status').dbSize
  end

  # Cluster leader id
  def leader_id
    @conn.handle(:maintenance, 'member_status').leader
  end

  # List active alarms
  def alarm_list
    @conn.handle(:maintenance, 'alarms', [:get, leader_id])
  end

  # Disarm alarms on a specified member.
  def alarm_deactivate
    @conn.handle(:maintenance, 'alarms', [:deactivate, leader_id])
  end

  # Enables authentication.
  def auth_enable(timeout: nil)
    @conn.handle(:auth, 'auth_enable', [timeout: timeout])
    true
  end

  # Disables authentication.
  # This will clear any active auth / token data.
  def auth_disable(timeout: nil)
    @conn.handle(:auth, 'auth_disable', [timeout: timeout])
    @conn.clear_authentication
    true
  end

  # key                           - string
  # optional :range_end           - string
  # optional :limit               - integer
  # optional :revision            - integer
  # optional :sort_order          - symbol - [:none, :ascend, :descend]
  # optional :sort_target         - symbol - [:key, :version, :create, :mode, :value]
  # optional :serializable        - boolean
  # optional :keys_only           - boolean
  # optional :count_only          - boolean
  # optional :min_mod_revision    - integer
  # optional :max_mod_revision    - integer
  # optional :min_create_revision - integer
  # optional :max_create_revision - integer
  # optional :timeout             - integer
  def get(key, opts={})
    @conn.handle(:kv, 'get', [key, opts])
  end

  # Locks distributed lock with the given name. The lock will unlock automatically
  # when lease with the given ID expires. If this is not desirable, provide a non-expiring
  # lease ID as an argument.
  # name                          - string
  # lease_id                      - integer
  # optional :timeout             - integer
  def lock(name, lease_id, timeout: nil)
    @conn.handle(:lock, 'lock', [name, lease_id, {timeout: timeout}])
  end

  # Unlock distributed lock using the key previously obtained from lock.
  # key                           - string
  # optional :timeout             - integer
  def unlock(key, timeout: nil)
    @conn.handle(:lock, 'unlock', [key, {timeout: timeout}])
  end

  # Yield into the critical section while holding lock with the given
  # name. The lock will be unlocked even if the block throws.
  # name                          - string
  # lease_id                      - integer
  # optional :timeout             - integer
  def with_lock(name, lease_id, timeout: nil)
    key = lock(name, lease_id, timeout: timeout).key
    begin
      yield
    ensure
      unlock(key, timeout: timeout)
    end
  end

  # Inserts a new key.
  # key                           - string
  # value                         - string
  # optional :lease               - integer
  # optional :timeout             - integer
  def put(key, value, opts={})
    @conn.handle(:kv, 'put', [key, value, opts])
  end

  # Deletes a specified key
  # key                           - string
  # optional :range_end           - string
  # optional :timeout             - integer
  def del(key, opts={})
    @conn.handle(:kv, 'del', [key, opts])
  end

  # Grant a lease with a specified TTL
  def lease_grant(ttl, timeout: nil)
    @conn.handle(:lease, 'lease_grant', [ttl, timeout: timeout])
  end

  # Revokes lease and delete all attached keys
  def lease_revoke(id, timeout: nil)
    @conn.handle(:lease, 'lease_revoke', [id, timeout: timeout])
  end

  # Returns information regarding the current state of the lease
  def lease_ttl(id, timeout: nil)
    @conn.handle(:lease, 'lease_ttl', [id, timeout: timeout])
  end

  # Sends one lease keep-alive request
  def lease_keep_alive_once(id, timeout: nil)
    @conn.handle(:lease, 'lease_keep_alive_once', [id, timeout: timeout])
  end

  # List all roles.
  def role_list(timeout: nil)
    @conn.handle(:auth, 'role_list', [timeout: timeout])
  end

  # Add role with specified name.
  def role_add(name, timeout: nil)
    @conn.handle(:auth, 'role_add', [name, timeout: timeout])
  end

  # Fetches a specified role.
  def role_get(name, timeout: nil)
    @conn.handle(:auth, 'role_get', [name, timeout: timeout])
  end

  # Delete role.
  def role_delete(name, timeout: nil)
    @conn.handle(:auth, 'role_delete', [name, timeout: timeout])
  end

  # Grants a new permission to an existing role.
  def role_grant_permission(name, permission, key, range_end: '', timeout: nil)
    @conn.handle(:auth, 'role_grant_permission', [name, permission, key, range_end, timeout: timeout])
  end

  def role_revoke_permission(name, permission, key, range_end: '', timeout: nil)
    @conn.handle(:auth, 'role_revoke_permission', [name, permission, key, range_end, timeout: timeout])
  end

  # Fetch specified user
  def user_get(user, timeout: nil)
    @conn.handle(:auth, 'user_get', [user, timeout: timeout])
  end

  # Creates new user.
  def user_add(user, password, timeout: nil)
    @conn.handle(:auth, 'user_add', [user, password, timeout: timeout])
  end

  # Delete specified user.
  def user_delete(user, timeout: nil)
    @conn.handle(:auth, 'user_delete', [user, timeout: timeout])
  end

  # Changes the specified users password.
  def user_change_password(user, new_password, timeout: nil)
    @conn.handle(:auth, 'user_change_password', [user, new_password, timeout: timeout])
  end

  # List all users.
  def user_list(timeout: nil)
    @conn.handle(:auth, 'user_list', [timeout: timeout])
  end

  # Grants role to an existing user.
  def user_grant_role(user, role, timeout: nil)
    @conn.handle(:auth, 'user_grant_role', [user, role, timeout: timeout])
  end

  # Revokes role from a specified user.
  def user_revoke_role(user, role, timeout: nil)
    @conn.handle(:auth, 'user_revoke_role', [user, role, timeout: timeout])
  end

  # Watches for changes on a specified key range.
  def watch(key, range_end: nil, start_revision: nil, timeout: nil, &block)
    @conn.handle(:watch, 'watch', [key, range_end, start_revision, block, timeout: timeout])
  end

  def transaction(timeout: nil, &block)
    @conn.handle(:kv, 'transaction', [block, timeout: timeout])
  end

  private

  def sanitized_endpoints
    (@options[:endpoints] || @options[:url]).split(',').map(&:strip)
  end
end
