class TheatrePostSelector
  Candidate = Struct.new(:post, :weight, :penalty, :tags, keyword_init: true)
  ELIGIBLE_POST_URL_CONDITION =
      ["url LIKE '%nicovideo.jp%'",
       "url LIKE '%youtube.com/watch%'",
       "url LIKE '%youtu.be/%'"]
      .join(' OR ')

  def initialize theatre:
    @theatre = theatre
  end

  def select
    candidates = weighted_candidates
    return nil if candidates.empty?

    total = candidates.sum(&:weight)
    target = rand * total

    candidates.each do |candidate|
      target -= candidate.weight
      return candidate.post if target <= 0
    end

    candidates.last.post
  end

  def weight_json limit: 20
    candidates = weighted_candidates
    sorted = candidates.sort_by { |candidate| [candidate.weight, candidate.post.id] }

    { tag_penalties: tag_penalty_json,
      lightest_posts: post_weight_json(sorted.first(limit)),
      heaviest_posts: post_weight_json(sorted.reverse.first(limit)) }
  end

  private

  attr_reader :theatre

  def weighted_candidates
    @weighted_candidates ||= begin
      penalties = tag_penalties
      posts = eligible_posts.includes(tags: :tag_name).to_a

      posts.map do |post|
        post_tags = post.tags.to_a
        penalty = post_tags.sum { |tag| penalties[tag.id].to_i }

        Candidate.new(
            post:,
            penalty:,
            tags: post_tags,
            weight: 1.0 / (1.0 + penalty))
      end
    end
  end

  def eligible_posts
    posts = Post.where(ELIGIBLE_POST_URL_CONDITION)
    posts = posts.where.not(id: theatre.current_post_id) if theatre.current_post_id
    posts
  end

  def active_user_ids
    @active_user_ids ||= theatre.watching_users.ids
  end

  def tag_penalties
    @tag_penalties ||=
      if active_user_ids.empty?
        {}
      else
        TheatreSkipEventVoter
          .joins(theatre_skip_event: :event_tags)
          .where(user_id: active_user_ids)
          .group('theatre_skip_event_tags.tag_id')
          .count
      end
  end

  def tag_penalty_json
    return [] if tag_penalties.empty?

    tags = Tag.where(id: tag_penalties.keys).includes(:tag_name).index_by(&:id)

    tag_penalties
      .map { |tag_id, penalty|
        tag = tags[tag_id]
        next unless tag

        { tag: light_tag_json(tag),
          penalty: }
      }
      .compact
      .sort_by { |row| [-row[:penalty], row[:tag][:name].to_s] }
  end

  def post_weight_json candidates
    candidates.map { |candidate|
      { post: light_post_json(candidate.post),
        weight: candidate.weight,
        penalty: candidate.penalty,
        tags: candidate.tags.map { |tag| light_tag_json(tag) } }
    }
  end

  def light_post_json post
    { id: post.id,
      title: post.title,
      url: post.url }
  end

  def light_tag_json tag
    { id: tag.id,
      name: tag.name,
      category: tag.category }
  end
end
