import { apiGet, apiPost } from '@/lib/api'

import type { Post } from '@/types'

export type GekanatorAnswerValue =
    | 'yes'
    | 'no'
    | 'partial'
    | 'probably_no'
    | 'unknown'

export type LearnedSemanticSide =
    | 'positive'
    | 'negative'
    | 'unknown'

export type GekanatorQuestionPurpose =
    | 'effective_user_suggested'
    | 'learning_user_suggested'
    | 'normal'

export type GekanatorAnswerLog = {
    questionId:       string
    questionText:     string
    questionCondition?:  GekanatorQuestionCondition
    questionMode?:    'normal' | 'winning_run'
    questionPurpose?: GekanatorQuestionPurpose
    effectiveQuestion?: boolean
    learningQuestion?: boolean
    answer:           GekanatorAnswerValue
    originalAnswer:   GekanatorAnswerValue }

export type GekanatorQuestionKind =
    | 'tag'
    | 'source'
    | 'title'
    | 'original_date'
    | 'post_similarity'

export type GekanatorQuestionSource =
    | 'default'
    | 'user_suggested'
    | 'ai_generated'
    | 'admin_curated'

export type GekanatorPerformanceMode = 'normal'

export type GekanatorQuestionCondition =
    | { type: 'tag'; key: string }
    | { type: 'source'; host: string }
    | { type: 'original-year'; year: number }
    | { type: 'original-month'; month: number }
    | { type: 'original-month-day'; monthDay: string }
    | { type: 'title-length-at-least'; length: number }
    | { type: 'title-length-greater-than'; length: number }
    | { type: 'title-has-ascii' }
    | { type: 'title-contains'; text: string }
    | {
	  type: 'post-similarity'
	  postId: number
	  answer: GekanatorAnswerValue
	  threshold: number
      }


type NonPostSimilarityCondition = Exclude<
    GekanatorQuestionCondition,
    { type: 'post-similarity' }
>

export type GekanatorExtraQuestion = {
    id:              number
    text:            string
    source:          GekanatorQuestionSource
    priorityWeight:  number }

export type StoredGekanatorQuestion = {
    recordId?:       number
    id:              string
    text:            string
    kind:            GekanatorQuestionKind
    condition:       GekanatorQuestionCondition
    source?:         GekanatorQuestionSource
    priorityWeight?: number
    exampleAnswers?: Record<`${ number }`, GekanatorAnswerValue> }

export type GekanatorQuestion = {
    recordId?:       number
    id:              string
    text:            string
    kind:            GekanatorQuestionKind
    condition:       GekanatorQuestionCondition
    source:          GekanatorQuestionSource
    priorityWeight:  number
    exampleAnswers?: Record<`${ number }`, GekanatorAnswerValue>
    test:            (post: Post) => boolean }

export type BuildGekanatorQuestionsOptions = {
    includeTitleContains?: boolean
    tagQuestionCap?: number
    titleContainsCap?: number
    totalQuestionCap?: number
}


export const normalizeTitleLengthCondition = (
    condition:  GekanatorQuestionCondition): GekanatorQuestionCondition => {
  switch (condition.type)
  {
  case 'title-length-greater-than':
    return {
	type:   'title-length-at-least',
	length: condition.length + 1 }
  default:
    return condition
  }
}


export const titleLengthMinimumForCondition = (
    condition:  GekanatorQuestionCondition): number | null => {
  switch (condition.type)
  {
  case 'title-length-at-least':
    return condition.length
  case 'title-length-greater-than':
    return condition.length + 1
  default:
    return null
  }
}


export const questionIdForCondition = (
    condition:  NonPostSimilarityCondition): string => {
  switch (condition.type)
  {
  case 'tag':
    return `tag:${ condition.key }`
  case 'source':
    return `source:${ condition.host }`
  case 'original-year':
    return `original-year:${ condition.year }`
  case 'original-month':
    return `original-month:${ condition.month }`
  case 'original-month-day':
    return `original-month-day:${ condition.monthDay }`
  case 'title-length-at-least':
  case 'title-length-greater-than':
    return `title:length-at-least:${ titleLengthMinimumForCondition (condition) }`
  case 'title-has-ascii':
    return 'title:ascii'
  case 'title-contains':
    return `title:contains:${ condition.text }`
  }
}


const directExampleAnswerFor = (
    question:  StoredGekanatorQuestion,
    post:      Post): GekanatorAnswerValue | null => {
  if (question.kind !== 'post_similarity' && question.kind !== 'tag')
    return null

  const direct = question.exampleAnswers?.[String (post.id) as `${ number }`]
  if (direct)
    return direct

  if (question.condition.type === 'post-similarity' && question.condition.postId === post.id)
    return question.condition.answer

  return null
}


export const isLearnedSemanticQuestion = (
    question:  StoredGekanatorQuestion | GekanatorQuestion): boolean =>
    question.kind === 'post_similarity'
    && question.source === 'user_suggested'


export const learnedSemanticSideForAnswer = (
    answer:  GekanatorAnswerValue | null): LearnedSemanticSide => {
  if (answer === 'yes' || answer === 'partial')
    return 'positive'

  if (answer === 'no' || answer === 'probably_no')
    return 'negative'

  return 'unknown'
}

const countBy = <T extends string | number> (values: T[]): Map<T, number> => {
  const counts = new Map<T, number> ()
  values.forEach (value => counts.set (value, (counts.get (value) ?? 0) + 1))
  return counts
}


const median = (values: number[]): number => {
  const sorted = [...values].sort ((a, b) => a - b)
  return sorted[Math.floor (sorted.length / 2)] ?? 0
}


const hostOf = (post: Post): string | null => {
  try
  {
    return new URL (post.url).hostname.replace (/^www\./, '')
  }
  catch
  {
    return null
  }
}


const originalYearOf = (post: Post): number | null => {
  const value = post.originalCreatedFrom || post.originalCreatedBefore
  if (!(value))
    return null

  const date = new Date (value)
  if (Number.isNaN (date.getTime ()))
    return null

  return date.getFullYear ()
}


const originalDateOf = (post: Post): Date | null => {
  const value = post.originalCreatedFrom || post.originalCreatedBefore
  if (!(value))
    return null

  const date = new Date (value)
  if (Number.isNaN (date.getTime ()))
    return null

  return date
}


const originalMonthOf = (post: Post): number | null => {
  const date = originalDateOf (post)
  if (!(date))
    return null

  return date.getMonth () + 1
}


const originalMonthDayOf = (post: Post): string | null => {
  const date = originalDateOf (post)
  if (!(date))
    return null

  return `${ date.getMonth () + 1 }-${ date.getDate () }`
}


const tagQuestionKey = ({ category, name }: { category: string; name: string }): string =>
    `${ category }:${ name }`


const tagFromQuestionKey = (key: string): { category: string; name: string } => {
  const [category, ...rest] = key.split (':')
  return { category: category ?? '', name: rest.join (':') }
}


const nicoTagLabel = (name: string): string => name.replace (/^nico:/, '')


const tagQuestionText = (category: string, label: string): string => {
  switch (category)
  {
  case 'deerjikist':
    return `作者・ニジラーとして「${ label }」に関係してゐる?`
  case 'meme':
    return `元ネタ・ミームとして「${ label }」に関係しさう?`
  case 'character':
    return `「${ label }」といふキャラクターが関係してゐる?`
  case 'material':
    return `素材として「${ label }」に関係してゐる?`
  case 'nico':
    return `ニコニコに「${ label }」といふタグが付いてゐる?`
  case 'general':
  case 'meta':
  default:
    return `内容として「${ label }」に関係しさう?`
  }
}


const questionableTag = (post: Post, key: string): boolean => {
  const { category, name } = tagFromQuestionKey (key)

  return (
    post.tags.some (tag =>
	tag.name === name
	&& tag.category === category
	&& !(tag.category === 'meta')
	&& !(tag.name.includes ('タグ希望'))
	&& !(tag.name.includes ('bot操作'))))
}


const questionMatches = (
    post:       Post,
    question:   StoredGekanatorQuestion): boolean => {
  const directAnswer = directExampleAnswerFor (question, post)
  if (directAnswer)
    return question.kind === 'post_similarity'
	? learnedSemanticSideForAnswer (directAnswer) === 'positive'
	: directAnswer === 'yes'

  switch (question.condition.type)
  {
  case 'tag':
    return questionableTag (post, question.condition.key)
  case 'source':
    return hostOf (post) === question.condition.host
  case 'original-year':
    return originalYearOf (post) === question.condition.year
  case 'original-month':
    return originalMonthOf (post) === question.condition.month
  case 'original-month-day':
    return originalMonthDayOf (post) === question.condition.monthDay
  case 'title-length-at-least':
    return (post.title?.length ?? 0) >= question.condition.length
  case 'title-length-greater-than':
    return (post.title?.length ?? 0) > question.condition.length
  case 'title-has-ascii':
    return /[A-Za-z0-9]/.test (post.title ?? '')
  case 'title-contains':
    return (post.title ?? '').includes (question.condition.text)
  case 'post-similarity':
    return false
  }
}


export const expectedAnswerForQuestion = (
    question:  StoredGekanatorQuestion | GekanatorQuestion | undefined,
    post:      Post | null): GekanatorAnswerValue | null => {
  if (!(question) || !(post))
    return null

  const directAnswer = directExampleAnswerFor (question, post)
  if (directAnswer)
    return directAnswer

  switch (question.condition.type)
  {
  case 'post-similarity':
    if (question.condition.postId === post.id)
      return question.condition.answer

    return null
  case 'tag':
  case 'source':
  case 'original-year':
  case 'original-month':
  case 'original-month-day':
  case 'title-length-at-least':
  case 'title-length-greater-than':
  case 'title-has-ascii':
  case 'title-contains':
    return questionMatches (post, question) ? 'yes' : 'no'
  }
}


export const learnedSemanticSideForPost = (
    question:  StoredGekanatorQuestion | GekanatorQuestion | undefined,
    post:      Post | null): LearnedSemanticSide =>
    learnedSemanticSideForAnswer (expectedAnswerForQuestion (question, post))


export const restoreGekanatorQuestion = (
    question:  StoredGekanatorQuestion): GekanatorQuestion => {
  const normalizedCondition = normalizeTitleLengthCondition (question.condition)
  const normalizedQuestion = {
      ...question,
      recordId: question.recordId,
      id: normalizedCondition.type === 'title-length-at-least'
	  ? `title:length-at-least:${ normalizedCondition.length }`
	  : question.id,
      condition: normalizedCondition,
      source: question.source ?? 'default',
      priorityWeight: question.priorityWeight ?? 1 }

  return {
      ...normalizedQuestion,
      test: (post: Post) => questionMatches (post, normalizedQuestion) }
}


export const storeGekanatorQuestion = (
    question:  GekanatorQuestion): StoredGekanatorQuestion => ({
  id: question.condition.type === 'title-length-greater-than'
      ? `title:length-at-least:${ question.condition.length + 1 }`
      : question.id,
  recordId: question.recordId,
  text: question.text,
  kind: question.kind,
  condition: normalizeTitleLengthCondition (question.condition),
  source: question.source,
  priorityWeight: question.priorityWeight,
  exampleAnswers: question.exampleAnswers })


export const fetchGekanatorPosts = async (): Promise<Post[]> => {
  const data = await apiGet<{ posts: Post[] }> ('/gekanator/posts')
  return data.posts
}


export const fetchGekanatorQuestions = async (): Promise<StoredGekanatorQuestion[]> => {
  const data = await apiGet<{ questions: StoredGekanatorQuestion[] }> ('/gekanator/questions')
  return data.questions
}


export const fetchGekanatorExtraQuestions = async (
    gameId:  number,
    nonce?: string): Promise<GekanatorExtraQuestion[]> => {
  const data = await apiGet<{ questions: GekanatorExtraQuestion[] }> (
      `/gekanator/games/${ gameId }/extra_questions`,
      { params: nonce ? { nonce } : undefined })
  return data.questions
}


export const buildGekanatorQuestions = (
    posts:    Post[],
    options:  BuildGekanatorQuestionsOptions = { }): GekanatorQuestion[] => {
  const {
      includeTitleContains = true,
      tagQuestionCap = 192,
      titleContainsCap = 24,
      totalQuestionCap = Number.POSITIVE_INFINITY,
  } = options
  const tagCounts = countBy (posts.flatMap (post =>
      post.tags
      .filter (tag =>
	  !(tag.category === 'meta')
	  && !(tag.name.includes ('タグ希望'))
	  && !(tag.name.includes ('bot操作')))
      .map (tag => tagQuestionKey (tag))))
  const hosts = countBy (posts.map (hostOf).filter ((host): host is string => Boolean (host)))
  const originalYears = countBy (
      posts
      .map (originalYearOf)
      .filter ((year): year is number => year != null))
  const originalMonths = countBy (
      posts
      .map (originalMonthOf)
      .filter ((month): month is number => month != null))
  const originalMonthDays = countBy (
      posts
      .map (originalMonthDayOf)
      .filter ((monthDay): monthDay is string => monthDay != null))
  const titleLengthMedian = median (posts.map (post => post.title?.length ?? 0))
  const titleWordCounts =
      includeTitleContains
	? countBy (
	    posts.flatMap (post =>
		Array.from (
		    new Set (
			(post.title ?? '')
			.match (
			    /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}A-Za-z0-9]{2,}/gu)
			?? []))))
	: new Map<string, number> ()

  const usefulEntries = <T extends string | number> (
      counts:  Map<T, number>,
      cap:     number) =>
      [...counts.entries ()]
      .filter (([, count]) => count > 0 && count < posts.length)
      .sort ((a, b) => Math.abs (posts.length / 2 - a[1])
	       - Math.abs (posts.length / 2 - b[1]))
      .slice (0, cap)

  const tagQuestions = usefulEntries (tagCounts, Math.max (tagQuestionCap, 80))
      .filter (([, count]) => count >= 2 && count <= Math.max (2, posts.length * .7))
      .slice (0, tagQuestionCap)
      .map (([key]) => {
	const { category, name } = tagFromQuestionKey (String (key))
	const label = category === 'nico' ? nicoTagLabel (name) : name

	return {
	    id:   `tag:${ key }`,
	    text: tagQuestionText (category, label),
	    kind: 'tag' as const,
	    condition: { type: 'tag' as const, key: String (key) },
	    source: 'default' as const,
	    priorityWeight: 1,
	    test: (post: Post) => questionableTag (post, String (key)) }
      })

  const sourceQuestions = usefulEntries (hosts, 20)
      .filter (([, count]) => count >= 2 && count <= Math.max (2, posts.length * .7))
      .slice (0, 20)
      .map (([host]) => ({
	  id:   `source:${ host }`,
	  text: `${ host } の投稿を思ひ浮かべてゐる?`,
	  kind: 'source' as const,
	  condition: { type: 'source' as const, host },
	  source: 'default' as const,
	  priorityWeight: 1,
	  test: (post: Post) => hostOf (post) === host }))

  const originalYearQuestions = usefulEntries (originalYears, 20)
      .filter (([, count]) => count >= 2 && count <= Math.max (2, posts.length * .7))
      .slice (0, 20)
      .map (([year]) => ({
	  id:   `original-year:${ year }`,
	  text: `オリジナルの投稿年は ${ year } 年?`,
	  kind: 'original_date' as const,
	  condition: { type: 'original-year' as const, year },
	  source: 'default' as const,
	  priorityWeight: 1,
	  test: (post: Post) => originalYearOf (post) === year }))

  const originalMonthQuestions = usefulEntries (originalMonths, 20)
      .filter (([, count]) => count >= 2 && count <= Math.max (2, posts.length * .7))
      .slice (0, 20)
      .map (([month]) => ({
	  id:   `original-month:${ month }`,
	  text: `オリジナルの投稿月は ${ month } 月?`,
	  kind: 'original_date' as const,
	  condition: { type: 'original-month' as const, month },
	  source: 'default' as const,
	  priorityWeight: 1,
	  test: (post: Post) => originalMonthOf (post) === month }))

  const originalMonthDayQuestions = usefulEntries (originalMonthDays, 20)
      .filter (([, count]) => count >= 2 && count <= Math.max (2, posts.length * .7))
      .slice (0, 20)
      .map (([monthDay]) => {
	const [month, day] = String (monthDay).split ('-')

	return {
	    id:   `original-month-day:${ monthDay }`,
	    text: `オリジナルの投稿日は ${ month } 月 ${ day } 日?`,
	    kind: 'original_date' as const,
	    condition: { type: 'original-month-day' as const, monthDay: String (monthDay) },
	    source: 'default' as const,
	    priorityWeight: 1,
	    test: (post: Post) => originalMonthDayOf (post) === monthDay }
      })

  const titleQuestions = [
      {
	  id:   `title:length-at-least:${ titleLengthMedian }`,
	  text: `タイトルは ${ titleLengthMedian } 文字以上?`,
	  kind: 'title' as const,
	  condition: {
	      type: 'title-length-at-least' as const,
	      length: titleLengthMedian },
	  source: 'default' as const,
	  priorityWeight: 1,
	  test: (post: Post) => (post.title?.length ?? 0) >= titleLengthMedian },
      {
	  id:   'title:ascii',
	  text: '題名に英数字が混じってゐる?',
	  kind: 'title' as const,
	  condition: { type: 'title-has-ascii' as const },
	  source: 'default' as const,
	  priorityWeight: 1,
	  test: (post: Post) => /[A-Za-z0-9]/.test (post.title ?? '') }]
      .filter (question => {
	const yes = posts.filter (post => question.test (post)).length
	const no = posts.length - yes
	return yes >= 2 && no >= 2 && yes <= posts.length * .7 && no <= posts.length * .7
      })
  const titleContainsQuestions =
      includeTitleContains
	? usefulEntries (titleWordCounts, titleContainsCap)
	    .filter (([word, count]) =>
		String (word).length <= 24
		&& count >= 2
		&& count <= Math.max (2, posts.length * .7))
	    .slice (0, titleContainsCap)
	    .map (([word]) => ({
		id:   `title:contains:${ word }`,
		text: `題名に「${ word }」が含まれる?`,
		kind: 'title' as const,
		condition: { type: 'title-contains' as const, text: String (word) },
		source: 'default' as const,
		priorityWeight: .96,
		test: (post: Post) => (post.title ?? '').includes (String (word)) }))
	: []

  return [
      ...sourceQuestions,
      ...originalYearQuestions,
      ...originalMonthQuestions,
      ...originalMonthDayQuestions,
      ...titleQuestions,
      ...titleContainsQuestions,
      ...tagQuestions].slice (0, totalQuestionCap)
}


export const saveGekanatorGame = async ({
    guessedPostId,
    correctPostId,
    answers,
}: {
    guessedPostId:  number
    correctPostId:  number
    answers:        GekanatorAnswerLog[]
}): Promise<{ id: number; learnedExampleCount: number }> =>
    await apiPost ('/gekanator/games', {
	guessed_post_id: guessedPostId,
	correct_post_id: correctPostId,
	answers: answers.map (answer => ({
	    question_id: answer.questionId,
	    question_text: answer.questionText,
	    question_condition: answer.questionCondition ?? null,
	    question_mode: answer.questionMode,
	    question_purpose: answer.questionPurpose,
	    effective_question: answer.effectiveQuestion,
	    learning_question: answer.learningQuestion,
	    answer: answer.answer,
	    original_answer: answer.originalAnswer })) })


export const saveGekanatorQuestionSuggestion = async ({
    gekanatorGameId,
    existingQuestionId,
    questionText,
    answer,
}: {
    gekanatorGameId:  number
    existingQuestionId?: number
    questionText?:    string
    answer:           GekanatorAnswerValue
}): Promise<{ id: number; count: number }> =>
    await apiPost ('/gekanator/question_suggestions', {
	gekanator_game_id: gekanatorGameId,
	existing_question_id: existingQuestionId,
	question_text: questionText,
	answer })


export const saveGekanatorExtraQuestionAnswers = async ({
    gameId,
    answers,
}: {
    gameId:   number
    answers:  { questionId: number; answer: GekanatorAnswerValue }[]
}) =>
    await apiPost (`/gekanator/games/${ gameId }/extra_question_answers`, {
	answers: answers.map (item => ({
	    question_id: item.questionId,
	    answer: item.answer })) })
