require 'base64'
require 'net/http'
require 'openssl'
require 'yaml'
require 'open-uri'
require_relative 'filelogger'

# class representing a Wendelin client
class WendelinClient

  HTTP_MESSAGE_401 = "Unauthorized access. Please check your user credentials and try again."
  HTTP_MESSAGE_400 = "There was a problem with the http request. If the error persists, please contact the administrator."
  HTTP_MESSAGE_5XX = "Internal Server Error: if the error persists, please contact the administrator."
  HTTP_MESSAGE_OTHER = "Sorry, an error ocurred. If the error persists, please contact the administrator."
  HTTP_MESSAGE_NOT_2XX = "HTTP-NOT-OK"
  HTTP_MESSAGE_EXCEPTION = "HTTP-ERROR"
  HTTP_MEMORY_ERROR = "MEMORY-ERROR"
  HTTP_REFERENCE_EXIST = "REFERENCE-EXIST"

  def initialize(erp5_url, user, password)
    @erp5_url = erp5_url
    @user = user
    @password = password
    @banned_references_list = []
    @logger = LogManager.instance()
    @last_ingestion = Time.new - 2
  end

  def checkReferenceChars(reference)
    if ["&", ";", "#", "%", '"', "+"].any? { |char| reference.include?(char) }
      raise "invalid char in filename. Following chars are not allowed for filenames: \& \; \% \" \+ \# Please rename it."
    end
  end

  def exists(reference)
    checkReferenceChars(reference)
    uri = URI(URI.escape("#{@erp5_url}/ERP5Site_checkIngestionReferenceExists?reference=#{reference}"))
    begin
      response = handleRequest(uri)
    rescue Exception => e
      @logger.error("An error occurred while checking if reference exists: " + e.to_s)
      @logger.error(e.backtrace)
      if e.to_s.include? "Unauthorized" or e.to_s.include? "401"
	raise e
      end
      return FALSE
    else
      return response.to_s == 'TRUE'
    end
  end

  def delete(reference)
    @logger.info("Deletion requested for reference #{reference}", print=TRUE)
    checkReferenceChars(reference)
    uri = URI(URI.escape("#{@erp5_url}/ERP5Site_invalidateIngestionObjects?reference=#{reference}"))
    res = handleRequest(uri)
    if res["success"] == FALSE
      @logger.abortExecution()
    end
    @logger.info("Remote file successfully ingested.", print=TRUE)
  end

  def rename(reference, new_reference)
    @logger.info("Rename requested for reference #{reference}, new reference #{new_reference}", print=TRUE)
    checkReferenceChars(reference)
    checkReferenceChars(new_reference)
    uri = URI(URI.escape("#{@erp5_url}/ERP5Site_renameIngestion?reference=#{reference}&new_reference=#{new_reference}"))
    res = handleRequest(uri)
    if res["success"] == FALSE
      @logger.abortExecution()
    end
    @logger.info("Remote file successfully renamed.", print=TRUE)
  end

  def increaseDatasetVersion(reference)
    if reference == ""
      @logger.warn("Could not increase data set version because dataset reference is empty.")
    else
      @logger.info("Increasing dataset version")
      begin
        uri = URI(URI.escape("#{@erp5_url}/ERP5Site_increaseDatasetVersion?reference=#{reference}"))
        response = handleRequest(uri)
      rescue Exception => e
	@logger.error("An error occurred while increasing dataset version: " + e.to_s)
	@logger.error(e.backtrace)
      end
    end
  end

  def setDatasetDescription(data_set, description_text)
    puts
    @logger.info("Setting remote dataset description...", print=TRUE)
      begin
	uri = URI(URI.escape("#{@erp5_url}/ERP5Site_setDatasetDescription?dataset=#{data_set}"))
	encoded_description = Base64.encode64(description_text)
	response = handleRequest(uri, reference=nil, data_chunk=encoded_description)
	raise response["message"] if not response["success"]
	dict = JSON.parse(response["message"])
	raise dict["message"] if dict["status_code"] != 0
	@logger.info(dict["message"], print=TRUE)
      rescue Exception => e
	puts
	@logger.error("An error occurred while setting dataset description: " + e.to_s, print=TRUE)
	@logger.error(e.backtrace)
      end
      puts
  end

  def ingest(reference, data_chunk, split)
      @logger.info("Ingestion reference: #{reference}", print=TRUE)
      if split and Time.new - @last_ingestion < 3
	# avoid to send split ingestions to close
	sleep 3
      end
      if exists(reference)
	@logger.info("There is another ingestion already done for the pair dataset-filename. Reference "\
	      + reference, print=TRUE)
	@logger.info("Rename your file or download the full dataset to make local changes.", print=TRUE)
	return {"success"=>FALSE, "message"=>HTTP_REFERENCE_EXIST}
      end
      uri = URI(URI.escape("#{@erp5_url}/ingest?reference=#{reference}"))
      n_retry = 0
      response = handleRequest(uri, reference, data_chunk)
      while response["success"] == FALSE && n_retry < 10
	if response["message"] != HTTP_MESSAGE_EXCEPTION && response["message"] != HTTP_MESSAGE_NOT_2XX
	  return response
	else
	  n_retry += 1
	  @logger.info("Retrying #{n_retry}/10...", print=TRUE)
	  sleep 30
	  response = handleRequest(uri, reference, data_chunk)
	end
      end
      if response["success"] == FALSE
	return response
      end
      @logger.info("Record successfully ingested.", print=TRUE)
      @last_ingestion = Time.new
      return {"success"=>TRUE, "message"=>"success"}
  end

  def eachDataStreamContentChunk(id, chunk_size, n_chunk=0, split_operation=FALSE)
    n_part = n_chunk
    done = FALSE
    first = TRUE
    while not done
      start_offset = n_part*chunk_size
      end_offset = n_part*chunk_size+chunk_size
      uri = URI(URI.escape("#{@erp5_url}ERP5Site_getDataStreamChunk?id=#{id}&start_offset=#{start_offset}&end_offset=#{end_offset}"))
      success = FALSE
      n_retry = 0
      while ! success && n_retry < 10
	begin
	  res = open(uri, http_basic_authentication: [@user, @password], ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE)  {
	    |content|
	      chunk = content.read()
	      if chunk.nil? || chunk.empty?
		if first
		  yield chunk
		end
	        if split_operation
	          @logger.info("File chunk downloaded", print=TRUE)
	        else
	          @logger.info("Done", print=TRUE)
	        end
	        done = TRUE
	      else
		first = FALSE
	        n_part += 1
	        yield chunk
	      end
	  }
	  success = TRUE
	rescue Exception => e
	  exception = e
	  @logger.error("Error downloading data: " + e.to_s, print=TRUE)
	  @logger.error(e.backtrace)
	  n_retry += 1
	  @logger.info("Retrying #{n_retry}/10...", print=TRUE)
	  sleep 30
	end
      end
      raise exception if not success
    end
  end

  def getDataStreamList(data_set_reference, data_stream_count, batch_size)
    #keep backward compatibility
    if data_stream_count = -1
      return getDataStreams(data_set_reference)
    end
    data_stream_list = []
    total_batches = data_stream_count/batch_size.ceil()
    if total_batches > 4
      puts
      @logger.info("Due to its size, the remote file list will be fetch in batches. This could take some minutes.", print=TRUE)
    end
    nbatch = 0
    while nbatch <= total_batches
      batch_dict = getDataStreamBatch(data_set_reference, batch_size*nbatch, batch_size)
      if batch_dict["status_code"] != 0
        return batch_dict
      end
      data_stream_list += batch_dict["result"]
      nbatch += 1
    end
    return_dict = Hash.new
    return_dict["status_code"] = 0
    return_dict["result"] = data_stream_list
    return return_dict
  end

  def getDataStreamBatch(data_set_reference, offset, batch_size)
    uri = URI(URI.escape("#{@erp5_url}ERP5Site_getDataStreamList?data_set_reference=#{data_set_reference}&offset=#{offset}&batch_size=#{batch_size}"))
    response = handleRequest(uri)
    if response["success"] == FALSE
      @logger.abortExecution()
    end
    str = response["message"]
    if not str.nil?
	str.gsub!(/(\,)(\S)/, "\\1 \\2")
    	return YAML::load(str)
    end
    return {'status_code': 0, 'result': []}
  end

  def getDataStreams(data_set_reference)
    uri = URI(URI.escape("#{@erp5_url}ERP5Site_getDataStreamList?data_set_reference=#{data_set_reference}"))
    response = handleRequest(uri)
    if response["success"] == FALSE
      @logger.abortExecution()
    end
    str = response["message"]
    if not str.nil?
	str.gsub!(/(\,)(\S)/, "\\1 \\2")
    	return YAML::load(str)
    end
    return {'status_code': 0, 'result': []}
  end

  def getDataStreamCount(data_set_reference)
    uri = URI(URI.escape("#{@erp5_url}ERP5Site_getDataStreamCount?data_set_reference=#{data_set_reference}"))
    response = handleRequest(uri)
    if response["success"] == FALSE
      #keep backward compatibiliy if script doesn't exist in wendelin instance
      if response["message"] == 404
        @logger.info("Continue with backward compatibility...", print=TRUE)
        return {"status_code"=>0, "result"=>-1}
      else
        @logger.abortExecution()
      end
    end
    str = response["message"]
    if not str.nil?
    str.gsub!(/(\,)(\S)/, "\\1 \\2")
        return YAML::load(str)
    end
    return {"status_code"=>0, "result"=>0}
  end

  private
  def handleRequest(uri, reference=nil, data_chunk=nil)
    req = Net::HTTP::Post.new(uri)
    req.basic_auth @user, @password
    if data_chunk != nil
      @logger.info("Setting request form data...", print=TRUE) if reference != nil
      begin
        req.set_form_data('data_chunk' => data_chunk)
      rescue java.lang.OutOfMemoryError
        @logger.logOutOfMemoryError(reference)
        return {"success"=>FALSE, "message"=>HTTP_MEMORY_ERROR}
      rescue Exception => e
        @logger.error("Error setting form data: " + e.to_s, print=TRUE)
        @logger.error(e.backtrace)
        return {"success"=>FALSE, "message"=>HTTP_MESSAGE_EXCEPTION}
      end
      @logger.info("Sending record:'#{reference}'...", print=TRUE) if reference != nil
    end
    begin
      res = Net::HTTP.start(uri.hostname, uri.port,
              :use_ssl      => (uri.scheme == 'https'),
              :verify_mode  => OpenSSL::SSL::VERIFY_NONE,
              :ssl_timeout  => 300, :open_timeout => 300, :read_timeout => 300,
            ) do |http|
              http.request(req)
            end
    rescue Exception => e
      puts
      @logger.error("HTTP ERROR: " + e.to_s, print=TRUE)
      @logger.error("During request to " + uri.hostname.to_s, print=TRUE)
      @logger.error(e.backtrace)
      if e.to_s.include? "handshake_failure" or e.to_s.include? "protocol_version"
        @logger.error("This issue could be due to a cipher or certificates problem, please check your frontend instance or try with a http url", print=TRUE)
      end
      return {"success"=>FALSE, "message"=>HTTP_MESSAGE_EXCEPTION}
    else
      if res.kind_of?(Net::HTTPSuccess) # res.code is 2XX
        @logger.info("Done")
        return {"success"=>TRUE, "message"=>res.body}
      else
        @logger.error("HTTP FAIL -  code: #{res.code}", print=TRUE)
        @logger.error("During request to " + uri.hostname.to_s, print=TRUE)
        @logger.error(uri.to_s)
        if res.code == '500' or res.code == '502' or res.code == '503'
          @logger.error(HTTP_MESSAGE_5XX, print=TRUE)
        elsif res.code == '401'
          @logger.error(HTTP_MESSAGE_401, print=TRUE)
          @logger.abortExecution()
        elsif res.code == '400'
          @logger.error(HTTP_MESSAGE_400, print=TRUE)
          @logger.abortExecution()
        elsif res.code == '404'
          return {"success"=>FALSE, "message"=>404}
        else
          @logger.error(HTTP_MESSAGE_OTHER, print=TRUE)
        end
        return {"success"=>FALSE, "message"=>HTTP_MESSAGE_NOT_2XX}
      end
    end
  end
end
