import { isLearnedSemanticQuestion,
	 learnedSemanticSideForPost } from '@/lib/gekanator'

import type { GekanatorAnswerLog, GekanatorAnswerValue, GekanatorQuestion } from '@/lib/gekanator'
import type { Post } from '@/types'

export type RecoveredCandidatePost = {
    postId:                number
    answerCountAtRecovery: number
    scoreAtRecovery:       number }

export type RecoveredCandidateState = {
    answerCountAtRecovery: number
    scoreAtRecovery:       number }


const questionSupportsAnswerBasedHardFiltering = (question:  GekanatorQuestion): boolean =>
    !(isLearnedSemanticQuestion (question)
      || (question.kind === 'tag'
	  && question.condition.type === 'tag'
	  && !(question.condition.key.startsWith ('nico:'))))


export const candidatePostsFor = (
    { posts,
      questions,
      answers,
      softenedQuestionIds,
      rejectedPostIds,
      recoveredCandidatePosts }: { posts:                   Post[]
				   questions:               GekanatorQuestion[]
				   answers:                 GekanatorAnswerLog[]
				   softenedQuestionIds:     Set<string>
				   rejectedPostIds:         Set<number>
				   recoveredCandidatePosts: Map<number, RecoveredCandidateState> }): Post[] => {
  const questionById = new Map (questions.map (question => [question.id, question]))

  return posts.filter (post => {
    if (rejectedPostIds.has (post.id))
      return false

    const recoveredCandidate = recoveredCandidatePosts.get (post.id)

    return answers.every ((answer, index) => {
      if (recoveredCandidate != null && index < recoveredCandidate.answerCountAtRecovery)
	return true

      if (softenedQuestionIds.has (answer.questionId))
	return true

      const question = questionById.get (answer.questionId)
      if (!(question))
	return true
      if (!(questionSupportsAnswerBasedHardFiltering (question)))
	return true

      switch (answer.answer)
      {
      case 'yes':
      case 'no':
	{
	  const expected = learnedSemanticSideForPost (question, post)
	  return expected === 'unknown'
	      || (answer.answer === 'yes' && expected === 'positive')
	      || (answer.answer === 'no' && expected === 'negative')
	}
      default:
	return true
      }
    })
  })
}


export const hardFilteredPostsForAnswer = (
    { posts, question, answer }: { posts:     Post[]
				   question:  GekanatorQuestion
				   answer:    GekanatorAnswerValue }): Post[] => {
  if (!(questionSupportsAnswerBasedHardFiltering (question)))
    return posts

  if (!(answer === 'yes' || answer === 'no'))
    return posts

  return posts.filter (post => {
    const side = learnedSemanticSideForPost (question, post)
    return side === 'unknown'
	|| (answer === 'yes' && side === 'positive')
	|| (answer === 'no' && side === 'negative')
  })
}


const concreteAnswerOptions: GekanatorAnswerValue[] = ['yes', 'no', 'partial', 'probably_no']


export const allConcreteAnswerOptionsExhausted = (
    posts:      Post[],
    question:   GekanatorQuestion | null): boolean => {
  if (!(question))
    return false

  return concreteAnswerOptions.every (answer =>
      hardFilteredPostsForAnswer ({ posts, question, answer }).length === 0)
}


const nextRecoveryTargetSize = (recoveryStepCount: number): number =>
    6 * (2 ** recoveryStepCount)


export const recoverCandidatePosts = (
    { posts,
      scores,
      rejectedPostIds,
      recoveredCandidatePosts,
      eligiblePostIds,
      answerCountAtRecovery,
      recoveryStepCount }: { posts:                   Post[]
			     scores:                  Map<number, number>
			     rejectedPostIds:         Set<number>
			     recoveredCandidatePosts: Map<number, RecoveredCandidateState>
			     eligiblePostIds:         Set<number>
			     answerCountAtRecovery:   number
			     recoveryStepCount:       number }): { recoveredCandidatePosts: Map<number, RecoveredCandidateState>
     recoveryStepCount:       number } | null => {
  const recovered = new Map (recoveredCandidatePosts)
  const targetSize = nextRecoveryTargetSize (recoveryStepCount)
  const countedPostIds = new Set ([...eligiblePostIds, ...recovered.keys ()])
  const addCount = targetSize - countedPostIds.size
  if (addCount <= 0)
    {
      return { recoveredCandidatePosts: recovered,
	       recoveryStepCount: recoveryStepCount + 1 }
    }

  const candidates =
      posts
      .filter (post => (!(rejectedPostIds.has (post.id))
			&& !(eligiblePostIds.has (post.id))
			&& !(recovered.has (post.id))))
      .sort ((a, b) => ((scores.get (b.id) ?? Number.NEGATIVE_INFINITY)
			- (scores.get (a.id) ?? Number.NEGATIVE_INFINITY)))
      .slice (0, addCount)

  if (candidates.length === 0)
    return null

  candidates.forEach (post => recovered.set (post.id, {
      answerCountAtRecovery,
      scoreAtRecovery: scores.get (post.id) ?? 0 }))

  return { recoveredCandidatePosts: recovered,
	   recoveryStepCount: recoveryStepCount + 1 }
}
