type ParseOptions = {
  /** Ручной сезон — перебивает распознанный/дефолтный. */
  seasonOverride?: number | null
  /** Пользовательский regex для номера серии: 1 группа = серия, 2 группы = (сезон, серия). */
  episodeRegex?: string | null
  /**
   * Запасной сезон (обычно распознанный из имени папки) — берётся, когда в имени файла сезона нет.
   * Ручной сезон его перебивает, а сам он перебивает подстановку первого сезона по умолчанию.
   */
  seasonFallback?: number | null
}

export type ParsedEpisode = { season: number | null; episode: number | null }

/** Встроенные шаблоны: SxxEyy, NxNN, ведущий номер серии. */
function parseBuiltin(name: string): ParsedEpisode {
  const se = /[Ss](\d{1,2})[ ._-]*[Ee](\d{1,3})/.exec(name)
  if (se) return { season: Number(se[1]), episode: Number(se[2]) }

  const nx = /(?:^|[^\d])(\d{1,2})x(\d{1,3})(?:[^\d]|$)/i.exec(name)
  if (nx) return { season: Number(nx[1]), episode: Number(nx[2]) }

  // Производственный код: «2ACV01» (Футурама), «1ACX05» (Гриффины), «1ACG02» (Разочарование).
  // Сезон слева, номер серии справа, между ними буквенный код студии.
  //
  // Регистр здесь принципиален и флага `i` нет: код всегда прописной, а со строчными буквами
  // под шаблон попало бы «Part1of10» — «1» сезоном, «10» серией.
  const production = /(?:^|[^\dA-Za-z])(\d{1,2})[A-Z]{2,4}(\d{2,3})(?:[^\d]|$)/.exec(name)
  if (production) return { season: Number(production[1]), episode: Number(production[2]) }

  // Ведущий номер серии: «01. Название», «02 - Название», «03_Название», «4) Название».
  const lead = /^\s*(\d{1,3})[\s._)\]-]/.exec(name)
  return { season: null, episode: lead ? Number(lead[1]) : null }
}

/**
 * Пользовательский regex: 1 группа = серия, 2 группы = (сезон, серия). <c>null</c> — шаблон не
 * сработал (или невалиден), распознанное встроенными шаблонами остаётся как есть. <c>season: null</c>
 * при одной группе означает «сезон не трогаем».
 */
function parseCustom(name: string, pattern: string): ParsedEpisode | null {
  let match: RegExpExecArray | null
  try {
    match = new RegExp(pattern, 'i').exec(name)
  } catch {
    return null // невалидный regex — просто игнорируем
  }
  if (!match) return null
  if (match.length >= 3 && match[1] != null && match[2] != null) {
    return { season: Number(match[1]), episode: Number(match[2]) }
  }
  return match[1] != null ? { season: null, episode: Number(match[1]) } : null
}

const finiteOrNull = (value: number | null) =>
  value != null && Number.isFinite(value) ? value : null

/**
 * Пытается распознать сезон/серию из имени файла. Сначала встроенные шаблоны (SxxEyy, NxNN), затем —
 * пользовательский regex (перебивает серию, а при двух группах и сезон), в конце — ручной сезон.
 * Если серия распознана, а сезон нет — сезон считается первым.
 */
export function parseEpisodeName(name: string, opts?: ParseOptions): ParsedEpisode {
  let { season, episode } = parseBuiltin(name)

  const rawRegex = opts?.episodeRegex?.trim()
  const custom = rawRegex ? parseCustom(name, rawRegex) : null
  if (custom) {
    episode = custom.episode
    if (custom.season != null) season = custom.season
  }

  if (opts?.seasonOverride != null) season = opts.seasonOverride
  if (season == null && opts?.seasonFallback != null) season = opts.seasonFallback
  if (episode != null && season == null) season = 1

  return { season: finiteOrNull(season), episode: finiteOrNull(episode) }
}

/**
 * Диапазон сезонов в имени: «Season.3-4», «S01-S03». Такая папка накрывает несколько сезонов, и
 * подставлять из неё один номер нельзя — честнее не угадывать вовсе.
 */
// Разделитель вокруг тире — без самого дефиса: иначе «-» подходит и под разделитель, и под тире,
// и движок перебирает варианты впустую (у Sonar это «super-linear backtracking»).
const SEASON_RANGES = [
  /(?:s|season|сезоны?)[\s._-]*\d{1,2}[\s._]*[-–][\s._]*(?:s|season)?\d{1,2}/i,
  /\d{1,2}[\s._]*[-–][\s._]*\d{1,2}[\s._-]*(?:season|сезон)/i,
]

/** Сезон в имени одного сегмента пути: «S02», «Season 2», «Сезон 2», «2 сезон», «3 Season». */
const SEASON_PATTERNS = [
  // S02 — не часть более длинного числа (год «S2007» сезоном не считаем).
  /(?:^|[^a-z0-9])s[\s._-]*(\d{1,2})(?!\d)/i,
  /(?:season|сезон)[\s._-]*(\d{1,2})(?!\d)/i,
  // Номер перед словом — «3 Season», «2 сезон». Ограничение слева не даёт принять за сезон
  // хвост года: в «2007 Season» это не третий сезон, а сезон 2007 года.
  /(?:^|[^\d])(\d{1,2})[\s._-]*(?:season|сезон)/i,
]

/**
 * Сезон из имени папки, в которой лежит файл: в раздачах номер сезона обычно есть только там
 * («Metalocalypse.S02.2007-2008.H264» / «01 Dethlessons.mkv»), и без этого все серии второго сезона
 * уехали бы в первый. Путь разбирается с самого глубокого сегмента вверх: «Шоу/Season 2» и
 * «Шоу.S02/extras» одинаково дают ответ. null — сезона нет или папка накрывает диапазон сезонов.
 */
export function parseSeasonFromFolder(folder: string): number | null {
  const segments = folder.split('/').filter(Boolean).reverse()
  for (const segment of segments) {
    if (SEASON_RANGES.some((range) => range.test(segment))) return null
    for (const pattern of SEASON_PATTERNS) {
      const match = pattern.exec(segment)
      if (match) return finiteOrNull(Number(match[1]))
    }
  }
  return null
}

const pad2 = (n: number) => String(n).padStart(2, '0')

/** «S14E17» либо null, если серия не распознана. */
export function formatSeasonEpisode(parsed: ParsedEpisode): string | null {
  if (parsed.episode == null) return null
  return `S${pad2(parsed.season ?? 1)}E${pad2(parsed.episode)}`
}

/** Проверяет корректность пользовательского regex (для подсветки ошибки в UI). */
export function isValidRegex(pattern: string): boolean {
  if (!pattern.trim()) return true
  try {
    new RegExp(pattern)
    return true
  } catch {
    return false
  }
}

/** Сортировка по (сезон, серия); нераспознанные — в конец по имени. */
export function compareParsed(
  a: { name: string; parsed: ParsedEpisode },
  b: { name: string; parsed: ParsedEpisode },
): number {
  const ae = a.parsed.episode
  const be = b.parsed.episode
  if (ae != null && be != null) {
    return (a.parsed.season ?? 1) - (b.parsed.season ?? 1) || ae - be
  }
  if (ae != null) return -1
  if (be != null) return 1
  return a.name.localeCompare(b.name)
}
