'use client'

import type {
  DocumentProcessingTask,
  LogicalDocument,
} from '@dify/contracts/knowledge-fs/types.gen'
import type { TaskProgressStore } from './task-progress-store'
import { Button } from '@langgenius/dify-ui/button'
import {
  Drawer,
  DrawerBackdrop,
  DrawerCloseButton,
  DrawerContent,
  DrawerDescription,
  DrawerPopup,
  DrawerPortal,
  DrawerTitle,
  DrawerViewport,
} from '@langgenius/dify-ui/drawer'
import { useMutation } from '@tanstack/react-query'
import { useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import { consoleQuery } from '@/service/client'
import { taskCanRetry, taskIsActive, taskVersionIsAfter } from './document-model'

type TaskAction = 'cancel' | 'retry'

const TASK_DRAWER_LIMIT = 100
const noopSubscribe = () => () => undefined

function taskTime(task: DocumentProcessingTask) {
  return task.completedAt ?? task.updatedAt
}

function taskLifecycle(task: DocumentProcessingTask) {
  return `${task.updatedAt}:${task.state}`
}

function responseStatus(error: unknown): number | undefined {
  if (error instanceof Response) return error.status
  if (error && typeof error === 'object' && 'status' in error)
    return typeof error.status === 'number' ? error.status : undefined
  if (error && typeof error === 'object' && 'data' in error) {
    const data = error.data
    if (data && typeof data === 'object' && 'status' in data)
      return typeof data.status === 'number' ? data.status : undefined
  }
}

function compareTaskRecency(left: DocumentProcessingTask, right: DocumentProcessingTask) {
  if (taskVersionIsAfter(left.updatedAt, right.updatedAt)) return -1
  if (taskVersionIsAfter(right.updatedAt, left.updatedAt)) return 1
  return right.id.localeCompare(left.id)
}

function newestTasks(
  tasks: DocumentProcessingTask[],
  limit: number,
  predicate: (task: DocumentProcessingTask) => boolean,
) {
  const selected: DocumentProcessingTask[] = []
  for (const task of tasks) {
    if (!predicate(task)) continue
    let low = 0
    let high = selected.length
    while (low < high) {
      const middle = Math.floor((low + high) / 2)
      if (compareTaskRecency(task, selected[middle]!) < 0) high = middle
      else low = middle + 1
    }
    selected.splice(low, 0, task)
    if (selected.length > limit) selected.pop()
  }
  return selected
}

export function ProcessingTasksDrawer({
  canEdit,
  actionResultsValid,
  documentQueryError,
  documentQueryFetching,
  documents,
  documentsPending,
  hasNextDocumentPage,
  hasNextTaskPage,
  hasUnresolvedTaskDocuments,
  isFetchingNextDocumentPage,
  isFetchingNextTaskPage,
  knowledgeSpaceId,
  onLoadMoreDocuments,
  onLoadMoreTasks,
  onOpenChange,
  onRefreshDocumentsAndTasks,
  onRetryPermissionQuery,
  onTaskUpdated,
  onWritePermissionDenied,
  open,
  permissionQueryError,
  permissionQueryFetching,
  permissionQueryPending,
  readOnlyReason,
  taskQueryError,
  taskQueryFetching,
  taskQueryPending,
  tasks,
  taskProgressStore,
  onRetryTaskQuery,
  onRetryDocumentQuery,
}: {
  canEdit: boolean
  actionResultsValid: boolean
  documentQueryError: boolean
  documentQueryFetching: boolean
  documents: LogicalDocument[]
  documentsPending: boolean
  hasNextDocumentPage: boolean
  hasNextTaskPage: boolean
  hasUnresolvedTaskDocuments: boolean
  isFetchingNextDocumentPage: boolean
  isFetchingNextTaskPage: boolean
  knowledgeSpaceId: string
  onLoadMoreDocuments: () => void
  onLoadMoreTasks: () => void
  onOpenChange: (open: boolean) => void
  onRefreshDocumentsAndTasks: () => void
  onRetryPermissionQuery: () => void
  onTaskUpdated: (task: DocumentProcessingTask) => void
  onWritePermissionDenied: () => void
  open: boolean
  permissionQueryError: boolean
  permissionQueryFetching: boolean
  permissionQueryPending: boolean
  readOnlyReason?: string
  taskQueryError: boolean
  taskQueryFetching: boolean
  taskQueryPending: boolean
  tasks: DocumentProcessingTask[]
  taskProgressStore: TaskProgressStore
  onRetryDocumentQuery: () => void
  onRetryTaskQuery: () => void
}) {
  const { t } = useTranslation('dataset')
  const { t: tCommon } = useTranslation('common')
  const { formatTimeFromNow } = useFormatTimeFromNow()
  const cancelTask = useMutation(
    consoleQuery.knowledgeFs.deleteKnowledgeSpacesByIdDocumentsByDocumentIdProcessingTasksByTaskId.mutationOptions(),
  )
  const retryTask = useMutation(
    consoleQuery.knowledgeFs.postKnowledgeSpacesByIdDocumentsByDocumentIdProcessingTasksByTaskIdRetry.mutationOptions(),
  )
  const pendingActionsRef = useRef(new Set<string>())
  const drawerCloseButtonRef = useRef<HTMLButtonElement>(null)
  const taskQueryRetryButtonRef = useRef<HTMLButtonElement>(null)
  const documentQueryRetryButtonRef = useRef<HTMLButtonElement>(null)
  const permissionQueryRetryButtonRef = useRef<HTMLButtonElement>(null)
  const focusedTaskActionRef = useRef<HTMLButtonElement | null>(null)
  const loadMoreRequestedRef = useRef(false)
  const queryRetryFocusRequestedRef = useRef(false)
  const permissionRetryFocusRequestedRef = useRef(false)
  const loadMoreButtonRef = useRef<HTMLButtonElement>(null)
  const openCycleRef = useRef(0)
  const openRef = useRef(open)
  const actionResultsValidRef = useRef(actionResultsValid)
  const previousOpenRef = useRef(open)
  const [pendingActions, setPendingActions] = useState<Set<string>>(() => new Set())
  const [actionErrors, setActionErrors] = useState<Record<string, string>>({})
  const [visibleTaskLimit, setVisibleTaskLimit] = useState(TASK_DRAWER_LIMIT)
  useSyncExternalStore(
    open ? taskProgressStore.subscribe : noopSubscribe,
    taskProgressStore.getSnapshot,
    taskProgressStore.getSnapshot,
  )
  const documentTitles = useMemo(
    () => new Map(documents.map((document) => [document.id, document.title])),
    [documents],
  )
  const orderedBaseTasks = useMemo(() => {
    if (!open) return []
    const reservedLimit = Math.min(TASK_DRAWER_LIMIT / 2, visibleTaskLimit)
    const retryableTasks = newestTasks(tasks, reservedLimit, taskCanRetry)
    const activeTasks = newestTasks(tasks, reservedLimit, taskIsActive)
    const attentionTaskIds = new Set([
      ...retryableTasks.map((task) => task.id),
      ...activeTasks.map((task) => task.id),
    ])
    const remainingAttentionTasks = newestTasks(
      tasks,
      visibleTaskLimit - attentionTaskIds.size,
      (task) => (taskCanRetry(task) || taskIsActive(task)) && !attentionTaskIds.has(task.id),
    )
    for (const task of remainingAttentionTasks) attentionTaskIds.add(task.id)
    const terminalTasks = newestTasks(
      tasks,
      visibleTaskLimit - attentionTaskIds.size,
      (task) => !attentionTaskIds.has(task.id),
    )
    return [...retryableTasks, ...activeTasks, ...remainingAttentionTasks, ...terminalTasks].sort(
      compareTaskRecency,
    )
  }, [open, tasks, visibleTaskLimit])
  const hasMoreTasks =
    open &&
    (tasks.length > orderedBaseTasks.length ||
      hasNextTaskPage ||
      (hasUnresolvedTaskDocuments && hasNextDocumentPage))
  const orderedTasks = orderedBaseTasks.map((task) => {
    const progress = taskProgressStore.get(task.id)
    if (!progress || !taskIsActive(task) || taskVersionIsAfter(task.updatedAt, progress.updatedAt))
      return task
    return {
      ...task,
      errorCode: undefined,
      errorMessage: undefined,
      ...progress,
    }
  })
  const activeActionCount = orderedTasks.filter(taskIsActive).length
  const retryActionCount = orderedTasks.filter(taskCanRetry).length
  const taskLifecycles = useMemo(
    () => new Map(tasks.map((task) => [task.id, taskLifecycle(task)])),
    [tasks],
  )
  const taskLifecycleGenerationsRef = useRef(
    new Map<string, { generation: number; lifecycle: string }>(),
  )
  useLayoutEffect(() => {
    actionResultsValidRef.current = actionResultsValid
    const currentTaskIds = new Set(tasks.map((task) => task.id))
    for (const task of tasks) {
      const lifecycle = taskLifecycle(task)
      const previous = taskLifecycleGenerationsRef.current.get(task.id)
      if (previous?.lifecycle === lifecycle) continue
      taskLifecycleGenerationsRef.current.set(task.id, {
        generation: (previous?.generation ?? 0) + 1,
        lifecycle,
      })
    }
    for (const taskId of taskLifecycleGenerationsRef.current.keys()) {
      if (!currentTaskIds.has(taskId)) taskLifecycleGenerationsRef.current.delete(taskId)
    }
  }, [actionResultsValid, tasks])

  useEffect(() => {
    openRef.current = open
    const wasOpen = previousOpenRef.current
    previousOpenRef.current = open
    if (!wasOpen || open) return
    openCycleRef.current += 1
    loadMoreRequestedRef.current = false
    // oxlint-disable-next-line eslint-react/set-state-in-effect -- Every committed controlled close resets drawer-local pagination and errors.
    setVisibleTaskLimit(TASK_DRAWER_LIMIT)
    // oxlint-disable-next-line eslint-react/set-state-in-effect -- Every committed controlled close starts a fresh action-error cycle.
    setActionErrors({})
  }, [open])

  useEffect(() => {
    // oxlint-disable-next-line eslint-react/set-state-in-effect -- Task lifecycle changes retire action errors from older task versions.
    setActionErrors((current) => {
      const staleTaskIds = Object.keys(current).filter(
        (taskId) => taskLifecycles.get(taskId) !== current[taskId],
      )
      if (!staleTaskIds.length) return current
      const next = { ...current }
      for (const taskId of staleTaskIds) delete next[taskId]
      return next
    })
  }, [taskLifecycles])

  useEffect(() => {
    if (!open) return
    // oxlint-disable-next-line eslint-react/set-state-in-effect -- A newly opened drawer starts a fresh action-error cycle.
    setActionErrors({})
  }, [open])

  useEffect(() => {
    if (!open || hasMoreTasks || !loadMoreRequestedRef.current) return
    loadMoreRequestedRef.current = false
    drawerCloseButtonRef.current?.focus()
  }, [hasMoreTasks, open])

  useEffect(() => {
    const queryRetryVisible = taskQueryError || (documentQueryError && hasUnresolvedTaskDocuments)
    if (!open || !queryRetryFocusRequestedRef.current) return
    if (queryRetryVisible) {
      if (taskQueryError) taskQueryRetryButtonRef.current?.focus()
      else documentQueryRetryButtonRef.current?.focus()
      return
    }
    queryRetryFocusRequestedRef.current = false
    drawerCloseButtonRef.current?.focus()
  }, [documentQueryError, hasUnresolvedTaskDocuments, open, taskQueryError])

  useEffect(() => {
    if (!open || !permissionRetryFocusRequestedRef.current) return
    if (permissionQueryError) {
      permissionQueryRetryButtonRef.current?.focus()
      return
    }
    permissionRetryFocusRequestedRef.current = false
    drawerCloseButtonRef.current?.focus()
  }, [open, permissionQueryError])

  useEffect(() => {
    const focusedAction = focusedTaskActionRef.current
    if (!open || !focusedAction || focusedAction.isConnected) return
    focusedTaskActionRef.current = null
    drawerCloseButtonRef.current?.focus()
  }, [open, orderedTasks])

  const performAction = async (task: DocumentProcessingTask, action: TaskAction) => {
    if (!canEdit || pendingActionsRef.current.has(task.id)) return
    pendingActionsRef.current.add(task.id)
    const actionOpenCycle = openCycleRef.current
    const actionLifecycleGeneration = taskLifecycleGenerationsRef.current.get(task.id)?.generation
    const actionFocusTarget = document.activeElement
    setPendingActions((current) => new Set(current).add(task.id))
    setActionErrors((current) => {
      const next = { ...current }
      delete next[task.id]
      return next
    })
    try {
      const input = {
        params: {
          documentId: task.documentId,
          id: knowledgeSpaceId,
          taskId: task.id,
        },
      }
      const updated =
        action === 'cancel'
          ? await cancelTask.mutateAsync(input)
          : await retryTask.mutateAsync(input)
      if (
        !actionResultsValidRef.current ||
        taskLifecycleGenerationsRef.current.get(task.id)?.generation !== actionLifecycleGeneration
      )
        return
      onTaskUpdated(updated)
      setActionErrors((current) => {
        const next = { ...current }
        delete next[task.id]
        return next
      })
      if (
        actionResultsValidRef.current &&
        openRef.current &&
        openCycleRef.current === actionOpenCycle &&
        document.activeElement === actionFocusTarget
      )
        drawerCloseButtonRef.current?.focus()
    } catch (error) {
      const permissionDenied = responseStatus(error) === 403
      if (permissionDenied) onWritePermissionDenied()
      if (
        !permissionDenied &&
        actionResultsValidRef.current &&
        openRef.current &&
        openCycleRef.current === actionOpenCycle &&
        taskLifecycleGenerationsRef.current.get(task.id)?.generation === actionLifecycleGeneration
      )
        setActionErrors((current) => ({ ...current, [task.id]: taskLifecycle(task) }))
    } finally {
      pendingActionsRef.current.delete(task.id)
      setPendingActions((current) => {
        const next = new Set(current)
        next.delete(task.id)
        return next
      })
      if (actionResultsValidRef.current) onRefreshDocumentsAndTasks()
    }
  }

  return (
    <Drawer
      open={open}
      modal
      swipeDirection="right"
      onOpenChange={(nextOpen) => {
        openRef.current = nextOpen
        onOpenChange(nextOpen)
      }}
    >
      <DrawerPortal>
        <DrawerBackdrop />
        <DrawerViewport>
          <DrawerPopup className="data-[swipe-direction=right]:w-[440px] data-[swipe-direction=right]:max-w-[calc(100vw-1rem)]">
            <DrawerContent className="flex min-h-0 flex-1 flex-col bg-components-panel-bg p-0 pb-0">
              <header className="shrink-0 pt-[calc(1.25rem+env(safe-area-inset-top,0px))] pr-[calc(1.5rem+env(safe-area-inset-right,0px))] pb-4 pl-[calc(1.5rem+env(safe-area-inset-left,0px))]">
                <div className="flex items-center justify-between gap-3">
                  <DrawerTitle className="system-md-semibold text-text-primary">
                    {t(($) => $['newKnowledge.backgroundTasks'])}
                  </DrawerTitle>
                  <DrawerCloseButton
                    ref={drawerCloseButtonRef}
                    aria-label={tCommon(($) => $['operation.close'])}
                    className="size-6 rounded-md"
                  />
                </div>
                <DrawerDescription className="mt-1 system-xs-regular text-text-tertiary">
                  {t(($) => $['newKnowledge.backgroundTasksDescription'])}
                </DrawerDescription>
                {readOnlyReason && (
                  <p
                    className="mt-2 inline-flex items-center gap-1.5 system-xs-regular text-text-warning"
                    role="status"
                  >
                    <span aria-hidden className="i-ri-lock-line size-3.5" />
                    {readOnlyReason}
                  </p>
                )}
                {permissionQueryPending && (
                  <p className="mt-2 system-xs-regular text-text-tertiary" role="status">
                    {t(($) => $['newKnowledge.permission'])}
                    {' · '}
                    {tCommon(($) => $.loading)}
                  </p>
                )}
              </header>
              <div className="min-h-0 flex-1 overflow-y-auto pr-[calc(1.5rem+env(safe-area-inset-right,0px))] pb-[calc(1.5rem+env(safe-area-inset-bottom,0px))] pl-[calc(1.5rem+env(safe-area-inset-left,0px))]">
                {permissionQueryError && (
                  <div className="mb-3 rounded-xl border border-divider-regular p-4" role="alert">
                    <p className="system-xs-regular text-text-destructive">
                      {t(($) => $['newKnowledge.permissionLoadFailed'])}
                    </p>
                    <Button
                      ref={permissionQueryRetryButtonRef}
                      aria-label={`${tCommon(($) => $['operation.retry'])} · ${t(($) => $['newKnowledge.permissionLoadFailed'])}`}
                      aria-busy={permissionQueryFetching}
                      className="mt-3"
                      loading={permissionQueryFetching}
                      size="small"
                      onBlur={(event) => {
                        if (event.relatedTarget) permissionRetryFocusRequestedRef.current = false
                      }}
                      onClick={() => {
                        permissionRetryFocusRequestedRef.current = true
                        onRetryPermissionQuery()
                      }}
                    >
                      {tCommon(($) => $['operation.retry'])}
                    </Button>
                  </div>
                )}
                {taskQueryError && (
                  <div className="mb-3 rounded-xl border border-divider-regular p-4" role="alert">
                    <p className="system-xs-regular text-text-destructive">
                      {t(($) => $['newKnowledge.tasksErrorDescription'])}
                    </p>
                    <Button
                      ref={taskQueryRetryButtonRef}
                      aria-label={`${tCommon(($) => $['operation.retry'])} · ${t(($) => $['newKnowledge.tasksErrorDescription'])}`}
                      aria-busy={taskQueryFetching}
                      className="mt-3"
                      loading={taskQueryFetching}
                      size="small"
                      onBlur={(event) => {
                        if (event.relatedTarget) queryRetryFocusRequestedRef.current = false
                      }}
                      onClick={() => {
                        queryRetryFocusRequestedRef.current = true
                        onRetryTaskQuery()
                      }}
                    >
                      {tCommon(($) => $['operation.retry'])}
                    </Button>
                  </div>
                )}
                {documentQueryError && hasUnresolvedTaskDocuments && (
                  <div className="mb-3 rounded-xl border border-divider-regular p-4" role="alert">
                    <p className="system-xs-regular text-text-destructive">
                      {t(($) => $['newKnowledge.documentsErrorDescription'])}
                    </p>
                    <Button
                      ref={documentQueryRetryButtonRef}
                      aria-label={`${tCommon(($) => $['operation.retry'])} · ${t(($) => $['newKnowledge.documentsErrorDescription'])}`}
                      aria-busy={documentQueryFetching}
                      className="mt-3"
                      loading={documentQueryFetching}
                      size="small"
                      onBlur={(event) => {
                        if (event.relatedTarget) queryRetryFocusRequestedRef.current = false
                      }}
                      onClick={() => {
                        queryRetryFocusRequestedRef.current = true
                        onRetryDocumentQuery()
                      }}
                    >
                      {tCommon(($) => $['operation.retry'])}
                    </Button>
                  </div>
                )}
                {taskQueryPending && !orderedTasks.length ? (
                  <div className="flex min-h-40 items-center justify-center">
                    <Loading />
                  </div>
                ) : orderedTasks.length ? (
                  <ul className="divide-y divide-divider-subtle">
                    {orderedTasks.map((task) => {
                      const title =
                        documentTitles.get(task.documentId) ??
                        (documentsPending
                          ? t(($) => $['newKnowledge.documentColumn'])
                          : task.documentId)
                      const timestamp = Date.parse(
                        taskIsActive(task) ? task.createdAt : taskTime(task),
                      )
                      const taskError = task.errorMessage ?? task.errorCode
                      const actionTarget = `${documentTitles.get(task.documentId) ?? task.documentId} · ${task.id}`
                      return (
                        <li key={task.id} className="flex min-h-[62px] items-center gap-2.5 py-3.5">
                          <span
                            aria-hidden
                            className={
                              task.state === 'failed'
                                ? 'i-ri-error-warning-fill size-4 shrink-0 text-text-destructive'
                                : taskIsActive(task)
                                  ? 'i-ri-loader-2-line size-4 shrink-0 animate-spin text-text-accent motion-reduce:animate-none'
                                  : task.state === 'succeeded'
                                    ? 'i-ri-check-line size-4 shrink-0 text-text-success'
                                    : 'i-ri-indeterminate-circle-line size-4 shrink-0 text-text-tertiary'
                            }
                          />
                          <div className="min-w-0 flex-1">
                            <p className="truncate system-xs-medium text-text-primary">
                              {t(($) => $['newKnowledge.processDocument'], { name: title })}
                            </p>
                            <p className="mt-0.5 truncate system-2xs-regular text-text-tertiary">
                              {t(($) => $[`newKnowledge.processingTaskState.${task.state}`], {
                                progress: task.progressPercent,
                              })}
                              {!Number.isNaN(timestamp) && (
                                <>
                                  <span aria-hidden> · </span>
                                  {formatTimeFromNow(timestamp)}
                                </>
                              )}
                            </p>
                            {taskError && (
                              <p className="mt-1 system-2xs-regular break-words whitespace-pre-wrap text-text-destructive">
                                {taskError}
                              </p>
                            )}
                            {actionErrors[task.id] === taskLifecycle(task) && (
                              <p
                                className="mt-1 system-2xs-regular text-text-destructive"
                                role="alert"
                              >
                                {t(($) => $['newKnowledge.taskActionFailed'])}
                              </p>
                            )}
                          </div>
                          {canEdit && taskIsActive(task) ? (
                            <Button
                              aria-label={
                                activeActionCount > 1
                                  ? `${t(($) => $['newKnowledge.interruptTask'])} · ${actionTarget}`
                                  : undefined
                              }
                              size="small"
                              aria-busy={pendingActions.has(task.id)}
                              disabled={pendingActions.has(task.id)}
                              loading={pendingActions.has(task.id)}
                              onBlur={(event) => {
                                if (event.relatedTarget) focusedTaskActionRef.current = null
                              }}
                              onFocus={(event) => {
                                focusedTaskActionRef.current = event.currentTarget
                              }}
                              onClick={() => void performAction(task, 'cancel')}
                            >
                              {t(($) => $['newKnowledge.interruptTask'])}
                            </Button>
                          ) : canEdit && taskCanRetry(task) ? (
                            <Button
                              aria-label={
                                retryActionCount > 1
                                  ? `${t(($) => $['newKnowledge.retryTask'])} · ${actionTarget}`
                                  : undefined
                              }
                              size="small"
                              aria-busy={pendingActions.has(task.id)}
                              disabled={pendingActions.has(task.id)}
                              loading={pendingActions.has(task.id)}
                              onBlur={(event) => {
                                if (event.relatedTarget) focusedTaskActionRef.current = null
                              }}
                              onFocus={(event) => {
                                focusedTaskActionRef.current = event.currentTarget
                              }}
                              onClick={() => void performAction(task, 'retry')}
                            >
                              {t(($) => $['newKnowledge.retryTask'])}
                            </Button>
                          ) : null}
                        </li>
                      )
                    })}
                  </ul>
                ) : !taskQueryError && !hasMoreTasks ? (
                  <p className="py-16 text-center system-xs-regular text-text-tertiary">
                    {t(($) => $['newKnowledge.noBackgroundTasks'])}
                  </p>
                ) : null}
                {hasMoreTasks && (
                  <div className="mt-4 flex justify-center">
                    <Button
                      ref={loadMoreButtonRef}
                      aria-busy={isFetchingNextTaskPage || isFetchingNextDocumentPage}
                      loading={isFetchingNextTaskPage || isFetchingNextDocumentPage}
                      onBlur={() => {
                        loadMoreRequestedRef.current = false
                      }}
                      onClick={() => {
                        loadMoreRequestedRef.current =
                          document.activeElement === loadMoreButtonRef.current
                        if (tasks.length <= orderedBaseTasks.length && hasNextTaskPage)
                          onLoadMoreTasks()
                        if (hasUnresolvedTaskDocuments && hasNextDocumentPage) onLoadMoreDocuments()
                        setVisibleTaskLimit((current) => current + TASK_DRAWER_LIMIT)
                      }}
                    >
                      {t(($) => $['newKnowledge.loadMore'])}
                    </Button>
                  </div>
                )}
              </div>
            </DrawerContent>
          </DrawerPopup>
        </DrawerViewport>
      </DrawerPortal>
    </Drawer>
  )
}
