import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { AlertTriangle, Check, HelpCircle, Search, Upload } from 'lucide-react'
import { useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { getMetadataProviders } from '@/features/admin/shows/api'
import { qk } from '@/shared/api/query-keys'
import type { MovieImportItem, MovieMatchDto, MovieMatchStatus } from '@/shared/api/types'
import { useApiError } from '@/shared/lib/use-api-error'
import { Button } from '@/shared/ui/button'
import { DialogFooter } from '@/shared/ui/dialog'
import { Input } from '@/shared/ui/input'
import { toast } from '@/shared/ui/toast-store'
import { importMovies, listManualInbox, matchMovie, matchMovies } from './api'
import { useUploadStore } from './upload-store'

type Source = 'manual' | 'disk'

/** Строка таблицы: разбор от сервера плюс то, что человек в ней поправил. */
type Row = MovieMatchDto & {
  /** Ключ строки — имя файла или папки; по нему же строка опознаётся при импорте. */
  selected: boolean
  chosenExternalId: string | null
  title: string
  year: string
}

const STATUS_TONE: Record<MovieMatchStatus, string> = {
  Confident: 'text-emerald-500',
  Uncertain: 'text-amber-500',
  NotFound: 'text-red-500',
  AlreadyInLibrary: 'text-muted-foreground',
}

function toRow(match: MovieMatchDto): Row {
  return {
    ...match,
    // Уже заведённое и ненайденное по умолчанию не отмечаем: первое создало бы дубль,
    // второе — шоу без метаданных, которого человек не просил.
    selected: match.status === 'Confident' || match.status === 'Uncertain',
    chosenExternalId: match.pickedExternalId,
    title: match.parsedTitle,
    year: match.parsedYear?.toString() ?? '',
  }
}

/**
 * Файлы `manual/` строками: папка релиза — это один фильм, а не набор файлов. Внутри папки берём
 * самый большой видеофайл (остальное — сэмплы и трейлеры), а показываем имя папки: оно чище,
 * чем «video.mkv» внутри.
 */
function manualRows(
  files: { relativePath: string; folder: string; name: string; sizeBytes: number }[],
) {
  const rows = new Map<string, { name: string; relativePath: string; sizeBytes: number }>()

  for (const file of files) {
    const key = file.folder || file.relativePath
    const name = file.folder || file.name
    const current = rows.get(key)
    if (!current || file.sizeBytes > current.sizeBytes)
      rows.set(key, { name, relativePath: file.relativePath, sizeBytes: file.sizeBytes })
  }

  return [...rows.values()]
}

/**
 * Разбор фильмов: имя файла → название и год → кандидат источника → шоу с метаданными.
 *
 * Источника два и оба идут одной таблицей: файлы из `manual/` на сервере и выбранные в браузере.
 * Во втором случае имена разбираются **до** загрузки — иначе, чтобы узнать, что половина строк
 * не распозналась, пришлось бы сначала залить десятки гигабайт.
 */
export function MovieImportPanel({ onClose }: Readonly<{ onClose: () => void }>) {
  const { t } = useTranslation()
  const onError = useApiError()
  const queryClient = useQueryClient()

  const [source, setSource] = useState<Source>('manual')
  const [rows, setRows] = useState<Row[]>([])
  const [files, setFiles] = useState<File[]>([])
  const fileInput = useRef<HTMLInputElement>(null)
  const enqueue = useUploadStore((s) => s.enqueue)

  const { data: providers } = useQuery({
    queryKey: qk.metadata.providers,
    queryFn: getMetadataProviders,
  })
  const [provider, setProvider] = useState('')
  const activeProvider = provider || providers?.[0] || ''

  const { data: manual } = useQuery({
    queryKey: qk.media.manual,
    queryFn: listManualInbox,
    enabled: source === 'manual',
  })

  const manualFiles = useMemo(() => manualRows(manual?.files ?? []), [manual])

  /** Имя строки → чем её импортировать: путь в manual/ либо файл из браузера. */
  const sources = useMemo(() => {
    const map = new Map<string, { relativePath?: string; file?: File }>()
    if (source === 'manual')
      for (const item of manualFiles) map.set(item.name, { relativePath: item.relativePath })
    else for (const file of files) map.set(file.name, { file })
    return map
  }, [source, manualFiles, files])

  const scan = useMutation({
    mutationFn: () => matchMovies([...sources.keys()], activeProvider),
    onSuccess: (matches) => setRows(matches.map(toRow)),
    onError,
  })

  const research = useMutation({
    mutationFn: (row: Row) =>
      matchMovie(row.name, row.title, row.year ? Number(row.year) : null, activeProvider),
    onSuccess: (match) =>
      setRows((current) =>
        current.map((row) => (row.name === match.name ? { ...toRow(match), selected: true } : row)),
      ),
    onError,
  })

  const importing = useMutation({
    mutationFn: async () => {
      const chosen = rows.filter((row) => row.selected)

      // Файлы с диска грузятся по одному и заводятся сразу после аплоада: ждать всю пачку,
      // чтобы увидеть первый фильм в библиотеке, незачем.
      if (source === 'disk') {
        const plans = new Map(
          chosen.map((row) => [
            row.name,
            {
              title: row.title,
              year: row.year ? Number(row.year) : null,
              externalId: row.chosenExternalId,
            },
          ]),
        )
        await enqueue(
          chosen.map((row) => sources.get(row.name)?.file).filter((f): f is File => Boolean(f)),
          { resolveMovie: (file) => plans.get(file.name), provider: activeProvider },
        )
        return null
      }

      const items: MovieImportItem[] = chosen.map((row) => ({
        relativePath: sources.get(row.name)?.relativePath ?? null,
        assetId: null,
        title: row.title,
        year: row.year ? Number(row.year) : null,
        externalId: row.chosenExternalId,
      }))
      return importMovies(items, activeProvider)
    },
    onSuccess: (result) => {
      void queryClient.invalidateQueries({ queryKey: qk.media.all })
      void queryClient.invalidateQueries({ queryKey: qk.media.manual })
      void queryClient.invalidateQueries({ queryKey: qk.shows.all })

      if (!result) {
        toast.success(t('admin.movies.uploadStarted'))
        onClose()
        return
      }

      toast.success(
        t('admin.movies.imported', { created: result.created, enriched: result.enriched }),
      )
      for (const failure of result.failed) toast.error(`${failure.title}: ${failure.reason}`)
      setRows((current) => current.filter((row) => !row.selected))
    },
    onError,
  })

  const patch = (name: string, changes: Partial<Row>) =>
    setRows((current) => current.map((row) => (row.name === name ? { ...row, ...changes } : row)))

  const selectedCount = rows.filter((row) => row.selected).length
  const allSelected = rows.length > 0 && selectedCount === rows.length
  const canScan = sources.size > 0 && Boolean(activeProvider) && !scan.isPending

  const toggleAll = (selected: boolean) =>
    setRows((current) => current.map((row) => ({ ...row, selected })))

  return (
    <>
      <p className="text-sm text-muted-foreground">{t('admin.movies.hint')}</p>

      <div className="flex flex-wrap items-center gap-2 text-sm">
        <div className="flex gap-1">
          {(['manual', 'disk'] as Source[]).map((item) => (
            <button
              key={item}
              type="button"
              onClick={() => {
                setSource(item)
                setRows([])
              }}
              className={`rounded border px-2 py-1 text-xs ${
                source === item
                  ? 'border-primary text-primary'
                  : 'border-border text-muted-foreground'
              }`}
            >
              {t(`admin.movies.sources.${item}`)}
            </button>
          ))}
        </div>

        {source === 'disk' && (
          <>
            <input
              ref={fileInput}
              type="file"
              multiple
              accept="video/*,.mkv,.avi,.mp4,.m4v,.mov,.ts,.mpg,.mpeg,.wmv,.flv"
              className="hidden"
              onChange={(e) => {
                setFiles([...(e.target.files ?? [])])
                setRows([])
              }}
            />
            <Button size="sm" variant="outline" onClick={() => fileInput.current?.click()}>
              <Upload className="h-4 w-4" />
              {t('admin.movies.pickFiles')}
            </Button>
          </>
        )}

        {(providers?.length ?? 0) > 1 && (
          <select
            className="h-8 rounded-md border border-border bg-transparent px-2 text-sm"
            value={activeProvider}
            onChange={(e) => setProvider(e.target.value)}
          >
            {providers?.map((key) => (
              <option key={key} value={key}>
                {key}
              </option>
            ))}
          </select>
        )}

        <Button size="sm" disabled={!canScan} onClick={() => scan.mutate()}>
          <Search className="h-4 w-4" />
          {t('admin.movies.scan', { count: sources.size })}
        </Button>

        {!activeProvider && (
          <span className="flex items-center gap-1.5 text-xs text-amber-500">
            <AlertTriangle className="h-3.5 w-3.5" />
            {t('admin.movies.noProvider')}
          </span>
        )}
      </div>

      <div className="max-h-[60vh] overflow-auto rounded border border-border">
        <table className="w-full table-fixed text-sm">
          <thead className="sticky top-0 bg-background text-xs uppercase text-muted-foreground">
            <tr>
              <th className="w-8 p-2">
                <input
                  type="checkbox"
                  title={t('admin.movies.toggleAll')}
                  disabled={rows.length === 0}
                  checked={allSelected}
                  // Часть строк отмечена — галочка «в промежутке»: иначе по ней не понять,
                  // что клик сейчас сделает.
                  ref={(input) => {
                    if (input) input.indeterminate = !allSelected && selectedCount > 0
                  }}
                  onChange={(e) => toggleAll(e.target.checked)}
                />
              </th>
              <th className="w-[26%] p-2 text-left">{t('admin.movies.file')}</th>
              <th className="w-[38%] p-2 text-left">{t('admin.movies.parsed')}</th>
              <th className="p-2 text-left">{t('admin.movies.candidate')}</th>
            </tr>
          </thead>
          <tbody>
            {rows.map((row) => (
              <tr key={row.name} className="border-t border-border align-top">
                <td className="p-2">
                  <input
                    type="checkbox"
                    checked={row.selected}
                    onChange={(e) => patch(row.name, { selected: e.target.checked })}
                  />
                </td>
                <td className="max-w-0 p-2 font-mono text-xs" title={row.name}>
                  <div className="truncate">{row.name}</div>
                  <div
                    className={`mt-1 flex items-center gap-1 text-[11px] ${STATUS_TONE[row.status]}`}
                  >
                    {row.status === 'Confident' && <Check className="h-3 w-3" />}
                    {row.status === 'Uncertain' && <HelpCircle className="h-3 w-3" />}
                    {row.status === 'NotFound' && <AlertTriangle className="h-3 w-3" />}
                    {t(`admin.movies.statuses.${row.status}`)}
                    {row.existingShowName && <span>· {row.existingShowName}</span>}
                  </div>
                </td>
                <td className="p-2">
                  <div className="flex items-center gap-1.5">
                    <Input
                      className="h-8 min-w-0 flex-1"
                      value={row.title}
                      onChange={(e) => patch(row.name, { title: e.target.value })}
                    />
                    <Input
                      className="h-8 w-20 shrink-0"
                      placeholder={t('admin.movies.year')}
                      value={row.year}
                      onChange={(e) => patch(row.name, { year: e.target.value })}
                    />
                    <Button
                      size="sm"
                      variant="ghost"
                      disabled={research.isPending || !row.title.trim()}
                      onClick={() => research.mutate(row)}
                      title={t('admin.movies.research')}
                    >
                      <Search className="h-4 w-4" />
                    </Button>
                  </div>
                </td>
                <td className="p-2">
                  {row.candidates.length === 0 ? (
                    <span className="text-xs text-muted-foreground">{t('admin.movies.empty')}</span>
                  ) : (
                    <select
                      className="h-8 w-full rounded-md border border-border bg-transparent px-2 text-sm"
                      value={row.chosenExternalId ?? ''}
                      onChange={(e) =>
                        patch(row.name, { chosenExternalId: e.target.value || null })
                      }
                    >
                      <option value="">{t('admin.movies.noCandidate')}</option>
                      {row.candidates.map((candidate) => (
                        <option key={candidate.externalId} value={candidate.externalId}>
                          {candidate.title}
                          {candidate.year ? ` (${candidate.year})` : ''}
                        </option>
                      ))}
                    </select>
                  )}
                </td>
              </tr>
            ))}

            {rows.length === 0 && (
              <tr>
                <td colSpan={4} className="p-6 text-center text-sm text-muted-foreground">
                  {scan.isPending ? t('admin.movies.scanning') : t('admin.movies.nothingYet')}
                </td>
              </tr>
            )}
          </tbody>
        </table>
      </div>

      {rows.length > 0 && (
        <p className="text-xs text-muted-foreground">{t('admin.movies.collectionsHint')}</p>
      )}

      <DialogFooter>
        <Button size="sm" variant="ghost" onClick={onClose}>
          {t('common.close')}
        </Button>
        <Button
          size="sm"
          disabled={selectedCount === 0 || importing.isPending}
          onClick={() => importing.mutate()}
        >
          {t('admin.movies.import', { count: selectedCount })}
        </Button>
      </DialogFooter>
    </>
  )
}
