require 'snails/util'
require 'erb'
require 'tilt/erb'
require 'snails'
require 'stringio'

module Snails

  class Mailer
    include Snails::SimpleFormat
    Time.include(Snails::RelativeTime) unless Time.instance_methods.include?(:relative)

    @queue = :emails

    # class Bounce < StandardError; end
    # class SoftBounce < Bounce; end
    # class HardBounce < Bounce; end

    def self.backends
      {
        'test_backend' => 'Snails::Mailer::TestBackend',
        'smtp' => 'Snails::Mailer::TuktukBackend',    # requires tuktuk gem
        'mailgun' => 'Snails::Mailer::MailgunBackend' # requires rest-client gem
      }
    end

    def self.init_backend(backend_name, opts = {})
      backend_name = backend_name.presence || 'test_backend'
      backends[backend_name].constantize.new(opts)
    end

    def initialize(opts)
      @from_email    = opts[:from] or raise ":from required"
      @base_subject  = opts[:base_subject] || ''
      @views         = opts[:views] || Snails.root.join('lib', 'views')
      @logfile       = opts[:logfile] # || Snails.root.join('log', 'mailer.log')

      @backend       = self.class.init_backend(opts[:backend_name], opts[:backend_options])
    end

    def email(name, &block)
      define_singleton_method(name) do |*args|
        instance_exec(*args, &block)
      end
    end

    def helpers(&block)
      instance_eval(&block)
    end

    def perform(method, *args)
      send(method, *args)
    end

    # e.g. Notifier.queue(:some_notification, @project, "arg1")
    def queue(method, obj, *args)
      return unless Snails.env.production?
      Resque.enqueue(self, method, obj.id, *args)
    end

    def send_error(to: @from_email, err:, env:, params: {})
      @exception, @env, @params = err, env, params
      @url = "#{env['REQUEST_METHOD']} #{env['REQUEST_URI']}"

      subject = "#{@url} :: (#{@exception.class}) \"#{@exception.message}\""
      content = %{
A <%= @exception.class %> occurred in <%= @url %>:

  -----------------

<%= @exception.message %>

<%= @exception.backtrace.join("\n") %>

  -----------------

- Request    : <%= @url %>
- Parameters : <%= @params.inspect %>
- IP address : <%= @env['REMOTE_ADDR'] %>
- User agent : <%= @env['HTTP_USER_AGENT'] %>
- Process    : <%= $$ %>
- Server     : <%= `hostname -s`.chomp %>

  -----------------
      }.strip

      send_email(to: to, subject: subject, body: content)
    end

    private

    def render(view_path, layout_path = nil)
      view = File.read(File.join(@views, "#{view_path}.erb"))
      if layout_path
        layout = File.read(File.join(@views, "#{layout_path}.erb"))
        render_erb(layout) { render_erb(view) }
      else
        render_erb(view)
      end
    end

    def render_erb(template)
      ERB.new(template).result(binding)
    end

    def partial(name)
      partial_name = name.to_s["/"] ? name.to_s.reverse.sub("/", "_/").reverse : "_#{name}"
      render(partial_name)
    end

    def logger
      @logger ||= @logfile ? Logger.new(@logfile) : Snails.logger
    end

    def send_email(from: @from_email, to:, subject:, body: nil, template: nil, layout: nil, html_body: nil, html_template: nil, html_layout: nil, message_id: nil, attachments: nil, return_path: nil, list_unsubscribe: nil)
      raise "No recipient given for mail: #{subject}!" if to.blank?

      message = {
        to:      to,
        from:    from,
        subject: @base_subject + subject
      }

      raise "Blank from" if message[:from].blank?
      raise "Blank to" if message[:to].blank?

      if body or template
        message[:body]  = template ? render(template, layout) : body
      end

      if html_body or html_template
        message[:html_body] = html_template ? render(html_template, html_layout) : html_body
      end

      message[:attachments] = attachments if attachments
      message[:message_id] = message_id if message_id
      message[:return_path] = return_path if return_path
      message[:list_unsubscribe] = list_unsubscribe if list_unsubscribe

      logger.info "[#{to}] Delivering: #{subject}"
      @backend.deliver(message)
    end

    alias_method :deliver, :send_email

    class Backend
      def deliver(email, options = {})
        raise "Please redefine in subclass"
      end

      def deliver_many(emails, options = {})
        emails.map { |e| deliver(e, options) }
      end
    end

    class TestBackend < Backend
      def initialize(opts = {})
      end

      def deliver(email, options = {})
        puts "Deliverying single: #{email[:to]}"
      end

      def deliver_many(emails, options = {})
        puts "Deliverying many: #{emails.count}"
      end
    end

    class TuktukBackend < Backend
      def initialize(config = {})
        @debug = config[:debug]

        if key = config.dig(:dkim, :private_key) and File.exist?(key)
          config[:dkim][:private_key] = IO.read(key)
        elsif config[:dkim]
          puts "Private key for DKIM not found! Disabling..."
          config.delete(:dkim)
        end

        Tuktuk.options = config
      end

      def deliver(email, options = {})
        debug = @debug.nil? ? !Snails.env.production? : @debug # if debug isn't set, determine based on env
        resp, email = Tuktuk.deliver(email, debug: debug)

        if resp.is_a?(Tuktuk::Bounce)
          puts "[#{email[:to]}] Email bounced! [#{resp.code}] #{resp.message}"
        end

        return resp, email
      end

      def deliver_many(emails, options = {})
        Tuktuk.deliver_many(emails, options)
      end
    end

    class MailgunBackend < Backend

      class Attachment < StringIO
        attr_reader :original_filename, :content_type, :path

        def initialize(data, filename, content_type)
          super(data)
          @path = '' # path
          @original_filename = filename
          @content_type = content_type || 'application/octet-stream'
        end
      end

      def initialize(options = {})
        api_key = options[:api_key]
        domain_name = options[:domain_name]
        @url = "https://api:#{api_key}@api.mailgun.net/v3/#{domain_name}/messages"
      end

      def deliver(email, options = {})
        raise "No body!" if email[:body].nil?

        data = {
          from: email[:from],
          to: email[:to],
          subject: email[:subject],
        }

        data[:text] = email[:body] if email[:body]
        data[:html] = email[:html_body] if email[:html_body]

        if data[:text].blank? && data[:html].blank?
          raise ArgumentError, "Either text or html required"
        end

        if email[:attachments]
          data[:attachment] = email[:attachments].map do |att|
            Attachment.new(att[:content], att[:filename], att[:content_type])
          end
        end

        resp = RestClient.post(@url, data)
        return resp.code == 200 ? [resp.body, data[:to]] : nil
      end

    end

  end

end