import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { ChevronDown, ChevronRight, Folder } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { listShows } from '@/features/admin/shows/api'
import { ShowPicker } from '@/features/admin/shows/ShowPicker'
import { qk } from '@/shared/api/query-keys'
import type { ManualInboxFileDto } from '@/shared/api/types'
import { useApiError } from '@/shared/lib/use-api-error'
import { Badge } from '@/shared/ui/badge'
import { Button } from '@/shared/ui/button'
import { DialogFooter } from '@/shared/ui/dialog'
import { Input } from '@/shared/ui/input'
import { Label } from '@/shared/ui/label'
import { toast } from '@/shared/ui/toast-store'
import { importManualInbox, listManualInbox } from './api'
import {
  compareParsed,
  formatSeasonEpisode,
  isValidRegex,
  parseEpisodeName,
  parseSeasonFromFolder,
} from './episode-parse'
import { buildEpisodeRegex, findNumbers, REGEX_PRESETS } from './episode-regex'
import { EpisodeParseFields } from './EpisodeParseFields'
import { matchShowByName } from './match-show'

/** Байты → «1,4 ГБ»: в ручном разборе размер — главный ориентир, что это за файл. */
function formatSize(bytes: number): string {
  const units = ['Б', 'КБ', 'МБ', 'ГБ', 'ТБ']
  let value = bytes
  let unit = 0
  while (value >= 1024 && unit < units.length - 1) {
    value /= 1024
    unit++
  }
  return `${value.toFixed(value >= 10 || unit === 0 ? 0 : 1)} ${units[unit]}`
}

/**
 * Ручной разбор `manual/`: каталог не сканируется, файлы выбирает админ и сразу указывает шоу.
 * Импортированные файлы уходят из каталога — ровно как из обычного inbox, — а спутники (субтитры,
 * nfo) удаляются, чтобы не оставалось мусора.
 *
 * Номера сезона и серии распознаются здесь же и в таком виде и уходят на сервер: что показано,
 * то и сохранится.
 */
export function ManualInboxPanel({ onClose }: Readonly<{ onClose: () => void }>) {
  const { t } = useTranslation()
  const queryClient = useQueryClient()
  const [selected, setSelected] = useState<string[]>([])
  const [showId, setShowId] = useState('')
  // Ручной выбор шоу отключает автоопределение: перебивать решение человека нельзя.
  const [showPicked, setShowPicked] = useState(false)
  const [query, setQuery] = useState('')
  const [seasonStr, setSeasonStr] = useState('')
  const [regexStr, setRegexStr] = useState('')
  const [collapsed, setCollapsed] = useState<string[]>([])

  const { data, isLoading } = useQuery({
    queryKey: qk.media.manual,
    queryFn: listManualInbox,
  })
  const { data: shows } = useQuery({ queryKey: qk.shows.all, queryFn: () => listShows() })

  const regexOk = isValidRegex(regexStr)
  const seasonOverride = seasonStr.trim() ? Number(seasonStr) : null

  // Распознанные номера считаются один раз на всё: их показывает список и их же уходит на сервер.
  // Сезон, если его нет в имени файла, берётся из имени папки — в раздачах он обычно только там.
  const parsedByPath = useMemo(() => {
    const options = {
      seasonOverride:
        seasonOverride != null && Number.isFinite(seasonOverride) ? seasonOverride : null,
      episodeRegex: regexOk ? regexStr : null,
    }
    const seasonByFolder = new Map<string, number | null>()
    const map = new Map<string, ReturnType<typeof parseEpisodeName>>()
    for (const file of data?.files ?? []) {
      if (!seasonByFolder.has(file.folder))
        seasonByFolder.set(file.folder, parseSeasonFromFolder(file.folder))
      map.set(
        file.relativePath,
        parseEpisodeName(file.name, {
          ...options,
          seasonFallback: seasonByFolder.get(file.folder) ?? null,
        }),
      )
    }
    return map
  }, [data, seasonOverride, regexStr, regexOk])

  const folders = useMemo(() => {
    const q = query.trim().toLowerCase()
    const matched = (data?.files ?? []).filter((file) =>
      q ? file.relativePath.toLowerCase().includes(q) : true,
    )

    const grouped = new Map<string, ManualInboxFileDto[]>()
    for (const file of matched) {
      const list = grouped.get(file.folder) ?? []
      list.push(file)
      grouped.set(file.folder, list)
    }

    // Внутри папки — в порядке серий: так сразу видно пропуски и что регулярка сработала.
    return [...grouped.entries()]
      .sort(([a], [b]) => a.localeCompare(b))
      .map(([folder, files]) => ({
        folder,
        files: [...files].sort((a, b) =>
          compareParsed(
            {
              name: a.name,
              parsed: parsedByPath.get(a.relativePath) ?? { season: null, episode: null },
            },
            {
              name: b.name,
              parsed: parsedByPath.get(b.relativePath) ?? { season: null, episode: null },
            },
          ),
        ),
      }))
  }, [data, query, parsedByPath])

  const selectable = folders.flatMap((g) => g.files.filter((f) => !f.alreadyImported))

  // Образец для конструктора — первый файл списка: по нему и указывают, где номер серии.
  const sample = selectable[0] ?? folders[0]?.files[0]
  const sampleParts = useMemo(() => {
    if (!sample) return []
    const numbers = findNumbers(sample.name)
    // start — позиция куска в имени файла: она уникальна в пределах образца и годится как key,
    // в отличие от индекса (куски одинакового текста встречаются в имени по нескольку раз).
    const parts: { start: number; text: string; number: number | null }[] = []
    let cursor = 0
    for (const number of numbers) {
      if (number.start > cursor)
        parts.push({
          start: cursor,
          text: sample.name.slice(cursor, number.start),
          number: null,
        })
      parts.push({ start: number.start, text: number.text, number: number.index })
      cursor = number.start + number.text.length
    }
    if (cursor < sample.name.length)
      parts.push({ start: cursor, text: sample.name.slice(cursor), number: null })
    return parts
  }, [sample])
  /**
   * Автоопределение шоу по имени релиза — то же, что в загрузке в шоу. Сначала пробуем имя файла,
   * затем имя папки: в раздачах название сериала обычно есть и там, и там («Mr.Pickles.S01.1080p»).
   *
   * Смотрим только на отмеченное: в manual/ лежат раздачи сразу нескольких шоу, и определять по
   * первому файлу списка значило бы подставлять то, что человек даже не выбирал. Ищем в полном
   * списке, а не в отфильтрованном: отметки переживают поиск по названию.
   */
  const detectedShowId = useMemo(() => {
    const first = (data?.files ?? []).find((file) => selected.includes(file.relativePath))
    if (!shows || !first) return undefined
    return matchShowByName(first.name, shows) ?? matchShowByName(first.folder, shows)
  }, [shows, data, selected])

  // Без условия на пустой showId: отметили файлы другого шоу — подстановка следует за выбором.
  // Ручной выбор она по-прежнему не трогает.
  useEffect(() => {
    if (showPicked || !detectedShowId) return
    setShowId(detectedShowId)
  }, [detectedShowId, showPicked])

  const autoDetected = !showPicked && !!detectedShowId && showId === detectedShowId

  const recognized = selectable.filter(
    (f) => parsedByPath.get(f.relativePath)?.episode != null,
  ).length

  const onError = useApiError()

  const importMutation = useMutation({
    mutationFn: () =>
      importManualInbox(
        selected.map((relativePath) => {
          const parsed = parsedByPath.get(relativePath)
          return {
            relativePath,
            season: parsed?.episode != null ? (parsed.season ?? 1) : null,
            episode: parsed?.episode ?? null,
          }
        }),
        showId,
      ),
    onSuccess: (result) => {
      if (result.imported > 0)
        toast.success(t('admin.media.manualImported', { count: result.imported }))
      // Отказы показываем по одному: у каждого своя причина, и файл остаётся в каталоге.
      for (const failure of result.failed) toast.error(`${failure.relativePath}: ${failure.reason}`)

      setSelected([])
      void queryClient.invalidateQueries({ queryKey: qk.media.all })
      void queryClient.invalidateQueries({ queryKey: qk.shows.all })
      if (result.failed.length === 0) onClose()
    },
    onError,
  })

  const toggle = (path: string) =>
    setSelected((current) =>
      current.includes(path) ? current.filter((p) => p !== path) : [...current, path],
    )

  const toggleCollapsed = (folder: string) =>
    setCollapsed((current) =>
      current.includes(folder) ? current.filter((f) => f !== folder) : [...current, folder],
    )

  const toggleFolder = (files: ManualInboxFileDto[]) => {
    const paths = files.filter((f) => !f.alreadyImported).map((f) => f.relativePath)
    const allSelected = paths.every((p) => selected.includes(p))
    setSelected((current) =>
      allSelected
        ? current.filter((p) => !paths.includes(p))
        : [...new Set([...current, ...paths])],
    )
  }

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

      <div className="flex min-h-0 flex-1 flex-col gap-3">
        {/* Шоу — первое решение в этом диалоге: остальные поля лишь помогают разложить файлы. */}
        <div className="flex flex-col gap-1.5">
          <Label>{t('admin.media.manualShow')}</Label>
          <ShowPicker
            value={showId}
            shows={shows}
            placeholder={t('admin.media.manualPickShow')}
            onChange={(value) => {
              setShowPicked(true)
              setShowId(value)
            }}
          />
          {autoDetected && (
            <p className="text-xs text-muted-foreground">{t('admin.media.manualDetected')}</p>
          )}
        </div>

        <div className="grid gap-3 sm:grid-cols-3">
          <div className="flex flex-col gap-1.5">
            <Label>{t('common.search')}</Label>
            <Input value={query} onChange={(e) => setQuery(e.target.value)} />
          </div>
          <EpisodeParseFields
            season={seasonStr}
            onSeason={setSeasonStr}
            regex={regexStr}
            onRegex={setRegexStr}
            regexOk={regexOk}
          />
        </div>
        {!regexOk && <p className="text-xs text-red-500">{t('admin.media.toShowRegexInvalid')}</p>}

        {/* Конструктор: указать число прямо в имени файла проще, чем сочинить regex руками. */}
        {sample && (
          <div className="flex flex-col gap-1.5">
            <span className="text-xs text-muted-foreground">{t('admin.media.regexPickHint')}</span>
            <div className="flex flex-wrap items-center gap-0.5 font-mono text-xs">
              {sampleParts.map((part) =>
                part.number === null ? (
                  <span key={part.start} className="text-muted-foreground">
                    {part.text}
                  </span>
                ) : (
                  <button
                    key={part.start}
                    type="button"
                    title={t('admin.media.regexPickTitle')}
                    className="rounded border border-primary/60 bg-primary/10 px-1 text-primary hover:bg-primary/25"
                    onClick={() => setRegexStr(buildEpisodeRegex(sample.name, part.number!))}
                  >
                    {part.text}
                  </button>
                ),
              )}
            </div>
            <div className="flex flex-wrap items-center gap-1">
              <span className="text-xs text-muted-foreground">{t('admin.media.regexPresets')}</span>
              {REGEX_PRESETS.map((preset) => (
                <button
                  key={preset.key}
                  type="button"
                  className="rounded border border-border px-1.5 py-0.5 text-xs text-muted-foreground hover:border-primary hover:text-primary"
                  onClick={() => setRegexStr(preset.pattern)}
                >
                  {t(`admin.media.regexPresetNames.${preset.key}`)}
                </button>
              ))}
              {regexStr && (
                <button
                  type="button"
                  className="rounded border border-border px-1.5 py-0.5 text-xs text-muted-foreground hover:border-primary hover:text-primary"
                  onClick={() => setRegexStr('')}
                >
                  {t('admin.media.regexClear')}
                </button>
              )}
            </div>
          </div>
        )}

        <div className="flex flex-wrap items-center gap-2 text-xs">
          <Button
            size="sm"
            variant="outline"
            disabled={selectable.length === 0}
            onClick={() =>
              setSelected(
                selected.length === selectable.length ? [] : selectable.map((f) => f.relativePath),
              )
            }
          >
            {t('admin.media.manualSelectAll')}
          </Button>
          <span className="text-muted-foreground">
            {t('admin.media.manualSelected', { count: selected.length })}
          </span>
          <span className="text-muted-foreground">
            {t('admin.media.manualRecognized', {
              count: recognized,
              total: selectable.length,
            })}
          </span>
        </div>

        <div className="crt-panel min-h-0 flex-1 overflow-y-auto rounded-md text-sm">
          {isLoading && <p className="px-3 py-2 text-muted-foreground">{t('common.loading')}</p>}
          {!isLoading && folders.length === 0 && (
            <p className="px-3 py-2 text-muted-foreground">{t('admin.media.manualEmpty')}</p>
          )}

          {folders.map(({ folder, files }) => {
            const isCollapsed = collapsed.includes(folder)
            return (
              <div key={folder || '/'} className="border-b border-border last:border-0">
                <div className="flex items-center gap-2 bg-muted/30 px-3 py-1.5">
                  <button
                    type="button"
                    className="text-muted-foreground hover:text-foreground"
                    onClick={() => toggleCollapsed(folder)}
                  >
                    {isCollapsed ? (
                      <ChevronRight className="h-4 w-4" />
                    ) : (
                      <ChevronDown className="h-4 w-4" />
                    )}
                  </button>
                  <input
                    type="checkbox"
                    className="shrink-0"
                    checked={files
                      .filter((f) => !f.alreadyImported)
                      .every((f) => selected.includes(f.relativePath))}
                    onChange={() => toggleFolder(files)}
                  />
                  <Folder className="h-4 w-4 shrink-0 text-muted-foreground" />
                  <span className="min-w-0 flex-1 truncate font-medium" title={folder}>
                    {folder || t('admin.media.manualRoot')}
                  </span>
                  <span className="shrink-0 text-xs text-muted-foreground">{files.length}</span>
                </div>

                {!isCollapsed && (
                  <ul className="divide-y divide-border">
                    {files.map((file) => {
                      const label = formatSeasonEpisode(
                        parsedByPath.get(file.relativePath) ?? { season: null, episode: null },
                      )
                      return (
                        <li
                          key={file.relativePath}
                          className="flex items-center gap-2 px-3 py-1.5 pl-9"
                        >
                          <input
                            type="checkbox"
                            className="shrink-0"
                            disabled={file.alreadyImported}
                            checked={selected.includes(file.relativePath)}
                            onChange={() => toggle(file.relativePath)}
                          />
                          {label ? (
                            <Badge className="shrink-0">{label}</Badge>
                          ) : (
                            <Badge variant="muted" className="shrink-0">
                              {t('admin.media.toShowUnknown')}
                            </Badge>
                          )}
                          <span
                            className={`min-w-0 flex-1 truncate ${file.alreadyImported ? 'text-muted-foreground' : ''}`}
                            title={file.name}
                          >
                            {file.name}
                          </span>
                          {file.alreadyImported && (
                            <Badge variant="muted">{t('admin.media.manualAlready')}</Badge>
                          )}
                          <span className="shrink-0 tabular-nums text-muted-foreground">
                            {formatSize(file.sizeBytes)}
                          </span>
                        </li>
                      )
                    })}
                  </ul>
                )}
              </div>
            )
          })}
        </div>

        {data?.truncated && (
          <p className="text-xs text-amber-500">{t('admin.media.manualTruncated')}</p>
        )}

        <p className="text-xs text-muted-foreground">{t('admin.media.manualCleanupHint')}</p>
      </div>

      <DialogFooter>
        <Button size="sm" variant="outline" onClick={onClose}>
          {t('common.cancel')}
        </Button>
        <Button
          size="sm"
          disabled={selected.length === 0 || !showId || importMutation.isPending}
          onClick={() => importMutation.mutate()}
        >
          {t('admin.media.manualImport')}
        </Button>
      </DialogFooter>
    </>
  )
}
