import { useMutation, useQuery } from '@tanstack/react-query'
import { useMemo, useState } from 'react'
import { Helmet } from 'react-helmet-async'

import PrefetchLink from '@/components/PrefetchLink'
import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config'
import { buildGekanatorQuestions,
	 fetchGekanatorPosts,
	 saveGekanatorGame } from '@/lib/gekanator'
import { gekanatorKeys } from '@/lib/queryKeys'
import { cn } from '@/lib/utils'

import type { FC } from 'react'

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

type Phase = 'intro' | 'question' | 'guess' | 'continue' | 'learned'

type AnswerOption = {
    label:  string
    value:  GekanatorAnswerValue }

type Confidence = {
    post:     Post
    score:    number
    percent:  number }

type AnswerPreview = {
    answer:              GekanatorAnswerValue
    top:                 Confidence | null
    candidateCount:      number
    effectiveCandidates: number
    entropy:             number }

type GameSnapshot = {
    phase:                   Phase
    scores:                  Map<number, number>
    answers:                 GekanatorAnswerLog[]
    askedIds:                Set<string>
    candidateIds:            Set<number> | null
    softenedQuestionIds:     Set<string>
    questionBank:            GekanatorQuestion[]
    search:                  string
    rejectedPostIds:         Set<number>
    lastGuessQuestionCount:  number
    lastRejectedGuessId:     number | null
    activeGuessId:           number | null }

const answerOptions: AnswerOption[] = [
    { label: 'はい', value: 'yes' },
    { label: 'いいえ', value: 'no' },
    { label: '部分的にそう', value: 'partial' },
    { label: 'たぶんいいえ', value: 'probably_no' },
    { label: 'わからない', value: 'unknown' }]

const questionsBetweenGuesses = 25
const hardMaxQuestions = 80
const softenedAnswerWeight = .35
const confidenceTemperature = 6


const deltaFor = (matched: boolean, answer: GekanatorAnswerValue): number => {
  switch (answer)
  {
  case 'yes':
    return matched ? 4 : -4
  case 'no':
    return matched ? -4 : 4
  case 'partial':
    return matched ? 2 : -1
  case 'probably_no':
    return matched ? -2 : 2
  case 'unknown':
    return 0
  }
}


const answerWeightFor = (
    questionId:           string,
    softenedQuestionIds:  Set<string>,
): number => softenedQuestionIds.has (questionId) ? softenedAnswerWeight : 1


const questionDifficulty = (question: GekanatorQuestion): number => {
  if (question.id === 'structure:many-tags')
    return 6
  if (question.id.startsWith ('date:'))
    return 5
  if (question.id === 'title:long' || question.id === 'title:ascii')
    return 4
  if (question.kind === 'source')
    return 4
  if (question.kind === 'tag')
    return 3
  if (question.kind === 'title' || question.kind === 'structure')
    return 2

  return 1
}


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

  answers.forEach (answer => {
    const question = questionById.get (answer.questionId)
    if (!(question))
      return

    const weight = answerWeightFor (answer.questionId, softenedQuestionIds)
    posts.forEach (post => {
      nextScores.set (
	  post.id,
	  (nextScores.get (post.id) ?? 0)
	      + deltaFor (question.test (post), answer.answer) * weight)
    })
  })

  return nextScores
}


const confidencesFor = (posts: Post[], scores: Map<number, number>): Confidence[] => {
  if (posts.length === 0)
    return []

  const raw = posts.map (post => ({ post, score: scores.get (post.id) ?? 0 }))
  const maxScore = Math.max (...raw.map (({ score }) => score))
  const weighted = raw.map (item => ({
      ...item,
      weight: Math.exp ((item.score - maxScore) / confidenceTemperature) }))
  const total = weighted.reduce ((sum, item) => sum + item.weight, 0) || 1

  return weighted
      .map (({ post, score, weight }) => ({
	  post,
	  score,
	  percent: weight / total * 100 }))
      .sort ((a, b) => b.percent - a.percent)
}


const entropyFor = (confidences: Confidence[]): number =>
    confidences.reduce ((sum, item) => {
      const p = item.percent / 100
      return p > 0 ? sum - p * Math.log2 (p) : sum
    }, 0)


const effectiveCandidatesFor = (confidences: Confidence[]): number => {
  const concentration = confidences.reduce ((sum, item) => {
    const p = item.percent / 100
    return sum + p * p
  }, 0)

  return concentration > 0 ? 1 / concentration : 0
}


const previewAnswer = ({
    posts,
    scores,
    question,
    answer,
}: {
    posts:     Post[]
    scores:    Map<number, number>
    question:  GekanatorQuestion
    answer:    GekanatorAnswerValue
}): AnswerPreview => {
  const hardFilteredPosts =
      answer === 'yes'
	? posts.filter (post => question.test (post))
	: answer === 'no'
	  ? posts.filter (post => !(question.test (post)))
	  : posts
  const nextPosts =
      (answer === 'yes' || answer === 'no') && hardFilteredPosts.length > 0
	? hardFilteredPosts
	: posts
  const nextScores = new Map (scores)
  nextPosts.forEach (post => {
    nextScores.set (
	post.id,
	(nextScores.get (post.id) ?? 0) + deltaFor (question.test (post), answer))
  })

  const confidences = confidencesFor (nextPosts, nextScores)

  return {
      answer,
      top: confidences[0] ?? null,
      candidateCount: nextPosts.length,
      effectiveCandidates: effectiveCandidatesFor (confidences),
      entropy: entropyFor (confidences) }
}


const mergeQuestions = (questions: GekanatorQuestion[]): GekanatorQuestion[] => {
  const byId = new Map<string, GekanatorQuestion> ()
  questions.forEach (question => byId.set (question.id, question))
  return [...byId.values ()]
}


const softenNextQuestionIds = ({
    questions,
    answers,
    softenedQuestionIds,
}: {
    questions:            GekanatorQuestion[]
    answers:              GekanatorAnswerLog[]
    softenedQuestionIds:  Set<string>
}): Set<string> | null => {
  const questionById = new Map (questions.map (question => [question.id, question]))
  const candidate = [...answers]
      .reverse ()
      .map (answer => {
	const question = questionById.get (answer.questionId)
	return { answer, question }
      })
      .filter ((item): item is {
	  answer:    GekanatorAnswerLog
	  question:  GekanatorQuestion } =>
	      item.question !== undefined
	      && item.answer.answer !== 'unknown'
	      && !(softenedQuestionIds.has (item.answer.questionId)))
      .sort ((a, b) => questionDifficulty (b.question) - questionDifficulty (a.question))[0]

  if (!(candidate))
    return null

  return new Set ([...softenedQuestionIds, candidate.answer.questionId])
}


const chooseQuestion = ({
    posts,
    questions,
    scores,
    askedIds,
}: {
    posts:      Post[]
    questions:  GekanatorQuestion[]
    scores:     Map<number, number>
    askedIds:   Set<string>
}): GekanatorQuestion | null => {
  const scoredPosts = posts
      .map (post => ({ post, score: scores.get (post.id) ?? 0 }))
      .sort ((a, b) => b.score - a.score)

  const signatureFor = (
      question:    GekanatorQuestion,
      candidates:  { post: Post; score: number }[],
  ): string => candidates.map (({ post }) => question.test (post) ? '1' : '0').join ('')

  const invertedSignature = (signature: string): string =>
      signature.replace (/[01]/g, value => value === '1' ? '0' : '1')

  const redundantSignatures = (
      candidates: { post: Post; score: number }[],
  ): Set<string> => {
    const signatures = new Set<string> ()
    questions
	.filter (question => askedIds.has (question.id))
	.forEach (question => {
	  const signature = signatureFor (question, candidates)
	  signatures.add (signature)
	  signatures.add (invertedSignature (signature))
	})

    return signatures
  }

  const rank = (
      questionsToRank:  GekanatorQuestion[],
      candidates:       { post: Post; score: number }[],
  ) => {
    const redundant = redundantSignatures (candidates)

    return questionsToRank
      .map (question => {
	const signature = signatureFor (question, candidates)
	if (redundant.has (signature))
	  return null

	const yes = signature.split ('').filter (value => value === '1').length
	const no = candidates.length - yes
	if (yes === 0 || no === 0)
	  return null

	const splitScore = Math.abs (candidates.length / 2 - yes)
	const answerPreviews = answerOptions.map (option =>
	    previewAnswer ({
		posts: candidates.map (({ post }) => post),
		scores,
		question,
		answer: option.value }))
	const expectedEntropy =
	    answerPreviews.reduce ((sum, preview) => sum + preview.entropy, 0)
	    / answerPreviews.length
	const expectedCandidateCount =
	    answerPreviews.reduce ((sum, preview) => sum + preview.candidateCount, 0)
	    / answerPreviews.length
	const kindPenalty = askedIds.has (question.kind) ? 2 : 0
	const tagPenalty = question.kind === 'tag' ? 0 : 10
	const minSide = candidates.length < 10 ? 1 : Math.max (3, candidates.length * .08)
	const narrowPenalty = yes < minSide || no < minSide ? candidates.length : 0

	return { question,
		 score: splitScore + expectedEntropy + expectedCandidateCount / 8
			+ kindPenalty + tagPenalty + narrowPenalty,
		 narrow: narrowPenalty > 0 }
      })
      .filter ((item): item is {
	  question: GekanatorQuestion
	  score:    number
	  narrow:   boolean } => item !== null && Number.isFinite (item.score))
      .sort ((a, b) => a.score - b.score)
  }

  const unansweredQuestions =
      questions.filter (question => !(askedIds.has (question.id)))
  const ranked = rank (unansweredQuestions, scoredPosts)

  return (ranked.find (item => !(item.narrow)) ?? ranked[0])?.question ?? null
}


const bestPost = (posts: Post[], scores: Map<number, number>): Post | null =>
    posts
    .map (post => ({ post, score: scores.get (post.id) ?? 0 }))
    .sort ((a, b) => b.score - a.score)[0]?.post ?? null


const PostMiniCard: FC<{ post: Post }> = ({ post }) => (
    <div className="flex gap-3 items-center min-w-0">
      <img
	  src={post.thumbnail || post.thumbnailBase || undefined}
	  alt={post.title || post.url}
	  className="w-16 h-16 rounded object-cover bg-yellow-100"/>
      <div className="min-w-0">
	<PrefetchLink
	    to={`/posts/${ post.id }`}
	    className="font-bold text-pink-700 dark:text-pink-200 break-words">
	  #{post.id} {post.title || post.url}
	</PrefetchLink>
	<div className="text-sm text-neutral-600 dark:text-neutral-300 line-clamp-1">
	  {post.tags.slice (0, 6).map (tag => tag.name).join (' / ')}
	</div>
      </div>
    </div>)


const GekanatorPage: FC = () => {
  const [phase, setPhase] = useState<Phase> ('intro')
  const [scores, setScores] = useState<Map<number, number>> (new Map ())
  const [answers, setAnswers] = useState<GekanatorAnswerLog[]> ([])
  const [askedIds, setAskedIds] = useState<Set<string>> (new Set ())
  const [candidateIds, setCandidateIds] = useState<Set<number> | null> (null)
  const [softenedQuestionIds, setSoftenedQuestionIds] = useState<Set<string>> (new Set ())
  const [questionBank, setQuestionBank] = useState<GekanatorQuestion[]> ([])
  const [search, setSearch] = useState ('')
  const [saved, setSaved] = useState (false)
  const [resultWon, setResultWon] = useState<boolean | null> (null)
  const [rejectedPostIds, setRejectedPostIds] = useState<Set<number>> (new Set ())
  const [lastGuessQuestionCount, setLastGuessQuestionCount] = useState (0)
  const [lastRejectedGuessId, setLastRejectedGuessId] = useState<number | null> (null)
  const [activeGuessId, setActiveGuessId] = useState<number | null> (null)
  const [history, setHistory] = useState<GameSnapshot[]> ([])

  const { data: posts = [], isLoading, error } = useQuery ({
      queryKey: gekanatorKeys.posts (),
      queryFn: fetchGekanatorPosts })

  const candidatePosts = useMemo (
      () => posts.filter (post => candidateIds === null || candidateIds.has (post.id)),
      [posts, candidateIds])
  const eligiblePosts = useMemo (
      () => candidatePosts.filter (post => !(rejectedPostIds.has (post.id))),
      [candidatePosts, rejectedPostIds])
  const questions = useMemo (
      () => buildGekanatorQuestions (eligiblePosts.length > 1 ? eligiblePosts : posts),
      [eligiblePosts, posts])
  const scoringQuestions = useMemo (() => {
    return mergeQuestions ([...questions, ...questionBank])
  }, [questions, questionBank])
  const topScoredPosts = useMemo (
      () => eligiblePosts
      .map (post => ({ post, score: scores.get (post.id) ?? 0 }))
      .sort ((a, b) => b.score - a.score)
      .slice (0, 3),
      [eligiblePosts, scores])
  const currentQuestion = chooseQuestion ({
      posts: eligiblePosts, questions: scoringQuestions, scores, askedIds })
  const answerPreviews = useMemo (
      () => currentQuestion
	  ? answerOptions.map (option => previewAnswer ({
	      posts: eligiblePosts,
	      scores,
	      question: currentQuestion,
	      answer: option.value }))
	  : [],
      [currentQuestion, eligiblePosts, scores])
  const guess = bestPost (eligiblePosts, scores)
  const displayedGuess =
      posts.find (post => post.id === activeGuessId) ?? guess
  const saveMutation = useMutation ({ mutationFn: saveGekanatorGame })

  const reset = () => {
    setPhase ('intro')
    setScores (new Map ())
    setAnswers ([])
    setAskedIds (new Set ())
    setCandidateIds (null)
    setSoftenedQuestionIds (new Set ())
    setQuestionBank ([])
    setSearch ('')
    setSaved (false)
    setResultWon (null)
    setRejectedPostIds (new Set ())
    setLastGuessQuestionCount (0)
    setLastRejectedGuessId (null)
    setActiveGuessId (null)
    setHistory ([])
  }

  const answer = (value: GekanatorAnswerValue) => {
    if (!(currentQuestion))
      {
	setActiveGuessId (guess?.id ?? null)
	setPhase ('guess')
	return
      }

    setHistory ([...history, {
	phase,
	scores: new Map (scores),
	answers: [...answers],
	askedIds: new Set (askedIds),
	candidateIds: candidateIds === null ? null : new Set (candidateIds),
	softenedQuestionIds: new Set (softenedQuestionIds),
	questionBank: [...questionBank],
	search,
	rejectedPostIds: new Set (rejectedPostIds),
	lastGuessQuestionCount,
	lastRejectedGuessId,
	activeGuessId }])
    const nextAnswers = [...answers, {
	questionId:    currentQuestion.id,
	questionText:  currentQuestion.text,
	answer:        value }]
    const nextAskedIds = new Set ([...askedIds, currentQuestion.id])
    const nextQuestionBank = [
	...questionBank.filter (question => question.id !== currentQuestion.id),
	currentQuestion]
    const hardFilteredPosts =
	value === 'yes'
	  ? eligiblePosts.filter (post => currentQuestion.test (post))
	  : value === 'no'
	    ? eligiblePosts.filter (post => !(currentQuestion.test (post)))
	    : eligiblePosts
    let nextCandidateIds =
	(value === 'yes' || value === 'no') && hardFilteredPosts.length > 0
	  ? new Set (hardFilteredPosts.map (post => post.id))
	  : candidateIds
    let nextSoftenedQuestionIds = new Set (softenedQuestionIds)
    let nextScores = recalculateScores ({
	posts,
	questions: nextQuestionBank,
	answers: nextAnswers,
	softenedQuestionIds: nextSoftenedQuestionIds })

    let nextEligiblePosts =
	posts.filter (post =>
	    (nextCandidateIds === null || nextCandidateIds.has (post.id))
	    && !(rejectedPostIds.has (post.id)))
    let nextScoringQuestions = mergeQuestions ([
	...buildGekanatorQuestions (nextEligiblePosts.length > 1 ? nextEligiblePosts : posts),
	...nextQuestionBank])
    while (
	nextAnswers.length < hardMaxQuestions
	&& nextEligiblePosts.length > 1
	&& !(chooseQuestion ({
	    posts: nextEligiblePosts,
	    questions: nextScoringQuestions,
	    scores: nextScores,
	    askedIds: nextAskedIds }))
    )
      {
	const softened = softenNextQuestionIds ({
	    questions: nextQuestionBank,
	    answers: nextAnswers,
	    softenedQuestionIds: nextSoftenedQuestionIds })
	if (!(softened))
	  break

	nextSoftenedQuestionIds = softened
	nextCandidateIds = null
	nextEligiblePosts = posts.filter (post => !(rejectedPostIds.has (post.id)))
	nextScoringQuestions = mergeQuestions ([
	    ...buildGekanatorQuestions (nextEligiblePosts),
	    ...nextQuestionBank])
	nextScores = recalculateScores ({
	    posts,
	    questions: nextQuestionBank,
	    answers: nextAnswers,
	    softenedQuestionIds: nextSoftenedQuestionIds })
      }

    setScores (nextScores)
    setAskedIds (nextAskedIds)
    setCandidateIds (nextCandidateIds)
    setSoftenedQuestionIds (nextSoftenedQuestionIds)
    setQuestionBank (nextQuestionBank)
    setAnswers (nextAnswers)

    const nextGuess = bestPost (nextEligiblePosts, nextScores)
    const nextQuestionCount = answers.length + 1
    const definitelyKnown = nextEligiblePosts.length === 1
    const enoughQuestions =
	nextQuestionCount - lastGuessQuestionCount >= questionsBetweenGuesses
    const shouldGuess =
	nextQuestionCount >= hardMaxQuestions
	|| definitelyKnown
	|| enoughQuestions
    if (shouldGuess)
      {
	setActiveGuessId (nextGuess?.id ?? null)
	setLastGuessQuestionCount (nextQuestionCount)
	setPhase ('guess')
      }
  }

  const saveResult = (won: boolean, correctPostId: number | null) => {
    const guessedPostId =
	won ? displayedGuess?.id : lastRejectedGuessId ?? displayedGuess?.id
    if (!(guessedPostId) || saved)
      return

    setSaved (true)
    setResultWon (won)
    saveMutation.mutate ({
	guessedPostId,
	correctPostId,
	won,
	answers })
    setPhase ('learned')
  }

  const rejectGuess = () => {
    if (!(displayedGuess))
      return

    setLastRejectedGuessId (displayedGuess.id)
    if (answers.length >= hardMaxQuestions)
      {
	setSearch (' ')
	return
      }

    setRejectedPostIds (new Set ([...rejectedPostIds, displayedGuess.id]))
    setActiveGuessId (null)
    setSearch ('')
    setLastGuessQuestionCount (answers.length)
    setPhase ('continue')
  }

  const undoAnswer = () => {
    const snapshot = history[history.length - 1]
    if (!(snapshot) || saved)
      return

    setPhase (snapshot.phase)
    setScores (snapshot.scores)
    setAnswers (snapshot.answers)
    setAskedIds (snapshot.askedIds)
    setCandidateIds (snapshot.candidateIds)
    setSoftenedQuestionIds (snapshot.softenedQuestionIds)
    setQuestionBank (snapshot.questionBank)
    setSearch (snapshot.search)
    setRejectedPostIds (snapshot.rejectedPostIds)
    setLastGuessQuestionCount (snapshot.lastGuessQuestionCount)
    setLastRejectedGuessId (snapshot.lastRejectedGuessId)
    setActiveGuessId (snapshot.activeGuessId)
    setHistory (history.slice (0, -1))
  }

  const softenAndContinue = () => {
    const softened = softenNextQuestionIds ({
	questions: scoringQuestions, answers, softenedQuestionIds })
    if (!(softened))
      return

    setSoftenedQuestionIds (softened)
    setCandidateIds (null)
    setScores (
	recalculateScores ({ posts,
			     questions: scoringQuestions,
			     answers,
			     softenedQuestionIds: softened }))
  }

  const filteredPosts = posts
      .filter (post => {
	const needle = search.trim ().toLowerCase ()
	if (!(needle))
	  return false
	if (/^\d+$/.test (needle) && post.id === Number (needle))
	  return true

	return [post.title, post.url, ...post.tags.map (tag => tag.name)]
	    .filter ((value): value is string => Boolean (value))
	    .some (value => value.toLowerCase ().includes (needle))
      })
      .sort ((a, b) => {
	const id = Number (search.trim ())
	if (Number.isFinite (id))
	  return Number (b.id === id) - Number (a.id === id)

	return 0
      })
      .slice (0, 20)

  const dialogue =
      phase === 'learned' && resultWon
	? <>グカカカカwwwww  <ruby>洗澡鹿<rt>シーザオグカ</rt></ruby>は何でもお見通し!</>
	: <>私は<ruby>洗澡鹿<rt>シーザオグカ</rt></ruby>．質問から投稿を何でも当ててみせるよ</>

  return (
      <MainArea className="bg-yellow-50 dark:bg-red-975">
	<Helmet>
	  <title>{`グカネータ | ${ SITE_TITLE }`}</title>
	</Helmet>

	<div className="mx-auto max-w-4xl space-y-6">
	  <header className="space-y-2">
	    <p className="text-sm text-pink-700 dark:text-pink-200">おたのしみ</p>
	    <h1 className="text-3xl font-bold text-pink-700 dark:text-pink-200">
	      グカネータ
	    </h1>
	  </header>

	  <section className="rounded-lg border border-yellow-300 bg-white p-4
			      shadow-sm dark:border-red-800 dark:bg-red-950">
	    <div className="flex gap-4">
	      <div className="grid h-24 w-24 shrink-0 place-items-center rounded-lg
			      bg-yellow-200 text-center text-sm font-bold
			      text-pink-700 dark:bg-red-900 dark:text-pink-100">
		洗澡鹿
	      </div>
	      <div className="min-w-0 flex-1 space-y-3">
		<p className="text-lg font-bold">
		  {dialogue}
		</p>

		{isLoading && <p>投稿を読み込んでゐます...</p>}
		{Boolean (error) && <p>投稿を読み込めませんでした．</p>}

		{phase === 'intro' && !(isLoading) && posts.length > 0 && (
		    <button
			type="button"
			className="rounded bg-pink-600 px-4 py-2 font-bold text-white
				   hover:bg-pink-500"
			onClick={() => setPhase ('question')}>
		      はじめる
		    </button>)}

		{phase === 'question' && currentQuestion && (
		    <div className="space-y-4">
		      <div>
			<p className="text-sm text-neutral-500">
			  質問 {answers.length + 1}
			</p>
			<p className="text-xl font-bold">{currentQuestion.text}</p>
		      </div>
		      <div className="rounded border border-yellow-100 px-3 py-2
				      text-sm dark:border-red-900">
			<div className="font-bold">現在候補: {eligiblePosts.length} 件</div>
			{topScoredPosts.length > 0 && (
			    <div className="mt-1 flex flex-wrap gap-x-4 gap-y-1">
			      {topScoredPosts.map (item => (
				  <span key={item.post.id}>
				    #{item.post.id}: score {item.score.toFixed (1)}
				  </span>))}
			    </div>)}
		      </div>
		      {answerPreviews.length > 0 && (
			  <div className="grid gap-2 text-sm md:grid-cols-2">
			    {answerOptions.map (option => {
			      const preview =
				  answerPreviews.find (item => item.answer === option.value)
			      return (
				  <div
				      key={option.value}
				      className="rounded border border-yellow-100 px-3 py-2
						 dark:border-red-900">
				    <span className="font-bold">{option.label}</span>
				    {' '}
				    <span className="text-neutral-600 dark:text-neutral-300">
				      なら候補 {preview ? preview.candidateCount : 0} 件
				    </span>
				  </div>)
			    })}
			  </div>)}
		      <div className="flex flex-wrap gap-2">
			{answerOptions.map (option => (
			    <button
				key={option.value}
				type="button"
				className="rounded border border-yellow-300 px-3 py-2
					   hover:bg-yellow-100 dark:border-red-700
					   dark:hover:bg-red-900"
				onClick={() => answer (option.value)}>
			      {option.label}
			    </button>))}
			{history.length > 0 && (
			    <button
				type="button"
				className="rounded border border-neutral-300 px-3 py-2
					   hover:bg-neutral-100 dark:border-neutral-700
					   dark:hover:bg-red-900"
				onClick={undoAnswer}>
			      戻る
			    </button>)}
		      </div>
		    </div>)}

		{phase === 'question' && !(currentQuestion) && (
		    <div className="space-y-4">
		      <p className="text-xl font-bold">
			さっきまでの答へを少し疑って考へ直すよ．
		      </p>
		      {answers.length >= hardMaxQuestions || eligiblePosts.length <= 1
		       ? (
			  <button
			      type="button"
			      className="rounded border border-yellow-300 px-4 py-2
					 hover:bg-yellow-100 dark:border-red-700
					 dark:hover:bg-red-900"
			      onClick={() => {
				setActiveGuessId (guess?.id ?? null)
				setPhase ('guess')
			      }}>
			    推測へ
			  </button>)
		       : (
			  <button
			      type="button"
			      className="rounded bg-pink-600 px-4 py-2 font-bold text-white
					 hover:bg-pink-500"
			      onClick={softenAndContinue}>
			    考へ直す
			  </button>)}
		    </div>)}

		{phase === 'guess' && displayedGuess && (
		    <div className="space-y-4">
		      <p className="text-xl font-bold">これを想像してゐたね?</p>
		      <PostMiniCard post={displayedGuess}/>
		      <div className="flex flex-wrap gap-2">
			<button
			    type="button"
			    className="rounded bg-pink-600 px-4 py-2 font-bold text-white
				       hover:bg-pink-500"
			    onClick={() => saveResult (true, null)}>
			  当たり
			</button>
			<button
			    type="button"
			    className="rounded border border-yellow-300 px-4 py-2
				       hover:bg-yellow-100 dark:border-red-700
				       dark:hover:bg-red-900"
			    onClick={rejectGuess}>
			  違ふ
			</button>
			{history.length > 0 && (
			    <button
				type="button"
				className="rounded border border-neutral-300 px-4 py-2
					   hover:bg-neutral-100 dark:border-neutral-700
					   dark:hover:bg-red-900"
				onClick={undoAnswer}>
			      戻る
			    </button>)}
		      </div>
		    </div>)}

		{phase === 'continue' && (
		    <div className="space-y-4">
		      <p className="text-xl font-bold">続けますか?</p>
		      <div className="flex flex-wrap gap-2">
			<button
			    type="button"
			    className="rounded bg-pink-600 px-4 py-2 font-bold text-white
				       hover:bg-pink-500"
			    onClick={() => setPhase ('question')}>
			  はい
			</button>
			<button
			    type="button"
			    className="rounded border border-yellow-300 px-4 py-2
				       hover:bg-yellow-100 dark:border-red-700
				       dark:hover:bg-red-900"
			    onClick={() => setSearch (' ')}>
			  いいえ
			</button>
			{history.length > 0 && (
			    <button
				type="button"
				className="rounded border border-neutral-300 px-4 py-2
					   hover:bg-neutral-100 dark:border-neutral-700
					   dark:hover:bg-red-900"
				onClick={undoAnswer}>
			      戻る
			    </button>)}
		      </div>
		    </div>)}

		{phase === 'learned' && (
		    <div className="space-y-3">
		      <p>覚えたよ．次はもっと見通す．</p>
		      {saveMutation.isError && (
			  <p className="text-sm text-red-600">
			    ただし学習ログの保存には失敗しました．
			  </p>)}
		      <button
			  type="button"
			  className="rounded bg-pink-600 px-4 py-2 font-bold text-white
				     hover:bg-pink-500"
			  onClick={reset}>
			もう一回
		      </button>
		    </div>)}
	      </div>
	    </div>
	  </section>

	  {['guess', 'continue', 'question'].includes (phase) && search !== '' && (
	      <section className="rounded-lg border border-yellow-300 bg-white p-4
				  dark:border-red-800 dark:bg-red-950">
		<label className="block space-y-2">
		  <span className="font-bold">正解の投稿</span>
		  <input
		      value={search}
		      onChange={ev => setSearch (ev.target.value)}
		      className="w-full rounded border border-yellow-300 bg-white px-3 py-2
				 dark:border-red-700 dark:bg-red-950"
		      placeholder="投稿 Id.・タイトル・URL・タグで検索"/>
		</label>
		<div className="mt-4 space-y-3">
		  {filteredPosts.map (post => (
		      <button
			  key={post.id}
			  type="button"
			  className={cn ('block w-full rounded border border-yellow-200 p-3',
					 'text-left hover:bg-yellow-100',
					 'dark:border-red-800 dark:hover:bg-red-900')}
			  onClick={() => saveResult (false, post.id)}>
			<PostMiniCard post={post}/>
		      </button>))}
		  {search.trim () && filteredPosts.length === 0 && '見つかりません．'}
		</div>
	      </section>)}
	</div>
      </MainArea>)
}


export default GekanatorPage
