import { useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useNavigate } from 'react-router-dom'

import FieldError from '@/components/common/FieldError'
import Form from '@/components/common/Form'
import FormField from '@/components/common/FormField'
import PageTitle from '@/components/common/PageTitle'
import TextArea from '@/components/common/TextArea'
import MainArea from '@/components/layout/MainArea'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
import { apiGet, isApiError } from '@/lib/api'
import {
    appendPostNewSessionId,
    serialisePostNewState,
} from '@/lib/postNewQueryState'
import { canEditContent } from '@/lib/users'
import { countImportSourceLines,
	 generatePostImportSessionId,
	 cleanupExpiredPostImportSessions,
	 initialisePreviewRows,
	 loadPostImportSession,
	 loadPostImportSourceDraft,
	 resultRepairMode,
	 serialisedPostImportRowsEqual,
	 savePostImportSession,
	 savePostImportSourceDraft,
	 validateImportSource } from '@/lib/postImportSession'
import Forbidden from '@/pages/Forbidden'

import type { FC } from 'react'

import type { PostImportRow } from '@/lib/postImportSession'
import type { User } from '@/types'

type Props = { user: User | null }

type PreviewResponse = {
  url:                   string
  title?:                string
  thumbnailBase?:        string
  originalCreatedFrom?:  string
  originalCreatedBefore?: string
  duration?:             string
  videoMs?:              number
  tags?:                 string
  fieldWarnings?:        Record<string, string[]>
  baseWarnings?:         string[]
  validationErrors?:     Record<string, string[]>
  existingPost?:         {
    id:           number
    title:        string
    url:          string
    thumbnailUrl?: string } | null }

const MAX_ROWS = 100
const SOURCE_ERROR_ID = 'post-import-source-error'
const SOURCE_ISSUES_ID = 'post-import-source-issues'

const urlIssuesFromRows = (rows: PostImportRow[], source: string) => {
  const sourceLines = source.split (/\r\n|\n|\r/)

  return rows.flatMap (row =>
      (row.validationErrors.url ?? []).map (message => ({
	  sourceRow: row.sourceRow,
	  message,
	  url: sourceLines[row.sourceRow - 1]?.trim () ?? row.url })))
}


const previewRowFromResult = (
    sourceRow: number,
    result:    PreviewResponse,
): PostImportRow => {
  const fieldWarnings = result.fieldWarnings ?? { }
  const baseWarnings = result.baseWarnings ?? []
  const validationErrors = result.validationErrors ?? { }
  const hasWarnings =
      Object.values (fieldWarnings).some (messages => messages.length > 0)
      || baseWarnings.length > 0
  const hasErrors =
      Object.values (validationErrors).some (messages => messages.length > 0)
  const row: PostImportRow = {
      sourceRow,
      url: result.url,
      attributes: {
	  title: result.title ?? '',
	  thumbnailBase: result.thumbnailBase ?? '',
	  originalCreatedFrom: result.originalCreatedFrom ?? '',
	  originalCreatedBefore: result.originalCreatedBefore ?? '',
	  duration: result.duration ?? '',
	  videoMs: result.videoMs ?? '',
	  tags: result.tags ?? '',
	  parentPostIds: '' },
      fieldWarnings,
      baseWarnings,
      validationErrors,
      provenance: {
	  url: 'manual',
	  title: 'automatic',
	  thumbnailBase: 'automatic',
	  originalCreatedFrom: 'automatic',
	  originalCreatedBefore: 'automatic',
	  duration: 'automatic',
	  videoMs: 'automatic',
	  tags: 'automatic',
	  parentPostIds: 'automatic' },
      tagSources: {
	  automatic: result.tags ?? '',
	  manual: '' },
      status:
	  hasErrors
	  ? 'error'
	  : hasWarnings
	    ? 'warning'
	    : 'ready',
      skipReason: result.existingPost?.id != null ? 'existing' : undefined,
      existingPostId: result.existingPost?.id,
      existingPost: result.existingPost ?? undefined,
      resetSnapshot: {
	  url: result.url,
	  attributes: {
	      title: result.title ?? '',
	      thumbnailBase: result.thumbnailBase ?? '',
	      originalCreatedFrom: result.originalCreatedFrom ?? '',
	      originalCreatedBefore: result.originalCreatedBefore ?? '',
	      duration: result.duration ?? '',
	      videoMs: result.videoMs ?? '',
	      tags: result.tags ?? '',
	      parentPostIds: '' },
	  provenance: {
	      url: 'manual',
	      title: 'automatic',
	      thumbnailBase: 'automatic',
	      originalCreatedFrom: 'automatic',
	      originalCreatedBefore: 'automatic',
	      duration: 'automatic',
	      videoMs: 'automatic',
	      tags: 'automatic',
	      parentPostIds: 'automatic' },
	  tagSources: {
	      automatic: result.tags ?? '',
	      manual: '' },
	  fieldWarnings,
	  baseWarnings } }
  return row
}


const fetchPreviewRows = async (
    rows:        Array<{ sourceRow: number
			 url:       string }>,
    signal:      AbortSignal,
): Promise<{
    rows:    PostImportRow[]
    issues:  Array<{ sourceRow: number
		     message:   string
		     url:       string }>
}> => {
  const results: Array<PostImportRow | null> = new Array (rows.length).fill (null)
  const issues: Array<{ sourceRow: number
			message:   string
			url:       string }> = []
  let nextIndex = 0

  const previewIssues = (
      sourceRow: number,
      url:       string,
      requestError: unknown,
  ): Array<{ sourceRow: number
	     message:   string
	     url:       string }> => {
    if (!(isApiError<{
	    message?:    string
	    errors?:     Record<string, string[]>
	    baseErrors?: string[]
	}> (requestError)))
      {
	return [{
	    sourceRow,
	    message: '入力を確認してください．',
	    url }]
      }

    const fieldMessages = Object.values (requestError.response?.data?.errors ?? { }).flat ()
    const baseMessages = requestError.response?.data?.baseErrors ?? []
    const messages = [...new Set ([...fieldMessages, ...baseMessages])]
    return (messages.length > 0 ? messages : [requestError.response?.data?.message ?? '入力を確認してください．'])
        .map (message => ({
	    sourceRow,
	    message,
	    url }))
  }

  const worker = async () => {
    while (nextIndex < rows.length)
    {
      const index = nextIndex
      ++nextIndex
      const row = rows[index]
      try
      {
        const result = await apiGet<PreviewResponse> ('/posts/preview', {
	    params: { url: row.url },
	    signal })
        results[index] = previewRowFromResult (row.sourceRow, result)
      }
      catch (requestError)
      {
        if (signal.aborted)
          return
        issues.push (...previewIssues (row.sourceRow, row.url, requestError))
      }
    }
  }

  await Promise.all (
      Array.from ({ length: Math.min (4, rows.length) }, () => worker ()))
  return {
      rows: results.filter ((row): row is PostImportRow => row != null),
      issues }
}


const PostImportSourcePage: FC<Props> = ({ user }) => {
  const editable = canEditContent (user)
  const navigate = useNavigate ()
  const [source, setSource] = useState ('')
  const [loading, setLoading] = useState (false)
  const [sourceIssues, setSourceIssues] = useState<ReturnType<typeof validateImportSource>> ([])
  const [sourceError, setSourceError] = useState<string | null> (null)
  const saveTimer = useRef<number | null> (null)
  const editedRef = useRef (false)
  const previewController = useRef<AbortController | null> (null)

  const lineCount = countImportSourceLines (source)
  const messages = sourceError != null ? [sourceError] : []
  const sourceDescribedBy =
      [sourceError != null ? SOURCE_ERROR_ID : null,
       sourceIssues.length > 0 ? SOURCE_ISSUES_ID : null]
      .filter (value => value != null)
      .join (' ')

  useEffect (() => {
    cleanupExpiredPostImportSessions (message =>
	toast ({ title: '保存済みデータを整理できませんでした', description: message }))

    const draft = loadPostImportSourceDraft (message =>
	toast ({ title: '保存済み入力を復元できませんでした', description: message }))

    if (!(editedRef.current))
      {
	setSource (current =>
	    current === ''
	    ? draft.source
	    : current)
      }
  }, [])

  useEffect (() => {
    if (saveTimer.current != null)
      window.clearTimeout (saveTimer.current)

    saveTimer.current = window.setTimeout (() => {
      savePostImportSourceDraft (source, message =>
	  toast ({ title: '入力内容を保存できませんでした', description: message }))
    }, 300)

    return () => {
      if (saveTimer.current != null)
	window.clearTimeout (saveTimer.current)
    }
  }, [source])

  useEffect (() => () => {
    previewController.current?.abort ()
  }, [])

  const preview = async () => {
    if (lineCount === 0)
      {
	setSourceIssues ([])
	setSourceError ('URL を入力してください．')
	return
      }

    const issues = validateImportSource (source)
    if (issues.length > 0)
      {
	setSourceIssues (issues)
	setSourceError (null)
	return
      }

    setLoading (true)
    setSourceIssues ([])
    setSourceError (null)
    try
    {
      previewController.current?.abort ()
      const controller = new AbortController ()
      previewController.current = controller
      const previewed = await fetchPreviewRows (
	  source
	  .split (/\r\n|\n|\r/)
	  .map ((line, index) => ({
	      sourceRow: index + 1,
	      url: line.trim () }))
	  .filter (row => row.url !== ''),
	  controller.signal)
      if (previewed.issues.length > 0)
	{
	  setSourceIssues (previewed.issues)
	  return
	}
      const urlIssues = urlIssuesFromRows (previewed.rows, source)
      const duplicateIssues = Object.entries (
	  previewed.rows.reduce<Record<string, PostImportRow[]>> ((acc, row) => {
	    acc[row.url] ||= []
	    acc[row.url].push (row)
	    return acc
	  }, { }))
	  .flatMap (([, rows]) =>
	      rows.length < 2
	      ? []
	      : rows.map (row => ({
		  sourceRow: row.sourceRow,
		  message: 'URL が重複しています．',
		  url: row.url })))
      if (urlIssues.length > 0 || duplicateIssues.length > 0)
	{
	  setSourceIssues ([...urlIssues, ...duplicateIssues])
	  return
	}
      const nextRows = initialisePreviewRows (previewed.rows)
      const serialised = await serialisePostNewState (nextRows)
      if (serialised == null)
	return

      const sessionId = generatePostImportSessionId ()
      const session = {
	  source,
	  rows: nextRows,
	  repairMode: resultRepairMode (nextRows) }
      const saved = savePostImportSession (sessionId, session)
      const sessionLoaded = loadPostImportSession (sessionId)
      if (!(serialised.complete)
          && (!(saved))
          && !(serialisedPostImportRowsEqual (sessionLoaded?.rows ?? [], nextRows)))
	return
      if (saved && !(serialisedPostImportRowsEqual (sessionLoaded?.rows ?? [], nextRows)))
	return

      navigate (appendPostNewSessionId (serialised.path, sessionId))
    }
    catch (requestError)
    {
      const message =
	  isApiError<{ message?: string, baseErrors?: string[] }> (requestError)
	  ? (requestError.response?.data?.message
	     ?? requestError.response?.data?.baseErrors?.[0])
	  : undefined
      setSourceError (message ?? '入力を確認してください．')
      toast ({ title: '投稿情報の取得に失敗しました',
	       description: message ?? '入力を確認してください．' })
    }
    finally
    {
      setLoading (false)
    }
  }

  if (!(editable))
	return <Forbidden/>

  return (
      <MainArea>
	<Helmet>
	  <title>{`広場に投稿を追加 | ${ SITE_TITLE }`}</title>
	</Helmet>

	<Form className="max-w-4xl">
	  <PageTitle>広場に投稿を追加</PageTitle>
	  <FormField label="URL リスト">
	    {() => (
		<TextArea
		    value={source}
		    rows={14}
		    invalid={messages.length > 0 || sourceIssues.length > 0}
		    aria-describedby={sourceDescribedBy || undefined}
		    className="h-80 font-mono text-sm"
		    onBlur={() => {
		      savePostImportSourceDraft (source, message =>
			  toast ({ title: '入力内容を保存できませんでした',
				   description: message }))
		    }}
		    onChange={ev => {
		      editedRef.current = true
		      setSource (ev.target.value)
		      setSourceError (null)
		      setSourceIssues ([])
		    }}/>)}
	  </FormField>
	  <div id={messages.length > 0 ? SOURCE_ERROR_ID : undefined}>
	    <FieldError messages={messages}/>
	  </div>
	  <ul
	      id={SOURCE_ISSUES_ID}
	      className="space-y-2 text-sm text-red-700 dark:text-red-300">
	    {sourceIssues.map (issue => (
		<li key={`${ issue.sourceRow }-${ issue.message }-${ issue.url }`}>
		  <div>{issue.sourceRow} 行目: {issue.message}</div>
		  <div className="break-all font-mono text-xs">{issue.url}</div>
		</li>))}
	  </ul>

	  <div className="flex items-center justify-between gap-3">
	    <div className="text-sm text-neutral-600 dark:text-neutral-300">
	      入力行数 {lineCount} / {MAX_ROWS}
	    </div>
	    <Button
		type="button"
		className="shrink-0"
		onClick={preview}
		disabled={loading}>
	      次へ
	    </Button>
	  </div>
	</Form>
      </MainArea>)
}

export default PostImportSourcePage
