require_dependency "service/proxy"
require_dependency "service/response"
require_dependency "service/response_logger"
require_dependency "paginated_array"
require_dependency "net_http_socks5_compat"
require_dependency "twitch_s2s2"

module Service
  class S2S
    include Singleton
    attr_accessor :client

    def initialize
      if Settings.s2s_addr
        if Settings.s2s_addr.start_with?('http:')
          @client = Twitch::S2SSidecar::ClientTwirp.new(Settings.s2s_addr)
        elsif Settings.s2s_addr.start_with?('unix:')
          @client = Twitch::S2SSidecar::Client.new(Settings.s2s_addr)
        end
      end
    end

    def s2s2_middleware
      @s2s2_middleware ||= [
        Twitch::S2S2::Middleware,
        {
          s2s2_service_uri: Settings.s2s2_service_uri,
          credentials_provider: credentials,
          logger: Rails.logger,
          hosts: Settings.s2s2.hosts
        }
      ]
      @s2s2_middleware
    end

    def credentials
      if ENV['AWS_ACCESS_KEY_ID'].blank?
        Aws::ECSCredentials.new
      else
        Aws::Credentials.new(
          ENV['AWS_ACCESS_KEY_ID'],
          ENV['AWS_SECRET_ACCESS_KEY'],
          ENV['AWS_SESSION_TOKEN'].presence
        )
      end
    end
  end

  class Base
    include ActiveModel::Model
    include ActiveModel::Validations::ClassMethods

    attr_accessor :persisted

    OPEN_TIMEOUT = 5
    # this is HUGE. moneypenny gets latencies of ~30s and will need to reduce
    # this when they improve their latency.
    READ_TIMEOUT = 55
    TOTAL_TIMEOUT = OPEN_TIMEOUT + READ_TIMEOUT
    HTTP_METHODS = [:get, :post, :put, :delete, :head, :patch]
    TRUE_VALUES = %w{1 t true on yes}

    def initialize(params = {}, options = {})
      @persisted = options[:persisted]

      if params.respond_to?(:to_unsafe_h)
        params = params.to_unsafe_h
      end

      params.each do |key, value|
        next if value.nil? && persisted? && key == self.class.primary_attribute
        mtd = "#{key}="
        send mtd, params[key] if respond_to?(mtd)
      end
    end

    def inspect
      max_attributes_length = 10
      truncated = ""
      inspect_attributes = attributes

      if inspect_attributes.length > max_attributes_length
        inspect_attributes = inspect_attributes.first(max_attributes_length).to_h
        truncated = "..."
      end

      attributes_list = inspect_attributes.map {|k, v| [k, v.inspect].join("=")}.join(" ")
      "#<#{self.class.name} #{attributes_list}#{truncated}>"
    end

    def to_param
      primary_attribute_value
    end

    def to_json(options = {})
      as_json(options).to_json
    end

    def as_json(options = {})
      self.class.attribute_names.inject({}.with_indifferent_access) do |_, attribute|
        value = send(attribute)
        value = value.as_json(options) if value.respond_to?(:as_json)
        _[attribute.to_s] = value
        _
      end
    end

    def primary_attribute_value
      raise "primary_attribute is not set in #{self.class}" if self.class.primary_attribute.blank?
      send(self.class.primary_attribute).to_s
    end

    def persisted?
      !!@persisted
    end

    def errors
      @errors ||= ActiveModel::Errors.new self
    end

    def errors=(new_errors)
      raise "Errors not set" unless new_errors

      if new_errors.is_a?(ActiveModel::Errors)
        @errors = new_errors
      else
        @errors = ActiveModel::Errors.new self
        [new_errors].flatten.compact.each do |error|
          @errors.add :base, error
        end
      end
    end

    def attributes
      self.class.attribute_names.inject({}.with_indifferent_access) do |_, attribute|
        _[attribute.to_s] = send(attribute)
        _
      end
    end

    def attributes=(new_attributes)
      new_attributes.each do |key, value|
        send "#{key}=", value
      end

      attributes
    end

    class << self
      CLASS_VARIABLES = [:attribute_names, :attribute_syms, :association_names, :service_endpoint, :primary_attribute]
      attr_accessor *CLASS_VARIABLES

      def inherited(base)
        CLASS_VARIABLES.each do |class_variable|
          old_value = base.send class_variable
          new_value = old_value || base.superclass.send(class_variable)
          base.send "#{class_variable}=", new_value if old_value.nil?
        end
      end

      def booleanize(value)
        value.to_s.downcase.in? TRUE_VALUES
      end

      def paginate(objs, options = {})
        PaginatedArray.new(objs).tap do |a|
          a.total_pages = options[:total_pages]
          a.total_count = options[:total_count]
          a.per_page = options[:per_page]
        end
      end

      def scoped
        Service::Proxy.new self
      end

      delegate *Service::Proxy::ALL_CHAINED_METHODS, to: :scoped

      def from_attributes(params)
        new(params, persisted: true)
      end

      def from_twirp_object(twirp_object)
        params = twirp_object.to_h.keys.inject({}) do |_, key|
          v = twirp_object[key.to_s]
          _[key] = case v
                   when Google::Protobuf::Timestamp
                     Time.at(v.seconds).utc
                   when Google::Protobuf::BoolValue,
                        Google::Protobuf::BytesValue,
                        Google::Protobuf::DoubleValue,
                        Google::Protobuf::FloatValue,
                        Google::Protobuf::Int32Value,
                        Google::Protobuf::Int64Value,
                        Google::Protobuf::StringValue,
                        Google::Protobuf::UInt32Value,
                        Google::Protobuf::UInt64Value
                     v.value
                   else
                     v
                   end
          _
        end

        from_attributes(params)
      end

      def from_errors(errors)
        new.tap do |obj|
          obj.errors = errors
        end
      end

      def attributes(*args)
        self.attribute_names ||= []
        # see ./app/concerns/attribute_read_authorized
        self.attribute_syms ||= Set.new

        args.each do |arg|
          self.attribute_names << arg.to_s
          self.attribute_syms << arg

          define_method arg do
            instance_variable_get "@#{arg}"
          end

          define_method "#{arg}=" do |value|
            instance_variable_set "@#{arg}", value
          end
        end

        def associations(*args)
          self.association_names ||= []

          args.each do |arg|
            self.association_names << arg.to_s
            attr_accessor arg
          end
        end
      end

      def connection_options(options = {})
        raise "service_endpoint is not set in #{self}" if self.service_endpoint.blank?

        opts = {
          url: self.service_endpoint,
          request: {
            params_encoder: options[:params_encoder] || Faraday::NestedParamsEncoder,
          },
        }

        if Rails.env.starts_with?('development')
          opts[:proxy] = Settings.proxy
        end

        opts
      end

      def s2s2_middleware
        S2S.instance.s2s2_middleware
      end

      def connection(options = {})
        Faraday.new(connection_options(options)) do |faraday|
          faraday.use Service::ResponseLogger
          faraday.request options[:request].presence || :url_encoded

          if S2S.instance.client
            faraday.use Twitch::S2SSidecar::SigningMiddleware, client: S2S.instance.client
          end

          if options.key?(:additional_middleware)
            options[:additional_middleware].each do |mw|
              faraday.use *mw
            end
          end

          faraday.options[:timeout] = TOTAL_TIMEOUT
          faraday.options[:open_timeout] = OPEN_TIMEOUT

          if Rails.env.starts_with?('development')
            # custom adapter that supports socks5 proxies for local development
            faraday.adapter :net_http_socks_compat
          else
            faraday.adapter Faraday.default_adapter
          end
        end
      end

      HTTP_METHODS.each do |mtd|
        define_method mtd do |endpoint, options = {}|
          response = connection.run_request(mtd, endpoint, options[:body], options[:headers]) do |request|
            request.params.update(options[:params]) if options[:params].is_a?(Hash)
            yield(request) if block_given?
          end
          Service::Response.new response
        end
      end
    end

    delegate :service_base_url, :primary_attribute, to: :class
    delegate *HTTP_METHODS, to: :class
  end
end
