import { useEffect, useMemo, useState } from 'react'
import { createFileRoute, Link } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import {
  Alert,
  Badge,
  Button,
  Checkbox,
  Drawer,
  EmptyState,
  LoadingBlock,
  Modal,
  PageHeader,
  Panel,
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeaderCell,
  TableRow,
  Tabs,
  TextArea,
  TextInput,
  QueryErrorState,
} from '@/ui'
import axios from 'axios'
import { useAuth } from '@/auth/useAuth'
import { useIsAdmin } from '@/auth/usePermissions'
import { ArrowCounterClockwiseIcon, CheckCircleIcon, XCircleIcon } from '@phosphor-icons/react'
import {
  useDictionaries,
  useDraft,
  useLiveRecord,
  useMyDrafts,
  useMySchemaDrafts,
  useReviewQueue,
  useSchemaReviewQueue,
} from '@/api/queries'
import { useApproveDraft, useRejectDraft, useWithdrawDraft } from '@/api/mutations'
import type { DraftOperation, DraftResponse, DraftStatus } from '@/api/client'
import { UserCell } from '@/lib/useUserDisplay'
import { shallowDiffSummary, isEmptyDiffSummary } from '@/lib/diff'
import { humanizeBulkReason } from '@/lib/bulkReason'
import {
  MyDraftsPanel,
  classifyRecordStatus,
  classifySchemaStatus,
} from '@/components/reviews/MyDraftsPanel'
import { JsonView } from '@/components/JsonView'

export const Route = createFileRoute('/reviews')({
  component: ReviewsPage,
})

const operationVariant = (op: DraftOperation): 'info' | 'success' | 'warning' => {
  if (op === 'CREATE') return 'success'
  if (op === 'CLOSE') return 'warning'
  return 'info'
}

/**
 * Record draft status badge — используется в drawer header'е, чтобы maker
 * сразу видел "в каком состоянии мой draft" без скролла к decision banner.
 *
 * <p>Visual mapping повторяет {@link MyDraftsPanel} badge'и для consistency:
 * PENDING=info, APPROVED=success, REJECTED=danger, WITHDRAWN=default.
 */
function DrawerStatusBadge({ status }: { status: DraftStatus }) {
  const { t } = useTranslation()
  const variant: 'info' | 'success' | 'danger' | 'default' = (() => {
    switch (status) {
      case 'PENDING':
        return 'info'
      case 'APPROVED':
        return 'success'
      case 'REJECTED':
        return 'danger'
      case 'WITHDRAWN':
        return 'default'
    }
  })()
  const fallback: Record<DraftStatus, string> = {
    PENDING: 'На ревью',
    APPROVED: 'Одобрено',
    REJECTED: 'Отклонён',
    WITHDRAWN: 'Отозван',
  }
  return (
    <Badge variant={variant}>
      {t(`reviews.mine.recordStatus.${status}`, { defaultValue: fallback[status] })}
    </Badge>
  )
}

/**
 * Diff summary chips for the ReviewDrawer — "+N added (fields), −M removed,
 * ~K changed" above the side-by-side JSON panes. Reviewer sees the gist
 * without eyeball-diffing long records.
 *
 * Backed by `shallowDiffSummary` from `@/lib/diff` (extracted there during
 * the 2026-05-14 eng review of MR !186 for reuse — RecordHistoryDrawer and
 * audit row summaries will plug into the same helper). Memo'd against the
 * three actual inputs so it doesn't recompute on unrelated state changes
 * (textarea typing, drawer resize, etc.).
 */
function DraftDiffSummary({
  live,
  proposed,
  operation,
}: {
  live: Record<string, unknown> | null | undefined
  proposed: Record<string, unknown> | null | undefined
  operation: DraftOperation
}) {
  const summary = useMemo(() => {
    // CLOSE op has no proposed payload by design — skip without computing.
    if (operation === 'CLOSE') return null
    return shallowDiffSummary(live, proposed)
  }, [live, proposed, operation])

  if (!summary || isEmptyDiffSummary(summary)) return null
  return (
    <div className="flex flex-wrap items-center gap-3 text-mono text-cap tabular-nums">
      {summary.added.length > 0 && (
        <span className="inline-flex items-center gap-1 text-aurora">
          <span className="font-bold">+</span>
          <span>{summary.added.length}</span>
          <span className="text-mute lowercase">({summary.added.join(', ')})</span>
        </span>
      )}
      {summary.removed.length > 0 && (
        <span className="inline-flex items-center gap-1 text-danger">
          <span className="font-bold">−</span>
          <span>{summary.removed.length}</span>
          <span className="text-mute lowercase">({summary.removed.join(', ')})</span>
        </span>
      )}
      {summary.changed.length > 0 && (
        <span className="inline-flex items-center gap-1 text-warn">
          <span className="font-bold">~</span>
          <span>{summary.changed.length}</span>
          <span className="text-mute lowercase">({summary.changed.join(', ')})</span>
        </span>
      )}
    </div>
  )
}

/**
 * Approval Workflow v2 — reviewer queue (D3=A pool model).
 *
 * <p>Все pending drafts в ordinis FIFO ordered by submitted_at. Click row →
 * drawer открывается с side-by-side current vs proposed JSON + Approve /
 * Reject actions. Reject reason required.
 */
type BulkResult = {
  approved: { id: string; bk: string }[]
  rejected: { id: string; bk: string }[]
  failed: { id: string; bk: string; reason: string }[]
}

type ReviewsTab = 'records' | 'schemas' | 'mine'

function ReviewsPage() {
  const { t } = useTranslation()
  const queue = useReviewQueue(0, 50)
  const schemaQueue = useSchemaReviewQueue(0, 50)
  // DraftResponse несёт только dictionaryId (UUID) — резолвим в name через
  // global dictionaries list (кешируется TanStack Query, ~free). Fallback на
  // UUID-prefix когда dict недоступен (scope-hide / deleted).
  const dictionaries = useDictionaries()
  const dictNameById = useMemo(() => {
    const map = new Map<string, string>()
    for (const d of dictionaries.data ?? []) map.set(d.id, d.name)
    return map
  }, [dictionaries.data])
  // Counts для "Мои" tab badge — sum of attention-needing drafts (pending +
  // WIP). Recompute дешёвый (linear over ≤100 items), refetch interval 30s
  // делает badge самозаживляющим без manual invalidation.
  const myDrafts = useMyDrafts(0, 50)
  const mySchemaDrafts = useMySchemaDrafts(0, 50)
  const recordsCount = queue.data?.totalElements ?? 0
  const schemasCount = schemaQueue.data?.totalElements ?? 0
  const myActiveCount = useMemo(() => {
    const records = (myDrafts.data?.items ?? []).filter(
      (r) => classifyRecordStatus(r.status) === 'pending',
    ).length
    const schemas = (mySchemaDrafts.data?.items ?? []).filter((s) => {
      const b = classifySchemaStatus(s.status)
      return b === 'pending' || b === 'wip'
    }).length
    return records + schemas
  }, [myDrafts.data, mySchemaDrafts.data])
  // Tab дефолтит на records (старее, чаще). Если schema queue не пустой
  // а record queue пустой — переключаемся на schemas чтобы reviewer'у не
  // пришлось кликать дважды.
  const [activeTab, setActiveTab] = useState<ReviewsTab>('records')
  // Auto-switch один раз когда queries loaded и records=0/schemas>0.
  useMemo(() => {
    if (recordsCount === 0 && schemasCount > 0 && activeTab === 'records') {
      setActiveTab('schemas')
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [recordsCount, schemasCount])
  const [selectedId, setSelectedId] = useState<string | undefined>(undefined)
  /** Phase 4: bulk approve/reject. Set<draftId> — стабильное id'шник. */
  const [selection, setSelection] = useState<Set<string>>(new Set())
  const [bulkRejectOpen, setBulkRejectOpen] = useState(false)
  const [bulkRejectReason, setBulkRejectReason] = useState('')
  const [bulkRunning, setBulkRunning] = useState(false)
  const [bulkResult, setBulkResult] = useState<BulkResult | undefined>(undefined)
  const approveMut = useApproveDraft()
  const rejectMut = useRejectDraft()

  const items = queue.data?.items ?? []
  const visibleIds = useMemo(() => items.map((d) => d.id), [items])
  const allVisibleSelected =
    visibleIds.length > 0 && visibleIds.every((id) => selection.has(id))
  const anySelected = selection.size > 0

  const toggleSelect = (id: string) => {
    setSelection((prev) => {
      const next = new Set(prev)
      if (next.has(id)) next.delete(id)
      else next.add(id)
      return next
    })
  }
  const toggleSelectAll = () => {
    if (allVisibleSelected) setSelection(new Set())
    else setSelection(new Set(visibleIds))
  }

  /**
   * Bulk-execute approve или reject параллельно. Каждый mutation independent
   * — если один fail (e.g. 409 self_approve_forbidden, 409 draft_not_pending
   * race), остальные продолжают. Result summary показывает кто прошёл и кто
   * нет с reason'ом.
   */
  const runBulk = async (
    op: 'approve' | 'reject',
    targets: DraftResponse[],
    sharedReason: string,
  ) => {
    setBulkRunning(true)
    const result: BulkResult = { approved: [], rejected: [], failed: [] }
    await Promise.all(
      targets.map(async (d) => {
        try {
          if (op === 'approve') {
            await approveMut.mutateAsync({ id: d.id, comment: undefined })
            result.approved.push({ id: d.id, bk: d.businessKey })
          } else {
            await rejectMut.mutateAsync({ id: d.id, comment: sharedReason })
            result.rejected.push({ id: d.id, bk: d.businessKey })
          }
        } catch (e: unknown) {
          const msg =
            (e as { response?: { data?: { code?: string; message?: string } } })
              ?.response?.data?.code ??
            (e as Error)?.message ??
            'unknown'
          result.failed.push({ id: d.id, bk: d.businessKey, reason: msg })
        }
      }),
    )
    setBulkResult(result)
    setBulkRunning(false)
    setSelection(new Set())
    setBulkRejectOpen(false)
    setBulkRejectReason('')
  }

  const handleBulkApprove = () => {
    const targets = items.filter((d) => selection.has(d.id))
    if (targets.length === 0) return
    if (!confirm(t('reviews.bulk.confirmApprove', { count: targets.length }))) return
    runBulk('approve', targets, '')
  }

  const handleBulkReject = () => {
    if (!bulkRejectReason.trim()) return
    const targets = items.filter((d) => selection.has(d.id))
    if (targets.length === 0) return
    runBulk('reject', targets, bulkRejectReason.trim())
  }

  return (
    <div className="space-y-4">
      <PageHeader
        title={t('reviews.title')}
        description={t('reviews.description')}
      />

      <Tabs
        value={activeTab}
        onValueChange={(v) => setActiveTab(v as ReviewsTab)}
        items={[
          {
            id: 'records',
            label: t('reviews.tab.records', { defaultValue: 'Записи' }),
            count:
              recordsCount > 0 ? (
                <Badge variant="info">{recordsCount}</Badge>
              ) : undefined,
          },
          {
            id: 'schemas',
            label: t('reviews.tab.schemas', { defaultValue: 'Схемы' }),
            count:
              schemasCount > 0 ? (
                <Badge variant="info">{schemasCount}</Badge>
              ) : undefined,
          },
          {
            id: 'mine',
            label: t('reviews.tab.mine', { defaultValue: 'Мои' }),
            count:
              myActiveCount > 0 ? (
                <Badge variant="info">{myActiveCount}</Badge>
              ) : undefined,
          },
        ]}
      />

      {activeTab === 'mine' && (
        <MyDraftsPanel onRecordClick={(id) => setSelectedId(id)} />
      )}

      {activeTab === 'schemas' && (
        <>
          {schemaQueue.isLoading && <LoadingBlock size="md" label={t('loading')} />}
          <SchemaReviewQueuePanel />
          {schemasCount === 0 && !schemaQueue.isLoading && (
            <EmptyState
              title={t('reviews.schema.empty', {
                defaultValue: 'Очередь пуста',
              })}
              description={t('reviews.schema.emptyDescription', {
                defaultValue: 'Схемы на ревью появятся здесь.',
              })}
            />
          )}
        </>
      )}

      {activeTab === 'records' && queue.isLoading && (
        <LoadingBlock size="md" label={t('loading')} />
      )}

      {activeTab === 'records' && queue.error && (
        <QueryErrorState error={queue.error} onRetry={() => queue.refetch()} />
      )}

      {activeTab === 'records' && bulkResult && (
        <Alert
          variant={bulkResult.failed.length === 0 ? 'success' : 'warning'}
          title={t('reviews.bulk.resultTitle')}
          action={
            <Button variant="ghost" size="sm" onClick={() => setBulkResult(undefined)}>
              {t('reviews.bulk.cancel')}
            </Button>
          }
        >
          <ul className="space-y-1 text-cell">
            {bulkResult.approved.length > 0 && (
              <li>
                {t('reviews.bulk.approvedCount', { count: bulkResult.approved.length })}
                : {bulkResult.approved.map((r) => r.bk).join(', ')}
              </li>
            )}
            {bulkResult.rejected.length > 0 && (
              <li>
                {t('reviews.bulk.rejectedCount', { count: bulkResult.rejected.length })}
                : {bulkResult.rejected.map((r) => r.bk).join(', ')}
              </li>
            )}
            {bulkResult.failed.length > 0 && (
              <li className="text-error">
                {t('reviews.bulk.failedCount', { count: bulkResult.failed.length })}:
                <ul className="ml-4 mt-1 list-disc">
                  {bulkResult.failed.map((f) => (
                    <li key={f.id}>
                      <span className="font-mono">{f.bk}</span>: {humanizeBulkReason(f.reason)}
                    </li>
                  ))}
                </ul>
              </li>
            )}
          </ul>
        </Alert>
      )}

      {activeTab === 'records' && queue.data && queue.data.totalElements === 0 && (
        <EmptyState
          icon={<CheckCircleIcon weight="duotone" size={32} />}
          title={t('reviews.empty')}
          description={t('reviews.emptyDescription', {
            defaultValue:
              'Очередь чистая. Прошлые решения можно посмотреть в журнале аудита.',
          })}
          action={
            <Link to="/audit" className="text-accent hover:underline text-cell">
              {t('reviews.emptyAuditLink', { defaultValue: 'Открыть /audit' })} →
            </Link>
          }
        />
      )}

      {activeTab === 'records' && queue.data && queue.data.totalElements > 0 && (
        <Panel>
          <div className="flex items-center justify-between mb-3 gap-3">
            <p className="text-cell text-mute">
              {t('reviews.queueTotal', { count: queue.data.totalElements })}
              {anySelected ? (
                <span className="ml-2 text-accent font-medium">
                  · {t('reviews.bulk.selectedCount', { count: selection.size })}
                </span>
              ) : null}
            </p>
            {anySelected && (
              <div className="flex items-center gap-2">
                <Button
                  variant="primary"
                  size="sm"
                  disabled={bulkRunning}
                  onClick={handleBulkApprove}
                >
                  {t('reviews.bulk.approveSelected')}
                </Button>
                <Button
                  variant="secondary"
                  size="sm"
                  disabled={bulkRunning}
                  onClick={() => setBulkRejectOpen(true)}
                >
                  {t('reviews.bulk.rejectSelected')}
                </Button>
                <Button
                  variant="ghost"
                  size="sm"
                  onClick={() => setSelection(new Set())}
                >
                  {t('reviews.bulk.clear')}
                </Button>
              </div>
            )}
          </div>
          <Table>
            <TableHead>
              <TableRow>
                <TableHeaderCell>
                  <Checkbox
                    aria-label={t('reviews.bulk.selectAll')}
                    checked={allVisibleSelected}
                    onChange={toggleSelectAll}
                  />
                </TableHeaderCell>
                <TableHeaderCell>{t('reviews.col.dict')}</TableHeaderCell>
                <TableHeaderCell>{t('reviews.col.bk')}</TableHeaderCell>
                <TableHeaderCell>{t('reviews.col.op')}</TableHeaderCell>
                <TableHeaderCell>{t('reviews.col.maker')}</TableHeaderCell>
                <TableHeaderCell>{t('reviews.col.submitted')}</TableHeaderCell>
                <TableHeaderCell>{t('reviews.col.actions')}</TableHeaderCell>
              </TableRow>
            </TableHead>
            <TableBody>
              {items.map((d) => (
                <TableRow key={d.id} data-selected={selection.has(d.id)}>
                  <TableCell>
                    <Checkbox
                      aria-label={`${t('reviews.bulk.selectRow')} ${d.businessKey}`}
                      checked={selection.has(d.id)}
                      onChange={() => toggleSelect(d.id)}
                    />
                  </TableCell>
                  <TableCell>
                    {(() => {
                      const dictName = dictNameById.get(d.dictionaryId)
                      if (dictName) {
                        return (
                          <Link
                            to="/dictionaries/$name"
                            params={{ name: dictName }}
                            className="text-mono text-accent hover:underline"
                          >
                            {dictName}
                          </Link>
                        )
                      }
                      // Dict скрыт scope-фильтром или удалён — UUID-prefix
                      // как fallback, без link'а (вёл бы в 404).
                      return (
                        <span className="text-mono text-mute" title={d.dictionaryId}>
                          {d.dictionaryId.slice(0, 8)}…
                        </span>
                      )
                    })()}
                  </TableCell>
                  <TableCell className="text-mono">{d.businessKey}</TableCell>
                  <TableCell>
                    <Badge variant={operationVariant(d.operation)}>{d.operation}</Badge>
                  </TableCell>
                  <TableCell><UserCell uuid={d.makerId} /></TableCell>
                  <TableCell className="text-cell text-ink-2">
                    {new Date(d.submittedAt).toLocaleString()}
                  </TableCell>
                  <TableCell>
                    <Button
                      variant="secondary"
                      size="sm"
                      onClick={() => setSelectedId(d.id)}
                    >
                      {t('reviews.action.review')}
                    </Button>
                  </TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </Panel>
      )}

      <ReviewDrawer
        draftId={selectedId}
        onClose={() => setSelectedId(undefined)}
      />

      <Modal
        isOpen={bulkRejectOpen}
        onClose={() => setBulkRejectOpen(false)}
        title={t('reviews.bulk.rejectModalTitle', { count: selection.size })}
      >
        <div className="space-y-3">
          <p className="text-cell text-ink-2">
            {t('reviews.bulk.rejectModalDescription')}
          </p>
          <TextArea
            label={t('reviews.bulk.rejectReasonLabel')}
            value={bulkRejectReason}
            onChange={(e) => setBulkRejectReason(e.target.value)}
            rows={3}
            placeholder={t('reviews.bulk.rejectReasonPlaceholder')}
          />
          <div className="flex justify-end gap-2">
            <Button
              variant="ghost"
              onClick={() => {
                setBulkRejectOpen(false)
                setBulkRejectReason('')
              }}
            >
              {t('reviews.bulk.cancel')}
            </Button>
            <Button
              variant="primary"
              disabled={!bulkRejectReason.trim() || bulkRunning}
              onClick={handleBulkReject}
            >
              {t('reviews.bulk.confirmReject', { count: selection.size })}
            </Button>
          </div>
        </div>
      </Modal>
    </div>
  )
}

type DrawerProps = {
  draftId: string | undefined
  onClose: () => void
}

/**
 * Превращает Axios error в human-readable title + body. Mapping для
 * известных backend кодов (см. {@code OrdinisException.conflict}):
 * - self_approve_forbidden — maker = reviewer (D3 maker-checker invariant)
 * - draft_not_pending — кто-то уже approve/reject'нул этот draft
 * - forbidden_role — у actor'а нет нужной workflow роли (Phase 1d enforcement)
 *
 * Default fallback — backend message + статус код. Сырой "AxiosError ... 409"
 * больше не показывается.
 */
function extractReviewError(err: unknown): { title: string; body: string } {
  if (!err) return { title: 'Ошибка', body: '' }
  if (axios.isAxiosError(err)) {
    const status = err.response?.status
    const data = err.response?.data as
      | { code?: string; message?: string; error?: string }
      | undefined
    const code = data?.code
    const msg = data?.message ?? data?.error ?? err.message

    if (code === 'self_approve_forbidden') {
      return {
        title: 'Нельзя одобрить свой собственный черновик',
        body:
          'Maker-checker invariant: тот, кто создал draft, не может его approve. Попроси другого reviewer\'а из пула.',
      }
    }
    if (code === 'self_reject_forbidden') {
      return {
        title: 'Нельзя отклонить свой собственный черновик',
        body:
          'Тот, кто создал draft, не может его reject. Если передумал — используй Отозвать (withdraw).',
      }
    }
    if (code === 'draft_not_pending') {
      return {
        title: 'Draft уже не pending',
        body: 'Кто-то уже принял решение по этому draft\'у. Обнови очередь.',
      }
    }
    if (code === 'forbidden_role') {
      return {
        title: 'Недостаточно прав',
        body: msg || 'У вас нет нужной workflow role для этого действия.',
      }
    }
    if (code === 'reject_reason_required') {
      return {
        title: 'Укажите причину отклонения',
        body: 'Поле «Комментарий ревьюера» обязательно для reject (для compliance).',
      }
    }
    return {
      title: `Ошибка ${status ?? ''}`.trim(),
      body: msg,
    }
  }
  return {
    title: 'Ошибка',
    body: err instanceof Error ? err.message : String(err),
  }
}

function ReviewDrawer({ draftId, onClose }: DrawerProps) {
  const { t } = useTranslation()
  const auth = useAuth()
  const currentSub = auth.user?.profile?.sub
  const draftQ = useDraft(draftId)
  const draft = draftQ.data
  // DraftResponse несёт только dictionaryId (UUID); /api/v1/dictionaries/{name}/...
  // ожидает имя словаря. Резолвим через useDictionaries (TanStack кеш).
  // До этого useLiveRecord(draft.dictionaryId, ...) валился с 404
  // "Dictionary not found: <uuid>" → live-блок drawer'а оставался пустым.
  const dictionariesQ = useDictionaries()
  const dictName = useMemo(
    () => dictionariesQ.data?.find((d) => d.id === draft?.dictionaryId)?.name,
    [dictionariesQ.data, draft?.dictionaryId],
  )
  // CREATE-черновик не имеет live-записи по определению — не пробуем её
  // загружать (GET /dictionaries/{name}/records/{key} → 404, красный шум в
  // консоли на каждый CREATE-draft). Для UPDATE/CLOSE — грузим как раньше.
  const isCreateDraft = draft?.operation === 'CREATE'
  const liveQ = useLiveRecord(
    draft && !isCreateDraft ? dictName : undefined,
    draft && !isCreateDraft ? draft.businessKey : undefined,
  )
  const approveMut = useApproveDraft()
  const rejectMut = useRejectDraft()
  const withdrawMut = useWithdrawDraft()
  const [comment, setComment] = useState('')
  /** Inline error для reject reason — show when reviewer кликнул Reject с
   * пустым comment'ом. Реально cleared автоматически когда user печатает. */
  const [rejectError, setRejectError] = useState<string | null>(null)
  /** Brief success affordance после approve/reject mutation — drawer
   * остаётся 600ms с зелёной/красной полосой, чтобы reviewer почувствовал
   * что click registered. Без этого drawer закрывается мгновенно, click
   * feedback теряется. */
  const [decisionFlash, setDecisionFlash] = useState<'approve' | 'reject' | null>(null)

  // Reset state когда reviewer переключается на другой draft без закрытия
  // drawer'а (click row → open new draft в already-open drawer). Раньше
  // {comment, rejectError} оставались от previous draft, и mutation .error
  // объекты от approveMut/rejectMut/withdrawMut held conflict banners
  // ('реверсное состояние draft', '409', etc.) из predыдущей попытки —
  // показывались на форме нового draft (observed bug).
  useEffect(() => {
    setComment('')
    setRejectError(null)
    setDecisionFlash(null)
    approveMut.reset()
    rejectMut.reset()
    withdrawMut.reset()
    // approveMut/rejectMut/withdrawMut references stable между renders
    // (TanStack mutation hook), но lint требует exhaustive deps. .reset
    // — stable function, безопасно включить.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [draftId])

  // Identity gates — определяют что показать в footer'е drawer'а.
  //
  // 1. `isPending` = false: terminal draft (APPROVED/REJECTED/WITHDRAWN).
  //    Никаких actions — read-only diff + decision banner.
  // 2. `isMineAndPending`: maker открыл свой собственный pending draft из
  //    "Мои" таба. Для regular maker'а Approve self запрещён backend'ом
  //    (self_approve_forbidden), показываем Withdraw. Admin override
  //    (MR !280): backend разрешает self-approve если у actor'а admin
  //    роль → frontend тоже снимает блокировку, иначе UI скрывает кнопку
  //    которую backend бы пропустил.
  // 3. Иначе — reviewer flow: approve/reject как было.
  const isAdmin = useIsAdmin()
  const isPending = draft?.status === 'PENDING'
  const isMineAndPending =
    isPending && Boolean(currentSub) && draft?.makerId === currentSub && !isAdmin

  const handleApprove = () => {
    if (!draftId) return
    approveMut.mutate(
      { id: draftId, comment: comment.trim() || undefined },
      {
        onSuccess: () => {
          setDecisionFlash('approve')
          // Hold drawer 600ms так чтобы reviewer увидел зелёную полосу;
          // потом reset state и close. Без флэша approve было instant.
          setTimeout(() => {
            setComment('')
            setDecisionFlash(null)
            onClose()
          }, 600)
        },
      },
    )
  }

  const handleReject = () => {
    if (!draftId) return
    if (!comment.trim()) {
      // Раньше return был silent — reviewer кликал Reject и ничего не
      // происходило. Backend всё равно вернёт 400 missing_reject_reason,
      // но user-friendly inline error лучше.
      setRejectError(
        t('reviews.drawer.rejectReasonRequired', {
          defaultValue:
            'Укажите причину отклонения — comment обязателен для reject (compliance).',
        }),
      )
      return
    }
    setRejectError(null)
    rejectMut.mutate(
      { id: draftId, comment: comment.trim() },
      {
        onSuccess: () => {
          setDecisionFlash('reject')
          setTimeout(() => {
            setComment('')
            setDecisionFlash(null)
            onClose()
          }, 600)
        },
      },
    )
  }

  const handleWithdraw = () => {
    if (!draftId) return
    if (!confirm(t('reviews.action.withdrawConfirm', {
      defaultValue: 'Отозвать свой черновик? После этого он попадёт в Decided.',
    }))) return
    withdrawMut.mutate(draftId, {
      onSuccess: () => {
        setComment('')
        onClose()
      },
    })
  }

  return (
    <Drawer
      isOpen={Boolean(draftId)}
      onClose={onClose}
      title={t('reviews.drawer.title')}
      side="right"
      widthClassName="w-full max-w-3xl"
    >
      <div className="space-y-4">
        {draftQ.isLoading && <LoadingBlock size="md" label={t('loading')} />}
        {draftQ.error && (
          <QueryErrorState error={draftQ.error} onRetry={() => draftQ.refetch()} />
        )}

        {draft && (
          <>
            {/* Tightened header: top row is identity (op + bk), bottom row is
                provenance (who/when). Was a single flex-wrap line with four
                spans — visually busy, hard to scan. */}
            <div className="space-y-1 text-cell">
              <div className="flex flex-wrap items-center gap-2">
                <Badge variant={operationVariant(draft.operation)}>{draft.operation}</Badge>
                {dictName ? (
                  <Link
                    to="/dictionaries/$name"
                    params={{ name: dictName }}
                    className="text-mono text-accent hover:underline"
                  >
                    {dictName}
                  </Link>
                ) : (
                  <span className="text-mono text-mute" title={draft.dictionaryId}>
                    {draft.dictionaryId.slice(0, 8)}…
                  </span>
                )}
                <span className="text-mute">/</span>
                <span className="font-mono">{draft.businessKey}</span>
                <DrawerStatusBadge status={draft.status} />
              </div>
              <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-mute">
                <span className="inline-flex items-center gap-1">
                  {t('reviews.drawer.maker')}: <UserCell uuid={draft.makerId} />
                </span>
                <span>
                  {t('reviews.drawer.submitted')}:{' '}
                  {new Date(draft.submittedAt).toLocaleString()}
                </span>
              </div>
            </div>

            {/* Read-only decision banner — для всех terminal статусов. Показывает
                reviewer'а + decision time + reviewer comment чтобы maker'у не
                пришлось гадать "что произошло". */}
            {!isPending && (
              <div className="px-3 py-2 rounded-sm border border-line bg-line/30 text-cell space-y-1">
                <div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-mute">
                  {draft.reviewerId && (
                    <span className="inline-flex items-center gap-1">
                      {t('reviews.drawer.reviewer', { defaultValue: 'Ревьюер' })}:{' '}
                      <UserCell uuid={draft.reviewerId} />
                    </span>
                  )}
                  {draft.reviewedAt && (
                    <span>
                      {t('reviews.drawer.reviewedAt', { defaultValue: 'Решение' })}:{' '}
                      {new Date(draft.reviewedAt).toLocaleString()}
                    </span>
                  )}
                </div>
                {draft.reviewComment && (
                  <div>
                    <span className="text-mute uppercase tracking-[0.10em] font-display">
                      {t('reviews.drawer.reviewComment', { defaultValue: 'Комментарий' })}:
                    </span>{' '}
                    {draft.reviewComment}
                  </div>
                )}
              </div>
            )}

            {draft.makerComment && (
              <div className="px-3 py-2 rounded-sm border border-line bg-line/30 text-cell">
                <span className="text-mute uppercase tracking-[0.10em] font-display">
                  {t('reviews.drawer.makerComment')}:
                </span>{' '}
                {draft.makerComment}
              </div>
            )}

            <DraftDiffSummary
              live={liveQ.data?.data as Record<string, unknown> | null | undefined}
              proposed={draft.data as Record<string, unknown> | null | undefined}
              operation={draft.operation}
            />

            {(() => {
              // Считаем diff один раз для обеих колонок: changed/added/removed
              // keys (top-level) подсветим через JsonView в обоих side-by-side
              // блоках — reviewer видит точку изменения визуально, не сравнивая
              // глазами длинные JSON-объекты.
              const liveData =
                (liveQ.data?.data as Record<string, unknown> | null | undefined) ?? null
              const proposedData =
                (draft.data as Record<string, unknown> | null | undefined) ?? null
              const diff =
                draft.operation === 'CLOSE'
                  ? null
                  : shallowDiffSummary(liveData, proposedData)
              const liveDiff = diff
                ? { removed: diff.removed, changed: diff.changed }
                : undefined
              const proposedDiff = diff
                ? { added: diff.added, changed: diff.changed }
                : undefined
              return (
                <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
                  <div>
                    <p className="text-cap text-mute mb-1">
                      {t('reviews.drawer.live')}
                    </p>
                    {liveQ.isLoading && <LoadingBlock size="sm" label={t('loading')} />}
                    {liveQ.error && (
                      <p className="text-cell text-danger italic">
                        {t('reviews.drawer.liveError', {
                          defaultValue: 'Не удалось загрузить текущую запись',
                        })}
                      </p>
                    )}
                    {!liveQ.isLoading && !liveQ.error && (liveQ.data === null || isCreateDraft) && (
                      <p className="text-cell text-mute italic">
                        {t('reviews.drawer.noLive')}
                      </p>
                    )}
                    {liveQ.data && (
                      <JsonView value={liveQ.data.data} diff={liveDiff} />
                    )}
                  </div>
                  <div>
                    <p className="text-cap text-mute mb-1">
                      {t('reviews.drawer.proposed')}
                    </p>
                    {draft.operation === 'CLOSE' ? (
                      <p className="text-cell text-aurora italic">
                        {t('reviews.drawer.closeNote')}
                      </p>
                    ) : (
                      <JsonView value={draft.data} diff={proposedDiff} />
                    )}
                  </div>
                </div>
              )
            })()}

            {(approveMut.error || rejectMut.error || withdrawMut.error) && (() => {
              const err = approveMut.error || rejectMut.error || withdrawMut.error
              const info = extractReviewError(err)
              return (
                <Alert variant="error" title={info.title}>
                  {info.body}
                </Alert>
              )
            })()}

            {/* Action footer split на три режима:
                - terminal (не pending) — только Close, нет input/actions
                - own pending — Withdraw, без comment input (maker не reviewer'ит)
                - peer pending — approve/reject (existing reviewer flow) */}
            {!isPending && (
              <div className="border-t border-line pt-3 flex items-center justify-end">
                <Button variant="secondary" onClick={onClose}>
                  {t('reviews.action.close', { defaultValue: 'Закрыть' })}
                </Button>
              </div>
            )}

            {isMineAndPending && (
              <div className="border-t border-line pt-3 space-y-2">
                <p className="text-cell text-mute">
                  {t('reviews.drawer.ownPendingHint', {
                    defaultValue: 'Это ваш черновик на ревью. Approve/reject запрещены — можно отозвать.',
                  })}
                </p>
                <div className="flex items-center justify-end gap-2 pt-2">
                  <Button
                    variant="secondary"
                    onClick={onClose}
                    disabled={withdrawMut.isPending}
                  >
                    {t('reviews.action.cancel')}
                  </Button>
                  <Button
                    variant="danger"
                    leftIcon={<ArrowCounterClockwiseIcon weight="bold" size={14} />}
                    onClick={handleWithdraw}
                    disabled={withdrawMut.isPending}
                    loading={withdrawMut.isPending}
                  >
                    {t('reviews.action.withdraw', { defaultValue: 'Отозвать' })}
                  </Button>
                </div>
              </div>
            )}

            {isPending && !isMineAndPending && (
              <div
                className={
                  'border-t pt-3 space-y-2 transition-colors duration-300 -mx-4 px-4 ' +
                  (decisionFlash === 'approve'
                    ? 'border-green bg-green-bg/30'
                    : decisionFlash === 'reject'
                      ? 'border-pink bg-pink-bg/30'
                      : 'border-line')
                }
              >
                <label
                  htmlFor="review-comment"
                  className="text-cap text-ink-2"
                >
                  {t('reviews.drawer.comment')}
                </label>
                <TextInput
                  id="review-comment"
                  value={comment}
                  onChange={(e) => {
                    setComment(e.target.value)
                    // Stale validation error gone как только user печатает.
                    if (rejectError && e.target.value.trim()) setRejectError(null)
                  }}
                  placeholder={t('reviews.drawer.commentPlaceholder')}
                  aria-invalid={Boolean(rejectError)}
                  aria-describedby={rejectError ? 'review-comment-error' : undefined}
                />
                {rejectError && (
                  <p
                    id="review-comment-error"
                    role="alert"
                    className="text-cell text-pink"
                  >
                    {rejectError}
                  </p>
                )}
                <div className="flex items-center justify-end gap-2 pt-2">
                  <Button
                    variant="secondary"
                    onClick={onClose}
                    disabled={approveMut.isPending || rejectMut.isPending}
                  >
                    {t('reviews.action.cancel')}
                  </Button>
                  <Button
                    variant="danger"
                    leftIcon={<XCircleIcon weight="bold" size={14} />}
                    onClick={handleReject}
                    disabled={rejectMut.isPending || approveMut.isPending}
                    loading={rejectMut.isPending}
                  >
                    {t('reviews.action.reject')}
                  </Button>
                  <Button
                    variant="primary"
                    leftIcon={<CheckCircleIcon weight="bold" size={14} />}
                    onClick={handleApprove}
                    disabled={approveMut.isPending || rejectMut.isPending}
                    loading={approveMut.isPending}
                  >
                    {t('reviews.action.approve')}
                  </Button>
                </div>
              </div>
            )}
          </>
        )}
      </div>
    </Drawer>
  )
}

/**
 * Schema review pool queue — global view всех \`review_pending\` schema
 * draft'ов через {@code GET /admin/schema-reviews/pending}. Reviewer
 * жмёт «Открыть» → переходит на dict detail page, banner+drawer
 * автоматически подхватятся через {@code useDictActiveSchemaDraft}.
 *
 * <p>Скрывается если pool пуст — экономит вертикальное место на странице
 * для основного record-review queue.
 */
function SchemaReviewQueuePanel() {
  const { t } = useTranslation()
  const queue = useSchemaReviewQueue(0, 20)
  const items = queue.data?.items ?? []
  if (queue.isLoading || items.length === 0) return null

  return (
    <Panel>
      <div className="flex items-center justify-between mb-3">
        <div>
          <h2 className="text-title-md font-semibold">
            {t('reviews.schema.title', { defaultValue: 'Изменения схем' })}
          </h2>
          <p className="text-cell text-mute mt-0.5">
            {t('reviews.schema.queueTotal', {
              count: queue.data?.totalElements ?? items.length,
              defaultValue: `${queue.data?.totalElements ?? items.length} draft(s) ждут ревью`,
            })}
          </p>
        </div>
        <Badge variant="info">
          {t('reviews.schema.label', { defaultValue: 'Schema workflow' })}
        </Badge>
      </div>
      <Table>
        <TableHead>
          <TableRow>
            <TableHeaderCell>{t('reviews.schema.dictionary', { defaultValue: 'Словарь' })}</TableHeaderCell>
            <TableHeaderCell>{t('reviews.schema.fromVersion', { defaultValue: 'От версии' })}</TableHeaderCell>
            <TableHeaderCell>{t('reviews.schema.maker', { defaultValue: 'Автор' })}</TableHeaderCell>
            <TableHeaderCell>{t('reviews.schema.submittedAt', { defaultValue: 'Отправлен' })}</TableHeaderCell>
            <TableHeaderCell align="right">{t('reviews.schema.action', { defaultValue: 'Действие' })}</TableHeaderCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {items.map((d) => (
            <TableRow key={d.draftId}>
              <TableCell>
                <Link
                  to="/dictionaries/$name"
                  params={{ name: d.dictionaryName }}
                  className="text-accent hover:underline font-mono"
                >
                  {d.dictionaryName}
                </Link>
              </TableCell>
              <TableCell className="font-mono tabular-nums">v{d.branchedFromVersion}</TableCell>
              <TableCell><UserCell uuid={d.makerId} /></TableCell>
              <TableCell className="tabular-nums">
                {d.submittedAt ? new Date(d.submittedAt).toLocaleString() : '—'}
              </TableCell>
              <TableCell align="right">
                <Link
                  to="/dictionaries/$name"
                  params={{ name: d.dictionaryName }}
                  className="text-accent hover:underline"
                >
                  {t('reviews.schema.open', { defaultValue: 'Открыть' })} →
                </Link>
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </Panel>
  )
}
