# Inspired in great part by Discourse's Email::Receiver
module Gitlab
  module Email
    class Receiver
      class ProcessingError < StandardError; end
      class EmailUnparsableError < ProcessingError; end
      class SentNotificationNotFoundError < ProcessingError; end
      class ProjectNotFound < ProcessingError; end
      class EmptyEmailError < ProcessingError; end
      class AutoGeneratedEmailError < ProcessingError; end
      class UserNotFoundError < ProcessingError; end
      class UserBlockedError < ProcessingError; end
      class UserNotAuthorizedError < ProcessingError; end
      class NoteableNotFoundError < ProcessingError; end
      class InvalidNoteError < ProcessingError; end
      class InvalidIssueError < ProcessingError; end

      def initialize(raw)
        @raw = raw
      end

      def execute
        raise EmptyEmailError if @raw.blank?

        if sent_notification
          process_create_note

        elsif message_project
          if message_sender.can?(:read_project, message_project)
            process_create_issue
          else # Must be private project without access
            raise ProjectNotFound
          end

        elsif reply_key =~ %r{/|\+}
          # Sent Notification reply_key would not have / or +
          raise ProjectNotFound
        else
          raise SentNotificationNotFoundError
        end
      end

      private
      def process_create_note
        raise AutoGeneratedEmailError if message.header.to_s =~ /auto-(generated|replied)/

        author = sent_notification.recipient
        project = sent_notification.project

        validate_permission!(author, project, :create_note)

        raise NoteableNotFoundError unless sent_notification.noteable

        reply = process_reply(project)
        raise EmptyEmailError if reply.blank?
        note = create_note(reply)

        unless note.persisted?
          msg = "The comment could not be created for the following reasons:"
          note.errors.full_messages.each do |error|
            msg << "\n\n- #{error}"
          end

          raise InvalidNoteError, msg
        end
      end

      def process_create_issue
        validate_permission!(message_sender, message_project, :create_issue)
        validate_authentication_token!(message_sender)

        issue = Issues::CreateService.new(
          message_project,
          message_sender,
          title:       message.subject,
          description: process_reply(message_project)
        ).execute

        unless issue.persisted?
          msg = "The issue could not be created for the following reasons:"
          issue.errors.full_messages.each do |error|
            msg << "\n\n- #{error}"
          end

          raise InvalidIssueError, msg
        end
      end

      def validate_permission!(author, project, permission)
        raise UserNotFoundError unless author
        raise UserBlockedError if author.blocked?
        # TODO: Give project not found error if author cannot read project
        raise UserNotAuthorizedError unless author.can?(permission, project)
      end

      def validate_authentication_token!(author)
        raise UserNotAuthorizedError unless author.authentication_token ==
                                              authentication_token
      end

      def message_sender
        @message_sender ||= message.from.find do |email|
          user = User.find_by_any_email(email)
          break user if user
        end
      end

      def message_project
        @message_project ||=
          Project.find_with_namespace(project_namespace) if reply_key
      end

      def process_reply(project)
        reply = ReplyParser.new(message).execute.strip

        add_attachments(reply, project)

        reply
      end

      def message
        @message ||= Mail::Message.new(@raw)
      rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError => e
        raise EmailUnparsableError, e
      end

      def reply_key
        key_from_to_header || key_from_additional_headers
      end

      def authentication_token
        reply_key[/[^\+]+$/]
      end

      def project_namespace
        reply_key[/^[^\+]+/]
      end

      def key_from_to_header
        key = nil
        message.to.each do |address|
          key = Gitlab::IncomingEmail.key_from_address(address)
          break if key
        end

        key
      end

      def key_from_additional_headers
        reply_key = nil

        Array(message.references).each do |message_id|
          reply_key = Gitlab::IncomingEmail.key_from_fallback_reply_message_id(message_id)
          break if reply_key
        end

        reply_key
      end

      def sent_notification
        return nil unless reply_key

        SentNotification.for(reply_key)
      end

      def add_attachments(reply, project)
        attachments = Email::AttachmentUploader.new(message).execute(project)

        attachments.each do |link|
          reply << "\n\n#{link[:markdown]}"
        end

        reply
      end

      def create_note(reply)
        Notes::CreateService.new(
          sent_notification.project,
          sent_notification.recipient,
          note:           reply,
          noteable_type:  sent_notification.noteable_type,
          noteable_id:    sent_notification.noteable_id,
          commit_id:      sent_notification.commit_id,
          line_code:      sent_notification.line_code
        ).execute
      end
    end
  end
end