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

import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
import PostImportRowForm from '@/components/posts/import/PostImportRowForm'
import PostImportRowSummary from '@/components/posts/import/PostImportRowSummary'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
import { apiPost } from '@/lib/api'
import useDialogue from '@/lib/dialogues/useDialogue'
import { canEditContent } from '@/lib/users'
import { loadPostImportSession,
	 creatableImportRows,
	 initialisePreviewRows,
	 mergeImportResults,
	 mergeValidatedImportRows,
	 processableImportRows,
	 reviewSummaryCounts,
	 savePostImportSession } from '@/lib/postImportSession'
import Forbidden from '@/pages/Forbidden'

import type { FC } from 'react'

import type { PostImportRowDraft } from '@/components/posts/import/PostImportRowForm'
import type { PostImportResultRow,
	      PostImportRow,
	      PostImportSession } from '@/lib/postImportSession'
import type { User } from '@/types'

type Props = { user: User | null }


const PostImportReviewPage: FC<Props> = ({ user }) => {
  const editable = canEditContent (user)
  const dialogue = useDialogue ()
  const navigate = useNavigate ()
  const { sessionId } = useParams ()

  const [session, setSession] = useState<PostImportSession | null> (null)
  const [loading, setLoading] = useState (false)
  const [missing, setMissing] = useState (false)
  const [editingRow, setEditingRow] = useState<PostImportRow | null> (null)
  const sessionRef = useRef<PostImportSession | null> (null)

  useEffect (() => {
    if (sessionId == null)
      return

    const loaded = loadPostImportSession (sessionId, message =>
	toast ({ title: '取込状態を復元できませんでした', description: message }))
    setSession (loaded)
    setMissing (loaded == null)
  }, [sessionId])

  useEffect (() => {
    if (sessionId == null || session == null)
      return

    savePostImportSession (sessionId, session, message =>
	toast ({ title: '取込状態を保存できませんでした', description: message }))
  }, [session, sessionId])

  useEffect (() => {
    sessionRef.current = session
  }, [session])

  const rows = session?.rows ?? []
  const counts = useMemo (() => reviewSummaryCounts (rows), [rows])
  const processable = useMemo (
      () => processableImportRows (rows),
      [rows])
  const creatable = useMemo (
      () => creatableImportRows (rows),
      [rows])
  const reviewRows =
      session?.repairMode === 'failed'
      ? (
	  [...rows].sort ((a, b) => {
	    const aFailed = a.importStatus === 'failed' ? 0 : 1
	    const bFailed = b.importStatus === 'failed' ? 0 : 1
	    return aFailed - bFailed || a.sourceRow - b.sourceRow
	  }))
      : rows

  useEffect (() => {
    if (editingRow == null || session?.repairMode !== 'failed')
      return

    const element = document.getElementById (`post-import-row-${ editingRow.sourceRow }`)
    element?.scrollIntoView ({ block: 'center', behavior: 'smooth' })
  }, [editingRow, session?.repairMode])

  const updateSessionRows = (nextRows: PostImportRow[]) =>
      setSession (current =>
	  current != null ? { ...current, rows: nextRows } : current)

  const saveDraft = async (
      row: PostImportRow,
      { draft, resetRequested }: {
	  draft:            PostImportRowDraft
	  resetRequested:   boolean },
  ): Promise<{ saved: boolean
	       row:   PostImportRow | null }> => {
    const currentSession = sessionRef.current
    if (currentSession == null)
      return { saved: false, row: null }

    const baseRow =
	resetRequested
	? { ...row,
	    url: row.resetSnapshot.url,
	    attributes: { ...row.resetSnapshot.attributes },
	    provenance: { ...row.resetSnapshot.provenance },
	    tagSources: { ...row.resetSnapshot.tagSources },
	    fieldWarnings: Object.fromEntries (
		Object.entries (row.resetSnapshot.fieldWarnings)
		    .map (([key, values]) => [key, [...values]])),
	    baseWarnings: [...row.resetSnapshot.baseWarnings],
	    metadataUrl: row.resetSnapshot.metadataUrl }
	: row
    const urlChanged = draft.url !== baseRow.url
    const nextRow = buildNextEditedRow (baseRow, draft, urlChanged)

    const nextRows = currentSession.rows.map (row =>
		row.sourceRow === baseRow.sourceRow
		? nextRow
		: row)
    try
    {
      const validated = await apiPost<{ rows: PostImportRow[] }> ('/posts/import/validate', {
	  rows:
	      nextRows
	      .filter (row => row.importStatus !== 'created')
	      .map (row => ({ sourceRow: row.sourceRow,
			      url: row.url,
			      attributes: row.attributes,
			      provenance: row.provenance,
			      tagSources: row.tagSources,
			      metadataUrl: row.metadataUrl })),
	  changed_row: urlChanged ? baseRow.sourceRow : -1 })
      const validatedRows = initialisePreviewRows (validated.rows)
      const target = validatedRows.find (row => row.sourceRow === baseRow.sourceRow)
      if (target != null && Object.keys (target.validationErrors).length > 0)
	return { saved: false, row: target }
      updateSessionRows (mergeValidatedImportRows (nextRows, validatedRows))
      return { saved: true, row: null }
    }
    catch
    {
      toast ({ title: '行の再検証に失敗しました' })
      return { saved: false, row: null }
    }
  }

  const openEditingDialogue = async (row: PostImportRow) => {
    const saveRowDraft = (
	{ draft, resetRequested }: {
	    draft:          PostImportRowDraft
	    resetRequested: boolean },
    ) =>
	saveDraft (row, { draft, resetRequested })

    await dialogue.form ({
	title: '投稿を編輯',
	description: `投稿 ${ row.sourceRow } の内容を確認し、必要な項目を編輯してください．`,
	cancelText: '取消',
	size: 'large',
	body: controls => (
	    <PostImportRowForm
		row={row}
		controls={controls}
		onSave={saveRowDraft}/>) })
  }

  const editRow = async (row: PostImportRow) => {
    setEditingRow (row)
    try
    {
      await openEditingDialogue (row)
    }
    finally
    {
      setEditingRow (current =>
          current?.sourceRow === row.sourceRow
          ? null
          : current)
    }
  }

  const submit = async () => {
    if (sessionId == null || session == null || processable.length === 0)
      return

    setLoading (true)
    try
    {
      const validated = await apiPost<{ rows: PostImportRow[] }> ('/posts/import/validate', {
	  rows:
	      session.rows
	      .filter (row => row.importStatus !== 'created')
	      .map (row => ({ sourceRow: row.sourceRow,
			      url: row.url,
			      attributes: row.attributes,
			      provenance: row.provenance,
			      tagSources: row.tagSources,
			      metadataUrl: row.metadataUrl })),
	  changed_row: -1 })
      const validatedRows = initialisePreviewRows (validated.rows)
      const mergedRows = mergeValidatedImportRows (session.rows, validatedRows)
      const firstInvalid = mergedRows.find (row => Object.keys (row.validationErrors).length > 0)
      if (firstInvalid != null)
	{
	  const nextSession = { ...session, rows: mergedRows }
	  sessionRef.current = nextSession
	  setSession (nextSession)
	  void editRow (firstInvalid)
	  return
	}

      const result = await apiPost<{
	  created: number
	  skipped: number
	  failed:  number
	  rows:    PostImportResultRow[] }> ('/posts/import', {
	      rows: processableImportRows (mergedRows).map (row => ({
		  sourceRow: row.sourceRow,
		  url: row.url,
		  attributes: row.attributes,
		  provenance: row.provenance,
		  tagSources: row.tagSources,
		  metadataUrl: row.metadataUrl })) })
      const mergedResults = mergeImportResults (mergedRows, result.rows)
      const recoverableRows = result.rows.filter (row =>
	  row.status === 'failed'
	  && row.recoverable
	  && Object.keys (row.errors ?? { }).length > 0)
      const nextRows = mergedResults.map ((row): PostImportRow => {
	const recoverable = recoverableRows.find (rr => rr.sourceRow === row.sourceRow)
	if (recoverable == null)
	  return row
	return {
	    ...row,
	    importStatus: 'pending',
	    validationErrors: recoverable.errors ?? { },
	    importErrors: undefined }
      })
      const firstRecoverable = nextRows.find (row =>
	  Object.keys (row.validationErrors).length > 0
	  && row.importStatus !== 'created')
      if (firstRecoverable != null)
	{
	  const repairSession = { ...session,
				  rows: nextRows,
				  repairMode: 'failed' as const }
	  sessionRef.current = repairSession
	  setSession (repairSession)
	  const saved = savePostImportSession (sessionId, repairSession, message =>
	      toast ({ title: '取込状態を保存できませんでした', description: message }))
	  if (saved)
	    void editRow (firstRecoverable)
	  return
	}
      const nextSession = { ...session, rows: nextRows, repairMode: 'all' as const }
      setSession (nextSession)
      const saved = savePostImportSession (sessionId, nextSession, message =>
	  toast ({ title: '取込状態を保存できませんでした', description: message }))
      if (!(saved))
	return
      navigate (`/posts/import/${ sessionId }/result`)
    }
    catch
    {
      toast ({ title: '登録に失敗しました' })
    }
    finally
    {
      setLoading (false)
    }
  }

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

  if (missing || sessionId == null || session == null)
    {
      return (
	  <MainArea>
	    <div className="mx-auto max-w-4xl space-y-4 p-4">
	      <PageTitle>投稿インポート</PageTitle>
	      <div className="text-red-700 dark:text-red-300">取込状態が見つかりません．</div>
	      <Button type="button" onClick={() => navigate ('/posts/import')}>
		URL リスト入力へ戻る
	      </Button>
	    </div>
	  </MainArea>)
    }

  return (
      <>
	<Helmet>
	  <title>{`投稿インポート確認 | ${ SITE_TITLE }`}</title>
	</Helmet>

	<MainArea className="min-h-0">
	  <div className="mx-auto max-w-6xl space-y-4 p-4">
	    <PageTitle>投稿情報の確認</PageTitle>

	    <div className="space-y-3">
	      {reviewRows.map (row => (
		  <div key={row.sourceRow} id={`post-import-row-${ row.sourceRow }`}>
		    <PostImportRowSummary
			row={row}
			onEdit={() => void editRow (row)}/>
		  </div>))}
	    </div>
	  </div>
	</MainArea>

	<PostImportFooter
	    loading={loading}
	    processableCount={processable.length}
	    creatableCount={creatable.length}
	    skipPlannedCount={counts.skipPlanned}
	    onBack={() => navigate ('/posts/import')}
	    onSubmit={submit}/>
      </>)
}

const PostImportFooter = (
    { loading,
      processableCount,
      creatableCount,
      skipPlannedCount,
      onBack,
      onSubmit }: { loading:            boolean
		    processableCount:   number
		    creatableCount:     number
		    skipPlannedCount:   number
		    onBack:             () => void
		    onSubmit:           () => void },
) => (
    <div
	className="shrink-0 border-t bg-white/95 p-4 backdrop-blur
		   dark:border-neutral-700 dark:bg-neutral-950/95">
      <div className="mx-auto flex max-w-6xl flex-col gap-3 md:flex-row
		      md:items-center md:justify-between">
	<div className="flex flex-wrap items-center gap-2 text-sm">
	  <span>処理対象 {processableCount}件</span>
	  <span>登録対象 {creatableCount}件</span>
	  <span>スキップ予定 {skipPlannedCount}件</span>
	</div>
	<div className="flex flex-col gap-2 sm:flex-row">
	  <Button type="button" variant="outline" onClick={onBack}>
	    URL リスト入力へ戻る
	  </Button>
	  <Button
	      type="button"
	      onClick={onSubmit}
	      disabled={loading || processableCount === 0}>
	    取込実行
	  </Button>
	</div>
      </div>
    </div>)

const buildNextEditedRow = (
    editingRow:  PostImportRow,
    draft:       PostImportRowDraft,
    urlChanged:  boolean,
): PostImportRow => {
    const nextProvenance = { ...editingRow.provenance }
    const nextAttributes = { ...editingRow.attributes }
    const nextTagSources = {
	automatic: editingRow.tagSources?.automatic ?? '',
	manual: editingRow.tagSources?.manual ?? '' }
    const draftFields = [
	['title', draft.title],
	['thumbnailBase', draft.thumbnailBase],
	['originalCreatedFrom', draft.originalCreatedFrom],
	['originalCreatedBefore', draft.originalCreatedBefore],
	['duration', draft.duration],
	['parentPostIds', draft.parentPostIds]] as const
    draftFields.forEach (([field, value]) => {
	nextAttributes[field] = value
	nextProvenance[field] =
	    value !== String (editingRow.attributes[field] ?? '')
	    ? 'manual'
	    : (editingRow.provenance[field] ?? 'automatic')
    })
    nextAttributes.tags = draft.tags
    if (draft.tags !== String (editingRow.attributes.tags ?? ''))
      {
	nextProvenance.tags = 'manual'
	nextTagSources.manual = draft.tags
      }
    else
      {
	nextProvenance.tags = editingRow.provenance.tags ?? 'automatic'
	nextTagSources.manual = editingRow.tagSources?.manual ?? ''
      }

    return {
	...editingRow,
	url: draft.url,
	attributes: nextAttributes,
	provenance: {
	    ...nextProvenance,
	    url: urlChanged ? 'manual' : (editingRow.provenance.url ?? 'manual') },
	tagSources: nextTagSources,
	importStatus: editingRow.importStatus === 'created' ? 'created' : 'pending',
	importErrors: undefined }
}

export default PostImportReviewPage
