require "openssl"
require "digest/sha2"

module Flipper
  module Cloud
    class MessageVerifier
      class InvalidSignature < StandardError; end

      DEFAULT_VERSION = "v1"

      def self.header(signature, timestamp, version = DEFAULT_VERSION)
        raise ArgumentError, "timestamp should be an instance of Time" unless timestamp.is_a?(Time)
        raise ArgumentError, "signature should be a string" unless signature.is_a?(String)
        "t=#{timestamp.to_i},#{version}=#{signature}"
      end

      def initialize(secret:, version: DEFAULT_VERSION)
        @secret = secret
        @version = version || DEFAULT_VERSION

        raise ArgumentError, "secret should be a string" unless @secret.is_a?(String)
        raise ArgumentError, "version should be a string" unless @version.is_a?(String)
      end

      def generate(payload, timestamp)
        raise ArgumentError, "timestamp should be an instance of Time" unless timestamp.is_a?(Time)
        raise ArgumentError, "payload should be a string" unless payload.is_a?(String)

        OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), @secret, "#{timestamp.to_i}.#{payload}")
      end

      def header(signature, timestamp)
        self.class.header(signature, timestamp, @version)
      end

      # Public: Verifies the signature header for a given payload.
      #
      # Raises a InvalidSignature in the following cases:
      # - the header does not match the expected format
      # - no signatures found with the expected scheme
      # - no signatures matching the expected signature
      # - a tolerance is provided and the timestamp is not within the
      #   tolerance
      #
      # Returns true otherwise.
      def verify(payload, header, tolerance: nil)
        begin
          timestamp, signatures = get_timestamp_and_signatures(header)
        rescue StandardError
          raise InvalidSignature, "Unable to extract timestamp and signatures from header"
        end

        if signatures.empty?
          raise InvalidSignature, "No signatures found with expected version #{@version}"
        end

        expected_sig = generate(payload, timestamp)
        unless signatures.any? { |s| secure_compare(expected_sig, s) }
          raise InvalidSignature, "No signatures found matching the expected signature for payload"
        end

        if tolerance && timestamp < Time.now - tolerance
          raise InvalidSignature, "Timestamp outside the tolerance zone (#{Time.at(timestamp)})"
        end

        true
      end

      private

      # Extracts the timestamp and the signature(s) with the desired version
      # from the header
      def get_timestamp_and_signatures(header)
        list_items = header.split(/,\s*/).map { |i| i.split("=", 2) }
        timestamp = Integer(list_items.select { |i| i[0] == "t" }[0][1])
        signatures = list_items.select { |i| i[0] == @version }.map { |i| i[1] }
        [Time.at(timestamp), signatures]
      end

      # Private
      def fixed_length_secure_compare(a, b)
        raise ArgumentError, "string length mismatch." unless a.bytesize == b.bytesize
        l = a.unpack "C#{a.bytesize}"
        res = 0
        b.each_byte { |byte| res |= byte ^ l.shift }
        res == 0
      end

      # Private
      def secure_compare(a, b)
        fixed_length_secure_compare(::Digest::SHA256.digest(a), ::Digest::SHA256.digest(b)) && a == b
      end
    end
  end
end
