# frozen_string_literal: true

module Imap
  module Providers
    # Gmail has a special header for both labels (X-GM-LABELS) and their
    # threading system (X-GM-THRID). We need to monkey-patch Net::IMAP to
    # get access to these. Also the archiving functionality is custom,
    # all UIDs in a thread must have the \\Inbox label removed.
    #
    class Gmail < Generic
      X_GM_LABELS = 'X-GM-LABELS'
      X_GM_THRID = 'X-GM-THRID'

      def imap
        @imap ||= super.tap { |imap| apply_gmail_patch(imap) }
      end

      def emails(uids, fields, opts = {})

        # gmail has a special header for labels
        if fields.include?('LABELS')
          fields[fields.index('LABELS')] = X_GM_LABELS
        end

        emails = super(uids, fields, opts)

        emails.each do |email|
          email['LABELS'] = Array(email['LABELS'])

          if email[X_GM_LABELS]
            email['LABELS'] << Array(email.delete(X_GM_LABELS))
            email['LABELS'].flatten!
          end

          email['LABELS'] << '\\Inbox' if @open_mailbox_name == 'INBOX'

          email['LABELS'].uniq!
        end

        emails
      end

      def store(uid, attribute, old_set, new_set)
        attribute = X_GM_LABELS if attribute == 'LABELS'
        super(uid, attribute, old_set, new_set)
      end

      def to_tag(label)
        # Label `\\Starred` is Gmail equivalent of :Flagged (both present)
        return 'starred' if label == :Flagged
        return if label == '[Gmail]/All Mail'

        label = label.to_s.gsub('[Gmail]/', '')
        super(label)
      end

      def tag_to_flag(tag)
        return :Flagged if tag == 'starred'

        super(tag)
      end

      def tag_to_label(tag)
        return '\\Important' if tag == 'important'
        return '\\Starred' if tag == 'starred'

        super(tag)
      end

      # All emails in the thread must be archived in Gmail for the thread
      # to get removed from the inbox
      def archive(uid)
        thread_id = thread_id_from_uid(uid)
        emails_to_archive = emails_in_thread(thread_id)
        emails_to_archive.each do |email|
          labels = email['LABELS']
          new_labels = labels.reject { |l| l == "\\Inbox" }
          store(email["UID"], "LABELS", labels, new_labels)
        end
        ImapSyncLog.log("Thread ID #{thread_id} (UID #{uid}) archived in Gmail mailbox for #{@username}", :debug)
      end

      # Though Gmail considers the email thread unarchived if the first email
      # has the \\Inbox label applied, we want to do this to all emails in the
      # thread to be consistent with archive behaviour.
      def unarchive(uid)
        thread_id = thread_id_from_uid(uid)
        emails_to_unarchive = emails_in_thread(thread_id)
        emails_to_unarchive.each do |email|
          labels = email['LABELS']
          new_labels = labels.dup
          if !new_labels.include?("\\Inbox")
            new_labels << "\\Inbox"
          end
          store(email["UID"], "LABELS", labels, new_labels)
        end
        ImapSyncLog.log("Thread ID #{thread_id} (UID #{uid}) unarchived in Gmail mailbox for #{@username}", :debug)
      end

      def thread_id_from_uid(uid)
        fetched = imap.uid_fetch(uid, [X_GM_THRID])
        if !fetched
          raise "Thread not found for UID #{uid}!"
        end

        fetched.last.attr[X_GM_THRID]
      end

      def emails_in_thread(thread_id)
        uids_to_fetch = imap.uid_search("#{X_GM_THRID} #{thread_id}")
        emails(uids_to_fetch, ["UID", "LABELS"])
      end

      def trash_move(uid)
        thread_id = thread_id_from_uid(uid)
        email_uids_to_trash = emails_in_thread(thread_id).map { |e| e['UID'] }

        imap.uid_move(email_uids_to_trash, trash_mailbox)
        ImapSyncLog.log("Thread ID #{thread_id} (UID #{uid}) trashed in Gmail mailbox for #{@username}", :debug)
        { trash_uid_validity: open_trash_mailbox, email_uids_to_trash: email_uids_to_trash }
      end

      private

      def apply_gmail_patch(imap)
        class << imap.instance_variable_get('@parser')

          # Modified version of the original `msg_att` from here:
          # https://github.com/ruby/ruby/blob/1cc8ff001da217d0e98d13fe61fbc9f5547ef722/lib/net/imap.rb#L2346
          #
          # This is done so we can extract X-GM-LABELS, X-GM-MSGID, and
          # X-GM-THRID, all Gmail extended attributes.
          #
          # rubocop:disable Style/RedundantReturn
          def msg_att(n)
            match(T_LPAR)
            attr = {}
            while true
              token = lookahead
              case token.symbol
              when T_RPAR
                shift_token
                break
              when T_SPACE
                shift_token
                next
              end
              case token.value
              when /\A(?:ENVELOPE)\z/ni
                name, val = envelope_data
              when /\A(?:FLAGS)\z/ni
                name, val = flags_data
              when /\A(?:INTERNALDATE)\z/ni
                name, val = internaldate_data
              when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni
                name, val = rfc822_text
              when /\A(?:RFC822\.SIZE)\z/ni
                name, val = rfc822_size
              when /\A(?:BODY(?:STRUCTURE)?)\z/ni
                name, val = body_data
              when /\A(?:UID)\z/ni
                name, val = uid_data
              when /\A(?:MODSEQ)\z/ni
                name, val = modseq_data

              # Adding support for GMail extended attributes.
              when /\A(?:X-GM-LABELS)\z/ni
                name, val = label_data
              when /\A(?:X-GM-MSGID)\z/ni
                name, val = uid_data
              when /\A(?:X-GM-THRID)\z/ni
                name, val = uid_data
              # End custom support for Gmail.

              else
                parse_error("unknown attribute `%s' for {%d}", token.value, n)
              end
              attr[name] = val
            end
            return attr
          end

          def label_data
            token = match(T_ATOM)
            name = token.value.upcase

            match(T_SPACE)
            match(T_LPAR)

            result = []
            while true
              token = lookahead
              case token.symbol
              when T_RPAR
                shift_token
                break
              when T_SPACE
                shift_token
              end

              token = lookahead
              if string_token?(token)
                result.push(string)
              else
                result.push(atom)
              end
            end
            return name, result
          end
          # rubocop:enable Style/RedundantReturn
        end
      end
    end
  end
end
