module RedmineEventOutbox
  module Event
    module_function

    def record_issue_created(issue, actor)
      record(
        :event_type => 'issue.created',
        :source => issue,
        :project_id => issue.project_id,
        :issue_id => issue.id,
        :user_id => issue.author_id,
        :payload => issue_payload(issue, actor, 'issue.created')
      )
    end

    def record_issue_updated(issue, journal, actor)
      record(
        :event_type => 'issue.updated',
        :source => issue,
        :project_id => issue.project_id,
        :issue_id => issue.id,
        :journal_id => journal.try(:id),
        :user_id => actor.try(:id),
        :payload => issue_payload(issue, actor, 'issue.updated').merge(
          :journal_id => journal.try(:id),
          :changed_fields => journal_changed_fields(journal)
        )
      )
    end

    def record_journal_created(journal, actor)
      return unless journal && journal.journalized_type == 'Issue'

      issue = journal.issue || journal.journalized
      return unless issue

      record(
        :event_type => 'journal.created',
        :source => journal,
        :project_id => issue.project_id,
        :issue_id => issue.id,
        :journal_id => journal.id,
        :user_id => journal.user_id,
        :payload => journal_payload(journal, issue, actor)
      )
    end

    def record_contact_created(contact, actor)
      record_contact_event('contact.created', contact, actor)
    end

    def record_contact_updated(contact, actor)
      record_contact_event('contact.updated', contact, actor)
    end

    def record_helpdesk_ticket_created(helpdesk_ticket, actor)
      record_helpdesk_ticket_event('helpdesk_ticket.created', helpdesk_ticket, actor)
    end

    def record_helpdesk_ticket_updated(helpdesk_ticket, actor)
      record_helpdesk_ticket_event('helpdesk_ticket.updated', helpdesk_ticket, actor)
    end

    def record_journal_message_created(journal_message, actor)
      record_journal_message_event('journal_message.created', journal_message, actor)
    end

    def record_journal_message_updated(journal_message, actor)
      record_journal_message_event('journal_message.updated', journal_message, actor)
    end

    def record_contact_event(event_type, contact, actor)
      record(
        :event_type => event_type,
        :source => contact,
        :project_id => primary_project_id(contact),
        :user_id => actor.try(:id),
        :payload => contact_payload(contact, actor, event_type)
      )
    end

    def record_helpdesk_ticket_event(event_type, helpdesk_ticket, actor)
      issue = helpdesk_ticket.issue

      record(
        :event_type => event_type,
        :source => helpdesk_ticket,
        :project_id => issue.try(:project_id),
        :issue_id => helpdesk_ticket.issue_id,
        :user_id => actor.try(:id),
        :payload => helpdesk_ticket_payload(helpdesk_ticket, issue, actor, event_type)
      )
    end

    def record_journal_message_event(event_type, journal_message, actor)
      journal = journal_message.journal
      issue = journal.try(:issue) || journal.try(:journalized)

      record(
        :event_type => event_type,
        :source => journal_message,
        :project_id => issue.try(:project_id),
        :issue_id => issue.try(:id),
        :journal_id => journal_message.journal_id,
        :user_id => journal.try(:user_id) || actor.try(:id),
        :payload => journal_message_payload(journal_message, journal, issue, actor, event_type)
      )
    end

    def record(attributes)
      source = attributes.fetch(:source)
      payload = attributes.fetch(:payload)

      event = EventOutboxEvent.new(
        :event_type => attributes.fetch(:event_type),
        :source_type => source.class.name,
        :source_id => source.id,
        :project_id => attributes[:project_id],
        :issue_id => attributes[:issue_id],
        :journal_id => attributes[:journal_id],
        :user_id => attributes[:user_id],
        :occurred_at => Time.now
      )
      event.payload_data = payload
      event.save!
    rescue => e
      Rails.logger.error(
        "RedmineEventOutbox: failed to record #{attributes[:event_type]} " \
        "for #{source.class.name}##{source.try(:id)}: #{e.class}: #{e.message}"
      )
      nil
    end

    def issue_payload(issue, actor, event_type)
      {
        :event_type => event_type,
        :issue_id => issue.id,
        :project_id => issue.project_id,
        :tracker_id => issue.tracker_id,
        :status_id => issue.status_id,
        :priority_id => issue.priority_id,
        :author_id => issue.author_id,
        :author_name => principal_name(issue.author),
        :assigned_to_id => issue.assigned_to_id,
        :assigned_to_name => principal_name(issue.assigned_to),
        :actor_id => actor.try(:id),
        :actor_name => principal_name(actor),
        :subject => issue.subject,
        :created_on => iso8601(issue.created_on),
        :updated_on => iso8601(issue.updated_on)
      }
    end

    def journal_payload(journal, issue, actor)
      {
        :event_type => 'journal.created',
        :journal_id => journal.id,
        :issue_id => issue.id,
        :project_id => issue.project_id,
        :user_id => journal.user_id,
        :user_name => principal_name(journal.user),
        :actor_id => actor.try(:id),
        :actor_name => principal_name(actor),
        :subject => issue.subject,
        :private_notes => journal.private_notes?,
        :has_notes => journal.notes.present?,
        :changed_fields => journal_changed_fields(journal),
        :created_on => iso8601(journal.created_on)
      }
    end

    def contact_payload(contact, actor, event_type)
      {
        :event_type => event_type,
        :contact_id => contact.id,
        :project_ids => contact_project_ids(contact),
        :is_company => contact.is_company,
        :name => contact_name(contact),
        :company => contact.try(:company),
        :author_id => contact.try(:author_id),
        :author_name => principal_name(contact.try(:author)),
        :assigned_to_id => contact.try(:assigned_to_id),
        :assigned_to_name => principal_name(contact.try(:assigned_to)),
        :actor_id => actor.try(:id),
        :actor_name => principal_name(actor),
        :created_on => iso8601(contact.try(:created_on)),
        :updated_on => iso8601(contact.try(:updated_on))
      }
    end

    def helpdesk_ticket_payload(helpdesk_ticket, issue, actor, event_type)
      {
        :event_type => event_type,
        :helpdesk_ticket_id => helpdesk_ticket.id,
        :issue_id => helpdesk_ticket.issue_id,
        :project_id => issue.try(:project_id),
        :contact_id => helpdesk_ticket.contact_id,
        :message_id => helpdesk_ticket.message_id,
        :is_incoming => helpdesk_ticket.is_incoming?,
        :source => helpdesk_ticket.source,
        :from_address => helpdesk_ticket.from_address,
        :to_address => helpdesk_ticket.to_address,
        :cc_address => helpdesk_ticket.cc_address,
        :actor_id => actor.try(:id),
        :actor_name => principal_name(actor),
        :subject => issue.try(:subject),
        :ticket_date => iso8601(helpdesk_ticket.try(:ticket_date)),
        :updated_on => iso8601(helpdesk_ticket.try(:updated_on))
      }
    end

    def journal_message_payload(journal_message, journal, issue, actor, event_type)
      # Keep event rows useful for routing/index invalidation without copying
      # email bodies, private notes, attachments, or BCC addresses into outbox.
      {
        :event_type => event_type,
        :journal_message_id => journal_message.id,
        :journal_id => journal_message.journal_id,
        :issue_id => issue.try(:id),
        :project_id => issue.try(:project_id),
        :contact_id => journal_message.contact_id,
        :message_id => journal_message.message_id,
        :is_incoming => journal_message.is_incoming?,
        :source => journal_message.source,
        :from_address => journal_message.from_address,
        :to_address => journal_message.to_address,
        :cc_address => journal_message.cc_address,
        :has_bcc_address => journal_message.bcc_address.present?,
        :user_id => journal.try(:user_id),
        :user_name => principal_name(journal.try(:user)),
        :actor_id => actor.try(:id),
        :actor_name => principal_name(actor),
        :subject => issue.try(:subject),
        :private_notes => journal.try(:private_notes?),
        :has_notes => journal.try(:notes).present?,
        :message_date => iso8601(journal_message.try(:message_date))
      }
    end

    def journal_changed_fields(journal)
      return [] unless journal && journal.respond_to?(:details)
      journal.details.map(&:prop_key).compact.uniq
    end

    def contact_project_ids(contact)
      return [] unless contact.respond_to?(:projects)
      contact.projects.map(&:id).compact
    rescue
      []
    end

    def primary_project_id(contact)
      contact_project_ids(contact).first
    end

    def contact_name(contact)
      contact.try(:name).presence || [contact.try(:first_name), contact.try(:last_name)].compact.join(' ').presence
    end

    def principal_name(principal)
      principal.try(:name).presence if principal
    end

    def iso8601(value)
      value.try(:utc).try(:iso8601)
    end
  end
end
