class GekanatorGamesController < ApplicationController
  def create
    return head :unauthorized unless current_user

    guessed_post_id = params.require(:guessed_post_id)
    correct_post_id = params[:correct_post_id].presence
    answers = params.require(:answers).as_json

    game = GekanatorGame.new(
        user: current_user,
        guessed_post_id:,
        correct_post_id:,
        won: correct_post_id.present? && guessed_post_id.to_i == correct_post_id.to_i,
        question_count: answers.length,
        answers:)

    if game.invalid?
      render json: { errors: game.errors.full_messages }, status: :unprocessable_entity
      return
    end

    learned_example_count = 0

    ActiveRecord::Base.transaction do
      game.save!
      learned_example_count = learn_answers_from_game!(game)
    end

    render json: {
      id: game.id,
      learned_example_count:
    }, status: :created
  rescue ActiveRecord::RecordInvalid => e
    render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
  end

  def extra_questions
    game = find_owned_game
    return if performed?

    questions =
        GekanatorQuestion
        .accepted
        .includes(:gekanator_question_examples)
        .where(kind: 'post_similarity', source: 'user_suggested')
        .to_a

    selected =
        prioritized_extra_questions(
            questions,
            post_id: game.correct_post_id,
            user: current_user,
            limit: 6)

    render json: {
      questions: selected.map { |question| extra_question_json(question) }
    }
  end

  def extra_question_answers
    game = find_owned_game
    return if performed?

    answer_params = params.require(:answers)
    if !answer_params.is_a?(Array)
      return render_validation_error fields: { answers: ['配列で指定してください．'] }
    end

    answers = answer_params.map { |answer|
      {
        question_id: answer.require(:question_id).to_i,
        answer: answer.require(:answer)
      }
    }
    questions = GekanatorQuestion.where(id: answers.map { _1[:question_id] })
    question_by_id = questions.index_by(&:id)
    if questions.length != answers.length
      return render_validation_error fields: { answers: ['質問が見つかりません．'] }
    end
    if questions.any? { |question| question.status != 'accepted' || question.kind != 'post_similarity' }
      return render_validation_error fields: { answers: ['質問が不正です．'] }
    end

    ActiveRecord::Base.transaction do
      answers.each do |item|
        question = question_by_id[item[:question_id]]
        example =
            GekanatorQuestionExample.find_or_initialize_by(
                gekanator_question: question,
                post: game.correct_post,
                user: current_user)
        example.record_answer!(
            answer: item[:answer],
            source: 'post_game_extra',
            gekanator_game: game)
        example.save!
      end
    end

    render json: { count: answers.length }, status: :created
  end

  private

  def extra_question_json question
    {
      id: question.id,
      text: question.text,
      source: question.source,
      priority_weight: question.priority_weight
    }
  end

  def prioritized_extra_questions questions, post_id:, user:, limit:
    answered_question_ids =
        GekanatorQuestionExample
        .where(user:, gekanator_question_id: questions.map(&:id))
        .distinct
        .pluck(:gekanator_question_id)
    unanswered, answered =
        questions.partition { |question| !answered_question_ids.include?(question.id) }
    selected = weighted_sample_questions(unanswered, post_id:, limit:)
    return selected if selected.length >= limit

    selected + weighted_sample_questions(
        answered.reject { |question| selected.any? { _1.id == question.id } },
        post_id:,
        limit: limit - selected.length)
  end

  def weighted_sample_questions questions, post_id:, limit:
    remaining = questions.uniq(&:id)
    selected = []

    while selected.length < limit && remaining.any?
      weighted =
          remaining.map { |question|
            [question, selection_weight_for(question, post_id: post_id)]
          }
      total_weight = weighted.sum { |_question, weight| weight }
      break if total_weight <= 0

      target = rand * total_weight
      cumulative = 0.0
      chosen =
          weighted.find do |_question, weight|
            cumulative += weight
            cumulative >= target
          end&.first || weighted.first.first

      selected << chosen
      remaining.reject! { |question| question.id == chosen.id }
    end

    selected
  end

  def selection_weight_for question, post_id:
    sample_count =
        question.gekanator_question_examples.sum { |example|
          next 0 unless example.post_id == post_id

          example.sample_count.presence || 1
        }

    question.priority_weight.to_f / (1.0 + sample_count * 0.15)
  end

  def find_owned_game
    return head :unauthorized unless current_user

    game = GekanatorGame.find_by(id: params[:id])
    return head :not_found unless game
    if !current_user.admin? && game.user_id != current_user.id
      return head :not_found
    end

    game
  end

  def learn_answers_from_game! game
    correct_post = game.correct_post
    return 0 if correct_post.blank?

    accepted_questions =
        GekanatorQuestion
        .accepted
        .index_by { |question| public_question_id_for(question) }
    learned_count = 0

    Array(game.answers).each do |answer|
      answer_value = answer['answer'].to_s
      next if answer_value.blank? || answer_value == 'unknown'

      question_id = game_answer_question_id(answer)
      next if question_id.blank?

      question = accepted_questions[question_id.to_s]
      next unless learnable_game_answer_question?(question)

      example =
          GekanatorQuestionExample.find_or_initialize_by(
              gekanator_question: question,
              post: correct_post,
              user: current_user)
      example.record_answer!(
          answer: answer_value,
          source: 'post_game_answer',
          gekanator_game: game)
      example.save!
      learned_count += 1
    end

    learned_count
  end

  def public_question_id_for question
    condition = normalize_condition(question.condition)

    case condition[:type]
    when 'tag'
      "tag:#{condition[:key]}"
    when 'source'
      "source:#{condition[:host]}"
    when 'original-year'
      "original-year:#{condition[:year]}"
    when 'original-month'
      "original-month:#{condition[:month]}"
    when 'original-month-day'
      "original-month-day:#{condition[:monthDay] || condition[:month_day]}"
    when 'title-length-at-least'
      "title:length-at-least:#{condition[:length]}"
    when 'title-length-greater-than'
      "title:length-at-least:#{condition[:length].to_i + 1}"
    when 'title-has-ascii'
      'title:ascii'
    when 'title-contains'
      "title:contains:#{condition[:text]}"
    when 'post-similarity'
      "post-similarity:#{question.id}"
    else
      "catalog:#{question.id}"
    end
  end

  def normalize_condition condition
    json = condition.deep_dup.as_json

    if json['type'] == 'original-month-day' && json['monthDay'].blank?
      json['monthDay'] = json.delete('month_day')
    end

    json.deep_symbolize_keys
  end

  def learnable_game_answer_question? question
    return false if question.nil?
    return true if question.kind == 'post_similarity'
    return false unless question.kind == 'tag'

    condition = normalize_condition(question.condition)
    key = condition[:key].to_s
    !key.start_with?('nico:')
  end

  def game_answer_question_id answer
    answer['question_id'] ||
      answer[:question_id] ||
      answer['questionId'] ||
      answer[:questionId]
  end
end
