import { useQueries } from '@tanstack/react-query'
import { AnimatePresence, motion } from 'framer-motion'
import { useEffect, useMemo, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useLocation, useNavigate } from 'react-router-dom'

import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
import PrefetchLink from '@/components/PrefetchLink'
import PostThumbnailPreview from '@/components/posts/PostThumbnailPreview'
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 { apiGet, apiPost, isApiError } from '@/lib/api'
import useDialogue from '@/lib/dialogues/useDialogue'
import { fetchPost } from '@/lib/posts'
import {
    appendPostNewSessionId,
    parsePostNewState,
    parsePostNewSessionId,
    serialisePostNewState,
} from '@/lib/postNewQueryState'
import {
    buildNextEditedRow,
    canEditReviewRow,
    canRetryResultRow,
    clearPostImportSession,
    clearPostImportSourceDraft,
    generatePostImportSessionId,
    initialisePreviewRows,
    isCompletedReviewRow,
    isExistingSkipRow,
    isNonRecoverableFailedRow,
    mergeImportResults,
    processableImportRows,
    replaceImportRow,
    resultRepairMode,
    resultRowMessages,
    reviewSummaryCounts,
    retryImportRow,
    savePostImportSession,
    loadPostImportSession,
} from '@/lib/postImportSession'
import { postsKeys } from '@/lib/queryKeys'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { canEditContent } from '@/lib/users'
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 { Post, 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[]
  existingPost?:         { id: number } | null }

type BulkApiRow = {
  status:        'created' | 'skipped' | 'failed'
  post?:         { id: number }
  existingPost?: { id: number } | null
  fieldWarnings?: Record<string, string[]>
  baseWarnings?: string[]
  errors?:       Record<string, string[]>
  baseErrors?:   string[]
  recoverable?:  boolean }

type ExistingSkippedRowsProps = {
  open:  boolean
  rows:   PostImportRow[] }

const isRepairRow = (row: PostImportRow): boolean =>
    row.recoverable === true
    && (row.importStatus === 'failed'
        || (row.importStatus === 'pending'
            && Object.keys (row.validationErrors).length > 0))


const buildSession = (rows: PostImportRow[]): PostImportSession => ({
    version: 3,
    expiresAt: '',
    source: rows.map (row => row.url).join ('\n'),
    rows,
    repairMode: resultRepairMode (rows) })


const buildDryRunFormData = (row: PostImportRow): FormData => {
  const formData = new FormData ()
  formData.append ('url', row.url)
  formData.append ('title', String (row.attributes.title ?? ''))
  formData.append ('thumbnail_base', String (row.attributes.thumbnailBase ?? ''))
  formData.append ('tags', String (row.attributes.tags ?? ''))
  formData.append ('parent_post_ids', String (row.attributes.parentPostIds ?? ''))
  formData.append (
      'original_created_from',
      String (row.attributes.originalCreatedFrom ?? ''))
  formData.append (
      'original_created_before',
      String (row.attributes.originalCreatedBefore ?? ''))
  formData.append ('video_ms', String (row.attributes.videoMs ?? ''))
  formData.append ('duration', String (row.attributes.duration ?? ''))
  return formData
}


const mergeDryRunRow = (
    currentRow: PostImportRow,
    result:     PreviewResponse,
): PostImportRow =>
    initialisePreviewRows ([{
        ...currentRow,
        url: result.url,
        attributes: {
            ...currentRow.attributes,
            title: result.title ?? '',
            thumbnailBase: result.thumbnailBase ?? '',
            originalCreatedFrom: result.originalCreatedFrom ?? '',
            originalCreatedBefore: result.originalCreatedBefore ?? '',
            duration: result.duration ?? '',
            videoMs: result.videoMs ?? '',
            tags: result.tags ?? '',
            parentPostIds: String (currentRow.attributes.parentPostIds ?? '') },
        fieldWarnings: result.fieldWarnings ?? { },
        baseWarnings: result.baseWarnings ?? [],
        validationErrors: { },
        importErrors: undefined,
        status:
            Object.keys (result.fieldWarnings ?? { }).length > 0
            || (result.baseWarnings?.length ?? 0) > 0
            ? 'warning'
            : 'ready',
        skipReason: result.existingPost?.id != null ? 'existing' : undefined,
        existingPostId: result.existingPost?.id,
        importStatus:
            currentRow.importStatus === 'created'
            ? 'created'
            : 'pending',
        recoverable: undefined }])[0]


const indexedBulkResults = (
    rows:     PostImportRow[],
    results:  BulkApiRow[],
): PostImportResultRow[] =>
    rows.map ((row, index) => {
      const result = results[index]
      if (result?.status === 'created' && result.post != null)
        {
          return {
              sourceRow: row.sourceRow,
              status: 'created',
              post: result.post,
              fieldWarnings: result.fieldWarnings,
              baseWarnings: result.baseWarnings,
              errors: result.errors }
        }
      if (result?.status === 'skipped' && result.existingPost != null)
        {
          return {
              sourceRow: row.sourceRow,
              status: 'skipped',
              existingPostId: result.existingPost.id,
              fieldWarnings: result.fieldWarnings,
              baseWarnings: result.baseWarnings,
              errors: result.errors }
        }
      return {
          sourceRow: row.sourceRow,
          status: 'failed',
          fieldWarnings: result?.fieldWarnings,
          baseWarnings: result?.baseWarnings,
          errors: result?.errors,
          recoverable: result?.recoverable }
    })


const buildBulkFormData = (rows: PostImportRow[]): FormData => {
  const formData = new FormData ()
  formData.append (
      'posts',
      JSON.stringify (
          rows.map (row => ({
	      url: row.url,
	      title: row.attributes.title ?? '',
	      thumbnail_base: row.attributes.thumbnailBase ?? '',
	      tags: row.attributes.tags ?? '',
	      parent_post_ids: row.attributes.parentPostIds ?? '',
	      original_created_from: row.attributes.originalCreatedFrom ?? '',
	      original_created_before: row.attributes.originalCreatedBefore ?? '',
	      video_ms: row.attributes.videoMs ?? '',
	      duration: row.attributes.duration ?? '' }))))
  return formData
}


const ExistingSkippedRows: FC<ExistingSkippedRowsProps> = ({ open, rows }) => {
  const existingPostIds = useMemo (
      () =>
	  open
	  ? Array.from (
	      new Set (
		  rows
		  .map (row => row.existingPostId)
		  .filter ((postId): postId is number => postId != null),
	      ),
	  )
	  : [],
      [open, rows],
  )
  const posts = useQueries ({
      queries: existingPostIds.map (postId => ({
	  enabled:  open,
	  queryKey: postsKeys.show (String (postId)),
	  queryFn:  () => fetchPost (String (postId)) })),
  })
  const postsById = new Map<number, Post> (
      posts
      .map (query => query.data)
      .filter ((post): post is Post => post != null)
      .map (post => [post.id, post]),
  )

  return (
      <div className="mt-3 max-h-72 overflow-y-auto rounded border">
	<div className="divide-y">
	  {existingPostIds.map (postId => {
	    const post = postsById.get (postId)
	    if (post == null)
	      return <div key={postId} className="h-14"/>

	    return (
		<PrefetchLink
		    key={postId}
		    to={`/posts/${ post.id }`}
		    target="_blank"
		    rel="noopener noreferrer"
		    className="flex items-center gap-3 p-3">
		  <PostThumbnailPreview
		      url={post.thumbnail ?? ''}
		      className="h-10 w-10 shrink-0"/>
		  <div className="min-w-0 flex-1">
		    <div className="truncate text-sm font-medium">
		      {post.title ?? ''}
		    </div>
		    <div className="truncate text-xs text-neutral-600 dark:text-neutral-300">
		      {post.url}
		    </div>
		  </div>
		</PrefetchLink>)
	  })}
	</div>
      </div>)
}


const PostImportReviewPage: FC<Props> = ({ user }) => {
  const editable = canEditContent (user)
  const dialogue = useDialogue ()
  const location = useLocation ()
  const navigate = useNavigate ()
  const behaviourSettings = useClientBehaviourSettings ()
  const animationMode = behaviourSettings.animation ?? 'normal'
  const existingRowsTransition =
      animationMode === 'off'
      ? { duration: 0 }
      : animationMode === 'reduced'
	? { duration: .08, ease: 'linear' as const }
	: { duration: .2, ease: 'easeOut' as const }

  const [session, setSession] = useState<PostImportSession | null> (null)
  const [loading, setLoading] = useState (false)
  const [loadingRow, setLoadingRow] = useState<number | null> (null)
  const [editingRow, setEditingRow] = useState<PostImportRow | null> (null)
  const [showExistingRows, setShowExistingRows] = useState (false)
  const sessionRef = useRef<PostImportSession | null> (null)
  const sessionIdRef = useRef<string | null> (null)
  const persistedSearchRef = useRef<string | null> (null)
  const persistSequenceRef = useRef (0)

  const persistSession = async (
      nextSession: PostImportSession,
  ): Promise<PostImportSession | null> => {
    const sessionId =
	sessionIdRef.current ?? generatePostImportSessionId ()
    sessionIdRef.current = sessionId
    const sequence = ++persistSequenceRef.current
    const serialised = await serialisePostNewState (nextSession.rows)
    if (serialised == null)
      return null
    if (persistSequenceRef.current !== sequence)
      return sessionRef.current

    savePostImportSession (sessionId, nextSession)
    const loadedSession = loadPostImportSession (sessionId)
    if (!(serialised.complete)
        && loadedSession?.rows.length !== nextSession.rows.length)
      return null

    const persistedSession = buildSession (nextSession.rows)
    const path = appendPostNewSessionId (serialised.path, sessionId)
    persistedSearchRef.current = (new URL (path, window.location.origin)).search
    sessionRef.current = persistedSession
    setSession (persistedSession)
    navigate (path, { replace: true })
    return persistedSession
  }

  const finishImport = () => {
    const sessionId = sessionIdRef.current
    if (sessionId != null)
      clearPostImportSession (sessionId)
    clearPostImportSourceDraft (message =>
	toast ({ title: '入力内容を削除できませんでした', description: message }))
    navigate ('/posts')
  }

  useEffect (() => {
    let active = true

    if (sessionRef.current != null && persistedSearchRef.current === location.search)
      return () => {
      }

    const hydrate = async () => {
      const parsed = await parsePostNewState (location.search)
      if (!(active))
	return
      if (parsed == null)
	{
	  navigate ('/posts/new', { replace: true })
	  return
	}

      const parsedSessionId = parsePostNewSessionId (location.search)
      const sessionId = parsedSessionId ?? generatePostImportSessionId ()
      sessionIdRef.current = sessionId
      const loaded = loadPostImportSession (sessionId) ?? buildSession (parsed.rows)
      persistedSearchRef.current = location.search
      sessionRef.current = loaded
      setSession (loaded)

      if (parsed.shortcut)
	{
	  try
	  {
	    const preview = await apiGet<PreviewResponse> ('/posts/preview', {
		params: { url: parsed.source } })
	    if (!(active))
	      return
	    const rows = initialisePreviewRows ([{
		sourceRow: 1,
		url: preview.url,
		attributes: {
		    title: preview.title ?? '',
		    thumbnailBase: preview.thumbnailBase ?? '',
		    originalCreatedFrom: preview.originalCreatedFrom ?? '',
		    originalCreatedBefore: preview.originalCreatedBefore ?? '',
		    duration: preview.duration ?? '',
		    videoMs: preview.videoMs ?? '',
		    tags: preview.tags ?? '',
		    parentPostIds: '' },
		fieldWarnings: preview.fieldWarnings ?? { },
		baseWarnings: preview.baseWarnings ?? [],
		validationErrors: { },
		provenance: {
		    url: 'manual',
		    title: 'automatic',
		    thumbnailBase: 'automatic',
		    originalCreatedFrom: 'automatic',
		    originalCreatedBefore: 'automatic',
		    duration: 'automatic',
		    videoMs: 'automatic',
		    tags: 'automatic',
		    parentPostIds: 'automatic' },
		tagSources: {
		    automatic: preview.tags ?? '',
		    manual: '' },
		status:
		    Object.keys (preview.fieldWarnings ?? { }).length > 0
		    || (preview.baseWarnings?.length ?? 0) > 0
		    ? 'warning'
		    : 'ready',
		skipReason: preview.existingPost?.id != null ? 'existing' : undefined,
		existingPostId: preview.existingPost?.id,
		resetSnapshot: {
		    url: preview.url,
		    attributes: {
			title: preview.title ?? '',
			thumbnailBase: preview.thumbnailBase ?? '',
			originalCreatedFrom: preview.originalCreatedFrom ?? '',
			originalCreatedBefore: preview.originalCreatedBefore ?? '',
			duration: preview.duration ?? '',
			videoMs: preview.videoMs ?? '',
			tags: preview.tags ?? '',
			parentPostIds: '' },
		    provenance: {
			url: 'manual',
			title: 'automatic',
			thumbnailBase: 'automatic',
			originalCreatedFrom: 'automatic',
			originalCreatedBefore: 'automatic',
			duration: 'automatic',
			videoMs: 'automatic',
			tags: 'automatic',
			parentPostIds: 'automatic' },
		    tagSources: {
			automatic: preview.tags ?? '',
			manual: '' },
		    fieldWarnings: preview.fieldWarnings ?? { },
		    baseWarnings: preview.baseWarnings ?? [] } }])
	    const persisted = await persistSession (buildSession (rows))
	    if (persisted == null)
	      return
	  }
	  catch
	  {
	    if (active)
	      navigate ('/posts/new', { replace: true })
	  }
	  return
	}

      if (parsedSessionId == null)
	{
	  await persistSession (loaded)
	}
    }

    void hydrate ()

    return () => {
      active = false
    }
  }, [location.search, navigate])

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

  useEffect (() => {
    if (session == null || session.rows.length === 0)
      return
    if (session.rows.every (row => isCompletedReviewRow (row)))
      finishImport ()
  }, [session])

  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 rows = session?.rows ?? []
  const counts = useMemo (() => reviewSummaryCounts (rows), [rows])
  const sortedRows =
      session?.repairMode === 'failed'
      ? (
	  [...rows].sort ((a, b) => {
	    const aRepair = isRepairRow (a) ? 0 : 1
	    const bRepair = isRepairRow (b) ? 0 : 1
	    return aRepair - bRepair || a.sourceRow - b.sourceRow
	  }))
      : rows
  const existingRows = sortedRows.filter (row => isExistingSkipRow (row))
  const reviewRows = sortedRows.filter (row => !(isExistingSkipRow (row)))
  const busy = loading || loadingRow != null

  const toggleManualSkip = async (sourceRow: number, checked: boolean) => {
    if (busy)
      return

    const currentSession = sessionRef.current
    if (currentSession == null)
      return

    const nextRows = currentSession.rows.map (row => {
      if (row.sourceRow !== sourceRow)
	return row
      if (isExistingSkipRow (row)
          || row.importStatus === 'created'
          || isNonRecoverableFailedRow (row))
	return row
      return {
	  ...row,
	  skipReason: checked ? 'manual' : undefined,
	  existingPostId: checked ? undefined : row.existingPostId }
    })
    await persistSession (buildSession (nextRows))
  }

  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 }
    if (!(canEditReviewRow (row)))
      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)
    try
    {
      const dryRun = await apiPost<PreviewResponse> (
	  '/posts?dry=1',
	  buildDryRunFormData (nextRow))
      const latestSession = sessionRef.current
      if (latestSession == null)
	return { saved: false, row: null }

      const mergedRow = mergeDryRunRow (nextRow, dryRun)
      const mergedRows = replaceImportRow (latestSession.rows, mergedRow)
      const nextSession = await persistSession (buildSession (mergedRows))
      if (nextSession == null)
	return { saved: false, row: null }
      const mergedTarget = nextSession.rows.find (
	  currentRow => currentRow.sourceRow === baseRow.sourceRow)
      if (mergedTarget == null)
	return { saved: false, row: null }
      if (Object.keys (mergedTarget.validationErrors).length > 0)
	return { saved: false, row: mergedTarget }
      return { saved: true, row: null }
    }
    catch (requestError)
    {
      if (isApiError<{
	      errors?:      Record<string, string[]>
	      baseErrors?:  string[]
	  }> (requestError)
	  && requestError.response?.status === 422)
	{
	  const errorRow = {
	      ...nextRow,
	      validationErrors: requestError.response.data.errors ?? { },
	      baseWarnings: [],
	      fieldWarnings: { },
	      status: 'error' as const }
	  setMessageRow (errorRow)
	  return { saved: false, row: errorRow }
	}

      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 retry = async (sourceRow: number) => {
    if (busy)
      return

    const initialSession = sessionRef.current
    if (initialSession == null)
      return

    setLoadingRow (sourceRow)
    const originalRow = initialSession.rows.find (row => row.sourceRow === sourceRow)
    if (originalRow == null)
      {
	setLoadingRow (null)
	return
      }
    try
    {
      const pendingRows = retryImportRow (initialSession.rows, sourceRow)
      const pendingSession = await persistSession (buildSession (pendingRows))
      if (pendingSession == null)
	return

      const target = pendingSession.rows.find (row => row.sourceRow === sourceRow)
      if (target == null)
	return

      const result = await apiPost<{ results: BulkApiRow[] }>(
	  '/posts/bulk',
	  buildBulkFormData ([target]))
      if (result.results.length !== 1)
	{
	  const latest = sessionRef.current ?? pendingSession
	  const restoredRows = replaceImportRow (latest.rows, originalRow)
	  await persistSession (buildSession (restoredRows))
	  toast ({ title: '登録結果が不完全でした' })
	  return
	}

      const indexedResults = indexedBulkResults ([target], result.results)
      const latestAfterImport = sessionRef.current ?? pendingSession
      const mergedRows = mergeImportResults (latestAfterImport.rows, indexedResults)
      const recoverableRows = indexedResults.filter (row =>
	  row.status === 'failed'
	  && row.recoverable
	  && Object.keys (row.errors ?? { }).length > 0)
      const nextRows = mergedRows.map ((row): PostImportRow => {
	const recoverable = recoverableRows.find (
	    failedRow => failedRow.sourceRow === row.sourceRow)
	if (recoverable == null)
	  return row
	return {
	    ...row,
	    importStatus: 'pending',
	    recoverable: true,
	    validationErrors: recoverable.errors ?? { },
	    importErrors: undefined }
      })
      await persistSession (buildSession (nextRows))
    }
    catch
    {
      const latest = sessionRef.current ?? initialSession
      const restoredRows = replaceImportRow (latest.rows, originalRow)
      await persistSession (buildSession (restoredRows))
      toast ({ title: '再試行に失敗しました' })
    }
    finally
    {
      setLoadingRow (null)
    }
  }

  const submit = async () => {
    const currentSession = sessionRef.current
    if (currentSession == null || busy)
      return
    if (currentSession.rows.every (row => isCompletedReviewRow (row)))
      {
	finishImport ()
	return
      }

    setLoading (true)
    try
    {
      const processingRows = processableImportRows (currentSession.rows)
      if (processingRows.length === 0)
	{
	  finishImport ()
	  return
	}

      const result = await apiPost<{ results: BulkApiRow[] }>(
	  '/posts/bulk',
	  buildBulkFormData (processingRows))
      if (result.results.length !== processingRows.length)
	{
	  toast ({ title: '登録結果が不完全でした' })
	  return
	}

      const indexedResults = indexedBulkResults (processingRows, result.results)
      const latestAfterImport = sessionRef.current ?? buildSession (currentSession.rows)
      const mergedResults = mergeImportResults (latestAfterImport.rows, indexedResults)
      const recoverableRows = indexedResults.filter (row =>
	  row.status === 'failed'
	  && row.recoverable
	  && Object.keys (row.errors ?? { }).length > 0)
      const nextRows = mergedResults.map ((row): PostImportRow => {
	const recoverable = recoverableRows.find (
	    failedRow => failedRow.sourceRow === row.sourceRow)
	if (recoverable == null)
	  return row
	return {
	    ...row,
	    importStatus: 'pending',
	    recoverable: true,
	    validationErrors: recoverable.errors ?? { },
	    importErrors: undefined }
      })
      await persistSession (buildSession (nextRows))
    }
    catch
    {
      toast ({ title: '登録に失敗しました' })
    }
    finally
    {
      setLoading (false)
    }
  }

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

  if (session == null)
    return null

  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>

	    {counts.existingSkipped > 0 && (
		<div className="rounded-lg border p-4">
		  <button
		      type="button"
		      className="text-left text-sm font-medium"
		      onClick={() => setShowExistingRows (current => !(current))}>
		    既存投稿による自動スキップ {counts.existingSkipped}件
		  </button>
		  <AnimatePresence initial={false}>
		    {showExistingRows && (
			<motion.div
			    initial={
				animationMode === 'off'
				? false
				: { height: 0, opacity: 0 }
			    }
			    animate={{ height: 'auto', opacity: 1 }}
			    exit={{ height: 0, opacity: 0 }}
			    transition={existingRowsTransition}
			    className="overflow-hidden">
			  <ExistingSkippedRows open={showExistingRows} rows={existingRows}/>
			</motion.div>)}
		  </AnimatePresence>
		</div>)}

	    <div className="space-y-3">
	      {reviewRows.map ((row, index) => (
		  <div key={row.sourceRow} id={`post-import-row-${ row.sourceRow }`}>
		    <PostImportRowSummary
			row={row}
			displayNumber={index + 1}
			showSkipToggle={true}
			editDisabled={busy}
			retryDisabled={busy}
			skipDisabled={
			    busy
			    || isExistingSkipRow (row)
			    || row.importStatus === 'created'
			    || isNonRecoverableFailedRow (row)}
			onEdit={() => editRow (row)}
			onToggleSkip={checked => toggleManualSkip (row.sourceRow, checked)}
			onRetry={canRetryResultRow (row) ? () => retry (row.sourceRow) : undefined}
			rowMessages={resultRowMessages (row)}/>
		  </div>))}
	    </div>
	  </div>
	</MainArea>

	<PostImportFooter
	    loading={busy}
	    creatableCount={counts.creatable}
	    manualSkippedCount={counts.manualSkipped}
	    existingSkippedCount={counts.existingSkipped}
	    pendingOrErrorCount={counts.pendingOrError}
	    onBack={() => navigate (-1)}
	    onSubmit={() => submit ()}/>
      </>)
}

const PostImportFooter = (
    { loading,
      creatableCount,
      manualSkippedCount,
      existingSkippedCount,
      pendingOrErrorCount,
      onBack,
      onSubmit }: { loading:              boolean
		    creatableCount:       number
		    manualSkippedCount:   number
		    existingSkippedCount: number
		    pendingOrErrorCount:  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>作成対象 {creatableCount}件</span>
	  <span>手動スキップ {manualSkippedCount}件</span>
	  <span>既存投稿による自動スキップ {existingSkippedCount}件</span>
	  <span>error／未処理 {pendingOrErrorCount}件</span>
	</div>
	<div className="flex flex-col gap-2 sm:flex-row">
	  <Button type="button" variant="outline" onClick={onBack} disabled={loading}>
	    URL リスト入力へ戻る
	  </Button>
	  <Button
	      type="button"
	      onClick={onSubmit}
	      disabled={loading || creatableCount === 0}>
	    {creatableCount > 1 ? '一括追加' : '追加'}
	  </Button>
	</div>
      </div>
    </div>)

export default PostImportReviewPage
