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 { 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 {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from '@/shared/ui/dialog'
import { Input } from '@/shared/ui/input'
import { Label } from '@/shared/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select'
import { toast } from '@/shared/ui/toast-store'
import { importManualInbox, listManualInbox } from './api'
import { compareParsed, formatSeasonEpisode, isValidRegex, parseEpisodeName } from './episode-parse'
import { buildEpisodeRegex, findNumbers, REGEX_PRESETS } from './episode-regex'
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 ManualInboxDialog({ onClose }: { 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 map = new Map<string, ReturnType<typeof parseEpisodeName>>()
    for (const file of data?.files ?? []) map.set(file.relativePath, parseEpisodeName(file.name, options))
    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»).
   */
  const detectedShowId = useMemo(
    () =>
      shows && sample
        ? (matchShowByName(sample.name, shows) ?? matchShowByName(sample.folder, shows))
        : undefined,
    [shows, sample],
  )

  useEffect(() => {
    if (showPicked || showId || !detectedShowId) return
    setShowId(detectedShowId)
  }, [detectedShowId, showPicked, showId])

  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 (
    <Dialog open onOpenChange={(open) => !open && onClose()}>
      <DialogContent className="max-w-4xl">
        <DialogHeader>
          <DialogTitle>{t('admin.media.manualTitle')}</DialogTitle>
          <DialogDescription>{t('admin.media.manualHint')}</DialogDescription>
        </DialogHeader>

        <div className="flex flex-col gap-3">
          {/* Шоу — первое решение в этом диалоге: остальные поля лишь помогают разложить файлы. */}
          <div className="flex flex-col gap-1.5">
            <Label>{t('admin.media.manualShow')}</Label>
            <Select
              value={showId}
              onValueChange={(value) => {
                setShowPicked(true)
                setShowId(value)
              }}
            >
              <SelectTrigger>
                <SelectValue placeholder={t('admin.media.manualPickShow')} />
              </SelectTrigger>
              <SelectContent>
                {(shows ?? []).map((show) => (
                  <SelectItem key={show.id} value={show.id}>
                    {show.name}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
            {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>
            <div className="flex flex-col gap-1.5">
              <Label>{t('admin.media.toShowSeason')}</Label>
              {/* Ноль — законный сезон: в нём живут спецвыпуски и пилоты. */}
              <Input
                type="number"
                min={0}
                placeholder={t('admin.media.toShowAuto')}
                value={seasonStr}
                onChange={(e) => setSeasonStr(e.target.value)}
              />
            </div>
            <div className="flex flex-col gap-1.5">
              <Label>{t('admin.media.toShowRegex')}</Label>
              <Input
                placeholder="^(\d+)"
                value={regexStr}
                onChange={(e) => setRegexStr(e.target.value)}
                className={!regexOk ? 'border-red-500' : undefined}
              />
            </div>
          </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 max-h-80 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>
      </DialogContent>
    </Dialog>
  )
}
