# Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/

class Ticket < ApplicationModel
  include CanBeImported
  include HasActivityStreamLog
  include ChecksClientNotification
  include CanCsvImport
  include ChecksHtmlSanitized
  include HasHistory
  include HasTags
  include HasSearchIndexBackend
  include HasOnlineNotifications
  include HasLinks
  include HasObjectManagerAttributes
  include HasTaskbars
  include Ticket::CallsStatsTicketReopenLog
  include Ticket::EnqueuesUserTicketCounterJob
  include Ticket::ResetsPendingTimeSeconds
  include Ticket::SetsCloseTime
  include Ticket::SetsOnlineNotificationSeen
  include Ticket::TouchesAssociations
  include Ticket::TriggersSubscriptions
  include Ticket::ChecksReopenAfterCertainTime

  include ::Ticket::Escalation
  include ::Ticket::Subject
  include ::Ticket::Assets
  include ::Ticket::SearchIndex
  include ::Ticket::Search
  include ::Ticket::MergeHistory

  store          :preferences
  before_create  :check_generate, :check_defaults, :check_title, :set_default_state, :set_default_priority
  before_update  :check_defaults, :check_title, :reset_pending_time, :check_owner_active

  # This must be loaded late as it depends on the internal before_create and before_update handlers of ticket.rb.
  include Ticket::SetsLastOwnerUpdateTime

  include HasTransactionDispatcher

  # workflow checks should run after before_create and before_update callbacks
  include ChecksCoreWorkflow

  validates :group_id, presence: true

  activity_stream_permission 'ticket.agent'

  core_workflow_screens 'create_middle', 'edit', 'overview_bulk'

  activity_stream_attributes_ignored :organization_id, # organization_id will change automatically on user update
                                     :create_article_type_id,
                                     :create_article_sender_id,
                                     :article_count,
                                     :first_response_at,
                                     :first_response_escalation_at,
                                     :first_response_in_min,
                                     :first_response_diff_in_min,
                                     :close_at,
                                     :close_escalation_at,
                                     :close_in_min,
                                     :close_diff_in_min,
                                     :update_escalation_at,
                                     :update_in_min,
                                     :update_diff_in_min,
                                     :last_close_at,
                                     :last_contact_at,
                                     :last_contact_agent_at,
                                     :last_contact_customer_at,
                                     :last_owner_update_at,
                                     :preferences

  search_index_attributes_relevant :organization_id,
                                   :group_id,
                                   :state_id,
                                   :priority_id

  history_attributes_ignored :create_article_type_id,
                             :create_article_sender_id,
                             :article_count,
                             :preferences

  history_relation_object 'Ticket::Article', 'Mention', 'Ticket::SharedDraftZoom'

  validates :note, length: { maximum: 250 }
  sanitized_html :note

  belongs_to    :group, optional: true
  belongs_to    :organization, optional: true
  has_many      :articles,               class_name: 'Ticket::Article', after_add: :cache_update, after_remove: :cache_update, dependent: :destroy, inverse_of: :ticket
  has_many      :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', dependent: :destroy, inverse_of: :ticket
  has_many      :flags,                  class_name: 'Ticket::Flag', dependent: :destroy
  has_many      :mentions,               as: :mentionable, dependent: :destroy
  has_one       :shared_draft,           class_name: 'Ticket::SharedDraftZoom', inverse_of: :ticket, dependent: :destroy
  belongs_to    :state,                  class_name: 'Ticket::State', optional: true
  belongs_to    :priority,               class_name: 'Ticket::Priority', optional: true
  belongs_to    :owner,                  class_name: 'User', optional: true
  belongs_to    :customer,               class_name: 'User', optional: true
  belongs_to    :created_by,             class_name: 'User', optional: true
  belongs_to    :updated_by,             class_name: 'User', optional: true
  belongs_to    :create_article_type,    class_name: 'Ticket::Article::Type', optional: true
  belongs_to    :create_article_sender,  class_name: 'Ticket::Article::Sender', optional: true

  association_attributes_ignored :flags, :mentions

  attr_accessor :callback_loop

=begin

processes tickets which have reached their pending time and sets next state_id

  processed_tickets = Ticket.process_pending

returns

  processed_tickets = [<Ticket>, ...]

=end

  def self.process_pending
    result = []

    # process pending action tickets
    pending_action = Ticket::StateType.find_by(name: 'pending action')
    ticket_states_pending_action = Ticket::State.where(state_type_id: pending_action)
                                                .where.not(next_state_id: nil)
    if ticket_states_pending_action.present?
      next_state_map = {}
      ticket_states_pending_action.each do |state|
        next_state_map[state.id] = state.next_state_id
      end

      tickets = where(state_id: next_state_map.keys)
                .where('pending_time <= ?', Time.zone.now)

      tickets.find_each(batch_size: 500) do |ticket|
        Transaction.execute do
          ticket.state_id      = next_state_map[ticket.state_id]
          ticket.updated_at    = Time.zone.now
          ticket.updated_by_id = 1
          ticket.save!
        end
        result.push ticket
      end
    end

    # process pending reminder tickets
    pending_reminder = Ticket::StateType.find_by(name: 'pending reminder')
    ticket_states_pending_reminder = Ticket::State.where(state_type_id: pending_reminder)

    if ticket_states_pending_reminder.present?
      reminder_state_map = {}
      ticket_states_pending_reminder.each do |state|
        reminder_state_map[state.id] = state.next_state_id
      end

      tickets = where(state_id: reminder_state_map.keys)
                .where('pending_time <= ?', Time.zone.now)

      tickets.find_each(batch_size: 500) do |ticket|

        article_id = nil
        article = Ticket::Article.last_customer_agent_article(ticket.id)
        if article
          article_id = article.id
        end

        # send notification
        TransactionJob.perform_now(
          object:     'Ticket',
          type:       'reminder_reached',
          object_id:  ticket.id,
          article_id: article_id,
          user_id:    1,
        )

        result.push ticket
      end
    end

    result
  end

  def auto_assign(user)
    return if !persisted?
    return if Setting.get('ticket_auto_assignment').blank?
    return if owner_id != 1
    return if !TicketPolicy.new(user, self).full?

    user_ids_ignore = Array(Setting.get('ticket_auto_assignment_user_ids_ignore')).map(&:to_i)
    return if user_ids_ignore.include?(user.id)

    ticket_auto_assignment_selector = Setting.get('ticket_auto_assignment_selector')
    return if ticket_auto_assignment_selector.blank?

    condition = ticket_auto_assignment_selector[:condition].merge(
      'ticket.id' => {
        'operator' => 'is',
        'value'    => id,
      }
    )

    ticket_count, = Ticket.selectors(condition, limit: 1, current_user: user, access: 'full')
    return if ticket_count.to_i.zero?

    update!(owner: user)
  end

=begin

processes escalated tickets

  processed_tickets = Ticket.process_escalation

returns

  processed_tickets = [<Ticket>, ...]

=end

  def self.process_escalation
    result = []

    # fetch all escalated and soon to be escalating tickets
    where('escalation_at <= ?', 15.minutes.from_now).find_each(batch_size: 500) do |ticket|

      article_id = nil
      article = Ticket::Article.last_customer_agent_article(ticket.id)
      if article
        article_id = article.id
      end

      # send escalation
      if ticket.escalation_at < Time.zone.now
        TransactionJob.perform_now(
          object:     'Ticket',
          type:       'escalation',
          object_id:  ticket.id,
          article_id: article_id,
          user_id:    1,
        )
        result.push ticket
        next
      end

      # check if warning needs to be sent
      TransactionJob.perform_now(
        object:     'Ticket',
        type:       'escalation_warning',
        object_id:  ticket.id,
        article_id: article_id,
        user_id:    1,
      )
      result.push ticket
    end
    result
  end

=begin

processes tickets which auto unassign time has reached

  processed_tickets = Ticket.process_auto_unassign

returns

  processed_tickets = [<Ticket>, ...]

=end

  def self.process_auto_unassign

    # process pending action tickets
    state_ids = Ticket::State.by_category(:work_on).pluck(:id)
    return [] if state_ids.blank?

    result = []
    groups = Group.where(active: true).where('assignment_timeout IS NOT NULL AND groups.assignment_timeout != 0')
    return [] if groups.blank?

    groups.each do |group|
      next if group.assignment_timeout.blank?

      ticket_ids = Ticket.where('state_id IN (?) AND owner_id != 1 AND group_id = ? AND last_owner_update_at IS NOT NULL', state_ids, group.id).limit(600).pluck(:id)
      ticket_ids.each do |ticket_id|
        ticket = Ticket.find_by(id: ticket_id)
        next if !ticket

        minutes_since_last_assignment = Time.zone.now - ticket.last_owner_update_at
        next if (minutes_since_last_assignment / 60) <= group.assignment_timeout

        Transaction.execute do
          ticket.owner_id      = 1
          ticket.updated_at    = Time.zone.now
          ticket.updated_by_id = 1
          ticket.save!
        end
        result.push ticket
      end
    end

    result
  end

=begin

merge tickets

  ticket = Ticket.find(123)
  result = ticket.merge_to(
    ticket_id: 123,
    user_id:   123,
  )

returns

  result = true|false

=end

  def merge_to(data)

    # prevent cross merging tickets
    target_ticket = Ticket.find_by(id: data[:ticket_id])
    raise 'no target ticket given' if !target_ticket
    raise Exceptions::UnprocessableEntity, __('It is not possible to merge into an already merged ticket.') if target_ticket.state.state_type.name == 'merged'

    # check different ticket ids
    raise Exceptions::UnprocessableEntity, __('A ticket cannot be merged into itself.') if id == target_ticket.id

    # update articles
    Transaction.execute context: 'merge' do

      Ticket::Article.where(ticket_id: id).each(&:touch)

      # quiet update of reassign of articles
      Ticket::Article.where(ticket_id: id).update_all(['ticket_id = ?', data[:ticket_id]]) # rubocop:disable Rails/SkipsModelValidations

      # mark target ticket as updated
      # otherwise the "received_merge" history entry
      # will be the same as the last updated_at
      # which might be a long time ago
      target_ticket.updated_at = Time.zone.now

      # add merge event to both ticket's history (Issue #2469 - Add information "Ticket merged" to History)
      target_ticket.history_log(
        'received_merge',
        data[:user_id],
        id_to:   target_ticket.id,
        id_from: id,
      )
      history_log(
        'merged_into',
        data[:user_id],
        id_to:   target_ticket.id,
        id_from: id,
      )

      # create new merge article
      Ticket::Article.create(
        ticket_id:     id,
        type_id:       Ticket::Article::Type.lookup(name: 'note').id,
        sender_id:     Ticket::Article::Sender.lookup(name: 'Agent').id,
        body:          'merged',
        internal:      false,
        created_by_id: data[:user_id],
        updated_by_id: data[:user_id],
      )

      # search for mention duplicates and destroy them before moving mentions
      Mention.duplicates(self, target_ticket).destroy_all
      Mention.where(mentionable: self).update_all(mentionable_id: target_ticket.id) # rubocop:disable Rails/SkipsModelValidations

      # reassign links to the new ticket
      # rubocop:disable Rails/SkipsModelValidations
      ticket_source_id = Link::Object.find_by(name: 'Ticket').id

      # search for all duplicate source and target links and destroy them
      # before link merging
      Link.duplicates(
        object1_id:    ticket_source_id,
        object1_value: id,
        object2_value: data[:ticket_id]
      ).destroy_all
      Link.where(
        link_object_source_id:    ticket_source_id,
        link_object_source_value: id,
      ).update_all(link_object_source_value: data[:ticket_id])
      Link.where(
        link_object_target_id:    ticket_source_id,
        link_object_target_value: id,
      ).update_all(link_object_target_value: data[:ticket_id])
      # rubocop:enable Rails/SkipsModelValidations

      # link tickets
      Link.add(
        link_type:                'parent',
        link_object_source:       'Ticket',
        link_object_source_value: data[:ticket_id],
        link_object_target:       'Ticket',
        link_object_target_value: id
      )

      # external sync references
      ExternalSync.migrate('Ticket', id, target_ticket.id)

      # set state to 'merged'
      self.state_id = Ticket::State.lookup(name: 'merged').id

      # rest owner
      self.owner_id = 1

      # save ticket
      save!

      # touch new ticket (to broadcast change)
      target_ticket.touch # rubocop:disable Rails/SkipsModelValidations

      EventBuffer.add('transaction', {
                        object:     target_ticket.class.name,
                        type:       'update.received_merge',
                        data:       target_ticket,
                        changes:    {},
                        id:         target_ticket.id,
                        user_id:    UserInfo.current_user_id,
                        created_at: Time.zone.now,
                      })

      EventBuffer.add('transaction', {
                        object:     self.class.name,
                        type:       'update.merged_into',
                        data:       self,
                        changes:    {},
                        id:         id,
                        user_id:    UserInfo.current_user_id,
                        created_at: Time.zone.now,
                      })
    end
    true
  end

=begin

check if online notification should be shown in general as already seen with current state

  ticket = Ticket.find(1)
  seen = ticket.online_notification_seen_state(user_id_check)

returns

  result = true # or false

=end

  def online_notification_seen_state(user_id_check = nil)
    state      = Ticket::State.lookup(id: state_id)
    state_type = Ticket::StateType.lookup(id: state.state_type_id)

    # always to set unseen for ticket owner and users which did not the update
    return false if state_type.name != 'merged' && user_id_check && user_id_check == owner_id && user_id_check != updated_by_id

    # set all to seen if pending action state is a closed or merged state
    if state_type.name == 'pending action' && state.next_state_id
      state      = Ticket::State.lookup(id: state.next_state_id)
      state_type = Ticket::StateType.lookup(id: state.state_type_id)
    end

    # set all to seen if new state is pending reminder state
    if state_type.name == 'pending reminder'
      if user_id_check
        return false if owner_id == 1
        return false if updated_by_id != owner_id && user_id_check == owner_id

        return true
      end
      return true
    end

    # set all to seen if new state is a closed or merged state
    return true if state_type.name == 'closed'
    return true if state_type.name == 'merged'

    false
  end

=begin

get count of tickets and tickets which match on selector

@param  [Hash] selectors hash with conditions
@oparam [Hash] options

@option options [String]  :access can be 'full', 'read', 'create' or 'ignore' (ignore means a selector over all tickets), defaults to 'full'
@option options [Integer] :limit of tickets to return
@option options [User]    :user is a current user
@option options [Integer] :execution_time is a current user

@return [Integer, [<Ticket>]]

@example
  ticket_count, tickets = Ticket.selectors(params[:condition], limit: limit, current_user: current_user, access: 'full')

  ticket_count # count of found tickets
  tickets      # tickets

=end

  def self.selectors(selectors, options)
    limit = options[:limit] || 10
    current_user = options[:current_user]
    access = options[:access] || 'full'
    raise 'no selectors given' if !selectors

    query, bind_params, tables = selector2sql(selectors, options)
    return [] if !query

    ActiveRecord::Base.transaction(requires_new: true) do

      if !current_user || access == 'ignore'
        ticket_count = Ticket.distinct.where(query, *bind_params).joins(tables).count
        tickets = Ticket.distinct.where(query, *bind_params).joins(tables).limit(limit)
        next [ticket_count, tickets]
      end

      tickets = "TicketPolicy::#{access.camelize}Scope".constantize
                                                       .new(current_user).resolve
                                                       .distinct
                                                       .where(query, *bind_params)
                                                       .joins(tables)

      next [tickets.count, tickets.limit(limit)]
    rescue ActiveRecord::StatementInvalid => e
      Rails.logger.error e
      raise ActiveRecord::Rollback
    end
  end

=begin

generate condition query to search for tickets based on condition

  query_condition, bind_condition, tables = selector2sql(params[:condition], current_user: current_user)

condition example

  {
    'ticket.title' => {
      operator: 'contains', # contains not
      value: 'some value',
    },
    'ticket.state_id' => {
      operator: 'is',
      value: [1,2,5]
    },
    'ticket.created_at' => {
      operator: 'after (absolute)', # after,before
      value: '2015-10-17T06:00:00.000Z',
    },
    'ticket.created_at' => {
      operator: 'within next (relative)', # within next, within last, after, before
      range: 'day', # minute|hour|day|month|year
      value: '25',
    },
    'ticket.owner_id' => {
      operator: 'is', # is not
      pre_condition: 'current_user.id',
    },
    'ticket.owner_id' => {
      operator: 'is', # is not
      pre_condition: 'specific',
      value: 4711,
    },
    'ticket.escalation_at' => {
      operator: 'is not', # not
      value: nil,
    },
    'ticket.tags' => {
      operator: 'contains all', # contains all|contains one|contains all not|contains one not
      value: 'tag1, tag2',
    },
  }

=end

  def self.selector2sql(selectors, options = {})
    Ticket::Selector::Sql.new(selector: selectors, options: options).get
  end

=begin

perform changes on ticket

  ticket.perform_changes(trigger, 'trigger', item, current_user_id)

  # or

  ticket.perform_changes(job, 'job', item, current_user_id)

=end

  def perform_changes(performable, perform_origin, item = nil, current_user_id = nil)

    perform = performable.perform
    logger.debug { "Perform #{perform_origin} #{perform.inspect} on Ticket.find(#{id})" }

    article = begin
      Ticket::Article.find_by(id: item.try(:dig, :article_id))
    rescue ArgumentError
      nil
    end

    # if the configuration contains the deletion of the ticket then
    # we skip all other ticket changes because they does not matter
    if perform['ticket.action'].present? && perform['ticket.action']['value'] == 'delete'
      perform.each_key do |key|
        (object_name, attribute) = key.split('.', 2)
        next if object_name != 'ticket'
        next if attribute == 'action'

        perform.delete(key)
      end
    end

    objects              = build_notification_template_objects(article)
    perform_notification = {}
    perform_article      = {}
    changed              = false
    perform.each do |key, value|
      (object_name, attribute) = key.split('.', 2)
      raise "Unable to update object #{object_name}.#{attribute}, only can update tickets, send notifications and create articles!" if object_name != 'ticket' && object_name != 'article' && object_name != 'notification'

      # send notification/create article (after changes are done)
      if object_name == 'article'
        perform_article[key] = value
        next
      end
      if object_name == 'notification'
        perform_notification[key] = value
        next
      end

      # Apply pending_time changes
      if key == 'ticket.pending_time'
        new_value = case value['operator']
                    when 'static'
                      value['value']
                    when 'relative'
                      TimeRangeHelper.relative(range: value['range'], value: value['value'])
                    end

        if new_value
          self[attribute] = new_value
          changed = true
          next
        end
      end

      # update tags
      if key == 'ticket.tags'
        next if value['value'].blank?

        tags = value['value'].split(',')
        case value['operator']
        when 'add'
          tags.each do |tag|
            tag_add(tag, current_user_id || 1)
          end
        when 'remove'
          tags.each do |tag|
            tag_remove(tag, current_user_id || 1)
          end
        else
          logger.error "Unknown #{attribute} operator #{value['operator']}"
        end
        next
      end

      # delete ticket
      if key == 'ticket.action'
        next if value['value'].blank?
        next if value['value'] != 'delete'

        logger.info { "Deleted ticket from #{perform_origin} #{perform.inspect} Ticket.find(#{id})" }
        destroy!
        next
      end

      # lookup pre_condition
      if value['pre_condition']
        if value['pre_condition'].start_with?('not_set')
          value['value'] = 1
        elsif value['pre_condition'].start_with?('current_user.')
          raise __("The required parameter 'current_user_id' is missing.") if !current_user_id

          value['value'] = current_user_id
        end
      end

      # update ticket
      next if self[attribute].to_s == value['value'].to_s

      changed = true

      if value['value'].is_a?(String)
        value['value'] = NotificationFactory::Mailer.template(
          templateInline: value['value'],
          objects:        objects,
          quote:          true,
        )
      end

      self[attribute] = value['value']
      logger.debug { "set #{object_name}.#{attribute} = #{value['value'].inspect} for ticket_id #{id}" }
    end

    if changed
      save!
    end

    perform_article.each do |key, value|
      raise __("Article could not be created. An unsupported key other than 'article.note' was provided.") if key != 'article.note'

      add_trigger_note(id, value, objects, perform_origin)
    end

    perform_notification.each do |key, value|

      # send notification
      case key
      when 'notification.sms'
        send_sms_notification(value, article, perform_origin)
        next
      when 'notification.email'
        send_email_notification(value, article, perform_origin)
      when 'notification.webhook'
        TriggerWebhookJob.perform_later(performable, self, article)
      end
    end

    true
  end

=begin

perform changes on ticket

  ticket.add_trigger_note(ticket_id, note, objects, perform_origin)

=end

  def add_trigger_note(ticket_id, note, objects, perform_origin)
    rendered_subject = NotificationFactory::Mailer.template(
      templateInline: note[:subject],
      objects:        objects,
      quote:          true,
    )

    rendered_body = NotificationFactory::Mailer.template(
      templateInline: note[:body],
      objects:        objects,
      quote:          true,
    )

    Ticket::Article.create!(
      ticket_id:     ticket_id,
      subject:       rendered_subject,
      content_type:  'text/html',
      body:          rendered_body,
      internal:      note[:internal],
      sender:        Ticket::Article::Sender.find_by(name: 'System'),
      type:          Ticket::Article::Type.find_by(name: 'note'),
      preferences:   {
        perform_origin: perform_origin,
        notification:   true,
      },
      updated_by_id: 1,
      created_by_id: 1,
    )
  end

=begin

perform active triggers on ticket

  Ticket.perform_triggers(ticket, article, item, options)

=end

  def self.perform_triggers(ticket, article, item, options = {})
    recursive = Setting.get('ticket_trigger_recursive')
    type = options[:type] || item[:type]
    local_options = options.clone
    local_options[:type] = type
    local_options[:reset_user_id] = true
    local_options[:disable] = ['Transaction::Notification']
    local_options[:trigger_ids] ||= {}
    local_options[:trigger_ids][ticket.id.to_s] ||= []
    local_options[:loop_count] ||= 0
    local_options[:loop_count] += 1

    ticket_trigger_recursive_max_loop = Setting.get('ticket_trigger_recursive_max_loop')&.to_i || 10
    if local_options[:loop_count] > ticket_trigger_recursive_max_loop
      message = "Stopped perform_triggers for this object (Ticket/#{ticket.id}), because loop count was #{local_options[:loop_count]}!"
      logger.info { message }
      return [false, message]
    end

    triggers = if Rails.configuration.db_case_sensitive
                 ::Trigger.where(active: true).order(Arel.sql('LOWER(name)'))
               else
                 ::Trigger.where(active: true).order(:name)
               end
    return [true, __('No triggers active')] if triggers.blank?

    # check if notification should be send because of customer emails
    send_notification = true
    if local_options[:send_notification] == false
      send_notification = false
    elsif item[:article_id]
      article = Ticket::Article.lookup(id: item[:article_id])
      if article&.preferences && article.preferences['send-auto-response'] == false
        send_notification = false
      end
    end

    Transaction.execute(local_options) do
      triggers.each do |trigger|
        logger.debug { "Probe trigger (#{trigger.name}/#{trigger.id}) for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }

        user_id = ticket.updated_by_id
        if article
          user_id = article.updated_by_id
        end

        user = User.lookup(id: user_id)

        # verify is condition is matching
        ticket_count, tickets = Ticket.selectors(trigger.condition, limit: 1, execution_time: true, current_user: user, access: 'ignore', ticket_action: type, ticket_id: ticket.id, article_id: article&.id, changes: item[:changes], changes_required: true)

        next if ticket_count.blank?
        next if ticket_count.zero?
        next if tickets.first.id != ticket.id

        if recursive == false && local_options[:loop_count] > 1
          message = "Do not execute recursive triggers per default until Zammad 3.0. With Zammad 3.0 and higher the following trigger is executed '#{trigger.name}' on Ticket:#{ticket.id}. Please review your current triggers and change them if needed."
          logger.info { message }
          return [true, message]
        end

        if article && send_notification == false && trigger.perform['notification.email'] && trigger.perform['notification.email']['recipient']
          recipient = trigger.perform['notification.email']['recipient']
          local_options[:send_notification] = false
          if recipient.include?('ticket_customer') || recipient.include?('article_last_sender')
            logger.info { "Skip trigger (#{trigger.name}/#{trigger.id}) because sender do not want to get auto responder for object (Ticket/#{ticket.id}/Article/#{article.id})" }
            next
          end
        end

        if local_options[:trigger_ids][ticket.id.to_s].include?(trigger.id)
          logger.info { "Skip trigger (#{trigger.name}/#{trigger.id}) because was already executed for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
          next
        end
        local_options[:trigger_ids][ticket.id.to_s].push trigger.id
        logger.info { "Execute trigger (#{trigger.name}/#{trigger.id}) for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }

        ticket.perform_changes(trigger, 'trigger', item, user_id)

        if recursive == true
          TransactionDispatcher.commit(local_options)
        end
      end
    end
    [true, ticket, local_options]
  end

=begin

get all email references headers of a ticket, to exclude some, parse it as array into method

  references = ticket.get_references

result

  ['message-id-1234', 'message-id-5678']

ignore references header(s)

  references = ticket.get_references(['message-id-5678'])

result

  ['message-id-1234']

=end

  def get_references(ignore = [])
    references = []
    Ticket::Article.select('in_reply_to, message_id').where(ticket_id: id).each do |article|
      if article.in_reply_to.present?
        references.push article.in_reply_to
      end
      next if article.message_id.blank?

      references.push article.message_id
    end
    ignore.each do |item|
      references.delete(item)
    end
    references
  end

=begin

get all articles of a ticket in correct order (overwrite active record default method)

  articles = ticket.articles

result

  [article1, article2]

=end

  def articles
    Ticket::Article.where(ticket_id: id).order(:created_at, :id)
  end

  # Get whichever #last_contact_* was later
  # This is not identical to #last_contact_at
  # It returns time to last original (versus follow up) contact
  # @return [Time, nil]
  def last_original_update_at
    [last_contact_agent_at, last_contact_customer_at].compact.max
  end

  # true if conversation did happen and agent responded
  # false if customer is waiting for response or agent reached out and customer did not respond yet
  # @return [Bool]
  def agent_responded?
    return false if last_contact_customer_at.blank?
    return false if last_contact_agent_at.blank?

    last_contact_customer_at < last_contact_agent_at
  end

  private

  def check_generate
    return true if number

    self.number = Ticket::Number.generate
    true
  end

  def check_title
    return true if !title

    title.gsub!(%r{\s|\t|\r}, ' ')
    true
  end

  def check_defaults
    if !owner_id
      self.owner_id = 1
    end
    return true if !customer_id

    customer = User.find_by(id: customer_id)
    return true if !customer
    return true if organization_id.present? && customer.organization_id?(organization_id)

    self.organization_id = customer.organization_id
    true
  end

  def reset_pending_time

    # ignore if no state has changed
    return true if !changes_to_save['state_id']

    # ignore if new state is blank and
    # let handle ActiveRecord the error
    return if state_id.blank?

    # check if new state isn't pending*
    current_state      = Ticket::State.lookup(id: state_id)
    current_state_type = Ticket::StateType.lookup(id: current_state.state_type_id)

    # in case, set pending_time to nil
    return true if current_state_type.name.match?(%r{^pending}i)

    self.pending_time = nil
    true
  end

  def set_default_state
    return true if state_id

    default_ticket_state = Ticket::State.find_by(default_create: true)
    return true if !default_ticket_state

    self.state_id = default_ticket_state.id
    true
  end

  def set_default_priority
    return true if priority_id

    default_ticket_priority = Ticket::Priority.find_by(default_create: true)
    return true if !default_ticket_priority

    self.priority_id = default_ticket_priority.id
    true
  end

  def check_owner_active
    return true if Setting.get('import_mode')

    # only change the owner for non closed Tickets for historical/reporting reasons
    return true if state.present? && Ticket::StateType.lookup(id: state.state_type_id)&.name == 'closed'

    # return when ticket is unassigned
    return true if owner_id.blank?
    return true if owner_id == 1

    # return if owner is active, is agent and has access to group of ticket
    return true if owner.active? && owner.permissions?('ticket.agent') && owner.group_access?(group_id, 'full')

    # else set the owner of the ticket to the default user as unassigned
    self.owner_id = 1
    true
  end

  # articles.last breaks (returns the wrong article)
  # if another email notification trigger preceded this one
  # (see https://github.com/zammad/zammad/issues/1543)
  def build_notification_template_objects(article)
    last_article = nil
    last_internal_article = nil
    last_external_article = nil
    all_articles = articles

    if article.nil?
      last_article = all_articles.last
      last_internal_article = all_articles.reverse.find(&:internal?)
      last_external_article = all_articles.reverse.find { |a| !a.internal? }
    else
      last_article = article
      last_internal_article = article.internal? ? article : all_articles.reverse.find(&:internal?)
      last_external_article = article.internal? ? all_articles.reverse.find { |a| !a.internal? } : article
    end

    {
      ticket:                   self,
      article:                  last_article,
      last_article:             last_article,
      last_internal_article:    last_internal_article,
      last_external_article:    last_external_article,
      created_article:          article,
      created_internal_article: article&.internal? ? article : nil,
      created_external_article: article&.internal? ? nil : article,
    }
  end

  def send_email_notification(value, article, perform_origin)
    # value['recipient'] was a string in the past (single-select) so we convert it to array if needed
    value_recipient = Array(value['recipient'])

    recipients_raw = []
    value_recipient.each do |recipient|
      case recipient
      when 'article_last_sender'
        if article.present?
          if article.reply_to.present?
            recipients_raw.push(article.reply_to)
          elsif article.from.present?
            recipients_raw.push(article.from)
          elsif article.origin_by_id
            email = User.find_by(id: article.origin_by_id).email
            recipients_raw.push(email)
          elsif article.created_by_id
            email = User.find_by(id: article.created_by_id).email
            recipients_raw.push(email)
          end
        end
      when 'ticket_customer'
        email = User.find_by(id: customer_id).email
        recipients_raw.push(email)
      when 'ticket_owner'
        email = User.find_by(id: owner_id).email
        recipients_raw.push(email)
      when 'ticket_agents'
        User.group_access(group_id, 'full').sort_by(&:login).each do |user|
          recipients_raw.push(user.email)
        end
      when %r{\Auserid_(\d+)\z}
        user = User.lookup(id: $1)
        if !user
          logger.warn "Can't find configured Trigger Email recipient User with ID '#{$1}'"
          next
        end
        recipients_raw.push(user.email)
      else
        logger.error "Unknown email notification recipient '#{recipient}'"
        next
      end
    end

    recipients_checked = []
    recipients_raw.each do |recipient_email|

      users = User.where(email: recipient_email)
      next if users.any? { |user| !trigger_based_notification?(user) }

      # send notifications only to email addresses
      next if recipient_email.blank?

      # check if address is valid
      begin
        Mail::AddressList.new(recipient_email).addresses.each do |address|
          recipient_email = address.address
          email_address_validation = EmailAddressValidation.new(recipient_email)
          break if recipient_email.present? && email_address_validation.valid?
        end
      rescue
        if recipient_email.present?
          if recipient_email !~ %r{^(.+?)<(.+?)@(.+?)>$}
            next # no usable format found
          end

          recipient_email = "#{$2}@#{$3}" # rubocop:disable Lint/OutOfRangeRegexpRef
        end
      end

      email_address_validation = EmailAddressValidation.new(recipient_email)
      next if !email_address_validation.valid?

      # do not send notification if system address
      next if EmailAddress.exists?(email: recipient_email.downcase)

      # do not sent notifications to this recipients
      send_no_auto_response_reg_exp = Setting.get('send_no_auto_response_reg_exp')
      begin
        next if recipient_email.match?(%r{#{send_no_auto_response_reg_exp}}i)
      rescue => e
        logger.error "Invalid regex '#{send_no_auto_response_reg_exp}' in setting send_no_auto_response_reg_exp"
        logger.error e
        next if recipient_email.match?(%r{(mailer-daemon|postmaster|abuse|root|noreply|noreply.+?|no-reply|no-reply.+?)@.+?}i)
      end

      # check if notification should be send because of customer emails
      if article.present? && article.preferences.fetch('is-auto-response', false) == true && article.from && article.from =~ %r{#{Regexp.quote(recipient_email)}}i
        logger.info "Send no trigger based notification to #{recipient_email} because of auto response tagged incoming email"
        next
      end

      # loop protection / check if maximal count of trigger mail has reached
      map = {
        10  => 10,
        30  => 15,
        60  => 25,
        180 => 50,
        600 => 100,
      }
      skip = false
      map.each do |minutes, count|
        already_sent = Ticket::Article.where(
          ticket_id: id,
          sender:    Ticket::Article::Sender.find_by(name: 'System'),
          type:      Ticket::Article::Type.find_by(name: 'email'),
        ).where('ticket_articles.created_at > ? AND ticket_articles.to LIKE ?', Time.zone.now - minutes.minutes, "%#{recipient_email.strip}%").count
        next if already_sent < count

        logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} for this ticket within last #{minutes} minutes (loop protection)"
        skip = true
        break
      end
      next if skip

      map = {
        10  => 30,
        30  => 60,
        60  => 120,
        180 => 240,
        600 => 360,
      }
      skip = false
      map.each do |minutes, count|
        already_sent = Ticket::Article.where(
          sender: Ticket::Article::Sender.find_by(name: 'System'),
          type:   Ticket::Article::Type.find_by(name: 'email'),
        ).where('ticket_articles.created_at > ? AND ticket_articles.to LIKE ?', Time.zone.now - minutes.minutes, "%#{recipient_email.strip}%").count
        next if already_sent < count

        logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} in total within last #{minutes} minutes (loop protection)"
        skip = true
        break
      end
      next if skip

      email = recipient_email.downcase.strip
      next if recipients_checked.include?(email)

      recipients_checked.push(email)
    end

    return if recipients_checked.blank?

    recipient_string = recipients_checked.join(', ')

    group_id = self.group_id
    return if !group_id

    email_address = Group.find(group_id).email_address
    if !email_address
      logger.info "Unable to send trigger based notification to #{recipient_string} because no email address is set for group '#{group.name}'"
      return
    end

    if !email_address.channel_id
      logger.info "Unable to send trigger based notification to #{recipient_string} because no channel is set for email address '#{email_address.email}' (id: #{email_address.id})"
      return
    end

    security = nil
    security_type = nil
    if Setting.get('smime_integration')
      security_type = 'S/MIME'
    elsif Setting.get('pgp_integration')
      security_type = 'PGP'
    end
    if security_type
      sign       = value['sign'].present? && value['sign'] != 'no'
      encryption = value['encryption'].present? && value['encryption'] != 'no'
      security   = {
        type: security_type,
        sign:       {
          success: false,
        },
        encryption: {
          success: false,
        },
      }

      if sign
        sign_found = false
        cert = nil
        if security_type == 'PGP'
          cert = PGPKeypair.for_sender_email_address(from)
        else
          cert = SMIMECertificate.for_sender_email_address(from)
        end
        begin
          list = Mail::AddressList.new(email_address.email)
          from = list.addresses.first.to_s

          if cert && !cert.expired?
            sign_found                = true
            security[:sign][:success] = true
            security[:sign][:comment] = "certificate for #{email_address.email} found"
          end
        rescue # rubocop:disable Lint/SuppressedException
        end

        if value['sign'] == 'discard' && !sign_found
          logger.info "Unable to send trigger based notification to #{recipient_string} because of missing group #{group.name} email #{email_address.email} certificate for signing (discarding notification)."
          return
        end
      end

      if encryption
        certs_found = false
        begin
          if security_type == 'PGP'
            PGPKeypair.for_recipient_email_addresses!(recipients_checked)
          else
            SMIMECertificate.for_recipipent_email_addresses!(recipients_checked)
          end
          certs_found                     = true
          security[:encryption][:success] = true
          security[:encryption][:comment] = "certificates found for #{recipient_string}"
        rescue # rubocop:disable Lint/SuppressedException
        end

        if value['encryption'] == 'discard' && !certs_found
          logger.info "Unable to send trigger based notification to #{recipient_string} because public certificate is not available for encryption (discarding notification)."
          return
        end
      end
    end

    objects = build_notification_template_objects(article)

    # get subject
    subject = NotificationFactory::Mailer.template(
      templateInline: value['subject'],
      objects:        objects,
      quote:          false,
    )
    subject = subject_build(subject)

    body = NotificationFactory::Mailer.template(
      templateInline: value['body'],
      objects:        objects,
      quote:          true,
    )

    (body, attachments_inline) = HtmlSanitizer.replace_inline_images(body, id)

    preferences                  = {}
    preferences[:perform_origin] = perform_origin
    if security.present?
      preferences[:security] = security
    end

    message = Ticket::Article.create(
      ticket_id:     id,
      to:            recipient_string,
      subject:       subject,
      content_type:  'text/html',
      body:          body,
      internal:      value['internal'] || false, # default to public if value was not set
      sender:        Ticket::Article::Sender.find_by(name: 'System'),
      type:          Ticket::Article::Type.find_by(name: 'email'),
      preferences:   preferences,
      updated_by_id: 1,
      created_by_id: 1,
    )

    attachments_inline.each do |attachment|
      Store.create!(
        object:      'Ticket::Article',
        o_id:        message.id,
        data:        attachment[:data],
        filename:    attachment[:filename],
        preferences: attachment[:preferences],
      )
    end

    original_article = objects[:article]

    if ActiveModel::Type::Boolean.new.cast(value['include_attachments']) == true && original_article&.attachments.present?
      original_article.clone_attachments('Ticket::Article', message.id, only_attached_attachments: true)
    end

    if original_article&.should_clone_inline_attachments? # rubocop:disable Style/GuardClause
      original_article.clone_attachments('Ticket::Article', message.id, only_inline_attachments: true)
      original_article.should_clone_inline_attachments = false # cancel the temporary flag after cloning
    end
  end

  def sms_recipients_by_type(recipient_type, article)
    case recipient_type
    when 'article_last_sender'
      return nil if article.blank?

      if article.origin_by_id
        article.origin_by_id
      elsif article.created_by_id
        article.created_by_id
      end
    when 'ticket_customer'
      customer_id
    when 'ticket_owner'
      owner_id
    when 'ticket_agents'
      User.group_access(group_id, 'full').sort_by(&:login)
    when %r{\Auserid_(\d+)\z}
      return $1 if User.exists?($1)

      logger.warn "Can't find configured Trigger SMS recipient User with ID '#{$1}'"
      nil
    else
      logger.error "Unknown sms notification recipient '#{recipient}'"
      nil
    end
  end

  def build_sms_recipients_list(value, article)
    Array(value['recipient'])
      .each_with_object([]) { |recipient_type, sum| sum.concat(Array(sms_recipients_by_type(recipient_type, article))) }
      .map { |user_or_id| user_or_id.is_a?(User) ? user_or_id : User.lookup(id: user_or_id) }
      .uniq(&:id)
      .select { |user| user.mobile.present? }
  end

  def send_sms_notification(value, article, perform_origin)
    sms_recipients = build_sms_recipients_list(value, article)

    if sms_recipients.blank?
      logger.debug "No SMS recipients found for Ticket# #{number}"
      return
    end

    sms_recipients_to = sms_recipients
                        .map { |recipient| "#{recipient.fullname} (#{recipient.mobile})" }
                        .join(', ')

    channel = Channel.find_by(area: 'Sms::Notification')
    if !channel.active?
      # write info message since we have an active trigger
      logger.info "Found possible SMS recipient(s) (#{sms_recipients_to}) for Ticket# #{number} but SMS channel is not active."
      return
    end

    objects = build_notification_template_objects(article)
    body = NotificationFactory::Renderer.new(
      objects:  objects,
      template: value['body'],
      escape:   false
    ).render.html2text.tr(' ', ' ') # convert non-breaking space to simple space

    # attributes content_type is not needed for SMS
    Ticket::Article.create(
      ticket_id:     id,
      subject:       'SMS notification',
      to:            sms_recipients_to,
      body:          body,
      internal:      value['internal'] || false, # default to public if value was not set
      sender:        Ticket::Article::Sender.find_by(name: 'System'),
      type:          Ticket::Article::Type.find_by(name: 'sms'),
      preferences:   {
        perform_origin: perform_origin,
        sms_recipients: sms_recipients.map(&:mobile),
        channel_id:     channel.id,
      },
      updated_by_id: 1,
      created_by_id: 1,
    )
  end

  def trigger_based_notification?(user)
    blocked_in_days = trigger_based_notification_blocked_in_days(user)
    return true if blocked_in_days.zero?

    logger.info "Send no trigger based notification to #{user.email} because email is marked as mail_delivery_failed for #{blocked_in_days} day(s)"
    false
  end

  def trigger_based_notification_blocked_in_days(user)
    return 0 if !user.preferences[:mail_delivery_failed]
    return 0 if user.preferences[:mail_delivery_failed_data].blank?

    # blocked for 60 full days; see #4459
    remaining_days = (user.preferences[:mail_delivery_failed_data].to_date - Time.zone.now.to_date).to_i + 61
    return remaining_days if remaining_days.positive?

    # cleanup user preferences
    user.preferences[:mail_delivery_failed] = false
    user.preferences[:mail_delivery_failed_data] = nil
    user.save!
    0
  end

end
