# frozen_string_literal: true

require "yaml"
require "net/http"
require "digest/md5"
require "acts_as_textcaptcha/textcaptcha_cache"
require "acts_as_textcaptcha/textcaptcha_api"

module ActsAsTextcaptcha
  module Textcaptcha
    def acts_as_textcaptcha(options = nil)
      cattr_accessor :textcaptcha_config
      attr_accessor :textcaptcha_question, :textcaptcha_answer, :textcaptcha_key

      # ensure these attrs are accessible (Rails 3)
      attr_accessible :textcaptcha_answer, :textcaptcha_key if respond_to?(:accessible_attributes) && respond_to?(:attr_accessible)

      self.textcaptcha_config = build_textcaptcha_config(options).symbolize_keys!

      validate :validate_textcaptcha, if: :perform_textcaptcha?

      include InstanceMethods
    end

    module InstanceMethods
      # override this method to toggle textcaptcha checking, by default this
      # will only allow new records to be protected with textcaptchas
      def perform_textcaptcha?
        (!respond_to?("new_record?") || new_record?)
      end

      def textcaptcha
        assign_textcaptcha(fetch_q_and_a || config_q_and_a) if perform_textcaptcha? && textcaptcha_config
      end

      private

      def fetch_q_and_a
        return unless should_fetch?

        TextcaptchaApi.new(
          api_key: textcaptcha_config[:api_key],
          api_endpoint: textcaptcha_config[:api_endpoint],
          raise_errors: textcaptcha_config[:raise_errors]
        ).fetch
      end

      def should_fetch?
        textcaptcha_config[:api_key] || textcaptcha_config[:api_endpoint]
      end

      def config_q_and_a
        return unless textcaptcha_config[:questions]

        random_question = textcaptcha_config[:questions][rand(textcaptcha_config[:questions].size)].symbolize_keys!
        answers = (random_question[:answers] || "").split(",").map { |answer| safe_md5(answer) }
        { "q" => random_question[:question], "a" => answers } if random_question && answers.present?
      end

      # check textcaptcha, if incorrect, generate a new textcaptcha
      def validate_textcaptcha
        valid_answers = textcaptcha_cache.read(textcaptcha_key) || []
        reset_textcaptcha
        if valid_answers.include?(safe_md5(textcaptcha_answer))
          # answer was valid, mutate the key again
          self.textcaptcha_key = textcaptcha_random_key
          textcaptcha_cache.write(textcaptcha_key, valid_answers, textcaptcha_cache_options)
          true
        else
          add_textcaptcha_error(too_slow: valid_answers.empty?)
          textcaptcha
          false
        end
      end

      def add_textcaptcha_error(too_slow: false)
        if too_slow
          errors.add(:textcaptcha_answer, :expired, message: "was not submitted quickly enough, try another question instead")
        else
          errors.add(:textcaptcha_answer, :incorrect, message: "is incorrect, try another question instead")
        end
      end

      def reset_textcaptcha
        if textcaptcha_key
          textcaptcha_cache.delete(textcaptcha_key)
          self.textcaptcha_key = nil
        end
      end

      def assign_textcaptcha(q_and_a)
        return unless q_and_a

        self.textcaptcha_question = q_and_a["q"]
        self.textcaptcha_key = textcaptcha_random_key
        textcaptcha_cache.write(textcaptcha_key, q_and_a["a"], textcaptcha_cache_options)
      end

      # strip whitespace pass through mb_chars (a multibyte safe proxy for
      # strings) then downcase
      def safe_md5(answer)
        Digest::MD5.hexdigest(answer.to_s.strip.mb_chars.downcase)
      end

      # a random cache key, time based, random
      def textcaptcha_random_key
        safe_md5(Time.now.to_i + rand(1_000_000))
      end

      def textcaptcha_cache_options
        if textcaptcha_config[:cache_expiry_minutes]
          { expires_in: textcaptcha_config[:cache_expiry_minutes].to_f.minutes }
        else
          {}
        end
      end

      def textcaptcha_cache
        @textcaptcha_cache ||= TextcaptchaCache.new
      end
    end

    private

    def build_textcaptcha_config(options)
      if options.is_a?(Hash)
        options
      else
        YAML.safe_load(ERB.new(read_textcaptcha_config).result, aliases: true)[Rails.env]
      end
    rescue StandardError
      raise ArgumentError, "could not find any textcaptcha options, in config/textcaptcha.yml or model - run rake textcaptcha:config to generate a template config file"
    end

    def read_textcaptcha_config
      File.read("#{Rails.root || "."}/config/textcaptcha.yml")
    end
  end
end
