import { useState, useMemo, useEffect, useCallback } from 'react'
import { useParams, Link, useNavigate } from 'react-router-dom'
import { useRun, useUpdateRun } from '../hooks/useRuns'
import { useAdvanceLeg } from '../hooks/useGenlockes'
import { useGameRoutes } from '../hooks/useGames'
import { useCreateEncounter, useUpdateEncounter, useBulkRandomize } from '../hooks/useEncounters'
import { usePokemonFamilies } from '../hooks/usePokemon'
import { useGameBosses, useBossResults, useCreateBossResult } from '../hooks/useBosses'
import {
  EggEncounterModal,
  EncounterModal,
  EncounterMethodBadge,
  HofTeamModal,
  TransferModal,
  StatCard,
  PokemonCard,
  StatusChangeModal,
  EndRunModal,
  RuleBadges,
  ShinyBox,
  ShinyEncounterModal,
  TypeBadge,
} from '../components'
import { BossDefeatModal } from '../components/BossDefeatModal'
import type {
  Route,
  RouteWithChildren,
  RunStatus,
  EncounterDetail,
  EncounterStatus,
  CreateEncounterInput,
  BossBattle,
  BossPokemon,
} from '../types'

type TeamSortKey = 'route' | 'level' | 'species' | 'dex'

function sortEncounters(encounters: EncounterDetail[], key: TeamSortKey): EncounterDetail[] {
  return [...encounters].sort((a, b) => {
    switch (key) {
      case 'route':
        return a.route.order - b.route.order
      case 'level':
        return (a.catchLevel ?? 0) - (b.catchLevel ?? 0)
      case 'species': {
        const nameA = (a.currentPokemon ?? a.pokemon).name
        const nameB = (b.currentPokemon ?? b.pokemon).name
        return nameA.localeCompare(nameB)
      }
      case 'dex':
        return (a.currentPokemon ?? a.pokemon).nationalDex - (b.currentPokemon ?? b.pokemon).nationalDex
      default:
        return 0
    }
  })
}

const statusStyles: Record<RunStatus, string> = {
  active: 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300',
  completed:
    'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300',
  failed: 'bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300',
}

function formatDuration(start: string, end: string) {
  const ms = new Date(end).getTime() - new Date(start).getTime()
  const days = Math.floor(ms / (1000 * 60 * 60 * 24))
  if (days === 0) return 'Less than a day'
  if (days === 1) return '1 day'
  return `${days} days`
}

type RouteStatus = 'caught' | 'fainted' | 'missed' | 'none'

function getRouteStatus(encounter?: EncounterDetail): RouteStatus {
  if (!encounter) return 'none'
  return encounter.status
}

const statusIndicator: Record<
  RouteStatus,
  { dot: string; label: string; bg: string }
> = {
  caught: {
    dot: 'bg-green-500',
    label: 'Caught',
    bg: 'bg-green-50 dark:bg-green-900/10',
  },
  fainted: {
    dot: 'bg-red-500',
    label: 'Fainted',
    bg: 'bg-red-50 dark:bg-red-900/10',
  },
  missed: {
    dot: 'bg-gray-400',
    label: 'Missed',
    bg: 'bg-gray-50 dark:bg-gray-900/10',
  },
  none: { dot: 'bg-gray-300 dark:bg-gray-600', label: '', bg: '' },
}

/**
 * Organize flat routes into hierarchical structure.
 * Routes with parentRouteId are grouped under their parent.
 */
function organizeRoutes(routes: Route[]): RouteWithChildren[] {
  const childrenByParent = new Map<number, Route[]>()
  const topLevel: Route[] = []

  for (const route of routes) {
    if (route.parentRouteId === null) {
      topLevel.push(route)
    } else {
      const children = childrenByParent.get(route.parentRouteId) ?? []
      children.push(route)
      childrenByParent.set(route.parentRouteId, children)
    }
  }

  return topLevel.map((route) => ({
    ...route,
    children: childrenByParent.get(route.id) ?? [],
  }))
}

/**
 * Check if any child route in a group has an encounter.
 * Returns the encounter if found, null otherwise.
 */
function getGroupEncounter(
  group: RouteWithChildren,
  encounterByRoute: Map<number, EncounterDetail>,
): EncounterDetail | null {
  for (const child of group.children) {
    const enc = encounterByRoute.get(child.id)
    if (enc) return enc
  }
  return null
}

/** Whether any child in this group has a pinwheelZone set. */
function groupHasZones(group: RouteWithChildren): boolean {
  return group.children.some((c) => c.pinwheelZone != null)
}

/** Get the effective zone for a route (null treated as 0). */
function effectiveZone(route: Route): number {
  return route.pinwheelZone ?? 0
}

/**
 * Get encounters grouped by zone within a route group.
 * Returns a Map from zone number to the encounter in that zone.
 */
function getZoneEncounters(
  group: RouteWithChildren,
  encounterByRoute: Map<number, EncounterDetail>,
): Map<number, EncounterDetail> {
  const zoneMap = new Map<number, EncounterDetail>()
  for (const child of group.children) {
    const enc = encounterByRoute.get(child.id)
    if (enc) {
      zoneMap.set(effectiveZone(child), enc)
    }
  }
  return zoneMap
}

/** Count distinct zones in a group. */
function countDistinctZones(group: RouteWithChildren): number {
  const zones = new Set(group.children.map(effectiveZone))
  return zones.size
}

function matchVariant(labels: string[], starterName?: string | null): string | null {
  if (!starterName || labels.length === 0) return null
  const lower = starterName.toLowerCase()
  const matches = labels.filter((l) => l.toLowerCase().includes(lower))
  return matches.length === 1 ? matches[0] : null
}

function BossTeamPreview({ pokemon, starterName }: { pokemon: BossPokemon[]; starterName?: string | null }) {
  const variantLabels = useMemo(() => {
    const labels = new Set<string>()
    for (const bp of pokemon) {
      if (bp.conditionLabel) labels.add(bp.conditionLabel)
    }
    return [...labels].sort()
  }, [pokemon])

  const hasVariants = variantLabels.length > 0
  const autoMatch = useMemo(() => matchVariant(variantLabels, starterName), [variantLabels, starterName])
  const showPills = hasVariants && autoMatch === null
  const [selectedVariant, setSelectedVariant] = useState<string | null>(
    autoMatch ?? (hasVariants ? variantLabels[0] : null),
  )

  const displayed = useMemo(() => {
    if (!hasVariants) return pokemon
    return pokemon.filter(
      (bp) => bp.conditionLabel === selectedVariant || bp.conditionLabel === null,
    )
  }, [pokemon, hasVariants, selectedVariant])

  return (
    <div className="mt-2">
      {showPills && (
        <div className="flex gap-1 mb-2 flex-wrap">
          {variantLabels.map((label) => (
            <button
              key={label}
              type="button"
              onClick={() => setSelectedVariant(label)}
              className={`px-2 py-0.5 text-xs font-medium rounded-full transition-colors ${
                selectedVariant === label
                  ? 'bg-blue-600 text-white'
                  : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
              }`}
            >
              {label}
            </button>
          ))}
        </div>
      )}
      <div className="flex gap-2 flex-wrap">
        {[...displayed]
          .sort((a, b) => a.order - b.order)
          .map((bp) => (
            <div key={bp.id} className="flex items-center gap-1">
              {bp.pokemon.spriteUrl ? (
                <img src={bp.pokemon.spriteUrl} alt={bp.pokemon.name} className="w-20 h-20" />
              ) : (
                <div className="w-20 h-20 bg-gray-200 dark:bg-gray-700 rounded-full" />
              )}
              <span className="text-xs text-gray-500 dark:text-gray-400">
                Lvl {bp.level}
              </span>
            </div>
          ))}
      </div>
    </div>
  )
}

interface RouteGroupProps {
  group: RouteWithChildren
  encounterByRoute: Map<number, EncounterDetail>
  isExpanded: boolean
  onToggleExpand: () => void
  onRouteClick: (route: Route) => void
  filter: 'all' | RouteStatus
  pinwheelClause: boolean
}

function RouteGroup({
  group,
  encounterByRoute,
  isExpanded,
  onToggleExpand,
  onRouteClick,
  filter,
  pinwheelClause,
}: RouteGroupProps) {
  const groupEncounter = getGroupEncounter(group, encounterByRoute)
  const usePinwheel = pinwheelClause && groupHasZones(group)
  const zoneEncounters = usePinwheel
    ? getZoneEncounters(group, encounterByRoute)
    : null

  // For pinwheel groups, determine status from all zone statuses
  let groupStatus: RouteStatus
  if (usePinwheel && zoneEncounters && zoneEncounters.size > 0) {
    // Use the first encounter's status as representative for the header
    groupStatus = groupEncounter ? groupEncounter.status : 'none'
  } else {
    groupStatus = groupEncounter ? groupEncounter.status : 'none'
  }
  const si = statusIndicator[groupStatus]

  // For groups, check if it matches the filter
  if (filter !== 'all') {
    if (usePinwheel) {
      // Show group if any zone matches the filter
      const anyChildMatches = group.children.some((child) => {
        const enc = encounterByRoute.get(child.id)
        return getRouteStatus(enc) === filter
      })
      // Also check children without encounters (for 'none' filter)
      if (!anyChildMatches) return null
    } else if (groupStatus !== filter) {
      return null
    }
  }

  const hasGroupEncounter = groupEncounter !== null

  return (
    <div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
      {/* Group header */}
      <button
        type="button"
        onClick={onToggleExpand}
        className={`w-full flex items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-gray-100 dark:hover:bg-gray-700/50 ${si.bg}`}
      >
        <span className={`w-2.5 h-2.5 rounded-full shrink-0 ${si.dot}`} />
        <div className="flex-1 min-w-0">
          <div className="text-sm font-medium text-gray-900 dark:text-gray-100 flex items-center gap-2">
            {group.name}
            <span className="text-xs text-gray-400 dark:text-gray-500">
              ({group.children.length} areas)
            </span>
          </div>
          {groupEncounter && (
            <div className="flex items-center gap-2 mt-0.5">
              {groupEncounter.pokemon.spriteUrl && (
                <img
                  src={groupEncounter.pokemon.spriteUrl}
                  alt={groupEncounter.pokemon.name}
                  className="w-10 h-10"
                />
              )}
              <span className="text-xs text-gray-500 dark:text-gray-400 capitalize">
                {groupEncounter.nickname ?? groupEncounter.pokemon.name}
                {groupEncounter.status === 'caught' &&
                  groupEncounter.faintLevel !== null &&
                  (groupEncounter.deathCause
                    ? ` — ${groupEncounter.deathCause}`
                    : ' (dead)')}
              </span>
            </div>
          )}
        </div>
        <span className="text-xs text-gray-400 dark:text-gray-500 shrink-0">
          {si.label}
        </span>
        <svg
          className={`w-4 h-4 text-gray-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
          fill="none"
          stroke="currentColor"
          viewBox="0 0 24 24"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth={2}
            d="M19 9l-7 7-7-7"
          />
        </svg>
      </button>

      {/* Expanded children */}
      {isExpanded && (
        <div className="border-t border-gray-200 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-800/50">
          {group.children.map((child) => {
            const childEncounter = encounterByRoute.get(child.id)
            const childStatus = getRouteStatus(childEncounter)
            const childSi = statusIndicator[childStatus]

            let isDisabled: boolean
            if (usePinwheel && zoneEncounters) {
              // Zone-aware: only lock if this child's zone already has an encounter
              const myZone = effectiveZone(child)
              isDisabled = zoneEncounters.has(myZone) && !childEncounter
            } else {
              // Classic: whole group shares one encounter
              isDisabled = hasGroupEncounter && !childEncounter
            }

            return (
              <button
                key={child.id}
                type="button"
                onClick={() => !isDisabled && onRouteClick(child)}
                disabled={isDisabled}
                className={`w-full flex items-center gap-3 px-4 py-2 pl-8 text-left transition-colors ${
                  isDisabled
                    ? 'opacity-50 cursor-not-allowed'
                    : 'hover:bg-gray-100 dark:hover:bg-gray-700/50'
                } ${childSi.bg}`}
              >
                <span
                  className={`w-2 h-2 rounded-full shrink-0 ${childSi.dot}`}
                />
                <div className="flex-1 min-w-0">
                  <div className="text-sm text-gray-700 dark:text-gray-300">
                    {child.name}
                  </div>
                  {!childEncounter && child.encounterMethods.length > 0 && (
                    <div className="flex flex-wrap gap-1 mt-0.5">
                      {child.encounterMethods.map((m) => (
                        <EncounterMethodBadge key={m} method={m} size="xs" />
                      ))}
                    </div>
                  )}
                </div>
                {childEncounter && (
                  <span className="text-xs text-gray-400 dark:text-gray-500">
                    {childSi.label}
                  </span>
                )}
                {isDisabled && (
                  <span className="text-xs text-gray-400 dark:text-gray-500 italic">
                    (locked)
                  </span>
                )}
              </button>
            )
          })}
        </div>
      )}
    </div>
  )
}

export function RunEncounters() {
  const { runId } = useParams<{ runId: string }>()
  const navigate = useNavigate()
  const runIdNum = Number(runId)
  const { data: run, isLoading, error } = useRun(runIdNum)
  const advanceLeg = useAdvanceLeg()
  const [showTransferModal, setShowTransferModal] = useState(false)
  const { data: routes, isLoading: loadingRoutes } = useGameRoutes(
    run?.gameId ?? null,
  )
  const createEncounter = useCreateEncounter(runIdNum)
  const updateEncounter = useUpdateEncounter(runIdNum)
  const bulkRandomize = useBulkRandomize(runIdNum)
  const updateRun = useUpdateRun(runIdNum)
  const { data: familiesData } = usePokemonFamilies()
  const { data: bosses } = useGameBosses(run?.gameId ?? null)
  const { data: bossResults } = useBossResults(runIdNum)
  const createBossResult = useCreateBossResult(runIdNum)

  const [selectedRoute, setSelectedRoute] = useState<Route | null>(null)
  const [selectedBoss, setSelectedBoss] = useState<BossBattle | null>(null)
  const [editingEncounter, setEditingEncounter] =
    useState<EncounterDetail | null>(null)
  const [selectedTeamEncounter, setSelectedTeamEncounter] =
    useState<EncounterDetail | null>(null)
  const [showEndRun, setShowEndRun] = useState(false)
  const [showHofModal, setShowHofModal] = useState(false)
  const [showShinyModal, setShowShinyModal] = useState(false)
  const [showEggModal, setShowEggModal] = useState(false)
  const [expandedBosses, setExpandedBosses] = useState<Set<number>>(new Set())
  const [showTeam, setShowTeam] = useState(true)
  const [teamSort, setTeamSort] = useState<TeamSortKey>('route')
  const [filter, setFilter] = useState<'all' | RouteStatus>('all')

  const storageKey = `expandedGroups-${runId}`
  const [expandedGroups, setExpandedGroups] = useState<Set<number>>(() => {
    try {
      const saved = localStorage.getItem(storageKey)
      if (saved) return new Set(JSON.parse(saved) as number[])
    } catch { /* ignore */ }
    return new Set<number>()
  })

  const updateExpandedGroups = useCallback(
    (updater: (prev: Set<number>) => Set<number>) => {
      setExpandedGroups((prev) => {
        const next = updater(prev)
        localStorage.setItem(storageKey, JSON.stringify([...next]))
        return next
      })
    },
    [storageKey],
  )

  // Organize routes into hierarchical structure
  const organizedRoutes = useMemo(() => {
    if (!routes) return []
    return organizeRoutes(routes)
  }, [routes])

  // Split encounters into normal (non-shiny) and shiny
  const transferIdSet = useMemo(
    () => new Set(run?.transferEncounterIds ?? []),
    [run?.transferEncounterIds],
  )

  const { normalEncounters, shinyEncounters, transferEncounters } = useMemo(() => {
    if (!run) return { normalEncounters: [], shinyEncounters: [], transferEncounters: [] }
    const normal: EncounterDetail[] = []
    const shiny: EncounterDetail[] = []
    const transfer: EncounterDetail[] = []
    for (const enc of run.encounters) {
      if (transferIdSet.has(enc.id)) {
        transfer.push(enc)
      } else if (enc.isShiny) {
        shiny.push(enc)
      } else {
        normal.push(enc)
      }
    }
    return { normalEncounters: normal, shinyEncounters: shiny, transferEncounters: transfer }
  }, [run, transferIdSet])

  // Map routeId → encounter for quick lookup (normal encounters only)
  const encounterByRoute = useMemo(() => {
    const map = new Map<number, EncounterDetail>()
    for (const enc of normalEncounters) {
      map.set(enc.routeId, enc)
    }
    return map
  }, [normalEncounters])

  // Build set of retired Pokemon IDs from genlocke context
  const retiredPokemonIds = useMemo(() => {
    const ids = run?.genlocke?.retiredPokemonIds
    if (!ids || ids.length === 0) return undefined
    return new Set(ids)
  }, [run])

  // Build set of duped Pokemon IDs (for duplicates clause)
  const dupedPokemonIds = useMemo(() => {
    const dupesClauseOn = run?.rules?.duplicatesClause ?? true
    if (!dupesClauseOn || !familiesData) return undefined

    // Build pokemonId → family members map
    const pokemonToFamily = new Map<number, number[]>()
    for (const family of familiesData.families) {
      for (const id of family) {
        pokemonToFamily.set(id, family)
      }
    }

    const duped = new Set<number>()

    // Seed with retired Pokemon IDs from prior genlocke legs
    if (retiredPokemonIds) {
      for (const id of retiredPokemonIds) {
        duped.add(id)
      }
    }

    for (const enc of normalEncounters) {
      if (enc.status !== 'caught') continue
      const pokemonId = enc.currentPokemonId ?? enc.pokemonId
      // Add the pokemon itself and all family members
      duped.add(pokemonId)
      duped.add(enc.pokemonId)
      const family = pokemonToFamily.get(pokemonId)
      if (family) {
        for (const memberId of family) {
          duped.add(memberId)
        }
      }
      // Also check original pokemon's family
      const origFamily = pokemonToFamily.get(enc.pokemonId)
      if (origFamily) {
        for (const memberId of origFamily) {
          duped.add(memberId)
        }
      }
    }
    return duped.size > 0 ? duped : undefined
  }, [run, normalEncounters, familiesData, retiredPokemonIds])

  // Find starter Pokemon name for auto-matching variant boss teams
  // Note: enc.route from the run detail doesn't include encounterMethods
  // (it's computed only in the game routes endpoint), so we look up the
  // route from the separately-fetched routes data instead.
  const starterName = useMemo(() => {
    if (!routes) return null
    const routeMap = new Map(routes.map((r) => [r.id, r]))
    for (const enc of normalEncounters) {
      const route = routeMap.get(enc.routeId)
      if (route?.encounterMethods.includes('starter')) {
        return enc.pokemon.name
      }
    }
    return null
  }, [normalEncounters, routes])

  // Boss battle data
  const defeatedBossIds = useMemo(() => {
    const set = new Set<number>()
    if (bossResults) {
      for (const r of bossResults) {
        if (r.result === 'won') set.add(r.bossBattleId)
      }
    }
    return set
  }, [bossResults])

  const sortedBosses = useMemo(() => {
    if (!bosses) return []
    return [...bosses].sort((a, b) => a.order - b.order)
  }, [bosses])

  const nextBoss = useMemo(() => {
    return sortedBosses.find((b) => !defeatedBossIds.has(b.id)) ?? null
  }, [sortedBosses, defeatedBossIds])

  const currentLevelCap = useMemo(() => {
    if (!nextBoss) {
      // All defeated — no cap (or use last boss's level)
      return sortedBosses.length > 0
        ? sortedBosses[sortedBosses.length - 1].levelCap
        : null
    }
    return nextBoss.levelCap
  }, [nextBoss, sortedBosses])

  // Pre-compute which bosses get a section divider rendered AFTER them
  // (when the next boss in order has a different section)
  const sectionDividerAfterBoss = useMemo(() => {
    const map = new Map<number, string>()
    for (let i = 0; i < sortedBosses.length - 1; i++) {
      const current = sortedBosses[i]
      const next = sortedBosses[i + 1]
      if (next.section != null && current.section !== next.section) {
        map.set(current.id, next.section)
      }
    }
    return map
  }, [sortedBosses])

  // Map afterRouteId → BossBattle[] for interleaving
  const bossesAfterRoute = useMemo(() => {
    const map = new Map<number, BossBattle[]>()
    if (!bosses) return map
    for (const boss of bosses) {
      if (boss.afterRouteId != null) {
        const list = map.get(boss.afterRouteId) ?? []
        list.push(boss)
        map.set(boss.afterRouteId, list)
      }
    }
    return map
  }, [bosses])

  // Auto-expand the first unvisited group on initial load
  useEffect(() => {
    if (organizedRoutes.length === 0 || expandedGroups.size > 0) return
    const firstUnvisited = organizedRoutes.find(
      (r) =>
        r.children.length > 0 &&
        getGroupEncounter(r, encounterByRoute) === null,
    )
    if (firstUnvisited) {
      updateExpandedGroups(() => new Set([firstUnvisited.id]))
    }
  }, [organizedRoutes, encounterByRoute]) // eslint-disable-line react-hooks/exhaustive-deps

  const alive = useMemo(
    () => sortEncounters(
      [...normalEncounters, ...transferEncounters, ...shinyEncounters].filter(
        (e) => e.status === 'caught' && e.faintLevel === null,
      ),
      teamSort,
    ),
    [normalEncounters, transferEncounters, shinyEncounters, teamSort],
  )

  const dead = useMemo(
    () => sortEncounters(
      normalEncounters.filter((e) => e.status === 'caught' && e.faintLevel !== null),
      teamSort,
    ),
    [normalEncounters, teamSort],
  )

  // Resolve HoF team encounters from IDs
  const hofTeam = useMemo(() => {
    if (!run?.hofEncounterIds || run.hofEncounterIds.length === 0) return null
    const idSet = new Set(run.hofEncounterIds)
    return alive.filter((e) => idSet.has(e.id))
  }, [run?.hofEncounterIds, alive])

  if (isLoading || loadingRoutes) {
    return (
      <div className="flex items-center justify-center py-16">
        <div className="w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full animate-spin" />
      </div>
    )
  }

  if (error || !run) {
    return (
      <div className="max-w-4xl mx-auto p-8">
        <div className="rounded-lg bg-red-50 dark:bg-red-900/20 p-4 text-red-700 dark:text-red-400">
          Failed to load run.
        </div>
        <Link
          to="/runs"
          className="inline-block mt-4 text-blue-600 hover:underline"
        >
          Back to runs
        </Link>
      </div>
    )
  }

  const pinwheelClause = run.rules?.pinwheelClause ?? true

  // Count completed locations (zone-aware when pinwheel clause is on)
  let completedCount = 0
  let totalLocations = 0
  for (const r of organizedRoutes) {
    if (r.children.length > 0) {
      const usePinwheel = pinwheelClause && groupHasZones(r)
      if (usePinwheel) {
        const distinctZones = countDistinctZones(r)
        const zoneEncs = getZoneEncounters(r, encounterByRoute)
        totalLocations += distinctZones
        completedCount += zoneEncs.size
      } else {
        totalLocations += 1
        if (getGroupEncounter(r, encounterByRoute) !== null) {
          completedCount += 1
        }
      }
    } else {
      totalLocations += 1
      if (encounterByRoute.has(r.id)) {
        completedCount += 1
      }
    }
  }

  const isActive = run.status === 'active'

  const toggleGroup = (groupId: number) => {
    updateExpandedGroups((prev) => {
      const next = new Set(prev)
      if (next.has(groupId)) {
        next.delete(groupId)
      } else {
        next.add(groupId)
      }
      return next
    })
  }

  const handleRouteClick = (route: Route) => {
    const existing = encounterByRoute.get(route.id)
    if (existing) {
      setEditingEncounter(existing)
    } else {
      setEditingEncounter(null)
    }
    setSelectedRoute(route)
  }

  const handleCreate = (data: CreateEncounterInput) => {
    createEncounter.mutate(data, {
      onSuccess: () => {
        setSelectedRoute(null)
        setEditingEncounter(null)
        setShowShinyModal(false)
        setShowEggModal(false)
      },
    })
  }

  const handleUpdate = (data: {
    id: number
    data: {
      nickname?: string
      status?: EncounterStatus
      faintLevel?: number
      deathCause?: string
    }
  }) => {
    updateEncounter.mutate(data, {
      onSuccess: () => {
        setSelectedRoute(null)
        setEditingEncounter(null)
      },
    })
  }

  // Filter routes
  const filteredRoutes = organizedRoutes.filter((r) => {
    if (filter === 'all') return true

    if (r.children.length > 0) {
      const usePinwheel = pinwheelClause && groupHasZones(r)
      if (usePinwheel) {
        // Show group if any child/zone matches the filter
        return r.children.some((child) => {
          const enc = encounterByRoute.get(child.id)
          return getRouteStatus(enc) === filter
        })
      }
      // Classic: single status for whole group
      const groupEnc = getGroupEncounter(r, encounterByRoute)
      return getRouteStatus(groupEnc ?? undefined) === filter
    }

    // Standalone route
    const enc = encounterByRoute.get(r.id)
    return getRouteStatus(enc) === filter
  })

  return (
    <div className="max-w-4xl mx-auto p-8">
      {/* Header */}
      <div className="mb-6">
        <Link
          to="/runs"
          className="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 mb-2 inline-block"
        >
          &larr; All Runs
        </Link>
        <div className="flex items-start justify-between">
          <div>
            <h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">
              {run.name}
            </h1>
            <p className="text-gray-600 dark:text-gray-400 mt-1">
              {run.game.name} &middot; {run.game.region.charAt(0).toUpperCase() + run.game.region.slice(1)} &middot; Started{' '}
              {new Date(run.startedAt).toLocaleDateString(undefined, {
                year: 'numeric',
                month: 'short',
                day: 'numeric',
              })}
            </p>
            {run.genlocke && (
              <p className="text-sm text-purple-600 dark:text-purple-400 mt-1 font-medium">
                Leg {run.genlocke.legOrder} of {run.genlocke.totalLegs} &mdash; {run.genlocke.genlockeName}
              </p>
            )}
          </div>
          <div className="flex items-center gap-2">
            {isActive && run.rules?.shinyClause && (
              <button
                onClick={() => setShowShinyModal(true)}
                className="px-3 py-1 text-sm border border-yellow-400 dark:border-yellow-600 text-yellow-600 dark:text-yellow-400 rounded-full font-medium hover:bg-yellow-50 dark:hover:bg-yellow-900/20 transition-colors"
              >
                &#10022; Log Shiny
              </button>
            )}
            {isActive && (
              <button
                onClick={() => setShowEggModal(true)}
                className="px-3 py-1 text-sm border border-green-400 dark:border-green-600 text-green-600 dark:text-green-400 rounded-full font-medium hover:bg-green-50 dark:hover:bg-green-900/20 transition-colors"
              >
                &#x1F95A; Log Egg
              </button>
            )}
            {isActive && (
              <button
                onClick={() => setShowEndRun(true)}
                className="px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded-full font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
              >
                End Run
              </button>
            )}
            <span
              className={`px-3 py-1 rounded-full text-sm font-medium capitalize ${statusStyles[run.status]}`}
            >
              {run.status}
            </span>
          </div>
        </div>
      </div>

      {/* Completion Banner */}
      {!isActive && (
        <div
          className={`rounded-lg p-4 mb-6 ${
            run.status === 'completed'
              ? 'bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800'
              : 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800'
          }`}
        >
          <div className="flex items-center justify-between">
            <div className="flex items-center gap-3">
              <span className="text-2xl">{run.status === 'completed' ? '\u{1f3c6}' : '\u{1faa6}'}</span>
              <div>
                <p
                  className={`font-semibold ${
                    run.status === 'completed'
                      ? 'text-blue-800 dark:text-blue-200'
                      : 'text-red-800 dark:text-red-200'
                  }`}
                >
                  {run.status === 'completed'
                    ? run.genlocke?.isFinalLeg
                      ? 'Genlocke Complete!'
                      : 'Victory!'
                    : run.genlocke
                      ? 'Genlocke Failed'
                      : 'Defeat'}
                </p>
                <p
                  className={`text-sm ${
                    run.status === 'completed'
                      ? 'text-blue-600 dark:text-blue-400'
                      : 'text-red-600 dark:text-red-400'
                  }`}
                >
                  {run.completedAt && (
                    <>
                      Ended{' '}
                      {new Date(run.completedAt).toLocaleDateString(undefined, {
                        year: 'numeric',
                        month: 'short',
                        day: 'numeric',
                      })}
                      {' \u00b7 '}
                      Duration: {formatDuration(run.startedAt, run.completedAt)}
                    </>
                  )}
                </p>
              </div>
            </div>
            {run.status === 'completed' && run.genlocke && !run.genlocke.isFinalLeg && (
              <button
                onClick={() => {
                  if (hofTeam && hofTeam.length > 0) {
                    setShowTransferModal(true)
                  } else {
                    advanceLeg.mutate(
                      { genlockeId: run.genlocke!.genlockeId, legOrder: run.genlocke!.legOrder },
                      {
                        onSuccess: (genlocke) => {
                          const nextLeg = genlocke.legs.find(
                            (l) => l.legOrder === run.genlocke!.legOrder + 1,
                          )
                          if (nextLeg?.runId) {
                            navigate(`/runs/${nextLeg.runId}`)
                          }
                        },
                      },
                    )
                  }
                }}
                disabled={advanceLeg.isPending}
                className="px-4 py-2 text-sm font-medium rounded-lg bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 transition-colors"
              >
                {advanceLeg.isPending ? 'Advancing...' : 'Advance to Next Leg'}
              </button>
            )}
          </div>
          {/* HoF Team Display */}
          {run.status === 'completed' && (
            <div className="mt-3 pt-3 border-t border-blue-200 dark:border-blue-800">
              <div className="flex items-center justify-between mb-2">
                <span className="text-xs font-medium text-blue-600 dark:text-blue-400 uppercase tracking-wider">
                  Hall of Fame
                </span>
                <button
                  type="button"
                  onClick={() => setShowHofModal(true)}
                  className="text-xs text-blue-500 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"
                >
                  {hofTeam ? 'Edit' : 'Select team'}
                </button>
              </div>
              {hofTeam ? (
                <div className="flex gap-2 flex-wrap">
                  {hofTeam.map((enc) => {
                    const dp = enc.currentPokemon ?? enc.pokemon
                    return (
                      <div key={enc.id} className="flex flex-col items-center">
                        {dp.spriteUrl ? (
                          <img src={dp.spriteUrl} alt={dp.name} className="w-12 h-12" />
                        ) : (
                          <div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center text-sm font-bold">
                            {dp.name[0].toUpperCase()}
                          </div>
                        )}
                        <span className="text-[10px] text-blue-600 dark:text-blue-400 capitalize mt-0.5">
                          {enc.nickname || dp.name}
                        </span>
                      </div>
                    )
                  })}
                </div>
              ) : (
                <p className="text-xs text-blue-500/60 dark:text-blue-400/60 italic">
                  No HoF team selected yet
                </p>
              )}
            </div>
          )}
        </div>
      )}

      {/* Stats */}
      <div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
        <StatCard
          label="Encounters"
          value={normalEncounters.length}
          color="blue"
        />
        <StatCard label="Alive" value={alive.length} color="green" />
        <StatCard label="Deaths" value={dead.length} color="red" />
        <StatCard
          label="Routes"
          value={completedCount}
          total={totalLocations}
          color="purple"
        />
      </div>

      {/* Level Cap Bar */}
      {run.rules?.levelCaps && sortedBosses.length > 0 && (
        <div className="sticky top-0 z-10 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg px-4 py-3 mb-6 shadow-sm">
          <div className="flex items-center justify-between mb-2">
            <div className="flex items-center gap-3">
              <span className="text-sm font-semibold text-gray-900 dark:text-gray-100">
                Level Cap: {currentLevelCap ?? '—'}
              </span>
              {nextBoss && (
                <span className="text-sm text-gray-500 dark:text-gray-400">
                  Next: {nextBoss.name}
                </span>
              )}
              {!nextBoss && (
                <span className="text-sm text-green-600 dark:text-green-400">
                  All bosses defeated!
                </span>
              )}
            </div>
            <span className="text-xs text-gray-400 dark:text-gray-500">
              {defeatedBossIds.size}/{sortedBosses.length} defeated
            </span>
          </div>
          {/* Badge row — gym leaders only */}
          {sortedBosses.some((b) => b.bossType === 'gym_leader') && (
            <div className="flex gap-2 flex-wrap">
              {sortedBosses
                .filter((b) => b.bossType === 'gym_leader')
                .map((boss) => {
                  const earned = defeatedBossIds.has(boss.id)
                  return (
                    <div
                      key={boss.id}
                      className={`flex flex-col items-center transition-opacity ${earned ? '' : 'opacity-30 grayscale'}`}
                      title={`${boss.badgeName ?? boss.name}${earned ? ' (earned)' : ''}`}
                    >
                      {boss.badgeImageUrl ? (
                        <img
                          src={boss.badgeImageUrl}
                          alt={boss.badgeName ?? boss.name}
                          className="w-6 h-6"
                        />
                      ) : (
                        <div className={`w-6 h-6 rounded-full border-2 flex items-center justify-center text-xs font-bold ${
                          earned
                            ? 'border-yellow-500 bg-yellow-100 dark:bg-yellow-900/40 text-yellow-700 dark:text-yellow-300'
                            : 'border-gray-300 dark:border-gray-600 text-gray-400'
                        }`}>
                          {boss.order}
                        </div>
                      )}
                    </div>
                  )
                })}
            </div>
          )}
        </div>
      )}

      {/* Rules */}
      <div className="mb-6">
        <h2 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
          Active Rules
        </h2>
        <RuleBadges rules={run.rules} />
      </div>

      {/* Team Section */}
      {(alive.length > 0 || dead.length > 0) && (
        <div className="mb-6">
          <div className="flex items-center justify-between mb-3">
            <button
              type="button"
              onClick={() => setShowTeam(!showTeam)}
              className="flex items-center gap-2 group"
            >
              <h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
                {isActive ? 'Team' : 'Final Team'}
              </h2>
              <span className="text-xs text-gray-400 dark:text-gray-500">
                {alive.length} alive{dead.length > 0 ? `, ${dead.length} dead` : ''}
              </span>
              <svg
                className={`w-4 h-4 text-gray-400 transition-transform ${showTeam ? 'rotate-180' : ''}`}
                fill="none"
                stroke="currentColor"
                viewBox="0 0 24 24"
              >
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth={2}
                  d="M19 9l-7 7-7-7"
                />
              </svg>
            </button>
            {showTeam && alive.length > 1 && (
              <select
                value={teamSort}
                onChange={(e) => setTeamSort(e.target.value as TeamSortKey)}
                className="text-sm border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-1.5 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
              >
                <option value="route">Route Order</option>
                <option value="level">Catch Level</option>
                <option value="species">Species Name</option>
                <option value="dex">National Dex</option>
              </select>
            )}
          </div>
          {showTeam && (
            <>
              {alive.length > 0 && (
                <div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-2 mb-3">
                  {alive.map((enc) => (
                    <PokemonCard
                      key={enc.id}
                      encounter={enc}
                      onClick={isActive ? () => setSelectedTeamEncounter(enc) : undefined}
                    />
                  ))}
                </div>
              )}
              {dead.length > 0 && (
                <>
                  <h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
                    Graveyard
                  </h3>
                  <div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-2">
                    {dead.map((enc) => (
                      <PokemonCard
                        key={enc.id}
                        encounter={enc}
                        showFaintLevel
                        onClick={isActive ? () => setSelectedTeamEncounter(enc) : undefined}
                      />
                    ))}
                  </div>
                </>
              )}
            </>
          )}
        </div>
      )}

      {/* Shiny Box */}
      {run.rules?.shinyClause && shinyEncounters.length > 0 && (
        <div className="mb-6">
          <ShinyBox
            encounters={shinyEncounters}
            onEncounterClick={isActive ? (enc) => setSelectedTeamEncounter(enc) : undefined}
          />
        </div>
      )}

      {/* Transfer Encounters */}
      {transferEncounters.length > 0 && (
        <div className="mb-6">
          <h2 className="text-sm font-medium text-indigo-600 dark:text-indigo-400 mb-2">
            Transferred Pokemon
          </h2>
          <div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-2">
            {transferEncounters.map((enc) => (
              <PokemonCard
                key={enc.id}
                encounter={enc}
                onClick={isActive ? () => setSelectedTeamEncounter(enc) : undefined}
              />
            ))}
          </div>
        </div>
      )}

      {/* Progress bar */}
      <div className="mb-4">
        <div className="flex items-center justify-between mb-1">
          <div className="flex items-center gap-3">
            <h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
              Encounters
            </h2>
            {isActive && completedCount < totalLocations && (
              <button
                type="button"
                disabled={bulkRandomize.isPending}
                onClick={() => {
                  const remaining = totalLocations - completedCount
                  if (window.confirm(`Randomize encounters for all ${remaining} remaining locations?`)) {
                    bulkRandomize.mutate()
                  }
                }}
                className="px-2.5 py-1 text-xs font-medium rounded-lg border border-purple-300 dark:border-purple-600 text-purple-600 dark:text-purple-400 hover:bg-purple-50 dark:hover:bg-purple-900/20 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
              >
                {bulkRandomize.isPending ? 'Randomizing...' : 'Randomize All'}
              </button>
            )}
          </div>
          <span className="text-sm text-gray-500 dark:text-gray-400">
            {completedCount} / {totalLocations} locations
          </span>
        </div>
        <div className="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
          <div
            className="h-full bg-blue-500 rounded-full transition-all"
            style={{
              width: `${totalLocations > 0 ? (completedCount / totalLocations) * 100 : 0}%`,
            }}
          />
        </div>
      </div>

      {/* Filter tabs */}
      <div className="flex gap-2 mb-4 flex-wrap">
        {(
          [
            { key: 'all', label: 'All' },
            { key: 'none', label: 'Unvisited' },
            { key: 'caught', label: 'Caught' },
            { key: 'fainted', label: 'Fainted' },
            { key: 'missed', label: 'Missed' },
          ] as const
        ).map(({ key, label }) => (
          <button
            key={key}
            onClick={() => setFilter(key)}
            className={`px-3 py-1 rounded-full text-sm font-medium transition-colors ${
              filter === key
                ? 'bg-blue-600 text-white'
                : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
            }`}
          >
            {label}
          </button>
        ))}
      </div>

      {/* Route list */}
      <div className="space-y-1">
        {filteredRoutes.length === 0 && (
          <p className="text-gray-500 dark:text-gray-400 text-sm py-4 text-center">
            {filter === 'all'
              ? 'Click a route to log your first encounter'
              : 'No routes match this filter — try a different one'}
          </p>
        )}
        {filteredRoutes.map((route) => {
          // Collect all route IDs to check for boss cards after
          const routeIds: number[] = route.children.length > 0
            ? [route.id, ...route.children.map((c) => c.id)]
            : [route.id]

          // Find boss battles positioned after this route (or any of its children)
          const bossesHere: BossBattle[] = []
          for (const rid of routeIds) {
            const b = bossesAfterRoute.get(rid)
            if (b) bossesHere.push(...b)
          }

          const routeElement = route.children.length > 0 ? (
            <RouteGroup
              key={route.id}
              group={route}
              encounterByRoute={encounterByRoute}
              isExpanded={expandedGroups.has(route.id)}
              onToggleExpand={() => toggleGroup(route.id)}
              onRouteClick={handleRouteClick}
              filter={filter}
              pinwheelClause={pinwheelClause}
            />
          ) : (() => {
            const encounter = encounterByRoute.get(route.id)
            const rs = getRouteStatus(encounter)
            const si = statusIndicator[rs]

            return (
              <button
                key={route.id}
                type="button"
                onClick={() => handleRouteClick(route)}
                className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg text-left transition-colors hover:bg-gray-100 dark:hover:bg-gray-700/50 ${si.bg}`}
              >
                <span
                  className={`w-2.5 h-2.5 rounded-full shrink-0 ${si.dot}`}
                />
                <div className="flex-1 min-w-0">
                  <div className="text-sm font-medium text-gray-900 dark:text-gray-100">
                    {route.name}
                  </div>
                  {encounter ? (
                    <div className="flex items-center gap-2 mt-0.5">
                      {encounter.pokemon.spriteUrl && (
                        <img
                          src={encounter.pokemon.spriteUrl}
                          alt={encounter.pokemon.name}
                          className="w-10 h-10"
                        />
                      )}
                      <span className="text-xs text-gray-500 dark:text-gray-400 capitalize">
                        {encounter.nickname ?? encounter.pokemon.name}
                        {encounter.status === 'caught' &&
                          encounter.faintLevel !== null &&
                          (encounter.deathCause
                            ? ` — ${encounter.deathCause}`
                            : ' (dead)')}
                      </span>
                    </div>
                  ) : route.encounterMethods.length > 0 && (
                    <div className="flex flex-wrap gap-1 mt-0.5">
                      {route.encounterMethods.map((m) => (
                        <EncounterMethodBadge key={m} method={m} size="xs" />
                      ))}
                    </div>
                  )}
                </div>
                <span className="text-xs text-gray-400 dark:text-gray-500 shrink-0">
                  {si.label}
                </span>
              </button>
            )
          })()

          return (
            <div key={route.id}>
              {routeElement}
              {/* Boss battle cards after this route */}
              {bossesHere.map((boss) => {
                const isDefeated = defeatedBossIds.has(boss.id)
                const sectionAfter = sectionDividerAfterBoss.get(boss.id)
                const bossTypeLabel: Record<string, string> = {
                  gym_leader: 'Gym Leader',
                  elite_four: 'Elite Four',
                  champion: 'Champion',
                  rival: 'Rival',
                  evil_team: 'Evil Team',
                  kahuna: 'Kahuna',
                  totem: 'Totem',
                  other: 'Boss',
                }
                const bossTypeColors: Record<string, string> = {
                  gym_leader: 'border-yellow-400 dark:border-yellow-600',
                  elite_four: 'border-purple-400 dark:border-purple-600',
                  champion: 'border-red-400 dark:border-red-600',
                  rival: 'border-blue-400 dark:border-blue-600',
                  evil_team: 'border-gray-500 dark:border-gray-400',
                  kahuna: 'border-orange-400 dark:border-orange-600',
                  totem: 'border-teal-400 dark:border-teal-600',
                  other: 'border-gray-400 dark:border-gray-500',
                }

                const isBossExpanded = expandedBosses.has(boss.id)
                const toggleBoss = () => {
                  setExpandedBosses((prev) => {
                    const next = new Set(prev)
                    if (next.has(boss.id)) next.delete(boss.id)
                    else next.add(boss.id)
                    return next
                  })
                }

                return (
                  <div key={`boss-${boss.id}`}>
                    <div
                      className={`my-2 rounded-lg border-2 ${bossTypeColors[boss.bossType] ?? bossTypeColors.other} ${
                        isDefeated ? 'bg-green-50/50 dark:bg-green-900/10' : 'bg-white dark:bg-gray-800'
                      } px-4 py-3`}
                    >
                    <div
                      className="flex items-start justify-between cursor-pointer select-none"
                      onClick={toggleBoss}
                    >
                      <div className="flex items-center gap-3">
                        <svg
                          className={`w-4 h-4 text-gray-400 transition-transform ${isBossExpanded ? 'rotate-90' : ''}`}
                          fill="none"
                          viewBox="0 0 24 24"
                          stroke="currentColor"
                          strokeWidth={2}
                        >
                          <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
                        </svg>
                        {boss.spriteUrl && (
                          <img src={boss.spriteUrl} alt={boss.name} className="w-10 h-10" />
                        )}
                        <div>
                          <div className="flex items-center gap-2">
                            <span className="text-sm font-semibold text-gray-900 dark:text-gray-100">
                              {boss.name}
                            </span>
                            <span className="px-2 py-0.5 text-xs font-medium rounded-full bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300">
                              {bossTypeLabel[boss.bossType] ?? boss.bossType}
                            </span>
                            {boss.specialtyType && (
                              <TypeBadge type={boss.specialtyType} />
                            )}
                          </div>
                          <p className="text-xs text-gray-500 dark:text-gray-400">
                            {boss.location} &middot; Level Cap: {boss.levelCap}
                          </p>
                        </div>
                      </div>
                      <div onClick={(e) => e.stopPropagation()}>
                        {isDefeated ? (
                          <span className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium rounded-full bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300">
                            Defeated &#10003;
                          </span>
                        ) : isActive ? (
                          <button
                            onClick={() => setSelectedBoss(boss)}
                            className="px-3 py-1 text-xs font-medium rounded-full bg-blue-600 text-white hover:bg-blue-700 transition-colors"
                          >
                            Battle
                          </button>
                        ) : null}
                      </div>
                    </div>
                    {/* Boss pokemon team */}
                    {isBossExpanded && boss.pokemon.length > 0 && (
                      <BossTeamPreview pokemon={boss.pokemon} starterName={starterName} />
                    )}
                    </div>
                    {sectionAfter && (
                      <div className="flex items-center gap-3 my-4">
                        <div className="flex-1 h-px bg-gray-300 dark:bg-gray-600" />
                        <span className="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">{sectionAfter}</span>
                        <div className="flex-1 h-px bg-gray-300 dark:bg-gray-600" />
                      </div>
                    )}
                  </div>
                )
              })}
            </div>
          )
        })}
      </div>

      {/* Encounter Modal */}
      {selectedRoute && (
        <EncounterModal
          route={selectedRoute}
          gameId={run!.gameId}
          runId={runIdNum}
          namingScheme={run!.namingScheme}
          existing={editingEncounter ?? undefined}
          dupedPokemonIds={dupedPokemonIds}
          retiredPokemonIds={retiredPokemonIds}
          onSubmit={handleCreate}
          onUpdate={handleUpdate}
          onClose={() => {
            setSelectedRoute(null)
            setEditingEncounter(null)
          }}
          isPending={createEncounter.isPending || updateEncounter.isPending}
        />
      )}

      {/* Shiny Encounter Modal */}
      {showShinyModal && routes && (
        <ShinyEncounterModal
          routes={routes}
          gameId={run!.gameId}
          onSubmit={handleCreate}
          onClose={() => setShowShinyModal(false)}
          isPending={createEncounter.isPending}
        />
      )}

      {/* Egg Encounter Modal */}
      {showEggModal && routes && (
        <EggEncounterModal
          routes={routes}
          onSubmit={handleCreate}
          onClose={() => setShowEggModal(false)}
          isPending={createEncounter.isPending}
        />
      )}

      {/* Status Change Modal (team pokemon) */}
      {selectedTeamEncounter && (
        <StatusChangeModal
          encounter={selectedTeamEncounter}
          onUpdate={(data) => {
            updateEncounter.mutate(data, {
              onSuccess: () => setSelectedTeamEncounter(null),
            })
          }}
          onClose={() => setSelectedTeamEncounter(null)}
          isPending={updateEncounter.isPending}
          region={run?.game.region}
          onCreateEncounter={(data) => {
            createEncounter.mutate(data)
          }}
        />
      )}

      {/* Boss Defeat Modal */}
      {selectedBoss && (
        <BossDefeatModal
          boss={selectedBoss}
          onSubmit={(data) => {
            createBossResult.mutate(data, {
              onSuccess: () => setSelectedBoss(null),
            })
          }}
          onClose={() => setSelectedBoss(null)}
          isPending={createBossResult.isPending}
          hardcoreMode={run?.rules?.hardcoreMode}
          starterName={starterName}
        />
      )}

      {/* End Run Modal */}
      {showEndRun && (
        <EndRunModal
          onConfirm={(status) => {
            updateRun.mutate(
              { status },
              {
                onSuccess: () => {
                  setShowEndRun(false)
                  if (status === 'completed') {
                    setShowHofModal(true)
                  }
                },
              },
            )
          }}
          onClose={() => setShowEndRun(false)}
          isPending={updateRun.isPending}
          genlockeContext={run.genlocke}
        />
      )}

      {/* HoF Team Selection Modal */}
      {showHofModal && (
        <HofTeamModal
          alive={alive}
          onSubmit={(encounterIds) => {
            updateRun.mutate(
              { hofEncounterIds: encounterIds },
              { onSuccess: () => setShowHofModal(false) },
            )
          }}
          onSkip={() => setShowHofModal(false)}
          isPending={updateRun.isPending}
        />
      )}

      {/* Transfer Modal */}
      {showTransferModal && hofTeam && hofTeam.length > 0 && (
        <TransferModal
          hofTeam={hofTeam}
          onSubmit={(encounterIds) => {
            advanceLeg.mutate(
              {
                genlockeId: run!.genlocke!.genlockeId,
                legOrder: run!.genlocke!.legOrder,
                transferEncounterIds: encounterIds,
              },
              {
                onSuccess: (genlocke) => {
                  setShowTransferModal(false)
                  const nextLeg = genlocke.legs.find(
                    (l) => l.legOrder === run!.genlocke!.legOrder + 1,
                  )
                  if (nextLeg?.runId) {
                    navigate(`/runs/${nextLeg.runId}`)
                  }
                },
              },
            )
          }}
          onSkip={() => {
            advanceLeg.mutate(
              {
                genlockeId: run!.genlocke!.genlockeId,
                legOrder: run!.genlocke!.legOrder,
              },
              {
                onSuccess: (genlocke) => {
                  setShowTransferModal(false)
                  const nextLeg = genlocke.legs.find(
                    (l) => l.legOrder === run!.genlocke!.legOrder + 1,
                  )
                  if (nextLeg?.runId) {
                    navigate(`/runs/${nextLeg.runId}`)
                  }
                },
              },
            )
          }}
          isPending={advanceLeg.isPending}
        />
      )}
    </div>
  )
}
