"use client"

import { POLL_INTERVAL_MS, POLL_MAX_CONSECUTIVE_FAILURES } from "@/config/timeouts"
import { DEEPDEX_PAGES } from "@/components/deepdex/data/deepdex-page-routes"
import { errorMessageOr, toastApiError } from "@/lib/toast-api-error";

import * as React from "react"
import Link from "next/link"
import { useRouter, useSearchParams } from "next/navigation"
import { ArrowLeft, Loader2 } from "lucide-react"
import { toast } from "sonner"

import {
  getOcrCodeForSlot,
  getUnmappedSlots,
  isRealIsin,
  isStructureFinalValuationDateMissing,
  suggestAliasSourceFromBankName,
  type StructureQueueRow,
} from "@/components/deepdex/utils/structure-underlying-alias"
import {
  accumulatorNeedsMapping,
  getOcrCodeForAccumulator,
  type AccumulatorQueueRow,
} from "@/components/deepdex/utils/accumulator-underlying-alias"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"

import {
  fetchAliasSourceSuggestions,
  fetchUploadQueueReviewData,
  formatCreateBulkError,
  fetchSyncObservationDatesStatus,
  postCheckObservationDates,
  postSyncObservationDates,
  postUploadQueueAddUnderlyingAlias,
  postUploadQueueCreateBulk,
  postUploadQueueUpdateRow,
} from "./deepdex-upload-queue-review-api"
import { DeepdexUploadQueueReviewBulkActions } from "./deepdex-upload-queue-review-bulk-actions"
import { replaceRowValidation } from "@/components/deepdex/utils/review-validation"
import { DeepdexUploadQueueReviewValidation } from "@/components/deepdex/deepdex-upload-queue-review-validation"
import { reviewPageTitle } from "./deepdex-upload-queue-review-cells"
import { DeepdexUploadQueueReviewEditSheet } from "./deepdex-upload-queue-review-edit-sheet"
import { DeepdexUploadQueueReviewMapDialog } from "./deepdex-upload-queue-review-map-dialog"
import { DeepdexUploadQueueReviewObservationDatesDialog } from "./deepdex-upload-queue-review-observation-dates-dialog"
import { DeepdexUploadQueueReviewTable } from "./deepdex-upload-queue-review-table"
import type {
  MapModalState,
  ObservationDateStructure,
  ExistingChildData,
  ReviewData,
  ReviewResponse,
} from "./deepdex-upload-queue-review-types"
import { DeepdexUploadQueueReviewViewSheet } from "./deepdex-upload-queue-review-view-sheet"

const EMPTY_QUEUE_ROWS: NonNullable<ReviewData["queue"]> = []

function parseReviewQueueIds(id: string, idsParam: string): string[] {
  const fromIds = idsParam
    .split(",")
    .map((value) => value.trim())
    .filter(Boolean)
  if (fromIds.length > 0) return Array.from(new Set(fromIds))
  return id.trim() ? [id.trim()] : []
}

function collectQueueIds(queueId: unknown, fallbackIds: string[]): string[] {
  const raw = Array.isArray(queueId) ? queueId : queueId != null && queueId !== "" ? [queueId] : fallbackIds
  return raw.map((v) => String(v).trim()).filter(Boolean)
}

/** "33, 32, 25, 24, 23, 1" -> "1, 23–25, 32–33" (non-numeric ids are kept as they are). */
function formatQueueIdRanges(ids: string[]): string {
  const numeric = Array.from(new Set(ids.filter((v) => /^\d+$/.test(v)).map(Number))).sort((a, b) => a - b)
  const others = ids.filter((v) => !/^\d+$/.test(v))
  const parts: string[] = []
  for (let i = 0; i < numeric.length; ) {
    let j = i
    while (j + 1 < numeric.length && numeric[j + 1] === numeric[j]! + 1) j++
    parts.push(j > i ? `${numeric[i]}–${numeric[j]}` : String(numeric[i]))
    i = j + 1
  }
  return [...parts, ...others].join(", ") || "?"
}

/** The review payload without row `index`, keeping every index-aligned array in step with the queue. */
function withoutRow(data: ReviewData, index: number): ReviewData {
  const drop = <T,>(list: T[] | undefined): T[] | undefined =>
    Array.isArray(list) ? list.filter((_, i) => i !== index) : list
  const queue = drop(data.queue) ?? []
  return {
    ...data,
    queue,
    queueCount: queue.length,
    validation: drop(data.validation),
    existingInfo: drop(data.existingInfo),
    existingCurrentData: drop(data.existingCurrentData),
    existingCurrentChildData: drop(data.existingCurrentChildData),
  }
}

export function DeepdexUploadQueueReviewPage() {
  const router = useRouter()
  const searchParams = useSearchParams()
  const moduleId = searchParams.get("module") ?? ""
  const id = searchParams.get("id") ?? ""
  const idsParam = searchParams.get("ids") ?? ""
  const reviewIds = React.useMemo(
    () => parseReviewQueueIds(id, idsParam),
    [id, idsParam],
  )

  const [isLoading, setIsLoading] = React.useState(true)
  const [isSubmitting, setIsSubmitting] = React.useState(false)
  const [isEditOpen, setIsEditOpen] = React.useState(false)
  const [isViewOpen, setIsViewOpen] = React.useState(false)
  const [editingIndex, setEditingIndex] = React.useState<number | null>(null)
  const [editingExisting, setEditingExisting] = React.useState<Record<string, unknown> | null>(null)
  const [editingExistingChild, setEditingExistingChild] = React.useState<ExistingChildData | null>(null)
  const [viewingIndex, setViewingIndex] = React.useState<number | null>(null)
  const [editRow, setEditRow] = React.useState<Record<string, string>>({})
  const [editObs, setEditObs] = React.useState<Array<{ date: string; nonCall: string }>>([])
  const [editCoupons, setEditCoupons] = React.useState<Array<{ date: string }>>([])
  const [editAccRows, setEditAccRows] = React.useState<Array<{ date: string; quantityPerPeriod: string }>>([])
  const [error, setError] = React.useState<string | null>(null)
  const [payload, setPayload] = React.useState<ReviewResponse | null>(null)
  const [mapModal, setMapModal] = React.useState<MapModalState | null>(null)
  const [mapError, setMapError] = React.useState<string | null>(null)
  const [aliasSourceSuggestions, setAliasSourceSuggestions] = React.useState<string[]>([])
  const [isDatesOpen, setIsDatesOpen] = React.useState(false)
  const [isCheckingDates, setIsCheckingDates] = React.useState(false)
  const [dateStructures, setDateStructures] = React.useState<ObservationDateStructure[]>([])
  const [isStartingDateSync, setIsStartingDateSync] = React.useState(false)
  const [dateSyncJob, setDateSyncJob] = React.useState<{ jobId: string; total: number } | null>(null)

  const loadReview = React.useCallback(async (removeIndex?: number) => {
    setIsLoading(true)
    setError(null)
    if (!moduleId || reviewIds.length === 0) {
      setError("A valid module and queue id are required.")
      setIsLoading(false)
      return
    }
    try {
      const { response, data } = await fetchUploadQueueReviewData(moduleId, reviewIds, removeIndex)
      if (!response.ok || !data) {
        setError(data?.message ?? `Request failed (HTTP ${response.status || 500}).`)
        setPayload(data)
      } else {
        setPayload(data)
      }
    } catch {
      setError("Could not load review data.")
    } finally {
      setIsLoading(false)
    }
  }, [moduleId, reviewIds])

  React.useEffect(() => {
    // Fetch kickoff: the loader raises its own loading flag first (fetch-in-effect).
    // eslint-disable-next-line react-hooks/set-state-in-effect
    void loadReview()
  }, [loadReview])

  const reviewData = React.useMemo((): ReviewData | null => {
    if (!payload || !payload.data || typeof payload.data !== "object") return null
    return payload.data as ReviewData
  }, [payload])

  const queueRows = Array.isArray(reviewData?.queue) ? reviewData.queue : EMPTY_QUEUE_ROWS
  const queueId = Number(reviewData?.queue_id ?? reviewIds[0])
  const queueIdList = collectQueueIds(reviewData?.queue_id, reviewIds)
  const queueIdLabel = formatQueueIdRanges(queueIdList)
  const existingCount = queueRows.reduce(
    (count, _row, index) => count + (reviewData?.existingInfo?.[index]?.exists ? 1 : 0),
    0,
  )
  const fetchDateStructures = React.useCallback(async () => {
    const { response, data } = await postCheckObservationDates({
      queue_id: Number.isFinite(queueId) ? queueId : undefined,
      queue: queueRows,
    })
    if (!response.ok || data?.status === "error") {
      throw new Error(data?.message || "Could not check observation dates.")
    }
    return data?.data?.structures ?? []
  }, [queueId, queueRows])

  const checkObservationDates = React.useCallback(async () => {
    setIsCheckingDates(true)
    try {
      setDateStructures(await fetchDateStructures())
      setIsDatesOpen(true)
    } catch (e) {
      toastApiError(e, "Could not check observation dates.")
    } finally {
      setIsCheckingDates(false)
    }
  }, [fetchDateStructures])

  // Runs on the server as a background job; the page stays usable while it works.
  const startDateSync = React.useCallback(async () => {
    setIsStartingDateSync(true)
    try {
      const { response, data } = await postSyncObservationDates({
        queue_id: Number.isFinite(queueId) ? queueId : undefined,
        queue: queueRows,
      })
      if (!response.ok || data?.status === "error" || !data?.job_id) {
        throw new Error(data?.message || "Could not start the observation date update.")
      }
      setDateSyncJob({ jobId: data.job_id, total: data.total ?? 0 })
      toast.loading(data.message ?? "Updating observation dates in the background…", { id: `date-sync-${data.job_id}` })
    } catch (e) {
      toastApiError(e, "Could not start the observation date update.")
    } finally {
      setIsStartingDateSync(false)
    }
  }, [queueId, queueRows])

  const dateSyncJobId = dateSyncJob?.jobId ?? null
  const dateSyncTotal = dateSyncJob?.total ?? 0
  React.useEffect(() => {
    if (!dateSyncJobId) return
    const toastId = `date-sync-${dateSyncJobId}`
    let cancelled = false
    let failures = 0

    const finish = (kind: "success" | "error", message: string) => {
      toast[kind](message, { id: toastId, duration: kind === "error" ? 10000 : 6000 })
      setDateSyncJob(null)
    }

    const tick = async () => {
      if (cancelled) return
      try {
        const { response, data } = await fetchSyncObservationDatesStatus(dateSyncJobId)
        if (cancelled) return
        if (!response.ok || !data || data.status === "error") throw new Error(data?.message || "Progress unavailable.")
        failures = 0
        if (data.job_status === "completed") {
          const failed = data.failed ?? 0
          finish(failed > 0 ? "error" : "success", data.message || "Observation dates updated.")
          // Refresh the comparison in place; the review queue itself is left untouched.
          try {
            const structures = await fetchDateStructures()
            if (!cancelled) setDateStructures(structures)
          } catch {
            // The update itself succeeded; a stale comparison list is not worth an extra error.
          }
          return
        }
        if (data.job_status === "failed") {
          finish("error", data.message || "The observation date update failed.")
          return
        }
        toast.loading(`Updating observation dates… ${data.done ?? 0} / ${data.total ?? dateSyncTotal}`, { id: toastId })
      } catch (e) {
        failures += 1
        if (failures >= POLL_MAX_CONSECUTIVE_FAILURES) {
          finish("error", errorMessageOr(e, "Lost track of the observation date update."))
          return
        }
      }
      timer = window.setTimeout(() => void tick(), POLL_INTERVAL_MS.progress)
    }

    let timer = window.setTimeout(() => void tick(), POLL_INTERVAL_MS.firstCheck)
    return () => {
      cancelled = true
      window.clearTimeout(timer)
    }
  }, [dateSyncJobId, dateSyncTotal, fetchDateStructures])

  // Takes a row out of the review list (removed by the user, or saved to the master). Every array that is
  // aligned with the queue by index goes with it, or the New/Existing badges of the rows below would shift.
  const dropRow = React.useCallback((index: number) => {
    setPayload((current) => {
      if (!current?.data || typeof current.data !== "object") return current
      const currentData = current.data as ReviewData
      if (!Array.isArray(currentData.queue) || index < 0 || index >= currentData.queue.length) return current
      return { ...current, data: withoutRow(currentData, index) }
    })
    if (editingIndex === index) {
      setIsEditOpen(false)
      setEditingIndex(null)
    } else if (editingIndex != null && editingIndex > index) {
      setEditingIndex(editingIndex - 1)
    }
    if (viewingIndex === index) {
      setIsViewOpen(false)
      setViewingIndex(null)
    } else if (viewingIndex != null && viewingIndex > index) {
      setViewingIndex(viewingIndex - 1)
    }
  }, [editingIndex, viewingIndex])

  const openEdit = React.useCallback((row: Record<string, unknown>, index: number) => {
    const normalized: Record<string, string> = {}
    Object.entries(row).forEach(([key, value]) => {
      if (key.startsWith("_")) return
      normalized[key] = value == null ? "" : String(value)
    })
    setEditRow(normalized)
    setEditingIndex(index)
    const existing = Array.isArray(reviewData?.existingCurrentData)
      ? reviewData?.existingCurrentData[index]
      : null
    setEditingExisting(existing && typeof existing === "object" ? existing : null)
    const existingChild = Array.isArray(reviewData?.existingCurrentChildData)
      ? reviewData?.existingCurrentChildData[index]
      : null
    setEditingExistingChild(existingChild && typeof existingChild === "object" ? existingChild : null)
    const child = (row._child ?? null) as
      | {
          observations?: Array<{ date?: string; nonCall?: string }>
          coupons?: Array<{ date?: string }>
          rows?: Array<{ date?: string; quantityPerPeriod?: string }>
        }
      | null
    setEditObs(
      Array.isArray(child?.observations)
        ? child!.observations!.map((o) => ({ date: o.date ?? "", nonCall: o.nonCall ?? "N" }))
        : [],
    )
    setEditCoupons(
      Array.isArray(child?.coupons)
        ? child!.coupons!.map((c) => ({ date: c.date ?? "" }))
        : [],
    )
    setEditAccRows(
      Array.isArray(child?.rows)
        ? child!.rows!.map((r) => ({ date: r.date ?? "", quantityPerPeriod: r.quantityPerPeriod ?? "" }))
        : [],
    )
    setIsEditOpen(true)
  }, [reviewData?.existingCurrentData, reviewData?.existingCurrentChildData])

  const openView = React.useCallback((index: number) => {
    setViewingIndex(index)
    setIsViewOpen(true)
  }, [])

  const openMapModal = React.useCallback((
    queueIndex: number,
    slot: number,
    row: StructureQueueRow,
  ) => {
    const aliasCode = getOcrCodeForSlot(row, slot)
    const existingIsin = String(row[`underlying${slot}_isin`] ?? "").trim()
    const bankName = String(row._bankName ?? "").trim()
    setMapModal({
      queueIndex,
      slot,
      mode: "structure",
      aliasCode,
      aliasIsin: isRealIsin(existingIsin) ? existingIsin.toUpperCase() : "",
      aliasSource: suggestAliasSourceFromBankName(bankName),
      underlyingName: String(row[`underlying${slot}_name`] ?? ""),
      underlyingCurrency: String(row[`underlying${slot}_currency`] ?? ""),
      underlyingSpot: String(row[`underlying${slot}_spot`] ?? ""),
      bankName,
      showDetails: !!(row[`underlying${slot}_name`] || row[`underlying${slot}_currency`] || row[`underlying${slot}_spot`]),
    })
    setMapError(null)
  }, [])

  const openAccumulatorMapModal = React.useCallback((
    queueIndex: number,
    row: AccumulatorQueueRow,
  ) => {
    const aliasCode = getOcrCodeForAccumulator(row)
    const existingIsin = String(row.UnderlyingISIN ?? "").trim()
    const bankName = String(row._bankName ?? "").trim()
    setMapModal({
      queueIndex,
      mode: "accumulator",
      aliasCode,
      aliasIsin: isRealIsin(existingIsin) ? existingIsin.toUpperCase() : "",
      aliasSource: suggestAliasSourceFromBankName(bankName),
      underlyingName: String(row.UnderlyingName ?? ""),
      underlyingCurrency: "",
      underlyingSpot: "",
      bankName,
      showDetails: !!row.UnderlyingName,
    })
    setMapError(null)
  }, [])

  // The edit form's single Save: normalise the edited row, then write it to the master — created when the ISIN
  // is not there yet, updated when it is — and publish its file. The row then leaves the review list.
  const saveEdit = React.useCallback(async () => {
    if (editingIndex == null) return
    const index = editingIndex

    if (moduleId === "structure") {
      if (isStructureFinalValuationDateMissing(editRow)) {
        toast.error("Cannot save: the final valuation date is required.")
        return
      }
      const draft: StructureQueueRow = { ...editRow }
      const unmapped = getUnmappedSlots(draft)
      if (unmapped.length > 0) {
        const messages = unmapped.map((slot) => {
          const ocr = getOcrCodeForSlot(draft, slot)
          return `Underlying ${slot}${ocr ? ` (${ocr})` : ""}`
        })
        toast.error(`Cannot save until all OCR underlyings are mapped to real ISINs:\n${messages.join("\n")}`)
        return
      }
    } else if (moduleId === "accumulator") {
      const draft: AccumulatorQueueRow = { ...queueRows[index], ...editRow }
      if (accumulatorNeedsMapping(draft)) {
        const ocr = getOcrCodeForAccumulator(draft)
        toast.error(`Cannot save until the OCR underlying is mapped to a real ISIN${ocr ? `: ${ocr}` : ""}`)
        return
      }
    }

    setIsSubmitting(true)
    try {
      const body: Record<string, unknown> = {
        module: moduleId,
        queue_index: index,
        queue_id: Number.isFinite(queueId) ? queueId : undefined,
        queue: queueRows,
        row: editRow,
      }
      if (moduleId === "structure") {
        body.child_observations = editObs
        body.child_coupons = editCoupons
        // Only normalise the draft here; the create/update below writes the master and its dates in one step.
        body.defer_persist = true
      } else if (moduleId === "accumulator") {
        body.child_rows = editAccRows
        body.child_section_present = true
      }

      const { response: draftResponse, data: draftData } = await postUploadQueueUpdateRow(body)
      if (!draftResponse.ok || draftData?.status === "error") {
        throw new Error(draftData?.message || "Failed to update row.")
      }
      const draftQueue = Array.isArray(draftData?.data?.queue) ? draftData.data.queue : queueRows
      const draftValidation = Array.isArray(draftData?.data?.validation) ? draftData.data.validation : undefined

      const { response, data } = await postUploadQueueCreateBulk({
        module: moduleId,
        queue_id: Number.isFinite(queueId) ? queueId : undefined,
        queue: draftQueue,
        create_selected: true,
        selected: [index],
      })
      if (!response.ok || data?.status === "error") {
        // Keep what the user typed (now normalised) so a failed save does not throw the edit away.
        setPayload((current) => {
          if (!current?.data || typeof current.data !== "object") return current
          return {
            ...current,
            data: {
              ...(current.data as ReviewData),
              queue: draftQueue,
              queueCount: draftQueue.length,
              ...(draftValidation ? { validation: draftValidation } : {}),
            },
          }
        })
        throw new Error(formatCreateBulkError(data))
      }

      toast.success(data?.message ?? "Saved.")
      const publishResults = Array.isArray(data?.data?.publishResults) ? data.data.publishResults : []
      if (publishResults.some((item) => item.duplicate)) {
        toast.message("File already existed in Deepdex; linked to existing record.")
      }

      if (draftQueue.length <= 1) {
        router.push(DEEPDEX_PAGES.uploadQueue)
        return
      }
      // The server's copy of the queue now excludes this row; mirror that locally, with the edits to the others kept.
      setPayload((current) => {
        if (!current?.data || typeof current.data !== "object") return current
        return {
          ...current,
          data: {
            ...(current.data as ReviewData),
            queue: draftQueue,
            queueCount: draftQueue.length,
            ...(draftValidation ? { validation: draftValidation } : {}),
          },
        }
      })
      dropRow(index)
    } catch (e) {
      toastApiError(e, "Failed to save row.")
    } finally {
      setIsSubmitting(false)
    }
  }, [dropRow, editAccRows, editCoupons, editObs, editRow, editingIndex, queueId, queueRows, router, moduleId])

  const saveMapModal = React.useCallback(async () => {
    if (!mapModal) return
    setIsSubmitting(true)
    setMapError(null)
    try {
      const { response, data } = await postUploadQueueAddUnderlyingAlias({
        module: mapModal.mode,
        alias_code: mapModal.aliasCode.trim(),
        alias_isin: mapModal.aliasIsin.trim(),
        alias_source: mapModal.aliasSource.trim(),
        underlying_name: mapModal.showDetails ? mapModal.underlyingName.trim() : "",
        underlying_currency: mapModal.mode === "structure" && mapModal.showDetails ? mapModal.underlyingCurrency.trim() : "",
        underlying_spot: mapModal.mode === "structure" && mapModal.showDetails ? mapModal.underlyingSpot.trim() : "",
        queue_index: mapModal.queueIndex,
        queue_id: Number.isFinite(queueId) ? queueId : undefined,
        queue: queueRows,
        slot: mapModal.mode === "structure" ? mapModal.slot : undefined,
      })
      if (!response.ok || !data?.success) {
        throw new Error(data?.error || "Could not save mapping.")
      }
      toast.success("Underlying mapping saved.")
      setMapModal(null)
      if (data.row && typeof data.row === "object") {
        setPayload((current) => {
          if (!current?.data || typeof current.data !== "object") return current
          const currentData = current.data as ReviewData
          const queue = Array.isArray(currentData.queue) ? [...currentData.queue] : []
          if (mapModal.queueIndex < 0 || mapModal.queueIndex >= queue.length) return current
          queue[mapModal.queueIndex] = data.row as Record<string, unknown>
          const validation = replaceRowValidation(currentData.validation, mapModal.queueIndex, data.validation)
          return {
            ...current,
            data: {
              ...currentData,
              queue,
              ...(validation ? { validation } : {}),
            },
          }
        })
      } else {
        await loadReview()
      }
      if (isEditOpen && editingIndex === mapModal.queueIndex && data.row) {
        const normalized: Record<string, string> = {}
        Object.entries(data.row).forEach(([key, value]) => {
          if (key.startsWith("_")) return
          normalized[key] = value == null ? "" : String(value)
        })
        setEditRow(normalized)
      }
    } catch (e) {
      setMapError(errorMessageOr(e, "Could not save mapping."))
    } finally {
      setIsSubmitting(false)
    }
  }, [editingIndex, isEditOpen, loadReview, mapModal, queueId, queueRows])

  React.useEffect(() => {
    if (!mapModal) return
    const term = mapModal.aliasSource.trim()
    const timer = window.setTimeout(() => {
      void fetchAliasSourceSuggestions(moduleId, term)
        .then(setAliasSourceSuggestions)
        .catch(() => setAliasSourceSuggestions([]))
    }, 200)
    return () => window.clearTimeout(timer)
  }, [mapModal, moduleId])

  return (
    <div className="space-y-4">
      <div className="flex items-center justify-between">
        <h1 className="text-lg font-semibold">{reviewPageTitle(moduleId)}</h1>
        <Button variant="outline" size="sm" asChild>
          <Link href={DEEPDEX_PAGES.uploadQueue}>
            <ArrowLeft className="size-4" />
            Back to queue
          </Link>
        </Button>
      </div>

      <Card>
        <CardHeader>
          <CardTitle>
            {isLoading
              ? reviewPageTitle(moduleId)
              : `${queueRows.length} document${queueRows.length === 1 ? "" : "s"} to review`}
          </CardTitle>
        </CardHeader>
        <CardContent>
          {isLoading ? (
            <div className="flex items-center gap-2 text-sm text-muted-foreground">
              <Loader2 className="size-4 animate-spin" />
              Loading review data...
            </div>
          ) : error ? (
            <p className="text-sm text-destructive">{error}</p>
          ) : (
            <div className="space-y-2">
              <div className="flex flex-wrap items-center gap-2">
                <Badge variant="outline" title={queueIdList.join(", ")}>
                  Queue {queueIdList.length === 1 ? "ID" : "IDs"}: {queueIdLabel}
                </Badge>
                {moduleId === "structure" ? (
                  <>
                    <Badge variant="outline">{queueRows.length - existingCount} new</Badge>
                    <Badge variant="outline">{existingCount} existing</Badge>
                  </>
                ) : null}
              </div>
              {moduleId === "structure" || moduleId === "accumulator" ? (
                <p className="text-xs text-muted-foreground">
                  Unknown OCR underlyings show an orange badge. Use Map ISIN to add the OCR code to real ISIN mapping.
                  Open a row with Edit, change anything (including its underlyings) and Save: the {moduleId === "structure" ? "structure" : "accumulator"} is
                  created if it is new and updated if it already exists. Save is blocked until every OCR underlying is mapped to a real ISIN.
                </p>
              ) : (
                <p className="text-xs text-muted-foreground">
                  Review extracted rows, then open one with Edit and Save it.
                </p>
              )}
            </div>
          )}
        </CardContent>
      </Card>

      {!isLoading && !error && moduleId === "structure" && queueRows.length > 0 ? (
        <DeepdexUploadQueueReviewValidation queueRows={queueRows} validation={reviewData?.validation} />
      ) : null}

      {!isLoading && !error && queueRows.length > 0 ? (
        <DeepdexUploadQueueReviewBulkActions
          isSubmitting={isSubmitting}
          onCheckObservationDates={moduleId === "structure" ? () => void checkObservationDates() : undefined}
          isCheckingObservationDates={isCheckingDates}
        />
      ) : null}

      {!isLoading && !error && queueRows.length > 0 ? (
        <DeepdexUploadQueueReviewTable
          module={moduleId}
          queueRows={queueRows}
          reviewData={reviewData}
          onEdit={openEdit}
          onView={openView}
          onRemove={dropRow}
          onMapStructure={openMapModal}
          onMapAccumulator={openAccumulatorMapModal}
        />
      ) : null}

      <DeepdexUploadQueueReviewEditSheet
        open={isEditOpen}
        onOpenChange={setIsEditOpen}
        module={moduleId}
        editingIndex={editingIndex}
        editRow={editRow}
        onEditRowChange={setEditRow}
        editingExisting={editingExisting}
        editingExistingChild={editingExistingChild}
        queueRows={queueRows}
        editObs={editObs}
        onEditObsChange={setEditObs}
        editCoupons={editCoupons}
        onEditCouponsChange={setEditCoupons}
        editAccRows={editAccRows}
        onEditAccRowsChange={setEditAccRows}
        isSubmitting={isSubmitting}
        onSave={() => void saveEdit()}
        onMapStructure={openMapModal}
        onMapAccumulator={openAccumulatorMapModal}
      />

      <DeepdexUploadQueueReviewViewSheet
        open={isViewOpen}
        onOpenChange={setIsViewOpen}
        module={moduleId}
        viewingIndex={viewingIndex}
        queueRows={queueRows}
        onStudioSynced={() => void loadReview()}
      />

      <DeepdexUploadQueueReviewObservationDatesDialog
        open={isDatesOpen}
        onOpenChange={setIsDatesOpen}
        structures={dateStructures}
        onUpdateAll={() => void startDateSync()}
        isUpdating={isStartingDateSync || dateSyncJob !== null}
      />

      <DeepdexUploadQueueReviewMapDialog
        mapModal={mapModal}
        onMapModalChange={setMapModal}
        aliasSourceSuggestions={aliasSourceSuggestions}
        mapError={mapError}
        isSubmitting={isSubmitting}
        onSave={() => void saveMapModal()}
        onClose={() => setMapModal(null)}
      />
    </div>
  )
}
