# frozen_string_literal: true

# == Schema Information
#
# Table name: custom_filters
#
#  id         :bigint(8)        not null, primary key
#  action     :integer          default("warn"), not null
#  context    :string           default([]), not null, is an Array
#  expires_at :datetime
#  phrase     :text             default(""), not null
#  created_at :datetime         not null
#  updated_at :datetime         not null
#  account_id :bigint(8)        not null
#

class CustomFilter < ApplicationRecord
  self.ignored_columns += %w(whole_word irreversible)

  alias_attribute :title, :phrase
  alias_attribute :filter_action, :action

  VALID_CONTEXTS = %w(
    home
    notifications
    public
    thread
    account
  ).freeze

  EXPIRATION_DURATIONS = [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].freeze

  TITLE_LENGTH_LIMIT = 256

  include Expireable
  include Redisable

  enum :action, { warn: 0, hide: 1, blur: 2 }, suffix: :action, validate: true

  belongs_to :account
  has_many :keywords, class_name: 'CustomFilterKeyword', inverse_of: :custom_filter, dependent: :destroy
  has_many :statuses, class_name: 'CustomFilterStatus', inverse_of: :custom_filter, dependent: :destroy
  accepts_nested_attributes_for :keywords, reject_if: :all_blank, allow_destroy: true

  validates :title, :context, presence: true
  validates :title, length: { maximum: TITLE_LENGTH_LIMIT }
  validate :context_must_be_valid

  normalizes :context, with: ->(context) { context.map(&:strip).filter_map(&:presence) }
  scope :unexpired, -> { where(expires_at: nil).or where.not(expires_at: ..Time.zone.now) }

  before_save :prepare_cache_invalidation!
  before_destroy :prepare_cache_invalidation!
  after_commit :invalidate_cache!

  def expires_in
    return nil if expires_at.nil?

    EXPIRATION_DURATIONS.find { |expires_in| expires_in.from_now >= expires_at }
  end

  def irreversible=(value)
    self.action = ActiveModel::Type::Boolean.new.cast(value) ? :hide : :warn
  end

  def irreversible?
    hide_action?
  end

  def self.cached_filters_for(account_id)
    raise ArgumentError unless account_id.is_a?(String) || account_id.is_a?(Integer)

    active_filters = Rails.cache.fetch("filters:v3:#{account_id}") do
      filters_hash = {}

      scope = CustomFilterKeyword.left_outer_joins(:custom_filter).merge(unexpired.where(account_id: account_id))

      scope.to_a.group_by(&:custom_filter).each do |filter, keywords|
        keywords.map!(&:to_regex)

        filters_hash[filter.id] = { keywords: Regexp.union(keywords), filter: filter }
      end.to_h

      scope = CustomFilterStatus.left_outer_joins(:custom_filter).merge(unexpired.where(account_id: account_id))

      scope.to_a.group_by(&:custom_filter).each do |filter, statuses|
        filters_hash[filter.id] ||= { filter: filter }
        filters_hash[filter.id].merge!(status_ids: statuses.map(&:status_id))
      end

      filters_hash.values.map { |cache| [cache.delete(:filter), cache] }
    end.to_a

    active_filters.reject { |custom_filter, _| custom_filter.expired? }
  end

  def self.apply_cached_filters(cached_filters, status)
    cached_filters.filter_map do |filter, rules|
      match = rules[:keywords].match(status.proper.searchable_text) if rules[:keywords].present?
      keyword_matches = [match.to_s] unless match.nil?

      status_matches = [status.id, status.reblog_of_id].compact & rules[:status_ids] if rules[:status_ids].present?

      next if keyword_matches.blank? && status_matches.blank?

      FilterResultPresenter.new(filter: filter, keyword_matches: keyword_matches, status_matches: status_matches)
    end
  end

  def prepare_cache_invalidation!
    @should_invalidate_cache = true
  end

  def invalidate_cache!
    return unless @should_invalidate_cache

    @should_invalidate_cache = false

    Rails.cache.delete("filters:v3:#{account_id}")
    redis.publish("timeline:#{account_id}", { event: :filters_changed }.to_json)
    redis.publish("timeline:system:#{account_id}", { event: :filters_changed }.to_json)
  end

  private

  def context_must_be_valid
    errors.add(:context, I18n.t('filters.errors.invalid_context')) if invalid_context_value?
  end

  def invalid_context_value?
    context.blank? || context.difference(VALID_CONTEXTS).any?
  end
end
