"use client"

import { useState, useEffect, useRef } from "react"
import useSWR from "swr"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Button } from "./ui/button"
import { Badge } from "./ui/badge"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "./ui/dialog"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { Input } from "./ui/input"
import { Label } from "./ui/label"
import { Checkbox } from "./ui/checkbox"
import { ScrollArea } from "./ui/scroll-area"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"
import {
  DatabaseBackup,
  Clock,
  HardDrive,
  Server,
  CheckCircle2,
  AlertTriangle,
  XCircle,
  Loader2,
  PlayCircle,
  Archive,
  FileSearch,
  Calendar,
  Trash2,
  Plus,
  ChevronRight,
  ChevronLeft,
  Pencil,
  Save,
  Download,
  FileText,
  Power,
  RefreshCw,
  Lock,
  Eye,
  Network,
  Cpu,
  Package,
  ShieldCheck,
  Info,
  ListTree,
  History,
} from "lucide-react"
import { ScriptTerminalModal } from "./script-terminal-modal"
import { RestoreProgressCard } from "./restore-progress-card"
import { fetchApi, getApiUrl } from "../lib/api-config"
import { fetchTerminalTicket } from "../lib/terminal-ws"
import { formatStorage, formatBytes } from "../lib/utils"
import { getStorageUsageColor } from "../lib/storage-usage-color"
import { useT } from "../lib/i18n/provider"

type TFunction = (key: string, params?: Record<string, string | number>) => string

// ── Shape contracts with the backend (flask_server.py: api_host_backups_*) ──

interface BackupJob {
  id: string
  destination: string
  method: string                 // "pbs" | "borg" | "local" | "unknown"
  on_calendar: string            // "OnCalendar=..." for timer jobs, "attached → storage:X" for attached
  retention: string              // "last=5, daily=7, ..."
  timer_enabled: boolean         // legacy — only meaningful for non-attached jobs
  enabled: boolean               // unified state (timer for non-attached, ENABLED= for attached)
  attached: boolean              // PVE vzdump-attached: no own timer, trigger from hook
  pve_storage: string | null     // storage id the attached job listens for
  profile_mode: string           // "default" | "custom"
  manual: boolean                // MANUAL_RUN=1 — one-shot, no schedule, won't re-fire
  last_status: string | null
  next_run: string | null
  // True when this job ships encrypted backups — PBS_KEYFILE set for
  // PBS, or BORG_ENCRYPT_MODE != "none" for Borg. Drives the lock
  // badge in the row + the toggle's initial state in Edit.
  encrypted?: boolean
}

// Sprint H — remote backups (PBS + Borg). Listing is cheap (metadata
// only); the heavy lifting happens on demand when the operator clicks
// Download. Same shape for both backends so the UI renders them
// uniformly — `backend` is the only switch.
interface RemoteSnapshot {
  backend: "pbs" | "borg"
  repo_name: string
  repo_repository: string
  snapshot: string             // canonical id used to extract / restore
  backup_type: string
  backup_id: string
  backup_time: number          // unix seconds
  size_bytes: number           // 0 for Borg (omitted by `borg list`)
  owner: string | null
  protected: boolean
  files: Array<{ filename: string; size: number }>
  fingerprint: string | null
  // Whether THIS snapshot is encrypted. For PBS it's derived from
  // any file having crypt-mode != "none"; for Borg it follows the
  // target's encrypt_mode.
  encrypted?: boolean
  borg_id?: string             // Borg-only fields when available
  borg_start_iso?: string
}

interface RemoteArchivesResp {
  snapshots: RemoteSnapshot[]
  errors: Array<{ backend: string; repo_name: string; error: string }>
}

interface ExportTask {
  task_id: string
  backend: string
  repo_name: string
  snapshot: string
  state: "queued" | "restoring" | "packing" | "completed" | "failed"
  message: string
  size_bytes: number
  output_path: string | null
  error: string | null
}

// Unified archive descriptor — local .tar.zst files and remote PBS /
// Borg snapshots share the same row layout; `source` decides which
// code paths apply (inline download vs export-then-download, manifest
// available vs not, etc.).
interface UnifiedArchive {
  source: "local" | "pbs" | "borg"
  display_id: string           // what shows in mono in the row
  size_bytes: number
  created_at: number           // unix seconds
  source_label: string         // "/var/lib/vz/dump" or "PBS my-pbs" or "Borg my-borg"
  // One of the two is populated depending on `source`.
  local?: BackupArchive
  remote?: RemoteSnapshot      // for both pbs and borg
}

interface BackupArchive {
  id: string                     // basename of the .tar file (also the URL slug)
  path: string                   // absolute path on host
  size_bytes: number
  mtime: number                  // unix seconds
  // From the backend identifier — see _identify_host_backup() in flask_server.py.
  // kind: "manual" / "scheduled" when we know; "legacy" when only the in-tar
  //       marker confirmed it's a ProxMenux backup (no sidecar, no name match).
  job_id: string | null
  kind: "manual" | "scheduled" | "legacy"
  profile: string | null
  source_hostname: string | null
  // Which detection path identified this archive. Surfaced as a small tooltip
  // hint so the operator knows whether the metadata is authoritative
  // (sidecar) or inferred (filename / tar-peek).
  detected_via: "sidecar" | "job_id_match" | "hostcfg_prefix" | "tar_peek"
}

interface ManifestSourceHost {
  hostname: string
  pve_version: string | null
  roles: string[]
  kernel: string
  boot_mode: string
  cpu_model: string
  memory_kb: number
}

interface PreflightCheck {
  id: string
  severity: "pass" | "warn" | "fail"
  message: string
  details: Record<string, unknown> | null
}

interface PreflightReport {
  source_host_at_backup: ManifestSourceHost
  selected_mode: {
    mode: string
    paths_include: string[]
    paths_exclude: string[]
    components_include: string[]
    storage_apply: boolean
    network_apply: boolean
  }
  preflight: {
    checks: PreflightCheck[]
    summary: { pass: number; warn: number; fail: number }
  }
  storage: {
    zfs: Array<{ name: string; action: string; present: string[]; missing: string[] }>
    lvm: Array<{ name: string; action: string }>
    pve_storage: Array<{ id: string; type: string; action: string; note: string | null }>
    in_selected_mode: boolean
  }
  network: {
    keep: Array<{ ifname: string; mac: string }>
    remap: Array<{ source_ifname: string; destination_ifname: string; mac: string }>
    orphan: Array<{ source_ifname: string; source_mac: string }>
    new: Array<{ ifname: string; mac: string }>
    in_selected_mode: boolean
  }
  driver_reinstall: {
    plan: Array<{
      component_id: string
      type: string
      version: string
      installer: string | null
      action: string
      reason: string
    }>
  }
  abort_reason: string | null
}

const fetcher = async (url: string) => fetchApi(url)

// Detect encrypted-backup key errors coming from proxmox-backup-client
// and pull the manifest + provided fingerprints out of the message so
// the UI can render them prominently instead of burying the info
// inside a raw stack-trace-shaped string. Returns null when the error
// is not encryption-related — the caller should fall back to plain
// text rendering in that case.
type KeyfileError = {
  manifestFp: string | null
  providedFp: string | null
  raw: string
}
const parseKeyfileError = (raw: string): KeyfileError | null => {
  if (!raw) return null
  const lower = raw.toLowerCase()
  if (!/missing key|wrong key|was created with key|manifest.?s key|does not match|no key found|unable to (load|read) key|failed to decrypt/.test(lower)) {
    return null
  }
  const manifest = raw.match(/(?:was created with key|manifest'?s key)\s+([0-9a-f:]+)/i)
  const provided = raw.match(/provided key\s+([0-9a-f:]+)/i)
  return {
    manifestFp: manifest?.[1] ?? null,
    providedFp: provided?.[1] ?? null,
    raw,
  }
}

const KeyfileErrorBlock: React.FC<{ err: KeyfileError; className?: string }> = ({ err, className }) => {
  const t = useT()

  return (
    <div className={`text-xs rounded-md border border-amber-500/40 bg-amber-500/10 text-amber-100 px-3 py-3 space-y-2 ${className ?? ""}`}>
      <div className="flex items-start gap-2">
        <Lock className="h-4 w-4 mt-0.5 shrink-0 text-amber-400" />
        <div className="flex-1 min-w-0 space-y-1">
          <div className="font-semibold text-amber-300">{t("backup.keyfileError.title")}</div>
          <div className="text-amber-100/90">{t("backup.keyfileError.description")}</div>
          {err.manifestFp && (
            <div className="pt-1">
              <div className="text-[10.5px] uppercase tracking-wider text-amber-300/80">{t("backup.keyfileError.requiredManifest")}</div>
              <div className="font-mono text-[11px] break-all">{err.manifestFp}</div>
            </div>
          )}
          {err.providedFp && (
            <div>
              <div className="text-[10.5px] uppercase tracking-wider text-amber-300/80">{t("backup.keyfileError.currentlyInstalled")}</div>
              <div className="font-mono text-[11px] break-all">{err.providedFp}</div>
            </div>
          )}
          <div className="pt-1 text-amber-100/90">
            {t("backup.keyfileError.importFrom")}{" "}
            <span className="font-medium">{t("backup.keyfileError.importPath")}</span>
            {" "}{t("backup.keyfileError.retryHint")}
          </div>
        </div>
      </div>
    </div>
  )
}

const formatMtime = (mtime: number) =>
  new Date(mtime * 1000).toLocaleString(undefined, {
    year: "numeric",
    month: "short",
    day: "numeric",
    hour: "2-digit",
    minute: "2-digit",
  })

const formatNext = (iso: string | null) => {
  if (!iso) return "—"
  try {
    return new Date(iso).toLocaleString()
  } catch {
    return iso
  }
}

// Best-effort human form of systemd OnCalendar expressions. Whatever
// we don't recognise falls back to "<raw> (systemd OnCalendar)" so the
// operator at least knows what kind of string they're looking at.
// Handles the patterns the host-backup wizard can emit ("hourly",
// "daily", "weekly", "monthly", "*-*-* HH:MM:SS", "Mon..Sun *-*-* …").
const humanizeOnCalendar = (raw: string | null | undefined, t: TFunction): string => {
  if (!raw) return "—"
  const s = raw.trim()
  if (!s) return "—"
  const lower = s.toLowerCase()
  if (lower === "hourly") return t("backup.schedule.everyHourAtMinuteZero")
  if (lower === "daily") return t("backup.schedule.everyDayAtMidnight")
  if (lower === "weekly") return t("backup.schedule.everyMondayAtMidnight")
  if (lower === "monthly") return t("backup.schedule.firstDayMonthly")
  if (lower === "yearly" || lower === "annually") return t("backup.schedule.janFirst")
  if (lower === "minutely") return t("backup.schedule.everyMinute")
  // *-*-* HH:MM[:SS]  → "Every day at HH:MM"
  let m = s.match(/^\*-\*-\*\s+(\d{1,2}):(\d{2})(?::(\d{2}))?$/)
  if (m) return t("backup.schedule.everyDayAt", { time: `${m[1].padStart(2, "0")}:${m[2]}` })
  // *-*-* *:MM:SS    → "Every hour at minute MM"
  m = s.match(/^\*-\*-\*\s+\*:(\d{2})(?::(\d{2}))?$/)
  if (m) return t("backup.schedule.everyHourAtMinute", { minute: m[1] })
  // Mon,Tue *-*-* HH:MM:SS   →  "<weekdays> at HH:MM"
  m = s.match(/^([A-Za-z,.\s]+)\s+\*-\*-\*\s+(\d{1,2}):(\d{2})(?::(\d{2}))?$/)
  if (m) {
    const expandWeekdays = (chunk: string): string => {
      const days: Record<string, string> = {
        mon: t("backup.weekdays.monday"), tue: t("backup.weekdays.tuesday"), wed: t("backup.weekdays.wednesday"),
        thu: t("backup.weekdays.thursday"), fri: t("backup.weekdays.friday"), sat: t("backup.weekdays.saturday"), sun: t("backup.weekdays.sunday"),
      }
      const rangeMatch = chunk.match(/^([A-Za-z]+)\.\.([A-Za-z]+)$/)
      if (rangeMatch) {
        const order = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
        const start = order.indexOf(rangeMatch[1].slice(0, 3).toLowerCase())
        const end = order.indexOf(rangeMatch[2].slice(0, 3).toLowerCase())
        if (start >= 0 && end >= 0 && start <= end) {
          return order.slice(start, end + 1).map((d) => days[d]).join(", ")
        }
      }
      return chunk
        .split(",")
        .map((d) => days[d.trim().slice(0, 3).toLowerCase()] || d.trim())
        .join(", ")
    }
    return t("backup.schedule.weekdaysAt", { days: expandWeekdays(m[1]), time: `${m[2].padStart(2, "0")}:${m[3]}` })
  }
  return t("backup.schedule.rawOnCalendar", { value: s })
}

// A job is "running" when its .status file has RUN_AT (runner started)
// but no RESULT yet (runner hasn't written the verdict). Lets the UI
// flag in-progress runs both inline (badge in the row) and from the
// Manual backups card so an operator who closes the dialog can re-open
// the live stream.
function isJobRunning(j: BackupJob | null | undefined): boolean {
  if (!j?.last_status) return false
  const s = parseJobStatus(j.last_status)
  return !!(s?.runAt && !s?.result)
}

// last_status is the raw RUN_AT=..., RESULT=..., LOG_FILE=... blob the
// scheduler runner persists. We only care about RESULT + RUN_AT for the
// UI; everything else is noise the operator doesn't need on a list view.
function parseJobStatus(raw: string | null): { result: string | null; runAt: string | null; logFile: string | null } | null {
  if (!raw) return null
  const map: Record<string, string> = {}
  for (const line of raw.split("\n")) {
    const m = line.match(/^([A-Z_]+)=(.*)$/)
    if (m) map[m[1]] = m[2]
  }
  return {
    result: map["RESULT"] || null,
    runAt: map["RUN_AT"] || null,
    logFile: map["LOG_FILE"] || null,
  }
}

// Backend-method color scheme, mirrored exactly from storage-overview.tsx
// (line 414 et al.) so a PBS job badge here looks identical to a PBS
// snapshot badge on the Storage page.
const methodBadgeCls = (m: string | undefined): string => {
  switch ((m || "").toLowerCase()) {
    case "pbs":
      return "bg-purple-500/10 text-purple-400 border-purple-500/20"
    case "borg":
      return "bg-fuchsia-500/10 text-fuchsia-400 border-fuchsia-500/20"
    case "local":
    default:
      return "bg-blue-500/10 text-blue-400 border-blue-500/20"
  }
}

const formatRunAt = (iso: string | null) => {
  if (!iso) return null
  try {
    return new Date(iso).toLocaleString(undefined, {
      day: "2-digit",
      month: "short",
      year: "numeric",
      hour: "2-digit",
      minute: "2-digit",
    })
  } catch {
    return iso
  }
}

// ── PBS keyfile management (shared between CreateJob + ManualBackup) ──
//
// Rendered inside the encryption section when a keyfile is already
// installed. Replaces the older static info box that pointed to a
// non-existent "Settings → Host backup" panel: the operator now has
// concrete actions (download keyfile, update stored passphrase,
// change recovery model, remove keyfile) with destructive-confirmation
// modals that call the /api/host-backups/pbs-encryption/* endpoints
// and mutate pbsRecoveryStatus so the parent dialog re-renders on
// success. There is no "Replace keyfile" action any more — replacing
// the key would silently orphan every encrypted backup that used it.
// The operator instead uses Remove + set up again via the normal flow
// (Generate a new keyfile / Use an existing keyfile), which surfaces
// the source picker properly.
// ──────────────────────────────────────────────────────────────
// PbsKeyfileActions — the trimmed action set the operator sees
// inside every PBS destination row: Download, Upload (import a new
// keyfile), Delete. No stored-passphrase rotation, no escrow toggle
// — those live in the setup dialogs where the operator makes the
// initial call. Placing these three next to the destination they
// belong to matches "the key is a PBS thing" without dragging in
// the fuller management surface.
// ──────────────────────────────────────────────────────────────
function PbsKeyfileActions({ pbsRepository }: { pbsRepository?: string }) {
  const t = useT()
  const { data: info, mutate: mutateInfo } = useSWR<{
    installed: boolean
    fingerprint?: string
    path: string
    escrow_mode?: "none" | "local" | "full"
  }>("/api/host-backups/pbs-encryption/keyfile-info", fetcher)

  const [uploadOpen, setUploadOpen] = useState(false)
  const [deleteOpen, setDeleteOpen] = useState(false)
  const [importFile, setImportFile] = useState<File | null>(null)
  const [importPath, setImportPath] = useState("")
  const [busy, setBusy] = useState(false)
  const [err, setErr] = useState<string | null>(null)

  // Auto-discover a PVE-managed keyfile at /etc/pve/priv/storage/<NAME>.enc
  // that matches this specific PBS repository. When present, the Upload
  // modal surfaces a one-click "Use PVE-managed key" shortcut at the
  // top so the operator doesn't have to hunt down the file path — the
  // exact convenience the shell TUI already provides during its import
  // wizard. Only fetched while the modal is open + a repository is known.
  const { data: pveDiscover } = useSWR<{
    entries: Array<{ name: string; server: string; datastore: string; path: string; matches_repository: boolean }>
  }>(
    uploadOpen && pbsRepository
      ? `/api/host-backups/pbs-encryption/discover-pve-keyfiles?repository=${encodeURIComponent(pbsRepository)}`
      : null,
    fetcher,
  )
  const pveMatch = pveDiscover?.entries?.find((e) => e.matches_repository) || null

  const closeUpload = () => {
    setUploadOpen(false)
    setImportFile(null)
    setImportPath("")
    setBusy(false)
    setErr(null)
  }
  const closeDelete = () => {
    setDeleteOpen(false)
    setBusy(false)
    setErr(null)
  }

  const download = () => {
    // Same plain-<a>-download trick used elsewhere: bypasses fetch so
    // an ad-blocker or a stale SWR cache can't intervene.
    const a = document.createElement("a")
    a.href = "/api/host-backups/pbs-encryption/download-keyfile"
    a.download = "pbs-key.conf"
    document.body.appendChild(a)
    a.click()
    document.body.removeChild(a)
  }

  const runUpload = async () => {
    if (!importFile && !importPath.trim()) {
      setErr(t("backup.errors.pickKeyfile"))
      return
    }
    setBusy(true)
    setErr(null)
    try {
      const body: Record<string, unknown> = { escrow_mode: "none" }
      if (importFile) {
        const buf = new Uint8Array(await importFile.arrayBuffer())
        let bin = ""
        for (let i = 0; i < buf.length; i++) bin += String.fromCharCode(buf[i])
        body.source = "content"
        body.content_b64 = btoa(bin)
      } else {
        body.source = "path"
        body.keyfile_path = importPath.trim()
      }
      await fetchApi("/api/host-backups/pbs-encryption/import", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      })
      await mutateInfo()
      closeUpload()
    } catch (e) {
      setErr(e instanceof Error ? e.message : String(e))
      setBusy(false)
    }
  }

  const runDelete = async () => {
    setBusy(true)
    setErr(null)
    try {
      await fetchApi("/api/host-backups/pbs-encryption/remove", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({}),
      })
      await mutateInfo()
      closeDelete()
    } catch (e) {
      setErr(e instanceof Error ? e.message : String(e))
      setBusy(false)
    }
  }

  const installed = !!info?.installed

  return (
    <div className="mt-2 border-t border-white/10 pt-2 space-y-1.5">
      <div className="flex items-center gap-2 flex-wrap">
        <div className="text-xs min-w-0 flex-1 flex items-center gap-1.5">
          <span className="font-medium text-muted-foreground">{t("backup.keyfileActions.keyfileLabel")}</span>{" "}
          {installed ? (
            <span
              className="inline-flex items-center gap-1 text-emerald-400 font-medium"
              title={info?.fingerprint ? t("backup.keyfileActions.fingerprintTitle", { fingerprint: info.fingerprint }) : undefined}
            >
              <CheckCircle2 className="h-4 w-4" />
              {t("backup.keyfileActions.installed")}
            </span>
          ) : (
            <span className="text-blue-400 font-medium">{t("backup.keyfileActions.notInstalled")}</span>
          )}
        </div>
        <div className="flex items-center gap-1.5 shrink-0">
          {installed && (
            <>
              <Button
                type="button"
                size="sm"
                variant="outline"
                className="h-7 text-[11px] !text-emerald-400 border-emerald-500/40 hover:bg-emerald-500/10"
                onClick={download}
                title={t("backup.keyfileActions.downloadTitle")}
              >
                <Download className="h-3.5 w-3.5 mr-1" />
                {t("backup.actions.download")}
              </Button>
              <Button
                type="button"
                size="sm"
                variant="outline"
                className="h-7 text-[11px] !text-red-400 border-red-500/40 hover:bg-red-500/10"
                onClick={() => setDeleteOpen(true)}
                title={t("backup.keyfileActions.deleteTitle")}
              >
                <Trash2 className="h-3.5 w-3.5 mr-1" />
                {t("backup.actions.delete")}
              </Button>
            </>
          )}
          {!installed && (
            <Button
              type="button"
              size="sm"
              variant="outline"
              className="h-7 text-[11px] !text-blue-400 border-blue-500/40 hover:bg-blue-500/10"
              onClick={() => setUploadOpen(true)}
              title={t("backup.keyfileActions.uploadTitle")}
            >
              <Lock className="h-3.5 w-3.5 mr-1" />
              {t("backup.actions.upload")}
            </Button>
          )}
        </div>
      </div>

      <Dialog open={uploadOpen} onOpenChange={(v) => !v && closeUpload()}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>{t("backup.keyfileActions.uploadDialogTitle")}</DialogTitle>
            <DialogDescription>
              {t("backup.keyfileActions.uploadDialogDescriptionBefore")} <code className="font-mono">/usr/local/share/proxmenux/pbs-key.conf</code> {t("backup.keyfileActions.uploadDialogDescriptionAfter")}
            </DialogDescription>
          </DialogHeader>
          <div className="space-y-3">
            {pveMatch && (
              <div className="rounded-md border border-emerald-500/40 bg-emerald-500/10 p-3 space-y-2 text-xs">
                <div className="flex items-start gap-2">
                  <Lock className="h-4 w-4 text-emerald-400 mt-0.5 shrink-0" />
                  <div className="flex-1 space-y-1">
                    <div className="font-semibold text-emerald-300">{t("backup.keyfileActions.pveKeyDetected")}</div>
                    <div className="text-muted-foreground">
                      {t("backup.keyfileActions.pveKeyDescriptionBefore")} <code className="font-mono text-[10.5px]">{pveMatch.name}</code> {t("backup.keyfileActions.pveKeyDescriptionMiddle")}{" "}
                      <code className="font-mono text-[10.5px] break-all">{pveMatch.path}</code>. {t("backup.keyfileActions.pveKeyDescriptionAfter")}
                    </div>
                  </div>
                </div>
                <div className="flex justify-end">
                  <Button
                    size="sm"
                    onClick={() => {
                      setImportPath(pveMatch.path)
                      setImportFile(null)
                    }}
                    disabled={busy}
                    className="!bg-emerald-600 hover:!bg-emerald-700 !text-white h-7 text-[11px]"
                  >
                    {t("backup.keyfileActions.useThisKey")}
                  </Button>
                </div>
              </div>
            )}
            {pveMatch && <div className="text-[10px] text-center text-muted-foreground">{t("backup.keyfileActions.orUploadOwn")}</div>}
            <div>
              <Label htmlFor="pbsKfUploadFile" className="text-xs">{t("backup.keyfileActions.uploadFromMachine")}</Label>
              <Input
                id="pbsKfUploadFile"
                type="file"
                accept=".conf,.key,application/json,text/plain"
                onChange={(e) => setImportFile(e.target.files?.[0] ?? null)}
                disabled={busy || !!importPath.trim()}
                className="h-9 mt-1"
              />
            </div>
            <div className="text-[10px] text-center text-muted-foreground">{t("backup.common.or")}</div>
            <div>
              <Label htmlFor="pbsKfUploadPath" className="text-xs">{t("backup.fields.absolutePathOnHost")}</Label>
              <Input
                id="pbsKfUploadPath"
                type="text"
                value={importPath}
                onChange={(e) => setImportPath(e.target.value)}
                disabled={busy || !!importFile}
                placeholder={t("backup.placeholders.keyfilePath")}
                className="h-9 mt-1 font-mono text-xs"
              />
            </div>
            {err && (
              <div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">{err}</div>
            )}
          </div>
          <DialogFooter>
            <Button variant="outline" onClick={closeUpload} disabled={busy}>{t("actions.cancel")}</Button>
            <Button
              onClick={runUpload}
              disabled={busy || (!importFile && !importPath.trim())}
              className="!bg-blue-500 hover:!bg-blue-600 !text-white"
            >
              {busy ? <Loader2 className="h-4 w-4 animate-spin" /> : t("backup.actions.import")}
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>

      <Dialog open={deleteOpen} onOpenChange={(v) => !v && closeDelete()}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle className="flex items-center gap-2">
              <AlertTriangle className="h-5 w-5 text-red-400" />
              {t("backup.keyfileActions.deleteDialogTitle")}
            </DialogTitle>
            <DialogDescription>
              {t("backup.keyfileActions.deleteDialogDescription")}
            </DialogDescription>
          </DialogHeader>
          <ul className="text-xs text-muted-foreground list-disc list-inside space-y-1 pl-2">
            <li>{t("backup.keyfileActions.deleteWarningNewBackups")}</li>
            <li>{t("backup.keyfileActions.deleteWarningDownloadsBefore")} <strong className="text-red-400">{t("backup.keyfileActions.willFail")}</strong> {t("backup.keyfileActions.deleteWarningDownloadsAfter")}</li>
            <li>{t("backup.keyfileActions.deleteWarningRecoveryBlobs")}</li>
          </ul>
          {err && (
            <div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">{err}</div>
          )}
          <DialogFooter>
            <Button variant="outline" onClick={closeDelete} disabled={busy}>{t("actions.cancel")}</Button>
            <Button onClick={runDelete} disabled={busy} className="!bg-red-500 hover:!bg-red-600 !text-white">
              {busy ? <Loader2 className="h-4 w-4 animate-spin" /> : t("backup.actions.delete")}
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </div>
  )
}

function KeyfileActionsBar({
  mutateStatus,
  escrowMode,
}: {
  mutateStatus: () => Promise<unknown>
  escrowMode?: "none" | "local" | "full"
}) {
  const t = useT()
  const [busy, setBusy] = useState(false)
  const [err, setErr] = useState<string | null>(null)
  const [pass1, setPass1] = useState("")
  const [pass2, setPass2] = useState("")
  // Pending radio state — reflects the operator's intended mode after
  // Apply. Seeded from the current mode so a No-op click on Apply is
  // rejected. Binary UX ('none' vs 'full') matches the setup wizard.
  const currentIsFull = escrowMode === "full"
  const [pendingMode, setPendingMode] = useState<"none" | "full">(currentIsFull ? "full" : "none")

  const runApply = async () => {
    // Yes → passphrase required + match. No → passphrase ignored.
    if (pendingMode === "full") {
      if (!pass1) {
        setErr(t("backup.errors.recoveryPassphraseRequired"))
        return
      }
      if (pass1 !== pass2) {
        setErr(t("backup.errors.passphrasesDoNotMatch"))
        return
      }
    }
    setBusy(true)
    setErr(null)
    try {
      const body: Record<string, unknown> = { escrow_mode: pendingMode }
      if (pendingMode === "full") body.escrow_passphrase = pass1
      await fetchApi("/api/host-backups/pbs-encryption/set-escrow-mode", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      })
      await mutateStatus()
      setPass1("")
      setPass2("")
    } catch (e) {
      setErr(e instanceof Error ? e.message : String(e))
    } finally {
      setBusy(false)
    }
  }

  // Contextual Apply-button label. Three transitions:
  //   No → Yes            → "Start uploading"
  //   Yes → No            → "Stop uploading"
  //   Yes → Yes (new pw)  → "Update passphrase" (rewraps envelope)
  const applyLabel =
    pendingMode === "full" && !currentIsFull
      ? t("backup.keyfileManagement.startUploading")
      : pendingMode === "none" && currentIsFull
        ? t("backup.keyfileManagement.stopUploading")
        : pendingMode === "full" && currentIsFull
          ? t("backup.keyfileManagement.updatePassphrase")
          : t("backup.actions.apply")

  // Apply is enabled when there is a real change to commit.
  const canApply = (
    (pendingMode !== (currentIsFull ? "full" : "none")) ||        // mode change
    (pendingMode === "full" && !!pass1 && pass1 === pass2)        // passphrase update
  )

  const download = () => {
    const a = document.createElement("a")
    a.href = "/api/host-backups/pbs-encryption/download-keyfile"
    a.download = "pbs-key.conf"
    document.body.appendChild(a)
    a.click()
    document.body.removeChild(a)
  }

  return (
    <div className="space-y-3">
      <div className="text-[11px] font-medium text-foreground">{t("backup.keyfileManagement.title")}</div>

      {/* Current status — icon + colour by state, no truncated fp. */}
      {escrowMode !== undefined && (
        <div className="flex items-center gap-2 text-xs bg-background/40 border border-white/10 rounded px-2.5 py-1.5">
          <span className="font-medium text-foreground">{t("backup.keyfileManagement.uploadToPbs")}</span>
          {currentIsFull ? (
            <span className="inline-flex items-center gap-1 text-emerald-400 font-medium">
              <CheckCircle2 className="h-3.5 w-3.5" />
              {t("backup.keyfileManagement.uploadYes")}
            </span>
          ) : (
            <span className="inline-flex items-center gap-1 text-blue-400 font-medium">
              {t("backup.keyfileManagement.uploadNo")}
            </span>
          )}
        </div>
      )}

      {/* Change mode + passphrase — one integrated form, no popover.
          Radio picks the intent; the passphrase pair unfolds when the
          intent is Yes (both for a first-time upload and for a
          passphrase rotation while already in Yes). */}
      <div className="space-y-2 rounded-md border border-blue-500/30 bg-blue-500/5 p-3">
        <div className="text-[11px] font-medium text-foreground">{t("backup.keyfileManagement.uploadQuestion")}</div>
        <div className="grid gap-1.5">
          <label className="flex items-start gap-2 cursor-pointer text-[11px]">
            <input
              type="radio"
              name="pendingModeChange"
              checked={pendingMode === "none"}
              onChange={() => { setPendingMode("none"); setPass1(""); setPass2("") }}
              className="mt-1"
            />
            <div className="flex-1">
              <div className="font-medium">{t("backup.keyfileManagement.keepLocalOnly")}</div>
              <div className="text-muted-foreground">{t("backup.keyfileManagement.keepLocalDescriptionBefore")} <code className="font-mono text-[10.5px]">/usr/local/share/proxmenux/pbs-key.conf</code>. {t("backup.keyfileManagement.keepLocalDescriptionAfter")} <em>{t("backup.keyfileManagement.downloadKeyfile")}</em> {t("backup.keyfileManagement.below")}.</div>
            </div>
          </label>
          <label className="flex items-start gap-2 cursor-pointer text-[11px]">
            <input
              type="radio"
              name="pendingModeChange"
              checked={pendingMode === "full"}
              onChange={() => setPendingMode("full")}
              className="mt-1"
            />
            <div className="flex-1">
              <div className="font-medium">{t("backup.keyfileManagement.yesUpload")}</div>
              <div className="text-muted-foreground">{t("backup.keyfileManagement.yesUploadDescription")}</div>
            </div>
          </label>
        </div>
        {pendingMode === "full" && (
          <div className="space-y-2 pt-2 border-t border-blue-500/20">
            <div>
              <Label htmlFor="mgmtPass1" className="text-[11px]">
                {currentIsFull ? t("backup.fields.newRecoveryPassphrase") : t("backup.fields.recoveryPassphrase")}
              </Label>
              <Input
                id="mgmtPass1"
                type="password"
                value={pass1}
                onChange={(e) => setPass1(e.target.value)}
                placeholder={currentIsFull ? t("backup.placeholders.newRecoveryPassphrase") : t("backup.placeholders.recoveryPassphrase")}
                className="font-mono mt-1 h-8 text-xs"
              />
            </div>
            <div>
              <Label htmlFor="mgmtPass2" className="text-[11px]">{t("backup.fields.confirmPassphrase")}</Label>
              <Input
                id="mgmtPass2"
                type="password"
                value={pass2}
                onChange={(e) => setPass2(e.target.value)}
                placeholder={t("backup.placeholders.typeItAgain")}
                className="font-mono mt-1 h-8 text-xs"
              />
              {pass1 && pass2 && pass1 !== pass2 && (
                <p className="text-[11px] text-red-400 mt-1">{t("backup.errors.passphrasesDontMatch")}</p>
              )}
            </div>
          </div>
        )}
        {err && (
          <div className="text-[11px] text-red-500 px-2 py-1.5 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">{err}</div>
        )}
        <div className="flex justify-end">
          <Button
            type="button"
            size="sm"
            onClick={runApply}
            disabled={busy || !canApply}
            className="!bg-blue-500 hover:!bg-blue-600 !text-white disabled:opacity-50"
          >
            {busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : applyLabel}
          </Button>
        </div>
      </div>

      <div className="flex flex-wrap gap-1.5">
        <Button
          type="button"
          size="sm"
          variant="outline"
          className="h-7 text-[11px] !text-emerald-400 border-emerald-500/40 hover:bg-emerald-500/10"
          onClick={download}
        >
          <Download className="h-3.5 w-3.5 mr-1" />
          {t("backup.keyfileManagement.downloadKeyfile")}
        </Button>
      </div>

      {/* Deletion lives inside the PBS destination row (PbsKeyfileActions);
          setup menus of Create Job / Manual Backup never surface it. */}
    </div>
  )
}

export function HostBackup() {
  const t = useT()
  const { data: jobsResp, error: jobsErr, mutate: mutateJobs } = useSWR<{ jobs: BackupJob[] }>(
    "/api/host-backups/jobs",
    fetcher,
    { refreshInterval: 30000 },
  )
  const [busyJobId, setBusyJobId] = useState<string | null>(null)
  const [actionError, setActionError] = useState<string | null>(null)
  const [jobToDelete, setJobToDelete] = useState<BackupJob | null>(null)
  const [creatingJob, setCreatingJob] = useState<boolean>(false)
  const [editingJobId, setEditingJobId] = useState<string | null>(null)
  const [viewingJobId, setViewingJobId] = useState<string | null>(null)
  const [watchingManualId, setWatchingManualId] = useState<string | null>(null)
  const [runningManual, setRunningManual] = useState<boolean>(false)
  // Independent tracker for the most recent manual backup that's still
  // running. Survives the operator closing the ManualJobWatchModal —
  // the modal only drives auto-refresh while it's OPEN, so if the
  // operator dismisses it before the job finishes, the archive lists
  // would only refresh on the next SWR tick (30-60 s). Polling the
  // job status here at 3 s and calling mutate once we see the run
  // transition to completed closes that gap regardless of modal
  // state. Set = single slot per launch; a fresh manual replaces the
  // slot (the previous one falls back to the periodic SWR tick).
  const [pendingManualJobId, setPendingManualJobId] = useState<string | null>(null)

  async function confirmDeleteJob() {
    if (!jobToDelete) return
    const id = jobToDelete.id
    setBusyJobId(id)
    setActionError(null)
    try {
      await fetchApi(`/api/host-backups/jobs/${encodeURIComponent(id)}`, { method: "DELETE" })
      mutateJobs()
      setJobToDelete(null)
    } catch (e) {
      setActionError(t("backup.errors.deleteFailed", { error: `"${id}": ${e instanceof Error ? e.message : String(e)}` }))
    } finally {
      setBusyJobId(null)
    }
  }
  const { data: destinationsResp, mutate: mutateDestinations } = useSWR<DestinationsResp>(
    "/api/host-backups/destinations",
    fetcher,
    { refreshInterval: 60000 },
  )
  const { data: remoteArchivesResp, error: remoteArchivesErr, mutate: mutateRemoteArchives } = useSWR<RemoteArchivesResp>(
    "/api/host-backups/remote-archives",
    fetcher,
    { refreshInterval: 60000 },
  )
  const { data: archivesResp, error: archivesErr, mutate: mutateArchives } = useSWR<{ archives: BackupArchive[] }>(
    "/api/host-backups/archives",
    fetcher,
    { refreshInterval: 30000 },
  )

  // Background poll for the pending manual backup, independent of the
  // watch modal being open. Fires mutateArchives/mutateRemoteArchives
  // as soon as the run flips to done so the operator sees the new
  // entry in Available Archives without a manual refresh.
  const { data: pendingManualPoll } = useSWR<JobDetail>(
    pendingManualJobId ? `/api/host-backups/jobs/${encodeURIComponent(pendingManualJobId)}` : null,
    fetcher,
    { refreshInterval: 3000 },
  )
  useEffect(() => {
    const d = pendingManualPoll as JobDetail | undefined
    if (pendingManualJobId && d?.last_run_at && d?.last_result) {
      mutateJobs()
      mutateArchives()
      mutateRemoteArchives()
      setPendingManualJobId(null)
    }
  }, [pendingManualPoll, pendingManualJobId, mutateJobs, mutateArchives, mutateRemoteArchives])

  const [inspectingArchive, setInspectingArchive] = useState<UnifiedArchive | null>(null)

  // Merge local archives + remote snapshots (PBS + Borg) into a single
  // sorted list. Newest first, regardless of source — operators care
  // about "the most recent backup", not "the most recent local backup".
  const unifiedArchives: UnifiedArchive[] = (() => {
    const out: UnifiedArchive[] = []
    if (archivesResp?.archives) {
      for (const a of archivesResp.archives) {
        out.push({
          source: "local",
          display_id: a.id,
          size_bytes: a.size_bytes,
          created_at: a.mtime,
          source_label: a.path.replace(/\/[^/]+$/, "") || "/",
          local: a,
        })
      }
    }
    if (remoteArchivesResp?.snapshots) {
      for (const s of remoteArchivesResp.snapshots) {
        out.push({
          source: s.backend,
          display_id: s.snapshot,
          size_bytes: s.size_bytes,
          created_at: s.backup_time,
          source_label: `${s.backend.toUpperCase()} ${s.repo_name}`,
          remote: s,
        })
      }
    }
    out.sort((a, b) => b.created_at - a.created_at)
    return out
  })()

  return (
    <div className="space-y-4 md:space-y-6">
      {/* ── Post-restore progress card ────────────────────
          Renders only while a restore is running or its
          summary hasn't been acknowledged. Once dismissed,
          it collapses to a "Past restores" ghost button. */}
      <RestoreProgressCard />

      {/* ── Scheduled jobs ───────────────────────────────── */}
      <Card className="bg-card border-border">
        <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
          <div className="flex items-center gap-2">
            <Calendar className="h-5 w-5 text-blue-500" />
            <CardTitle className="text-base font-semibold">{t("backup.jobs.scheduledTitle")}</CardTitle>
            <Badge variant="outline" className="ml-1">
              {jobsResp?.jobs?.filter((j) => !j.manual).length ?? 0}
            </Badge>
          </div>
          <Button
            size="sm"
            className="h-8 px-3 bg-blue-500 hover:bg-blue-600 text-white"
            onClick={() => setCreatingJob(true)}
          >
            <Plus className="h-4 w-4 mr-1" />
            {t("backup.jobs.createJob")}
          </Button>
        </CardHeader>
        <CardContent>
          {jobsErr ? (
            <div className="text-sm text-red-500 py-4">{t("backup.jobs.loadFailed")}</div>
          ) : !jobsResp ? (
            <div className="flex items-center gap-2 py-4 text-sm text-muted-foreground">
              <Loader2 className="h-4 w-4 animate-spin" />
              {t("backup.common.loading")}
            </div>
          ) : jobsResp.jobs.filter((j) => !j.manual).length === 0 ? null : (
            <div className="space-y-2">
              {actionError && (
                <div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
                  {actionError}
                </div>
              )}
              {jobsResp.jobs.filter((j) => !j.manual).map((j) => {
                const status = parseJobStatus(j.last_status)
                const running = isJobRunning(j)
                // While running we override the ok/failed pill with a
                // running spinner — same way the modal does. Clicking
                // the row re-opens JobDetailModal which auto-detects
                // the in-progress state and resumes streaming.
                const statusBadge = running
                  ? { label: t("backup.status.running"), cls: "bg-blue-500/10 border-blue-500/40 text-blue-300" }
                  : status?.result === "ok"
                    ? { label: "ok", cls: "bg-emerald-500/10 border-emerald-500/40 text-emerald-400" }
                    : status?.result
                      ? { label: status.result, cls: "bg-red-500/10 border-red-500/40 text-red-400" }
                      : null
                const lastRunWhen = formatRunAt(status?.runAt ?? null)
                return (
                  <button
                    key={j.id}
                    type="button"
                    onClick={() => setViewingJobId(j.id)}
                    className="w-full text-left flex items-start gap-3 p-3 rounded-md border border-border bg-card hover:bg-white/5 transition-colors group"
                    title={t("backup.jobs.openJobTitle")}
                  >
                    <div className="min-w-0 flex-1 w-full">
                      {/* Title row */}
                      <div className="flex items-center gap-2 flex-wrap">
                        <span className="font-semibold text-base truncate" title={j.id}>{j.id}</span>
                        <Badge variant="outline" className={`text-[10px] uppercase tracking-wide ${methodBadgeCls(j.method)}`}>
                          {j.method}
                        </Badge>
                        {j.manual && (
                          <Badge variant="outline" className="text-[10px] text-purple-400 border-purple-400/40 bg-purple-500/5">
                            {t("backup.status.manual")}
                          </Badge>
                        )}
                        {j.attached && (
                          <Badge variant="outline" className="text-[10px] text-blue-400 border-blue-400/40 bg-blue-500/5">
                            {t("backup.status.attached")}
                          </Badge>
                        )}
                        {j.encrypted && (
                          <Badge
                            variant="outline"
                            className="text-[10px] uppercase tracking-wide border-emerald-500/40 text-emerald-400 bg-emerald-500/5"
                            title={t("backup.jobs.encryptedTitle")}
                          >
                            <Lock className="h-3.5 w-3.5" />
                          </Badge>
                        )}
                        {/* Surface the profile mode at row level so the
                            operator can tell at a glance whether the
                            job ships the default path set or a custom
                            list — without having to open the detail
                            modal. Same wording the modal uses. */}
                        <Badge
                          variant="outline"
                          className={`text-[10px] uppercase tracking-wide ${
                            j.profile_mode === "custom"
                              ? "border-cyan-500/40 text-cyan-400 bg-cyan-500/5"
                              : "border-border text-muted-foreground bg-background/40"
                          }`}
                          title={
                            j.profile_mode === "custom"
                              ? t("backup.jobs.customProfileTitle")
                              : t("backup.jobs.defaultProfileTitle")
                          }
                        >
                          {j.profile_mode === "custom" ? t("backup.profile.custom") : t("backup.profile.default")}
                        </Badge>
                        {!j.enabled && !j.manual && (
                          <Badge variant="outline" className="text-[10px] text-amber-500 border-amber-500/40 bg-amber-500/5">
                            {t("status.disabled")}
                          </Badge>
                        )}
                      </div>

                      {/* Destination */}
                      <div className="mt-1 text-sm font-mono text-muted-foreground truncate" title={j.destination}>
                        {j.destination || "—"}
                      </div>

                      {/* Stats row — icons in green, values in foreground
                          white to match the rest of the dashboard. */}
                      <div className="mt-2 text-xs text-foreground flex items-center gap-x-4 gap-y-1 flex-wrap">
                        <span className="inline-flex items-center gap-1" title={j.on_calendar}>
                          <Calendar className="h-3.5 w-3.5 text-green-500" />
                          <span>{humanizeOnCalendar(j.on_calendar, t)}</span>
                        </span>
                        {j.retention && (
                          <span className="inline-flex items-center gap-1.5 flex-wrap" title={t("backup.jobs.retentionTitle")}>
                            <Archive className="h-3.5 w-3.5 text-green-500" />
                            {(() => {
                              // Backend returns retention as "last=7, daily=7, …".
                              // Parse and render as labelled chips so the row
                              // matches the visual style of the modal.
                              const pairs = j.retention
                                .split(",")
                                .map((s) => s.trim())
                                .map((s) => {
                                  const m = s.match(/^([a-z]+)=(\d+)$/i)
                                  return m && Number(m[2]) > 0 ? { label: m[1], value: m[2] } : null
                                })
                                .filter((x): x is { label: string; value: string } => x !== null)
                              if (pairs.length === 0) {
                                return <span>{j.retention}</span>
                              }
                              return pairs.map((p) => (
                                <span
                                  key={p.label}
                                  className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded border border-border bg-background/60"
                                >
                                  <span className="text-[10px] uppercase tracking-wide text-muted-foreground">{p.label}</span>
                                  <span className="font-mono text-xs text-foreground">{p.value}</span>
                                </span>
                              ))
                            })()}
                          </span>
                        )}
                        {!j.attached && j.next_run && (
                          <span className="inline-flex items-center gap-1" title={t("backup.jobs.nextRunTitle")}>
                            <Clock className="h-3.5 w-3.5 text-green-500" />
                            <span>{t("backup.jobs.nextRun", { time: formatNext(j.next_run) })}</span>
                          </span>
                        )}
                        {(statusBadge || lastRunWhen) && (
                          <span className="inline-flex items-center gap-1" title={status?.logFile ?? ""}>
                            <Clock className="h-3.5 w-3.5 text-green-500" />
                            <span>{t("backup.jobs.lastRunLabel")}</span>
                            {statusBadge && (
                              <span className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] uppercase tracking-wide border ${statusBadge.cls}`}>
                                {running && <Loader2 className="h-2.5 w-2.5 animate-spin" />}
                                {statusBadge.label}
                              </span>
                            )}
                            {lastRunWhen && <span>{lastRunWhen}</span>}
                          </span>
                        )}
                        {!status && (
                          <span className="inline-flex items-center gap-1 text-muted-foreground">
                            <Clock className="h-3.5 w-3.5 text-green-500" />
                            <span>{t("backup.jobs.neverRun")}</span>
                          </span>
                        )}
                      </div>
                    </div>

                    <ChevronRight className="h-4 w-4 text-muted-foreground group-hover:text-blue-400 transition-colors shrink-0 mt-1" />
                  </button>
                )
              })}
            </div>
          )}
        </CardContent>
      </Card>

      {/* ── Backup configuration (destinations + custom paths) ── */}
      <BackupConfigurationCard
        destinations={destinationsResp}
        onDestChanged={() => mutateDestinations()}
      />

      {/* ── Manual backups (entry point) ──────────────────── */}
      <Card className="bg-card border-border">
        <CardHeader className="pb-3">
          <div className="flex items-center gap-2">
            <PlayCircle className="h-5 w-5 text-purple-400" />
            <CardTitle className="text-base font-semibold">{t("backup.manual.title")}</CardTitle>
          </div>
        </CardHeader>
        <CardContent className="space-y-3">
          <p className="text-xs text-muted-foreground">
            {t("backup.manual.description")}
          </p>
          <Button
            className="w-full sm:w-auto bg-purple-500 hover:bg-purple-600 text-white"
            onClick={() => setRunningManual(true)}
          >
            <PlayCircle className="h-4 w-4 mr-2" />
            {t("backup.manual.run")}
          </Button>
          {/* In-progress manual jobs. If the operator closed the
              ManualBackupDialog before the runner finished, this
              banner is the way back into the live log — clicking
              opens the JobDetailModal which automatically resumes
              the streaming view for any in-progress job. */}
          {jobsResp?.jobs
            ?.filter((j) => j.manual && isJobRunning(j))
            .map((j) => (
              <button
                key={j.id}
                type="button"
                onClick={() => setWatchingManualId(j.id)}
                className="w-full flex items-center gap-2 px-3 py-2 rounded-md border border-blue-500/40 bg-blue-500/5 hover:bg-blue-500/10 transition-colors text-left"
                title={t("backup.manual.reopenLogTitle")}
              >
                <Loader2 className="h-4 w-4 animate-spin text-blue-400 shrink-0" />
                <span className="text-sm min-w-0 flex-1 truncate">
                  {t("backup.manual.inProgress")} — <span className="font-mono">{j.id}</span>
                </span>
                <span className="text-xs text-blue-300 shrink-0">{t("backup.manual.viewProgress")}</span>
              </button>
            ))}
        </CardContent>
      </Card>

      {/* ── Available archives ─────────────────────────────── */}
      <Card className="bg-card border-border">
        <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
          <div className="flex items-center gap-2">
            <Archive className="h-5 w-5 text-blue-500" />
            <CardTitle className="text-base font-semibold">{t("backup.archives.title")}</CardTitle>
          </div>
          <Badge variant="outline">{unifiedArchives.length}</Badge>
        </CardHeader>
        <CardContent>
          <p className="text-[11px] text-muted-foreground mb-3">
            {t("backup.archives.descriptionBefore")} <code className="font-mono">.tar.zst</code> {t("backup.archives.descriptionAfter")}
          </p>
          {remoteArchivesResp?.errors && remoteArchivesResp.errors.length > 0 && (
            <div className="text-[11px] text-amber-500 mb-3 px-3 py-2 rounded-md border border-amber-500/30 bg-amber-500/5 space-y-1">
              <div className="font-medium">{t("backup.archives.remoteQueryWarning")}</div>
              {remoteArchivesResp.errors.map((e, i) => (
                <div key={i} className="font-mono break-all">
                  {e.backend}/{e.repo_name}: {e.error}
                </div>
              ))}
            </div>
          )}
          {archivesErr && remoteArchivesErr ? (
            <div className="text-sm text-red-500 py-4">{t("backup.archives.loadFailed")}</div>
          ) : !archivesResp && !remoteArchivesResp ? (
            <div className="flex items-center gap-2 py-4 text-sm text-muted-foreground">
              <Loader2 className="h-4 w-4 animate-spin" />
              {t("backup.common.loading")}
            </div>
          ) : unifiedArchives.length === 0 ? (
            <div className="text-sm text-muted-foreground py-4">
              {t("backup.archives.emptyBefore")} <span className="font-medium">{t("backup.manual.run")}</span> {t("backup.archives.emptyAfter")}
            </div>
          ) : (
            <div className="space-y-2">
              {unifiedArchives.map((u) => {
                const localKind = u.local?.kind
                const localJobId = u.local?.job_id
                const localHost = u.local?.source_hostname
                const localPath = u.local?.path
                // Same palette as the DestinationRow accents so each
                // backend reads identically across the two cards.
                const sourceBadgeCls =
                  u.source === "pbs"
                    ? "text-purple-400 border-purple-500/20 bg-purple-500/10"
                    : u.source === "borg"
                      ? "text-fuchsia-400 border-fuchsia-500/20 bg-fuchsia-500/10"
                      : "text-blue-400 border-blue-500/20 bg-blue-500/10"
                return (
                  <button
                    key={`${u.source}:${u.display_id}`}
                    type="button"
                    onClick={() => setInspectingArchive(u)}
                    className="w-full text-left flex items-center justify-between gap-3 p-3 rounded-md border border-border bg-background/40 hover:bg-white/5 hover:border-blue-500/40 transition-colors group"
                    title={t("backup.archives.inspectTitle")}
                  >
                    <div className="min-w-0 flex-1">
                      <div className="font-mono text-xs truncate group-hover:text-blue-400 transition-colors" title={localPath || u.display_id}>
                        {u.display_id}
                      </div>
                      <div className="text-[11px] text-muted-foreground mt-0.5 flex items-center gap-3 flex-wrap">
                        <span className={`uppercase tracking-wide text-[10px] px-1.5 py-0.5 rounded border ${sourceBadgeCls}`}>
                          {u.source}
                        </span>
                        {u.remote?.encrypted && (
                          <span
                            className="uppercase tracking-wide text-[10px] px-1.5 py-0.5 rounded border border-emerald-500/40 text-emerald-400 bg-emerald-500/5 inline-flex items-center"
                            title={t("backup.archives.encryptedTitle")}
                          >
                            <Lock className="h-3.5 w-3.5" />
                          </span>
                        )}
                        <span className="inline-flex items-center gap-1">
                          <Clock className="h-3 w-3" />
                          {formatMtime(u.created_at)}
                        </span>
                        <span className="inline-flex items-center gap-1">
                          <HardDrive className="h-3 w-3" />
                          {formatBytes(u.size_bytes)}
                        </span>
                        <span title={u.source_label}>
                          {t("backup.archives.at")} <code className="font-mono">{u.source_label}</code>
                        </span>
                        {u.source === "local" && localKind === "scheduled" && localJobId ? (
                          <span>{t("backup.archives.job")} <code className="font-mono">{localJobId}</code></span>
                        ) : u.source === "local" && localKind === "legacy" ? (
                          <span className="uppercase tracking-wide text-[10px] px-1.5 py-0.5 rounded bg-amber-500/10 border border-amber-500/40 text-amber-400">
                            {t("backup.status.legacy")}
                          </span>
                        ) : u.source === "local" && localKind === "manual" ? (
                          <span className="uppercase tracking-wide text-[10px] px-1.5 py-0.5 rounded bg-white/5 border border-border">
                            {t("backup.status.manual")}
                          </span>
                        ) : null}
                        {(u.source === "pbs" || u.source === "borg") && u.remote?.backup_id && (
                          <span>{u.source === "pbs" ? t("backup.archives.group") : t("backup.archives.archive")}: <code className="font-mono">{u.remote.backup_id}</code></span>
                        )}
                        {u.source === "local" && localHost && (
                          <span>{t("backup.archives.host")} <code className="font-mono">{localHost}</code></span>
                        )}
                      </div>
                    </div>
                    <ChevronRight className="h-4 w-4 text-muted-foreground group-hover:text-blue-400 transition-colors shrink-0" />
                  </button>
                )
              })}
            </div>
          )}
        </CardContent>
      </Card>

      {/* ── Inspect / preflight modal ──────────────────────── */}
      <InspectModal
        archive={inspectingArchive}
        onClose={() => setInspectingArchive(null)}
        onDeleted={() => {
          setInspectingArchive(null)
          // Refresh both lists — a deleted item might have been local
          // or remote (PBS/Borg). Cheap enough to revalidate both.
          mutateArchives()
          mutateRemoteArchives()
        }}
      />

      {/* ── Create / Edit job wizard ─────────────────────── */}
      <CreateJobDialog
        open={creatingJob}
        onClose={() => setCreatingJob(false)}
        onCreated={() => {
          setCreatingJob(false)
          mutateJobs()
        }}
      />
      <CreateJobDialog
        open={editingJobId !== null}
        editingJobId={editingJobId}
        onClose={() => setEditingJobId(null)}
        onCreated={() => {
          setEditingJobId(null)
          mutateJobs()
        }}
      />

      {/* ── One-shot manual backup ─────────────────────── */}
      <ManualBackupDialog
        open={runningManual}
        onClose={() => setRunningManual(false)}
        onLaunched={(jobId) => {
          // Hand off straight to the live-progress modal so the
          // operator never has to chase the "View progress" banner
          // after pressing Run. Mutate jobs first so the new manual
          // entry is in cache when the watch modal opens.
          setRunningManual(false)
          mutateJobs()
          setWatchingManualId(jobId)
          // Register the job with the modal-independent background
          // poller so the Available Archives list refreshes the
          // instant the run finishes, even if the operator closes
          // the watch modal before then. Replaces the old fire-and-
          // forget 5 s mutate that used to miss slow backups.
          setPendingManualJobId(jobId)
        }}
      />

      {/* ── Job detail / actions modal ─────────────────────────── */}
      <JobDetailModal
        jobId={viewingJobId}
        onClose={() => setViewingJobId(null)}
        onEdit={(id) => {
          setViewingJobId(null)
          setEditingJobId(id)
        }}
        onRequestDelete={(id) => {
          const job = jobsResp?.jobs.find((j) => j.id === id)
          if (job) {
            setViewingJobId(null)
            setJobToDelete(job)
          }
        }}
        onChanged={() => {
          mutateJobs()
          mutateArchives()
          mutateRemoteArchives()
        }}
      />

      {/* Manual jobs use their own modal — no Schedule/Retention/
          Profile, no Edit/Run/Disable. Only the live log; the modal
          is closed with the dialog's own X (no extra footer). */}
      <ManualJobWatchModal
        jobId={watchingManualId}
        onClose={() => setWatchingManualId(null)}
        onChanged={() => {
          // The watch modal fires onChanged when a tracked run
          // finishes (last_result transitions from null → ok/failed).
          // Refresh jobs (status badge), local archives (new tar.zst)
          // and remote archives (Borg/PBS snapshots) so the operator
          // sees the new backup without waiting for the next tick.
          mutateJobs()
          mutateArchives()
          mutateRemoteArchives()
        }}
      />

      {/* ── Delete confirmation ────────────────────────────── */}
      <Dialog open={jobToDelete !== null} onOpenChange={(v) => { if (!v) setJobToDelete(null) }}>
        <DialogContent className="max-w-md">
          <DialogHeader>
            <DialogTitle className="flex items-center gap-2">
              <AlertTriangle className="h-5 w-5 text-amber-500" />
              {t("backup.deleteJob.title")}
            </DialogTitle>
            <DialogDescription>
              {t("backup.deleteJob.description")}
            </DialogDescription>
          </DialogHeader>
          {jobToDelete && (
            <div className="space-y-3 text-sm">
              <div className="rounded-md border border-border bg-background/50 p-3 space-y-1">
                <div className="text-[10px] uppercase tracking-wider text-muted-foreground">{t("backup.fields.jobId")}</div>
                <div className="font-mono text-sm">{jobToDelete.id}</div>
                {jobToDelete.attached && jobToDelete.pve_storage && (
                  <>
                    <div className="text-[10px] uppercase tracking-wider text-muted-foreground mt-2">{t("backup.fields.type")}</div>
                    <div className="text-xs">{t("backup.deleteJob.attachedToStorage")} <span className="font-mono">{jobToDelete.pve_storage}</span></div>
                  </>
                )}
              </div>
              {jobToDelete.attached ? (
                <p className="text-xs text-muted-foreground">
                  {t("backup.deleteJob.attachedWarning")}
                </p>
              ) : (
                <p className="text-xs text-muted-foreground">
                  {t("backup.deleteJob.timerWarning")}
                </p>
              )}
            </div>
          )}
          <div className="flex justify-end gap-2 mt-2">
            <Button
              variant="ghost"
              onClick={() => setJobToDelete(null)}
              disabled={busyJobId === jobToDelete?.id}
            >
              {t("actions.cancel")}
            </Button>
            <Button
              variant="destructive"
              onClick={confirmDeleteJob}
              disabled={busyJobId === jobToDelete?.id}
            >
              {busyJobId === jobToDelete?.id ? (
                <Loader2 className="h-4 w-4 animate-spin mr-2" />
              ) : (
                <Trash2 className="h-4 w-4 mr-2" />
              )}
              {t("backup.actions.delete")}
            </Button>
          </div>
        </DialogContent>
      </Dialog>
    </div>
  )
}

// ──────────────────────────────────────────────────────────────
// Inspect modal — shows manifest summary + lets the operator pick
// a restore mode and run the dry-run preflight + plan against this
// host. No mutating actions; --apply stays on the CLI for 1.3.0.
// ──────────────────────────────────────────────────────────────
function InspectModal({
  archive,
  onClose,
  onDeleted,
}: {
  archive: UnifiedArchive | null
  onClose: () => void
  onDeleted?: () => void
}) {
  const t = useT()
  const open = archive !== null
  // Aliases to the source-specific payloads — saves on `.local!` /
  // `.remote!` repetition later. PBS and Borg share the same shape.
  const localArc = archive?.source === "local" ? archive.local : undefined
  const remoteArc = archive && archive.source !== "local" ? archive.remote : undefined
  const isRemote = archive?.source === "pbs" || archive?.source === "borg"
  const backendLabel = archive?.source === "pbs" ? "PBS" : archive?.source === "borg" ? "Borg" : t("backup.backends.local")
  const [mode, setMode] = useState<string>("full")
  const [report, setReport] = useState<PreflightReport | null>(null)
  const [running, setRunning] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const [instructions, setInstructions] = useState<{
    archive_basename: string
    mode_label: string
    shell_command: string
    menu_path: string[]
    reboot_required: boolean
    notes: string[]
  } | null>(null)
  const [fetchingInstructions, setFetchingInstructions] = useState(false)
  const [downloading, setDownloading] = useState(false)
  // Per-archive runner log — only available for local archives, where
  // the runner writes `<stem>.log` next to the `<stem>.tar.zst` in
  // /var/log/proxmenux/backup-jobs/. PBS / Borg snapshots don't have
  // a co-located run log on this host.
  const { data: archiveLog } = useSWR<{
    log_path: string | null
    size: number
    content: string
    tail: string[]
  }>(
    open && archive?.source === "local" && localArc?.id
      ? `/api/host-backups/archives/${encodeURIComponent(localArc.id)}/log`
      : null,
    fetcher,
  )
  const [showArchiveFullLog, setShowArchiveFullLog] = useState(false)
  const [showDeleteArchiveConfirm, setShowDeleteArchiveConfirm] = useState(false)
  const [viewingContents, setViewingContents] = useState(false)
  // Restore flow state:
  //   restorePreparing  — extract running (blocking spinner overlay)
  //   restoreOptions    — modal open after prepare succeeded
  //   restoreTerminal   — ScriptTerminalModal open with monitor_apply.sh
  // The extract happens BEFORE the options modal so the custom
  // checklist can be filled from `components_available` (what's
  // actually inside this backup) instead of a hardcoded list.
  const [restorePreparing, setRestorePreparing] = useState(false)
  const [restoreError, setRestoreError] = useState<string | null>(null)
  const [restoreOptions, setRestoreOptions] = useState<{
    stagingPath: string
    pathsAvailable: string[]
    rollbackPlan?: RollbackPlan
    // Cross-kernel awareness passed through to RestoreOptionsModal:
    // when direction === "bk_older" the modal shows the safe-subset
    // banner and grays out any path prefixed by anything in
    // blockedPaths. "bk_newer" and "same" pass through as no-ops.
    crossKernel?: {
      direction: "same" | "bk_older" | "bk_newer"
      backupKernel: string
      targetKernel: string
      blockedPaths: string[]
    }
    // Kernel-agnostic hydration preview (bk_older only): what the
    // restore will re-apply to the target via merge instead of
    // whole-file copy — IOMMU cmdline tokens, VFIO modules,
    // operator's GRUB keys, whitelisted vfio/nvidia files.
    hydration?: {
      applies: boolean
      actions: string[]
    }
  } | null>(null)
  const [restoreTerminal, setRestoreTerminal] = useState<{
    stagingPath: string
    mode: "full" | "custom"
    paths: string[]
    rollbackExecute?: boolean
  } | null>(null)

  // ── Keyfile-required inline import ────────────────────────────
  // When this modal opens on an encrypted PBS backup and the local
  // keyfile is missing at /usr/local/share/proxmenux/pbs-key.conf,
  // Restore / Download / View contents would all fail with
  // "missing key". We block the buttons and offer an inline Import
  // panel that reuses /api/host-backups/pbs-encryption/import with
  // source=content + escrow_mode=none — same endpoint as the setup
  // flow, so the imported keyfile lands at the canonical path and is
  // reused by every subsequent backup + restore.
  const { data: keyfileInfo, mutate: mutateKeyfileInfo } = useSWR<{
    installed: boolean
    fingerprint?: string
    path: string
  }>(
    open && remoteArc?.encrypted ? "/api/host-backups/pbs-encryption/keyfile-info" : null,
    fetcher,
  )
  const needsKeyfile = !!(remoteArc?.encrypted && keyfileInfo && !keyfileInfo.installed)
  // Same PVE-managed keyfile auto-discovery the setup wizards use: when
  // the encrypted snapshot lives on a PBS whose PVE storage.cfg entry
  // has an `encryption-key` file at /etc/pve/priv/storage/<NAME>.enc,
  // surface a one-click "Use this key" shortcut in the amber panel so
  // the operator doesn't have to type the full path. Only fetched when
  // the amber gate is actually rendered (encrypted + no keyfile).
  const { data: pveDiscoverInspect } = useSWR<{
    entries: Array<{ name: string; server: string; datastore: string; path: string; matches_repository: boolean }>
  }>(
    needsKeyfile && remoteArc?.repo_repository
      ? `/api/host-backups/pbs-encryption/discover-pve-keyfiles?repository=${encodeURIComponent(remoteArc.repo_repository)}`
      : null,
    fetcher,
  )
  const pveMatchInspect = pveDiscoverInspect?.entries?.find((e) => e.matches_repository) || null
  const [importFile, setImportFile] = useState<File | null>(null)
  const [importPath, setImportPath] = useState<string>("")
  const [importing, setImporting] = useState(false)
  const [importError, setImportError] = useState<string | null>(null)

  const runImportKeyfile = async () => {
    // Either a file upload OR a typed absolute path on this host —
    // both land at /usr/local/share/proxmenux/pbs-key.conf with
    // escrow_mode='none'. When both are provided the file wins.
    if (!importFile && !importPath.trim()) {
      setImportError(t("backup.errors.pickKeyfile"))
      return
    }
    setImporting(true)
    setImportError(null)
    try {
      const body: Record<string, unknown> = { escrow_mode: "none" }
      if (importFile) {
        const buf = new Uint8Array(await importFile.arrayBuffer())
        let bin = ""
        for (let i = 0; i < buf.length; i++) bin += String.fromCharCode(buf[i])
        body.source = "content"
        body.content_b64 = btoa(bin)
      } else {
        body.source = "path"
        body.keyfile_path = importPath.trim()
      }
      await fetchApi("/api/host-backups/pbs-encryption/import", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      })
      await mutateKeyfileInfo()
      setImportFile(null)
      setImportPath("")
    } catch (e) {
      setImportError(e instanceof Error ? e.message : String(e))
    } finally {
      setImporting(false)
    }
  }

  const beginRestore = async () => {
    if (!archive) return
    setRestoreError(null)
    setRestorePreparing(true)
    const body: Record<string, string> = { source: archive.source }
    if (archive.source === "local") {
      if (!localArc?.path) { setRestoreError(t("backup.errors.localArchivePathMissing")); setRestorePreparing(false); return }
      body.path = localArc.path
    } else if (remoteArc) {
      body.repo_name = remoteArc.repo_name
      body.snapshot = remoteArc.snapshot
    } else {
      setRestoreError(t("backup.errors.snapshotInfoMissing"))
      setRestorePreparing(false)
      return
    }
    try {
      const r: any = await fetchApi("/api/host-backups/restore/prepare", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      })
      if (!r?.staging_path) throw new Error(t("backup.errors.backendNoStagingPath"))
      const ck = r.cross_kernel || {}
      const hyd = r.hydration || {}
      setRestoreOptions({
        stagingPath: r.staging_path,
        pathsAvailable: Array.isArray(r.paths_available) ? r.paths_available : [],
        rollbackPlan: r.rollback_plan,
        crossKernel: ck.direction
          ? {
              direction: ck.direction,
              backupKernel: ck.backup_kernel || "",
              targetKernel: ck.target_kernel || "",
              blockedPaths: Array.isArray(ck.blocked_paths) ? ck.blocked_paths : [],
            }
          : undefined,
        hydration: hyd.applies
          ? {
              applies: true,
              actions: Array.isArray(hyd.actions) ? hyd.actions : [],
            }
          : undefined,
      })
    } catch (e) {
      setRestoreError(e instanceof Error ? e.message : String(e))
    } finally {
      setRestorePreparing(false)
    }
  }
  const [deletingArchive, setDeletingArchive] = useState(false)
  const [archiveDeleteResult, setArchiveDeleteResult] = useState<string[] | null>(null)

  // Local download. Uses a single-use ticket appended to the URL +
  // `<a download>` instead of fetch+blob — the previous fetch+blob
  // approach loaded the whole archive into the browser's RAM via
  // URL.createObjectURL(), which broke around 2 GB on most browsers.
  // With the ticket-on-URL approach the browser streams the response
  // straight to disk, so archive size is bounded only by free space.
  const downloadLocalArchive = async () => {
    if (!localArc) return
    setDownloading(true)
    setError(null)
    try {
      const ticket = await fetchTerminalTicket()
      let url = getApiUrl(`/api/host-backups/archives/${encodeURIComponent(localArc.id)}/download`)
      if (ticket) url += `?ticket=${encodeURIComponent(ticket)}`
      const a = document.createElement("a")
      a.href = url
      a.download = localArc.id
      document.body.appendChild(a)
      a.click()
      document.body.removeChild(a)
    } catch (e) {
      setError(t("backup.errors.downloadFailed", { error: e instanceof Error ? e.message : String(e) }))
    } finally {
      // The download itself runs in the browser's network stack;
      // we just initiated it. Clear the spinner immediately.
      setDownloading(false)
    }
  }

  // Remote export-then-download (PBS or Borg) — kicks off the
  // server-side extract + pack, polls until the .tar.zst is ready,
  // then streams it like the local case. Stage feedback (queued /
  // restoring / packing) goes into `exportTask.state` so the modal
  // can show what's happening live.
  const [exportTask, setExportTask] = useState<ExportTask | null>(null)
  const downloadRemoteArchive = async () => {
    if (!remoteArc) return
    setDownloading(true)
    setError(null)
    setExportTask({
      task_id: "",
      backend: remoteArc.backend,
      repo_name: remoteArc.repo_name,
      snapshot: remoteArc.snapshot,
      state: "queued",
      message: t("backup.archives.startingExport"),
      size_bytes: 0,
      output_path: null,
      error: null,
    })
    try {
      const started = await fetchApi<{ task_id: string; state: string }>(
        "/api/host-backups/remote-archives/export",
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            backend: remoteArc.backend,
            repo_name: remoteArc.repo_name,
            snapshot: remoteArc.snapshot,
          }),
        },
      )
      // Poll until done. The export can take from seconds (small host
      // config) to minutes (multi-GB pxar) — poll every 1.5s so the
      // UI feels responsive without DoS-ing Flask.
      let task: ExportTask | null = null
      while (true) {
        await new Promise((res) => setTimeout(res, 1500))
        task = await fetchApi<ExportTask>(
          `/api/host-backups/remote-archives/export/${encodeURIComponent(started.task_id)}`,
        )
        setExportTask(task)
        if (task.state === "completed" || task.state === "failed") break
      }
      if (!task || task.state !== "completed") {
        throw new Error(task?.error || t("backup.errors.exportDidNotComplete"))
      }
      // Stream the resulting .tar.zst with a ticketed URL + <a download>.
      // Same rationale as downloadLocalArchive: bypass fetch+blob to
      // avoid the ~2 GB browser-RAM limit; the browser writes
      // straight to disk.
      const ticket = await fetchTerminalTicket()
      let url = getApiUrl(`/api/host-backups/remote-archives/export/${encodeURIComponent(started.task_id)}/download`)
      if (ticket) url += `?ticket=${encodeURIComponent(ticket)}`
      const safeSnap = remoteArc.snapshot.replace(/\//g, "_")
      const a = document.createElement("a")
      a.href = url
      a.download = `${remoteArc.backend}-${remoteArc.repo_name}-${safeSnap}.tar.zst`
      document.body.appendChild(a)
      a.click()
      document.body.removeChild(a)
      setExportTask(null)
    } catch (e) {
      setError(t("backup.errors.downloadFailed", { error: e instanceof Error ? e.message : String(e) }))
    } finally {
      setDownloading(false)
    }
  }

  const downloadArchive = () => {
    if (isRemote) return downloadRemoteArchive()
    return downloadLocalArchive()
  }

  // Archive deletion — works for local, PBS and Borg backends.
  //   • local: DELETE /archives/<id> → removes .tar.zst + sidecar + run log.
  //   • pbs:   DELETE /remote-archives → `proxmox-backup-client snapshot forget`.
  //   • borg:  DELETE /remote-archives → `borg delete <repo>::<archive>`.
  // PBS-protected snapshots come back as 409 with the original message so
  // the operator can decide whether to lift the protection first.
  const deleteArchive = async () => {
    if (!archive) return
    setDeletingArchive(true)
    setError(null)
    try {
      let removed: string[] = []
      if (archive.source === "local" && localArc) {
        const resp = await fetchApi<{ status: string; removed: string[] }>(
          `/api/host-backups/archives/${encodeURIComponent(localArc.id)}`,
          { method: "DELETE" },
        )
        removed = resp.removed || []
      } else if (remoteArc) {
        const resp = await fetchApi<{ status: string; removed: string }>(
          `/api/host-backups/remote-archives`,
          {
            method: "DELETE",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({
              backend: remoteArc.backend,
              repo_name: remoteArc.repo_name,
              snapshot: remoteArc.snapshot,
            }),
          },
        )
        removed = resp.removed ? [resp.removed] : []
      }
      setArchiveDeleteResult(removed)
      setShowDeleteArchiveConfirm(false)
      if (onDeleted) onDeleted(); else onClose()
    } catch (e) {
      setError(t("backup.errors.deleteFailed", { error: e instanceof Error ? e.message : String(e) }))
    } finally {
      setDeletingArchive(false)
    }
  }

  // Reset instructions when archive or mode changes — the previous
  // command is no longer relevant.
  useEffect(() => {
    setInstructions(null)
  }, [archive, mode])

  const fetchInstructions = async () => {
    if (!localArc) return
    setFetchingInstructions(true)
    setError(null)
    try {
      const res = await fetchApi<typeof instructions extends infer T ? T : never>(
        `/api/host-backups/archives/${encodeURIComponent(localArc.id)}/prepare-restore`,
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ mode }),
        }
      ) as NonNullable<typeof instructions>
      setInstructions(res)
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e))
    } finally {
      setFetchingInstructions(false)
    }
  }

  const { data: manifest, error: manifestErr } = useSWR<{
    source_host: ManifestSourceHost
    proxmenux_installed_components: Array<{ id: string; version_at_backup: string | null }>
    vms_lxcs_at_backup: { vms: unknown[]; lxcs: unknown[] }
    storage_inventory?: { zfs_pools?: unknown[]; lvm?: { vgs?: unknown[] } }
  }>(
    localArc ? `/api/host-backups/archives/${encodeURIComponent(localArc.id)}/manifest` : null,
    fetcher,
  )

  const runPreflight = async () => {
    if (!localArc) return
    setRunning(true)
    setError(null)
    setReport(null)
    try {
      const res = await fetchApi<PreflightReport>(
        `/api/host-backups/archives/${encodeURIComponent(localArc.id)}/preflight`,
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ mode }),
        },
      )
      setReport(res)
    } catch (e: any) {
      setError(e?.message || t("backup.errors.preflightFailed"))
    } finally {
      setRunning(false)
    }
  }

  // Reset state when archive changes — the key= prop on DialogContent
  // forces React to unmount + remount so all useState() values are
  // discarded automatically. Cleaner than manually tracking each.
  const archiveKey = archive ? `${archive.source}:${archive.display_id}` : ""

  return (
    <>
    <Dialog open={open} onOpenChange={(v) => { if (!v) onClose() }}>
      <DialogContent key={archiveKey} className="max-w-3xl bg-card border-border p-0 flex flex-col gap-0 max-h-[90vh]">
        <DialogHeader className="px-4 sm:px-6 pt-4 pb-3 shrink-0 border-b border-border">
          <DialogTitle className="flex items-center gap-2 flex-wrap text-base pr-8">
            <DatabaseBackup className="h-5 w-5 text-blue-500 shrink-0" />
            <span className="font-mono text-xs sm:text-sm break-all flex-1 min-w-0">{archive?.display_id}</span>
          </DialogTitle>
        </DialogHeader>

        {/* ── Body: same shape for the 3 backends ─────────────── */}
        <ScrollArea className="flex-1 min-h-0">
          <div className="px-4 sm:px-6 py-4 space-y-4">
            {/* Backup info — uniform grid, with backend-specific
                rows mixed in only when they carry data. Backend +
                encryption badges live here (instead of the header,
                where they used to overlap the close button). */}
            <section className="rounded-md border border-border bg-background/40 p-3 space-y-1 text-xs">
              <div className="flex items-center justify-between gap-2 flex-wrap">
                <div className="text-[10px] uppercase tracking-wider text-muted-foreground">{t("backup.archives.backup")}</div>
                <div className="flex items-center gap-1.5">
                  {archive && (
                    <Badge variant="outline" className={`text-[10px] uppercase tracking-wide ${
                      archive.source === "pbs"
                        ? "text-purple-400 border-purple-500/40 bg-purple-500/10"
                        : archive.source === "borg"
                          ? "text-fuchsia-400 border-fuchsia-500/40 bg-fuchsia-500/10"
                          : "text-blue-400 border-blue-500/40 bg-blue-500/10"
                    }`}>
                      {archive.source}
                    </Badge>
                  )}
                  {remoteArc?.encrypted && (
                    <Badge
                      variant="outline"
                      className="text-emerald-400 border-emerald-500/40 bg-emerald-500/10"
                      title={t("backup.archives.encrypted")}
                    >
                      <Lock className="h-3.5 w-3.5" />
                    </Badge>
                  )}
                </div>
              </div>
              <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-1">
                {/* Time + size — present for every backend. */}
                {archive && (archive.source === "pbs" || archive.source === "borg") && remoteArc ? (
                  <>
                    <div><span className="text-muted-foreground">{t("backup.archives.backupTimeLabel")}</span> {formatMtime(remoteArc.backup_time)}</div>
                    {remoteArc.size_bytes > 0 && <div><span className="text-muted-foreground">{t("backup.fields.sizeLabel")}</span> {formatBytes(remoteArc.size_bytes)}</div>}
                    <div><span className="text-muted-foreground">{t("backup.fields.repositoryLabel")}</span> <code className="font-mono break-all">{remoteArc.repo_repository}</code></div>
                    <div><span className="text-muted-foreground">{t("backup.fields.repoNameLabel")}</span> <code className="font-mono">{remoteArc.repo_name}</code></div>
                    <div>
                      <span className="text-muted-foreground">{remoteArc.backend === "pbs" ? t("backup.fields.backupGroupLabel") : t("backup.fields.archiveNameLabel")}</span>{" "}
                      <code className="font-mono break-all">{remoteArc.backend === "pbs" ? `${remoteArc.backup_type}/${remoteArc.backup_id}` : remoteArc.backup_id}</code>
                    </div>
                    {remoteArc.owner && <div><span className="text-muted-foreground">{t("backup.fields.ownerLabel")}</span> <code className="font-mono">{remoteArc.owner}</code></div>}
                    {remoteArc.borg_id && <div className="sm:col-span-2"><span className="text-muted-foreground">{t("backup.fields.borgIdLabel")}</span> <code className="font-mono text-[10px] break-all">{remoteArc.borg_id}</code></div>}
                  </>
                ) : localArc ? (
                  <>
                    <div><span className="text-muted-foreground">{t("backup.fields.createdLabel")}</span> {formatMtime(localArc.mtime)}</div>
                    <div><span className="text-muted-foreground">{t("backup.fields.sizeLabel")}</span> {formatBytes(localArc.size_bytes)}</div>
                    <div className="sm:col-span-2"><span className="text-muted-foreground">{t("backup.fields.pathLabel")}</span> <code className="font-mono break-all">{localArc.path}</code></div>
                    {localArc.job_id && <div><span className="text-muted-foreground">{t("backup.fields.jobIdLabel")}</span> <code className="font-mono">{localArc.job_id}</code></div>}
                    {localArc.profile && <div><span className="text-muted-foreground">{t("backup.fields.profileLabel")}</span> <code className="font-mono">{localArc.profile}</code></div>}
                    {localArc.source_hostname && <div><span className="text-muted-foreground">{t("backup.fields.sourceHostLabel")}</span> <code className="font-mono">{localArc.source_hostname}</code></div>}
                    <div><span className="text-muted-foreground">{t("backup.fields.detectedViaLabel")}</span> <code className="font-mono text-[10px]">{localArc.detected_via}</code></div>
                  </>
                ) : null}
              </div>
              {/* PBS pxar files list — only PBS exposes this. */}
              {remoteArc?.files && remoteArc.files.length > 0 && (
                <div className="pt-2 mt-2 border-t border-border/50">
                  <div className="text-[10px] uppercase tracking-wider text-muted-foreground mb-1">{t("backup.archives.filesInBackup")}</div>
                  <ul className="space-y-0.5">
                    {remoteArc.files.map((f) => (
                      <li key={f.filename} className="font-mono text-[11px] flex items-center justify-between gap-2">
                        <span>{f.filename}</span>
                        <span className="text-muted-foreground">{formatBytes(f.size)}</span>
                      </li>
                    ))}
                  </ul>
                </div>
              )}
            </section>

            {/* Run log — only Local has a co-located <stem>.log. */}
            {archive?.source === "local" && archiveLog && archiveLog.log_path && archiveLog.tail.length > 0 && (
              <section className="space-y-2">
                <h4 className="text-xs font-semibold uppercase tracking-wide flex items-center gap-1.5 text-green-500">
                  <FileText className="h-3.5 w-3.5" /> {t("backup.logs.runLog")}
                </h4>
                <div className="rounded-md border border-border bg-background/60 p-2">
                  <pre className="text-[11px] font-mono whitespace-pre-wrap break-all max-h-48 overflow-auto text-foreground/90">
{archiveLog.tail.join("\n")}
                  </pre>
                  <div className="flex items-center justify-between mt-2 gap-2 flex-wrap">
                    <span className="text-[10px] text-muted-foreground inline-flex items-center gap-2 min-w-0">
                      <span>{t("backup.logs.tail")} · {formatBytes(archiveLog.size)}</span>
                      <span className="font-mono break-all">{archiveLog.log_path}</span>
                    </span>
                    <Button size="sm" variant="ghost" className="h-7 text-xs shrink-0" onClick={() => setShowArchiveFullLog(true)}>
                      <FileText className="h-3.5 w-3.5 mr-1" />
                      {t("backup.logs.openFull")}
                    </Button>
                  </div>
                </div>
              </section>
            )}

            {/* In-flight export task feedback (Download for PBS/Borg). */}
            {exportTask && (
              <div className="text-[11px] space-y-1 px-3 py-2 rounded-md border border-border bg-background/40">
                <div className="flex items-center gap-2">
                  <Loader2 className={`h-3.5 w-3.5 ${exportTask.state === "completed" || exportTask.state === "failed" ? "" : "animate-spin"}`} />
                  <span className="font-medium">{t(`backup.taskStates.${exportTask.state}`)}</span>
                  <span className="text-muted-foreground">— {exportTask.message}</span>
                </div>
                {exportTask.state === "failed" && exportTask.error && (
                  <div className="text-red-500 mt-1">{exportTask.error}</div>
                )}
                {exportTask.state === "completed" && exportTask.size_bytes > 0 && (
                  <div className="text-emerald-400">{t("backup.archives.packedSizeLabel")} {formatBytes(exportTask.size_bytes)}</div>
                )}
              </div>
            )}

            {error && (() => {
              const kf = parseKeyfileError(error)
              return kf ? (
                <KeyfileErrorBlock err={kf} />
              ) : (
                <div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
                  {error}
                </div>
              )
            })()}

            {/* Encrypted-backup gate: no local keyfile → block the
                three actions until the operator imports the key. The
                import lands at the canonical path with escrow_mode=
                'none', so subsequent runs reuse it silently. */}
            {needsKeyfile && (
              <section className="rounded-md border border-amber-500/40 bg-amber-500/5 p-3 space-y-2 text-xs">
                <div className="flex items-start gap-2">
                  <AlertTriangle className="h-4 w-4 text-amber-400 mt-0.5 shrink-0" />
                  <div className="flex-1 space-y-1">
                    <div className="font-semibold text-amber-300">{t("backup.keyfileGate.title")}</div>
                    <div className="text-muted-foreground leading-relaxed">
                      {t("backup.keyfileGate.descriptionBefore")}
                      {" "}<code className="font-mono text-[10.5px]">/usr/local/share/proxmenux/pbs-key.conf</code>.
                      {" "}{t("backup.keyfileGate.descriptionAfter")}
                    </div>
                  </div>
                </div>
                {pveMatchInspect && (
                  <div className="rounded-md border border-emerald-500/40 bg-emerald-500/10 p-2.5 space-y-1.5 text-[11px]">
                    <div className="flex items-start gap-2">
                      <Lock className="h-3.5 w-3.5 text-emerald-400 mt-0.5 shrink-0" />
                      <div className="flex-1 space-y-1">
                        <div className="font-semibold text-emerald-300">{t("backup.keyfileActions.pveKeyDetected")}</div>
                        <div className="text-muted-foreground">
                          {t("backup.keyfileActions.pveKeyDescriptionBefore")} <code className="font-mono text-[10.5px]">{pveMatchInspect.name}</code> {t("backup.keyfileActions.pveKeyDescriptionMiddle")}{" "}
                          <code className="font-mono text-[10.5px] break-all">{pveMatchInspect.path}</code>. {t("backup.keyfileActions.pveKeyDescriptionAfter")}
                        </div>
                      </div>
                    </div>
                    <div className="flex justify-end">
                      <Button
                        size="sm"
                        onClick={() => {
                          setImportPath(pveMatchInspect.path)
                          setImportFile(null)
                        }}
                        disabled={importing}
                        className="!bg-emerald-600 hover:!bg-emerald-700 !text-white h-6 text-[10.5px] px-2"
                      >
                        {t("backup.keyfileActions.useThisKey")}
                      </Button>
                    </div>
                  </div>
                )}
                <div className="space-y-1.5 pt-1 border-t border-amber-500/30">
                  <Label htmlFor="keyfileImport" className="text-[11px] font-medium">
                    {pveMatchInspect ? t("backup.keyfileActions.orUploadFromMachine") : t("backup.keyfileActions.uploadFromMachine")}
                  </Label>
                  <Input
                    id="keyfileImport"
                    type="file"
                    accept=".conf,.key,application/json,text/plain"
                    onChange={(e) => setImportFile(e.target.files?.[0] ?? null)}
                    disabled={importing || !!importPath.trim()}
                    className="h-8 text-[11px]"
                  />
                </div>
                <div className="text-[10px] text-center text-muted-foreground">{t("backup.common.or")}</div>
                <div className="space-y-1.5">
                  <Label htmlFor="keyfileImportPath" className="text-[11px] font-medium">{t("backup.fields.absolutePathOnHost")}</Label>
                  <Input
                    id="keyfileImportPath"
                    type="text"
                    value={importPath}
                    onChange={(e) => setImportPath(e.target.value)}
                    disabled={importing || !!importFile}
                    placeholder={t("backup.placeholders.keyfilePath")}
                    className="h-8 text-[11px] font-mono"
                  />
                </div>
                {importError && (
                  <div className="text-red-500 px-2 py-1.5 rounded border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
                    {importError}
                  </div>
                )}
                <div className="flex justify-end">
                  <Button
                    size="sm"
                    onClick={runImportKeyfile}
                    disabled={importing || (!importFile && !importPath.trim())}
                    className="bg-amber-500/20 border border-amber-500/40 text-amber-300 hover:bg-amber-500/30"
                    variant="outline"
                  >
                    {importing ? (
                      <Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
                    ) : (
                      <Lock className="h-3.5 w-3.5 mr-1.5" />
                    )}
                    {t("backup.actions.importKeyfile")}
                  </Button>
                </div>
              </section>
            )}
          </div>
        </ScrollArea>

        {/* ── Footer: same 4 buttons for the 3 backends ─────────
            Restore (green) · Download (blue) · View contents (blue)
            on the left · Delete (red) on the right. On mobile only
            Restore keeps its label — the others are icon-only to fit
            the narrower viewport without wrapping. */}
        <div className="px-4 sm:px-6 py-3 border-t border-border shrink-0 flex items-center justify-between gap-2">
          <div className="flex items-center gap-2 flex-wrap">
            <Button
              onClick={beginRestore}
              disabled={restorePreparing || needsKeyfile}
              className="bg-green-600 hover:bg-green-700 text-white disabled:opacity-50"
              title={needsKeyfile ? t("backup.archives.importKeyToRestoreTitle") : t("backup.archives.restoreTitle")}
            >
              {restorePreparing ? (
                <Loader2 className="h-4 w-4 sm:mr-2 animate-spin" />
              ) : (
                <DatabaseBackup className="h-4 w-4 sm:mr-2" />
              )}
              <span>{t("backup.actions.restore")}</span>
            </Button>
            <Button
              onClick={downloadArchive}
              disabled={downloading || needsKeyfile}
              className="bg-blue-500/10 border border-blue-500/40 text-blue-400 hover:bg-blue-500/20 hover:text-blue-300 disabled:opacity-50"
              variant="outline"
              title={needsKeyfile ? t("backup.archives.importKeyToDownloadTitle") : t("backup.archives.downloadTitle")}
            >
              {downloading ? (
                <Loader2 className="h-4 w-4 sm:mr-2 animate-spin" />
              ) : (
                <Download className="h-4 w-4 sm:mr-2" />
              )}
              <span className="hidden sm:inline">{t("backup.actions.download")}</span>
            </Button>
            <Button
              onClick={() => setViewingContents(true)}
              disabled={needsKeyfile}
              className="bg-blue-500/10 border border-blue-500/40 text-blue-400 hover:bg-blue-500/20 hover:text-blue-300 disabled:opacity-50"
              variant="outline"
              title={needsKeyfile ? t("backup.archives.importKeyToViewTitle") : t("backup.archives.viewContentsTitle")}
            >
              <Eye className="h-4 w-4 sm:mr-2" />
              <span className="hidden sm:inline">{t("backup.actions.viewContents")}</span>
            </Button>
          </div>
          <Button
            onClick={() => setShowDeleteArchiveConfirm(true)}
            disabled={deletingArchive}
            className="bg-red-500/10 border border-red-500/40 text-red-400 hover:bg-red-500/20 hover:text-red-300"
            variant="outline"
            title={
              archive?.source === "local"
                ? t("backup.archives.deleteLocalTitle")
                : archive?.source === "pbs"
                ? t("backup.archives.deletePbsTitle")
                : t("backup.archives.deleteBorgTitle")
            }
          >
            {deletingArchive ? (
              <Loader2 className="h-4 w-4 sm:mr-2 animate-spin" />
            ) : (
              <Trash2 className="h-4 w-4 sm:mr-2" />
            )}
            <span className="hidden sm:inline">{t("backup.actions.delete")}</span>
          </Button>
        </div>
      </DialogContent>
    </Dialog>

    {/* ── View contents — extract + parse the snapshot, show HTML ──── */}
    <ArchiveContentsModal
      open={viewingContents}
      onClose={() => setViewingContents(false)}
      source={(archive?.source as "pbs" | "borg" | "local" | null) ?? null}
      repo_name={remoteArc?.repo_name}
      snapshot={remoteArc?.snapshot}
      path={localArc?.path}
      display_id={archive?.display_id}
    />

    {/* ── Restore error toast (prepare failed) ─────────────────── */}
    {restoreError && (() => {
      const kf = parseKeyfileError(restoreError)
      return (
        <Dialog open={true} onOpenChange={() => setRestoreError(null)}>
          <DialogContent className="max-w-md bg-card border-border">
            <DialogHeader>
              <DialogTitle className="text-base flex items-center gap-2">
                <AlertTriangle className="h-5 w-5 text-red-500" />
                {kf ? t("backup.restore.blockedEncryptedTitle") : t("backup.restore.preparationFailedTitle")}
              </DialogTitle>
              {!kf && (
                <DialogDescription className="text-xs text-red-400 break-all">
                  {restoreError}
                </DialogDescription>
              )}
            </DialogHeader>
            {kf && <KeyfileErrorBlock err={kf} />}
            <div className="flex justify-end">
              <Button variant="ghost" onClick={() => setRestoreError(null)}>{t("actions.close")}</Button>
            </div>
          </DialogContent>
        </Dialog>
      )
    })()}

    {/* ── Restore options modal: opens only AFTER the prepare step,
        so the Custom checklist already knows what's inside. ──── */}
    <RestoreOptionsModal
      open={restoreOptions !== null}
      stagingPath={restoreOptions?.stagingPath ?? ""}
      pathsAvailable={restoreOptions?.pathsAvailable ?? []}
      rollbackPlan={restoreOptions?.rollbackPlan}
      crossKernel={restoreOptions?.crossKernel}
      hydration={restoreOptions?.hydration}
      display_id={archive?.display_id}
      onClose={() => {
        const sp = restoreOptions?.stagingPath
        setRestoreOptions(null)
        if (sp) {
          fetchApi("/api/host-backups/restore/cleanup", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ staging_path: sp }),
          }).catch(() => undefined)
        }
      }}
      onLaunch={(mode, paths, rollbackExecute) => {
        if (!restoreOptions) return
        const sp = restoreOptions.stagingPath
        setRestoreOptions(null)
        setRestoreTerminal({ stagingPath: sp, mode, paths, rollbackExecute })
      }}
    />

    {/* ── Restore terminal — uses the canonical ScriptTerminalModal
        (the same component Hardware/Security/Settings use to run
        their scripts). Stable `key` per stagingPath so each restore
        session is a fresh terminal. */}
    {restoreTerminal && (
      <ScriptTerminalModal
        key={restoreTerminal.stagingPath}
        open={true}
        onClose={() => {
          const sp = restoreTerminal.stagingPath
          fetchApi("/api/host-backups/restore/cleanup", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ staging_path: sp }),
          }).catch(() => undefined)
          setRestoreTerminal(null)
        }}
        // Auto-dismiss when the script's PTY closes (i.e. the bash
        // wrapper exited cleanly — the operator pressed Enter at the
        // "Press Enter to close" prompt). Without this the operator
        // would have to click Close manually after each restore.
        onComplete={() => {
          const sp = restoreTerminal.stagingPath
          fetchApi("/api/host-backups/restore/cleanup", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ staging_path: sp }),
          }).catch(() => undefined)
          setRestoreTerminal(null)
        }}
        scriptPath="/usr/local/share/proxmenux/scripts/backup_restore/restore/monitor_apply.sh"
        scriptName="monitor_apply"
        title={t("backup.restore.terminalTitle", { mode: restoreTerminal.mode === "full" ? t("backup.restore.complete") : t("backup.restore.customByPaths") })}
        description={
          restoreTerminal.mode === "custom"
            ? t("backup.restore.pathsSelected", { count: restoreTerminal.paths.length })
            : t("backup.restore.completeDescription")
        }
        params={{
          EXECUTION_MODE: "web",
          STAGING: restoreTerminal.stagingPath,
          MODE: restoreTerminal.mode,
          ...(restoreTerminal.mode === "custom" && restoreTerminal.paths.length > 0
            ? { PATHS: restoreTerminal.paths.join(",") }
            : {}),
          ...(restoreTerminal.rollbackExecute ? { ROLLBACK_EXECUTE: "1" } : {}),
        }}
      />
    )}

    {/* ── Confirm archive deletion ───────────────────────────── */}
    <Dialog open={showDeleteArchiveConfirm} onOpenChange={setShowDeleteArchiveConfirm}>
      <DialogContent className="max-w-md bg-card border-border">
        <DialogHeader>
          <DialogTitle className="text-base flex items-center gap-2">
            <AlertTriangle className="h-5 w-5 text-red-500" />
            {t("backup.archives.deleteBackendTitle", { backend: backendLabel })}
          </DialogTitle>
          <DialogDescription className="text-xs">
            {archive?.source === "local"
              ? t("backup.archives.deleteLocalDescription")
              : archive?.source === "pbs"
              ? t("backup.archives.deletePbsDescription", { repo: remoteArc?.repo_name ?? "" })
              : t("backup.archives.deleteBorgDescription", { repo: remoteArc?.repo_name ?? "" })}
          </DialogDescription>
        </DialogHeader>
        <div className="text-sm font-mono px-3 py-2 rounded-md border border-border bg-background/40 break-all">
          {archive?.source === "local" ? localArc?.id : remoteArc?.snapshot}
        </div>
        <div className="flex justify-end gap-2">
          <Button variant="ghost" onClick={() => setShowDeleteArchiveConfirm(false)} disabled={deletingArchive}>
            {t("actions.cancel")}
          </Button>
          <Button variant="destructive" onClick={deleteArchive} disabled={deletingArchive}>
            {deletingArchive ? (
              <Loader2 className="h-4 w-4 mr-2 animate-spin" />
            ) : (
              <Trash2 className="h-4 w-4 mr-2" />
            )}
            {t("backup.actions.delete")}
          </Button>
        </div>
      </DialogContent>
    </Dialog>

    {/* ── Full run log viewer ─────────────────────────────────── */}
    <Dialog open={showArchiveFullLog} onOpenChange={setShowArchiveFullLog}>
      <DialogContent className="max-w-4xl bg-card border-border">
        <DialogHeader>
          <DialogTitle className="text-base flex items-center gap-2">
            <FileText className="h-5 w-5 text-blue-500" />
            {t("backup.logs.runLog")}
          </DialogTitle>
          <DialogDescription className="text-xs font-mono break-all">
            {archiveLog?.log_path}
          </DialogDescription>
        </DialogHeader>
        <pre className="text-[11px] font-mono whitespace-pre-wrap break-all max-h-[60vh] overflow-auto rounded-md border border-border bg-background/60 p-3 text-foreground/90">
{archiveLog?.content ?? ""}
        </pre>
        <div className="flex justify-end">
          <Button variant="ghost" onClick={() => setShowArchiveFullLog(false)}>{t("actions.close")}</Button>
        </div>
      </DialogContent>
    </Dialog>
    </>
  )
}

// ── Manifest summary panel ───────────────────────────────────
function ManifestSummary({
  manifest,
}: {
  manifest: {
    source_host: ManifestSourceHost
    proxmenux_installed_components: Array<{ id: string; version_at_backup: string | null }>
    vms_lxcs_at_backup: { vms: unknown[]; lxcs: unknown[] }
    storage_inventory?: { zfs_pools?: unknown[]; lvm?: { vgs?: unknown[] } }
  }
}) {
  const t = useT()
  const sh = manifest.source_host
  const zfsCount = manifest.storage_inventory?.zfs_pools?.length ?? 0
  const lvmCount = manifest.storage_inventory?.lvm?.vgs?.length ?? 0
  return (
    <div className="space-y-3 py-2">
      <div className="grid grid-cols-2 sm:grid-cols-3 gap-3 text-xs">
        <Field icon={<Server className="h-3.5 w-3.5" />} label={t("backup.fields.sourceHost")} value={sh.hostname} />
        <Field label={t("backup.fields.pveVersion")} value={sh.pve_version || "—"} />
        <Field label={t("backup.fields.roles")} value={sh.roles.join(", ")} />
        <Field label={t("backup.fields.kernel")} value={sh.kernel} mono />
        <Field label={t("backup.fields.bootMode")} value={sh.boot_mode} />
        <Field label={t("backup.fields.memory")} value={`${Math.round(sh.memory_kb / 1024)} MB`} />
        <Field label={t("backup.fields.zfsPools")} value={String(zfsCount)} />
        <Field label={t("backup.fields.lvmVgs")} value={String(lvmCount)} />
        <Field label={t("backup.fields.vmsLxcs")} value={`${manifest.vms_lxcs_at_backup.vms.length} VM / ${manifest.vms_lxcs_at_backup.lxcs.length} LXC`} />
      </div>
      {manifest.proxmenux_installed_components.length > 0 && (
        <div className="text-xs">
          <div className="text-muted-foreground mb-1">{t("backup.manifest.componentsAtBackup")}</div>
          <div className="flex flex-wrap gap-1.5">
            {manifest.proxmenux_installed_components.map((c) => (
              <Badge key={c.id} variant="outline" className="font-mono text-[10px]">
                {c.id}{c.version_at_backup ? ` @ ${c.version_at_backup}` : ""}
              </Badge>
            ))}
          </div>
        </div>
      )}
    </div>
  )
}

function Field({ icon, label, value, mono, labelClassName }: { icon?: React.ReactNode; label: string; value: string; mono?: boolean; labelClassName?: string }) {
  return (
    <div className="min-w-0">
      <div className={`text-[10px] uppercase tracking-wider flex items-center gap-1 ${labelClassName ?? "text-muted-foreground"}`}>
        {icon}
        {label}
      </div>
      <div className={`${mono ? "font-mono break-all" : "break-words"}`} title={value}>
        {value}
      </div>
    </div>
  )
}

// Renders the retention policy as a row of labeled badges. The runner's
// .env stores six discrete keys (KEEP_LAST / HOURLY / DAILY / WEEKLY /
// MONTHLY / YEARLY); the original UI dumped them as a comma-separated
// `key=val, key=val…` string which read like a config file. This view
// drops zero-valued entries and presents what survives as ordered chips.
function RetentionDisplay({ retention }: { retention: Record<string, string | undefined> }) {
  const t = useT()
  const order: Array<[string, string]> = [
    ["keep_last", "backup.retention.last"],
    ["keep_hourly", "backup.retention.hourly"],
    ["keep_daily", "backup.retention.daily"],
    ["keep_weekly", "backup.retention.weekly"],
    ["keep_monthly", "backup.retention.monthly"],
    ["keep_yearly", "backup.retention.yearly"],
  ]
  const items = order
    .map(([k, lbl]) => {
      const v = (retention[k] || "").trim()
      const n = Number.parseInt(v, 10)
      if (!v || !Number.isFinite(n) || n <= 0) return null
      return { label: lbl, value: n }
    })
    .filter((x): x is { label: string; value: number } => x !== null)

  return (
    <div>
      <div className="text-[10px] uppercase tracking-wider flex items-center gap-1 text-green-500/90">
        <Archive className="h-3 w-3 text-green-500/80" /> {t("backup.retention.title")}
      </div>
      {items.length === 0 ? (
        <div className="text-muted-foreground italic text-xs mt-1">{t("backup.retention.noneAccumulate")}</div>
      ) : (
        <div className="mt-1 flex flex-wrap gap-1.5">
          {items.map((it) => (
            <span
              key={it.label}
              className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded border border-border bg-background/60"
            >
              <span className="text-[10px] uppercase tracking-wide text-muted-foreground">{t(it.label)}</span>
              <span className="font-mono text-xs text-foreground">{it.value}</span>
            </span>
          ))}
        </div>
      )}
    </div>
  )
}

// Renders the backup profile paths as a 2-column grid of monospaced
// rows instead of a single ` · `-separated line. With ~30 paths the
// old layout forced a horizontal scroll on the dialog because the
// line couldn't be wrapped without breaking the path identifiers.
function PathsDisplay({ paths }: { paths: string[] }) {
  const t = useT()
  return (
    <div>
      <div className="text-[10px] uppercase tracking-wider flex items-center gap-1 text-green-500/90">
        <HardDrive className="h-3 w-3 text-green-500/80" /> {t("backup.paths.title")}
        <span className="ml-1 text-muted-foreground normal-case">({paths.length})</span>
      </div>
      <div className="mt-1 grid grid-cols-1 sm:grid-cols-2 gap-x-3 gap-y-0.5">
        {paths.map((p) => (
          <code
            key={p}
            className="font-mono text-[11px] text-foreground/90 truncate"
            title={p}
          >
            {p}
          </code>
        ))}
      </div>
    </div>
  )
}

// ── Preflight report view ────────────────────────────────────
function PreflightReportView({ report }: { report: PreflightReport }) {
  const t = useT()
  const { summary, checks } = report.preflight
  const passColor = "text-emerald-500"
  const warnColor = "text-amber-500"
  const failColor = "text-red-500"

  return (
    <div className="space-y-3 border border-border rounded-md p-3 bg-muted/30">
      {/* Summary line */}
      <div className="flex items-center gap-4 text-sm">
        <span className={`inline-flex items-center gap-1 ${passColor}`}>
          <CheckCircle2 className="h-4 w-4" />
          {t("backup.preflight.passCount", { count: summary.pass })}
        </span>
        <span className={`inline-flex items-center gap-1 ${warnColor}`}>
          <AlertTriangle className="h-4 w-4" />
          {t("backup.preflight.warnCount", { count: summary.warn })}
        </span>
        <span className={`inline-flex items-center gap-1 ${failColor}`}>
          <XCircle className="h-4 w-4" />
          {t("backup.preflight.failCount", { count: summary.fail })}
        </span>
        {summary.fail > 0 && (
          <span className="ml-auto text-xs text-red-500">
            {t("backup.preflight.applyWouldBeRefused")}
          </span>
        )}
      </div>

      {/* Per-check list */}
      <div className="space-y-1.5">
        {checks.map((c) => {
          const color =
            c.severity === "pass" ? passColor :
            c.severity === "warn" ? warnColor :
            failColor
          const Icon =
            c.severity === "pass" ? CheckCircle2 :
            c.severity === "warn" ? AlertTriangle :
            XCircle
          return (
            <div key={c.id} className="flex items-start gap-2 text-xs">
              <Icon className={`h-3.5 w-3.5 ${color} flex-shrink-0 mt-0.5`} />
              <div className="flex-1 min-w-0">
                <span className={`font-mono ${color}`}>{c.id}</span>
                <span className="text-muted-foreground ml-2">{c.message}</span>
              </div>
            </div>
          )
        })}
      </div>

      {/* Storage / network counts */}
      <div className="grid grid-cols-2 gap-3 text-xs pt-2 border-t border-border/40">
        <div>
          <div className="text-muted-foreground mb-1">{t("backup.preflight.storageInMode", { mode: String(report.storage.in_selected_mode) })}</div>
          <div>
            {t("backup.preflight.zfsPoolsCount", { count: report.storage.zfs.length })} ·
            {" "}{t("backup.preflight.lvmVgsCount", { count: report.storage.lvm.length })} ·
            {" "}{t("backup.preflight.pveStorageCount", { count: report.storage.pve_storage.length })}
          </div>
        </div>
        <div>
          <div className="text-muted-foreground mb-1">{t("backup.preflight.networkInMode", { mode: String(report.network.in_selected_mode) })}</div>
          <div>
            {t("backup.preflight.keepCount", { count: report.network.keep.length })} ·
            {" "}{t("backup.preflight.remapCount", { count: report.network.remap.length })} ·
            {" "}{t("backup.preflight.orphanCount", { count: report.network.orphan.length })} ·
            {" "}{t("backup.preflight.newCount", { count: report.network.new.length })}
          </div>
        </div>
      </div>

      {/* Driver plan */}
      {report.driver_reinstall.plan.length > 0 && (
        <div className="text-xs pt-2 border-t border-border/40">
          <div className="text-muted-foreground mb-1.5">{t("backup.preflight.driverReinstallPlan", { count: report.driver_reinstall.plan.length })}</div>
          <div className="space-y-1">
            {report.driver_reinstall.plan.map((p) => (
              <div key={p.component_id} className="flex items-center justify-between gap-2">
                <span className="font-mono">{p.component_id}</span>
                <Badge variant="outline" className="text-[10px]">{p.action}</Badge>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Abort reason (if --apply would have been refused) */}
      {report.abort_reason && (
        <div className="text-xs text-red-500 p-2 border border-red-500/30 rounded-md bg-red-500/10">
          {report.abort_reason}
        </div>
      )}
    </div>
  )
}

// ──────────────────────────────────────────────────────────────
// Create job wizard (A3.1 — attached jobs only).
//
// Pre-loads three things in parallel so the form can render selects
// the moment the operator opens it:
//   - PVE vzdump jobs (filtered by backend once they pick one)
//   - Pre-configured destinations (PBS repos, Borg targets, local)
//   - Default profile path list (used when profile_mode === "default"
//     and also as the pool the custom checklist is populated from)
// ──────────────────────────────────────────────────────────────

interface PveVzdumpJob {
  id: string
  storage: string | null
  storage_type: string | null
  schedule: string | null
  prune: string | null
  enabled: boolean
}

// All Destination shapes carry `jobs_using`: list of job_ids that
// currently depend on the destination. Surfaced in the Delete-
// destination confirm flow so the operator sees the cascade impact
// before agreeing.

interface PbsRepo {
  name: string
  repository: string
  fingerprint: string | null
  source: "proxmox" | "manual"
  jobs_using?: string[]
}

interface BorgRepo {
  name: string
  repository: string
  ssh_key_path?: string
  // Encryption + saved-passphrase metadata. Newer backends ship these;
  // older deployments without the fields default to "repokey" (the
  // shell installer's historical default) and unknown-passphrase.
  encrypt_mode?: "none" | "repokey" | "keyfile" | "authenticated"
  has_passphrase?: boolean
  jobs_using?: string[]
}

interface LocalTargetEntry {
  path: string
  source: "default" | "custom"
  removable: boolean
  jobs_using?: string[]
}

interface LocalTarget {
  configured: string | null
  default: string
  effective: string
  // Multi-path layout: the default plus any custom paths stacked on
  // top. Present on every recent backend; older deployments without
  // this field fall back to the single configured/effective accessors.
  entries?: LocalTargetEntry[]
}

interface DestinationsResp {
  pbs: PbsRepo[]
  borg: BorgRepo[]
  local: LocalTarget
}

interface JobDetail {
  id: string
  attached: boolean
  enabled: boolean
  manual: boolean
  method: string
  destination: string
  on_calendar: string             // human-formatted ("attached → storage:pbs" or "*-*-* 02:00")
  next_run: string | null
  pve_storage: string | null
  pve_parent_job_id: string | null
  profile_mode: string
  paths: string[]
  on_calendar_raw: string | null
  retention: Record<string, string | undefined>
  pbs_repository: string | null
  pbs_backup_id: string | null
  pbs_fingerprint: string | null
  has_pbs_password: boolean
  // Legacy — kept for backend responses that only track the on/off
  // toggle. Newer builds also send pbs_encrypt_mode ("none" | "new"
  // | "existing"); loadFromJobDetail prefers that when present.
  pbs_encrypt: boolean
  pbs_encrypt_mode?: "none" | "new" | "existing"
  local_dest_dir: string | null
  local_archive_ext: string | null
  borg_repo: string | null
  borg_encrypt_mode: string
  has_borg_passphrase: boolean
  // Log fields from Sprint Pulido P1
  last_result: string | null      // "ok" / "failed" / etc.
  last_run_at: string | null      // ISO-8601
  last_log_path: string | null
  last_log_size: number
  last_log_tail: string[]
}

function CreateJobDialog({
  open,
  onClose,
  onCreated,
  editingJobId,
}: {
  open: boolean
  onClose: () => void
  onCreated: () => void
  editingJobId?: string | null
}) {
  const t = useT()
  const isEdit = !!editingJobId
  const [step, setStep] = useState<1 | 2 | 3 | 4 | 5>(1)
  const [jobId, setJobId] = useState("")
  const [backend, setBackend] = useState<"pbs" | "local" | "borg">("pbs")
  const [mode, setMode] = useState<"new" | "attach">("new")
  const [pveJobId, setPveJobId] = useState<string>("")
  const [profileMode, setProfileMode] = useState<"default" | "custom">("default")
  const [customPaths, setCustomPaths] = useState<Set<string>>(new Set())

  // New (timer-based) mode fields — onCalendar is computed from the
  // builder controls below; the raw expression is only directly edited
  // when scheduleType === "advanced".
  const [scheduleType, setScheduleType] = useState<"daily" | "hourly" | "weekly" | "monthly" | "advanced">("daily")
  const [scheduleTime, setScheduleTime] = useState<string>("02:00")
  const [scheduleMinute, setScheduleMinute] = useState<string>("0")
  const [scheduleWeekdays, setScheduleWeekdays] = useState<Set<string>>(new Set(["Sun"]))
  const [scheduleDayOfMonth, setScheduleDayOfMonth] = useState<string>("1")
  const [scheduleAdvanced, setScheduleAdvanced] = useState<string>("daily")
  const [keepLast, setKeepLast] = useState<string>("7")
  const [keepHourly, setKeepHourly] = useState<string>("0")
  const [keepDaily, setKeepDaily] = useState<string>("7")
  const [keepWeekly, setKeepWeekly] = useState<string>("4")
  const [keepMonthly, setKeepMonthly] = useState<string>("3")
  const [keepYearly, setKeepYearly] = useState<string>("0")

  // Backend-specific fields
  const [pbsRepository, setPbsRepository] = useState<string>("")
  const [pbsBackupId, setPbsBackupId] = useState<string>("")
  // Client-side PBS encryption mode. Three modes match the shell wizard:
  //   - "none"     — plain backup, no --keyfile
  //   - "new"      — backend generates a fresh per-host keyfile (default)
  //   - "existing" — operator uploads their own keyfile (shared across
  //                  hosts). The upload happens via a dedicated endpoint
  //                  BEFORE the job create call — see handleCreate below.
  const [pbsEncryptMode, setPbsEncryptMode] = useState<"none" | "new" | "existing">("none")
  // File picked in the "existing" mode. Kept in a ref-like state so a
  // stale reference doesn't linger across modal opens.
  const [pbsImportFile, setPbsImportFile] = useState<File | null>(null)
  // Absolute-path alternative to the file upload — mirrors the shell
  // wizard's `_hb_pbs_import_dialog` when there is no PVE .enc match.
  const [pbsImportPath, setPbsImportPath] = useState<string>("")
  const [pbsImportBusy, setPbsImportBusy] = useState<boolean>(false)
  // Legacy alias for the many recovery / gating checks below — a truthy
  // value means "the operator wants an encrypted backup" regardless of
  // whether that keyfile came from a fresh generate or from an import.
  const pbsEncrypt = pbsEncryptMode !== "none"
  // Optional recovery passphrase. When set, the backend encrypts the
  // keyfile with openssl and the runner uploads that blob to PBS with
  // every backup so the keyfile can be rebuilt on a fresh host with
  // only the passphrase. Mirrors hb_pbs_setup_recovery from the shell.
  const [pbsRecoveryPass, setPbsRecoveryPass] = useState<string>("")
  const [pbsRecoveryPass2, setPbsRecoveryPass2] = useState<string>("")
  // Operator opted to replace an already-configured recovery escrow.
  // Default behavior matches the shell: don't prompt for the
  // passphrase if one is already saved — only show the inputs when
  // there's no escrow yet OR the operator explicitly asks to change.
  const [pbsRecoveryChange, setPbsRecoveryChange] = useState<boolean>(false)
  // Binary "upload key to PBS?" toggle. `true` maps to escrow_mode='full'
  // (passphrase required, envelope uploaded on every backup); `false`
  // maps to escrow_mode='none' (keyfile stays local, operator handles
  // the offsite copy). Defaults to 'No' — the keyfile is always
  // downloadable from the Monitor, so the operator can grab it and
  // save it offsite themselves without leaving a passphrase-wrapped
  // copy on the PBS server.
  const [pbsUploadToPbs, setPbsUploadToPbs] = useState<boolean>(false)
  // Whether the host already has an escrow blob configured. When
  // present, the passphrase input becomes optional ("leave blank to
  // keep saved"). Refreshed after a successful setup call.
  const { data: pbsRecoveryStatus, mutate: mutatePbsRecovery } = useSWR<{
    has_keyfile: boolean; has_recovery: boolean; has_keyfile_passphrase?: boolean; escrow_mode?: "none" | "local" | "full"
  }>(
    open && backend === "pbs" ? "/api/host-backups/pbs-recovery/status" : null,
    fetcher,
  )
  // Auto-discover a PVE-managed keyfile for the currently selected
  // PBS repository. When one is found, the "Use an existing keyfile"
  // radio uses it silently (matches the shell wizard's PVE auto-detect
  // path — the operator doesn't have to upload the same file twice).
  const { data: pbsPveDiscover } = useSWR<{
    entries: Array<{ name: string; server: string; datastore: string; path: string; matches_repository: boolean }>
  }>(
    open && backend === "pbs" && pbsRepository && !pbsRecoveryStatus?.has_keyfile
      ? `/api/host-backups/pbs-encryption/discover-pve-keyfiles?repository=${encodeURIComponent(pbsRepository)}`
      : null,
    fetcher,
  )
  const pbsPveMatch = pbsPveDiscover?.entries?.find((e) => e.matches_repository)
  const [localDestDir, setLocalDestDir] = useState<string>("")
  const [borgRepoSelected, setBorgRepoSelected] = useState<string>("")
  const [borgPassphrase, setBorgPassphrase] = useState<string>("")
  const [borgEncryptMode, setBorgEncryptMode] = useState<"none" | "repokey" | "keyfile" | "authenticated">("repokey")

  const [submitting, setSubmitting] = useState(false)
  const [error, setError] = useState<string | null>(null)

  // Build the canonical OnCalendar string from the builder controls.
  // The dropdown plus the small sub-inputs are friendlier than asking
  // the operator to remember the systemd Calendar Events grammar.
  const padHH = (s: string) => {
    const [hh = "00", mm = "00"] = s.split(":")
    return `${hh.padStart(2, "0")}:${mm.padStart(2, "0")}:00`
  }
  let onCalendar = "daily"
  if (scheduleType === "daily") {
    onCalendar = `*-*-* ${padHH(scheduleTime)}`
  } else if (scheduleType === "hourly") {
    const m = Math.max(0, Math.min(59, Number(scheduleMinute) || 0))
    onCalendar = `*-*-* *:${String(m).padStart(2, "0")}:00`
  } else if (scheduleType === "weekly") {
    const days = Array.from(scheduleWeekdays)
    onCalendar = days.length > 0
      ? `${days.join(",")} *-*-* ${padHH(scheduleTime)}`
      : `*-*-* ${padHH(scheduleTime)}`
  } else if (scheduleType === "monthly") {
    const d = Math.max(1, Math.min(31, Number(scheduleDayOfMonth) || 1))
    onCalendar = `*-*-${String(d).padStart(2, "0")} ${padHH(scheduleTime)}`
  } else if (scheduleType === "advanced") {
    onCalendar = scheduleAdvanced.trim()
  }

  // Edit mode: fetch the full job (paths, retention, backend-specific) once
  // the dialog opens, then pre-populate the form fields on arrival.
  const { data: jobDetail } = useSWR<JobDetail>(
    open && isEdit ? `/api/host-backups/jobs/${encodeURIComponent(editingJobId || "")}` : null,
    fetcher,
  )

  const [editPreloaded, setEditPreloaded] = useState(false)
  // Snapshot of the job's backend/mode at the moment we entered edit
  // mode, so Step 5 can call out a change explicitly ("you're now
  // configuring a Borg destination" etc.) instead of just dropping the
  // operator into an unfamiliar set of fields.
  const [originalBackend, setOriginalBackend] = useState<"pbs" | "local" | "borg" | null>(null)
  const [originalMode, setOriginalMode] = useState<"new" | "attach" | null>(null)
  useEffect(() => {
    if (!isEdit || !jobDetail || editPreloaded) return
    setJobId(jobDetail.id)
    setBackend((jobDetail.method as "pbs" | "local" | "borg") || "pbs")
    setMode(jobDetail.attached ? "attach" : "new")
    setOriginalBackend((jobDetail.method as "pbs" | "local" | "borg") || "pbs")
    setOriginalMode(jobDetail.attached ? "attach" : "new")
    setProfileMode((jobDetail.profile_mode as "default" | "custom") || "default")
    if (jobDetail.profile_mode === "custom") {
      setCustomPaths(new Set(jobDetail.paths))
    }
    if (!jobDetail.attached && jobDetail.on_calendar_raw) {
      setScheduleType("advanced")
      setScheduleAdvanced(jobDetail.on_calendar_raw)
    }
    if (jobDetail.attached && jobDetail.pve_parent_job_id) {
      setPveJobId(jobDetail.pve_parent_job_id)
    }
    const r = jobDetail.retention || {}
    if (r.keep_last !== undefined) setKeepLast(String(r.keep_last))
    if (r.keep_hourly !== undefined) setKeepHourly(String(r.keep_hourly))
    if (r.keep_daily !== undefined) setKeepDaily(String(r.keep_daily))
    if (r.keep_weekly !== undefined) setKeepWeekly(String(r.keep_weekly))
    if (r.keep_monthly !== undefined) setKeepMonthly(String(r.keep_monthly))
    if (r.keep_yearly !== undefined) setKeepYearly(String(r.keep_yearly))
    if (jobDetail.pbs_repository) setPbsRepository(jobDetail.pbs_repository)
    if (jobDetail.pbs_backup_id) setPbsBackupId(jobDetail.pbs_backup_id)
    // Prefer the new pbs_encrypt_mode string when the backend sends it;
    // fall back to the legacy bool for older `.env` layouts that only
    // remember the on/off toggle.
    const jd = jobDetail as unknown as { pbs_encrypt_mode?: string; pbs_encrypt?: boolean }
    if (jd.pbs_encrypt_mode === "new" || jd.pbs_encrypt_mode === "existing" || jd.pbs_encrypt_mode === "none") {
      setPbsEncryptMode(jd.pbs_encrypt_mode)
    } else {
      setPbsEncryptMode(jd.pbs_encrypt ? "new" : "none")
    }
    if (jobDetail.local_dest_dir) setLocalDestDir(jobDetail.local_dest_dir)
    if (jobDetail.borg_repo) setBorgRepoSelected(jobDetail.borg_repo)
    if (jobDetail.borg_encrypt_mode) {
      setBorgEncryptMode(jobDetail.borg_encrypt_mode as "none" | "repokey" | "keyfile" | "authenticated")
    }
    setEditPreloaded(true)
  }, [isEdit, jobDetail, editPreloaded])

  // Reset the preload flag whenever the dialog closes so the next open
  // (potentially for a different job) re-populates fresh.
  useEffect(() => {
    if (!open) {
      setEditPreloaded(false)
      setOriginalBackend(null)
      setOriginalMode(null)
    }
  }, [open])

  const { data: pveJobsResp } = useSWR<{ jobs: PveVzdumpJob[] }>(
    open && mode === "attach" && backend !== "borg" ? `/api/host-backups/pve-vzdump-jobs?backend=${backend}` : null,
    fetcher,
  )

  // Live calendar preview — only fetch when we're actually on Step 3 in
  // "new" mode, otherwise the request is noise.
  const calendarPreviewKey = open && mode === "new" && step === 3 && onCalendar.trim().length > 0
    ? `calendar-preview::${onCalendar}`
    : null
  const { data: calendarPreview } = useSWR<{
    valid: boolean
    error?: string
    normalized?: string
    next_elapse?: string
    from_now?: string
  }>(
    calendarPreviewKey,
    () =>
      fetchApi("/api/host-backups/calendar-preview", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ expr: onCalendar }),
      }),
    { dedupingInterval: 400 },
  )
  const { data: destResp, mutate: mutateDest } = useSWR<DestinationsResp>(
    open ? "/api/host-backups/destinations" : null,
    fetcher,
  )
  const { data: pathsResp } = useSWR<{ paths: string[] }>(
    open ? "/api/host-backups/default-paths" : null,
    fetcher,
  )
  // The wizard can spawn AddDestinationDialog inline so the operator
  // configures a PBS / Borg destination without leaving the flow.
  // When AddDestinationDialog saves, we capture the new repository
  // string and auto-select it on the next destResp refresh — saves the
  // operator from picking what they just configured from the dropdown.
  const [addingDestType, setAddingDestType] = useState<"pbs" | "borg" | "local" | null>(null)
  const [pendingAutoSelectDest, setPendingAutoSelectDest] = useState<{ kind: "pbs" | "borg"; repository: string } | null>(null)

  // When the destinations load, auto-pick the first PBS repo + fill in
  // the fingerprint reference so the operator doesn't have to navigate
  // away just to copy it. Same convenience for the backup-id.
  const selectedPbs: PbsRepo | undefined =
    destResp?.pbs?.find((r) => r.repository === pbsRepository) ?? destResp?.pbs?.[0]

  // Reset state on open / backend change
  useEffect(() => {
    if (!open) {
      setStep(1)
      setJobId("")
      setBackend("pbs")
      setMode("new")
      setPveJobId("")
      setProfileMode("default")
      setCustomPaths(new Set())
      setScheduleType("daily")
      setScheduleTime("02:00")
      setScheduleMinute("0")
      setScheduleWeekdays(new Set(["Sun"]))
      setScheduleDayOfMonth("1")
      setScheduleAdvanced("daily")
      setKeepLast("7")
      setKeepHourly("0")
      setKeepDaily("7")
      setKeepWeekly("4")
      setKeepMonthly("3")
      setKeepYearly("0")
      setPbsRepository("")
      setPbsBackupId("")
      setPbsEncryptMode("none")
      setPbsImportFile(null)
      setPbsImportPath("")
      setPbsImportBusy(false)
      setPbsRecoveryPass("")
      setPbsRecoveryPass2("")
      setPbsRecoveryChange(false)
      setPbsUploadToPbs(false)
      setLocalDestDir("")
      setBorgRepoSelected("")
      setBorgPassphrase("")
      setBorgEncryptMode("repokey")
      setError(null)
      setSubmitting(false)
    }
  }, [open])

  // Borg can only be timer-based; coerce mode back to "new" if the
  // operator picks borg after having selected attach.
  useEffect(() => {
    if (backend === "borg" && mode === "attach") {
      setMode("new")
    }
  }, [backend, mode])

  useEffect(() => {
    setPveJobId("")
  }, [backend, mode])

  useEffect(() => {
    if (backend === "pbs" && !pbsRepository && destResp?.pbs?.length) {
      setPbsRepository(destResp.pbs[0].repository)
    }
    if (backend === "borg" && !borgRepoSelected && destResp?.borg?.length) {
      setBorgRepoSelected(destResp.borg[0].repository)
    }
  }, [backend, destResp, pbsRepository, borgRepoSelected])

  useEffect(() => {
    if (!pendingAutoSelectDest || !destResp) return
    if (pendingAutoSelectDest.kind === "pbs") {
      const hit = destResp.pbs?.find((r) => r.repository === pendingAutoSelectDest.repository)
      if (hit) {
        setPbsRepository(hit.repository)
        setPendingAutoSelectDest(null)
      }
    } else {
      const hit = destResp.borg?.find((r) => r.repository === pendingAutoSelectDest.repository)
      if (hit) {
        setBorgRepoSelected(hit.repository)
        setPendingAutoSelectDest(null)
      }
    }
  }, [destResp, pendingAutoSelectDest])

  const togglePath = (path: string) => {
    setCustomPaths((prev) => {
      const next = new Set(prev)
      if (next.has(path)) next.delete(path)
      else next.add(path)
      return next
    })
  }

  const selectedPveJob: PveVzdumpJob | undefined = pveJobsResp?.jobs?.find((j) => j.id === pveJobId)
  const selectedBorgRepo: BorgRepo | undefined = destResp?.borg?.find((r) => r.repository === borgRepoSelected)

  // ── Step validation gates ──
  const idValid = /^[a-zA-Z0-9_-]+$/.test(jobId) && jobId.length > 0
  const canAdvanceFrom1 = idValid
  const canAdvanceFrom2 = true   // selecting New/Attach is always possible

  // Step 3 depends on mode. For "new" we additionally block advancing
  // if the live calendar preview already came back invalid, or if
  // weekly was picked without any day selected.
  const canAdvanceFrom3 =
    mode === "attach"
      ? !!pveJobId
      : onCalendar.trim().length > 0
        && (calendarPreview === undefined || calendarPreview.valid !== false)
        && (scheduleType !== "weekly" || scheduleWeekdays.size > 0)

  // Step 4: profile
  const canAdvanceFrom4 =
    profileMode === "default" || (profileMode === "custom" && customPaths.size > 0)

  // Step 5 (submit) — backend-specific requirements
  const backendValid =
    backend === "pbs"
      ? !!pbsRepository
      : backend === "local"
        ? true
        : /* borg */ !!borgRepoSelected

  // PBS encryption gate — choosing "Encrypt" makes the recovery
  // passphrase mandatory. The button stays disabled until a matching
  // passphrase pair is entered (or an already-saved escrow is reused).
  // There is no opt-out: encrypting without recovery would leave the
  // keyfile only on this host, and a reinstall would render every
  // encrypted backup unrecoverable.
  // Passphrase requirement only kicks in when: encryption is on AND
  // there's no keyfile installed yet (fresh setup) AND the operator
  // opted to upload the key to PBS. Every other case (reuse existing
  // keyfile, or "no upload" mode) doesn't need a passphrase.
  const pbsRecoveryOk = !(backend === "pbs" && pbsEncrypt) ||
    pbsRecoveryStatus?.has_keyfile ||
    !pbsUploadToPbs ||
    (pbsRecoveryPass !== "" && pbsRecoveryPass === pbsRecoveryPass2)

  const canSubmit =
    canAdvanceFrom1 && canAdvanceFrom2 && canAdvanceFrom3 && canAdvanceFrom4 && backendValid && pbsRecoveryOk

  async function handleCreate() {
    if (!canSubmit) return
    if (mode === "attach" && !selectedPveJob) return
    setSubmitting(true)
    setError(null)
    // Encryption submit paths:
    //   • enabled + keyfile already on disk → mode = "existing", no upload
    //     (backend reuses the canonical file at _PBS_KEYFILE_PATH).
    //   • enabled + no keyfile + Generate    → mode = "new", backend creates it.
    //   • enabled + no keyfile + Import      → upload the file first, then
    //     mode = "existing" so the backend uses the freshly-installed key.
    //
    // Only the third branch actually needs to POST to
    // /api/host-backups/pbs-encryption/import — the second is handled
    // server-side, and the first has nothing to upload.
    // Unified encryption setup: one atomic call to /pbs-encryption/import
    // covers keyfile install + escrow setup, whether the operator picked
    // "generate a new keyfile" or "import an existing one", and whether
    // they opted to upload the envelope to PBS or keep the key local.
    const needsInstall =
      backend === "pbs" &&
      pbsEncrypt &&
      !pbsRecoveryStatus?.has_keyfile
    if (needsInstall) {
      // Existing + no PVE auto-detect requires either a file upload
      // or a typed absolute path — mirrors the shell wizard.
      // Existing + PVE auto-detect uses source=pve-storage.
      if (pbsEncryptMode === "existing" && !pbsPveMatch && !pbsImportFile && !pbsImportPath.trim()) {
        setError(t("backup.errors.pickKeyfile"))
        setSubmitting(false)
        return
      }
      if (pbsUploadToPbs && pbsRecoveryPass !== pbsRecoveryPass2) {
        setError(t("backup.errors.passphrasesDoNotMatch"))
        setSubmitting(false)
        return
      }
      setPbsImportBusy(true)
      try {
        // Source picker mirrors the shell wizard's dispatch:
        //   generate       → new keyfile via proxmox-backup-client
        //   pve-storage    → reuse /etc/pve/priv/storage/<name>.enc
        //   content        → upload a file the operator picked
        //   path           → read a file at an absolute path
        let source: "generate" | "pve-storage" | "content" | "path"
        if (pbsEncryptMode === "new") source = "generate"
        else if (pbsPveMatch) source = "pve-storage"
        else if (pbsImportFile) source = "content"
        else source = "path"
        const body: Record<string, unknown> = {
          source,
          escrow_mode: pbsUploadToPbs ? "full" : "none",
        }
        if (pbsUploadToPbs) body.escrow_passphrase = pbsRecoveryPass
        if (source === "pve-storage") {
          body.pve_storage_name = pbsPveMatch!.name
        } else if (source === "content") {
          const buf = new Uint8Array(await pbsImportFile!.arrayBuffer())
          let bin = ""
          for (let i = 0; i < buf.length; i++) bin += String.fromCharCode(buf[i])
          body.content_b64 = btoa(bin)
        } else if (source === "path") {
          body.keyfile_path = pbsImportPath.trim()
        }
        await fetchApi("/api/host-backups/pbs-encryption/import", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(body),
        })
        mutatePbsRecovery()
      } catch (e) {
        const err = e as Error & { body?: { tool_output?: string; tool_exit_code?: number } }
        const detail = err.body?.tool_output
          ? `${err.message}\n\n${t("backup.errors.proxmoxBackupClientOutput")}:\n${err.body.tool_output}`
          : err.message
        setError(t("backup.errors.encryptionSetupFailed", { detail: detail || String(e) }))
        setPbsImportBusy(false)
        setSubmitting(false)
        return
      }
      setPbsImportBusy(false)
    }
    // NOTE: recovery escrow is now bundled into the /pbs-encryption/import
    // call above. No separate /pbs-recovery/setup pass is needed.
    try {
      const body: Record<string, unknown> = {
        id: jobId,
        backend,
        attached: mode === "attach",
        profile_mode: profileMode,
        enabled: true,
      }
      if (mode === "attach" && selectedPveJob) {
        body.pve_storage = selectedPveJob.storage
        body.pve_parent_job_id = selectedPveJob.id
      } else {
        body.on_calendar = onCalendar.trim()
        body.retention = {
          keep_last: Number(keepLast) || 0,
          keep_hourly: Number(keepHourly) || 0,
          keep_daily: Number(keepDaily) || 0,
          keep_weekly: Number(keepWeekly) || 0,
          keep_monthly: Number(keepMonthly) || 0,
          keep_yearly: Number(keepYearly) || 0,
        }
      }
      if (profileMode === "custom") {
        body.paths = Array.from(customPaths)
      }
      if (backend === "pbs") {
        body.pbs_repository = pbsRepository
        body.pbs_password = ""
        if (pbsBackupId) body.pbs_backup_id = pbsBackupId
        if (selectedPbs?.fingerprint) body.pbs_fingerprint = selectedPbs.fingerprint
        body.pbs_encrypt_mode = pbsEncryptMode
      } else if (backend === "local") {
        if (localDestDir.trim()) body.local_dest_dir = localDestDir.trim()
      } else if (backend === "borg") {
        // Passphrase + encrypt_mode are inherited from the destination
        // (the runner reads borg-pass-<name>.txt + the 4th field of
        // borg-targets.txt). Only send overrides when the operator
        // really typed a new one in an edit flow.
        body.borg_repo = borgRepoSelected
        if (isEdit && borgPassphrase) body.borg_passphrase = borgPassphrase
      }
      // fetchApi returns the parsed JSON and throws (with the backend's
      // error message in `error.message` when the body was JSON) on any
      // non-2xx status — no need to inspect .ok or call .json() ourselves.
      const url = isEdit
        ? `/api/host-backups/jobs/${encodeURIComponent(editingJobId || "")}`
        : "/api/host-backups/jobs"
      await fetchApi(url, {
        method: isEdit ? "PUT" : "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      })
      onCreated()
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e))
    } finally {
      setSubmitting(false)
    }
  }

  const compatibleJobs = pveJobsResp?.jobs ?? []
  const defaultPaths = pathsResp?.paths ?? []

  return (
    <Dialog open={open} onOpenChange={(v) => { if (!v) onClose() }}>
      <DialogContent className="max-w-2xl max-h-[85vh] flex flex-col">
        <DialogHeader>
          <DialogTitle className="flex items-center gap-2">
            {isEdit ? (
              <Pencil className="h-5 w-5 text-blue-500" />
            ) : (
              <Plus className="h-5 w-5 text-blue-500" />
            )}
            {isEdit ? t("backup.jobs.editScheduledJob") : t("backup.jobs.createScheduledJob")}
          </DialogTitle>
          <DialogDescription>
            {t("backup.jobs.stepOf", { step, total: 5 })} · {mode === "attach" ? t("backup.jobs.attachedToPveVzdump") : t("backup.jobs.standaloneScheduledJob")}
          </DialogDescription>
        </DialogHeader>

        {/* Step progress bar */}
        <div className="flex items-center gap-1">
          {[1, 2, 3, 4, 5].map((n) => (
            <div
              key={n}
              className={`h-1 flex-1 rounded-full ${n <= step ? "bg-blue-500" : "bg-border"}`}
            />
          ))}
        </div>

        <div className="overflow-y-auto pr-2 -mr-2 space-y-4 flex-1">
          {/* ── Step 1: ID + Backend ─────────────────────── */}
          {step === 1 && (
            <div className="space-y-4">
              <div>
                <Label htmlFor="jobId">{t("backup.fields.jobName")}</Label>
                <Input
                  id="jobId"
                  value={jobId}
                  onChange={(e) => setJobId(e.target.value)}
                  disabled={isEdit}
                  className="font-mono mt-1"
                  placeholder="moja-zaloha-servera"
                />
                <p className="text-xs text-muted-foreground mt-1">
                  {isEdit
                    ? t("backup.jobs.jobNameLocked")
                    : <>{t("backup.jobs.jobNameHelpBefore")} <code className="font-mono">_</code> {t("backup.jobs.jobNameHelpAnd")} <code className="font-mono">-</code> {t("backup.jobs.jobNameHelpAfter")}</>}
                </p>
                {!idValid && jobId.length > 0 && !isEdit && (
                  <p className="text-xs text-red-500 mt-1">{t("backup.jobs.invalidJobName")}</p>
                )}
              </div>

              <div>
                <Label>{t("backup.fields.backend")}</Label>
                {isEdit && (
                  <p className="text-xs text-muted-foreground mb-2">
                    {t("backup.jobs.backendEditHelp")}
                  </p>
                )}
                <div className="grid grid-cols-1 sm:grid-cols-3 gap-2 mt-1">
                  {(["pbs", "local", "borg"] as const).map((b) => {
                    const Icon = b === "pbs" ? Server : b === "local" ? HardDrive : Archive
                    const desc = b === "pbs"
                      ? t("backup.backends.pbsDescription")
                      : b === "local"
                        ? t("backup.backends.localDescription")
                        : t("backup.backends.borgDescriptionTimerOnly")
                    return (
                      <button
                        key={b}
                        type="button"
                        onClick={() => setBackend(b)}
                        className={`text-left p-3 rounded-md border transition-colors ${
                          backend === b ? "border-blue-500 bg-blue-500/5" : "border-border bg-background/40"
                        } hover:bg-white/5`}
                      >
                        <div className="flex items-center gap-2 font-medium text-sm">
                          <Icon className="h-4 w-4" />
                          {b === "pbs" ? "PBS" : b === "borg" ? "Borg" : t("backup.backends.local")}
                        </div>
                        <div className="text-xs text-muted-foreground mt-1">{desc}</div>
                      </button>
                    )
                  })}
                </div>
              </div>
            </div>
          )}

          {/* ── Step 2: How to schedule ──────────────────── */}
          {step === 2 && (
            <div className="space-y-3">
              <div>
                <Label>{t("backup.schedule.howToSchedule")}</Label>
                <p className="text-xs text-muted-foreground mb-2">
                  {backend === "borg"
                    ? t("backup.schedule.borgTimerOnly")
                    : t("backup.schedule.modeHelp")}
                </p>
              </div>
              <div className="space-y-2">
                <button
                  type="button"
                  onClick={() => setMode("new")}
                  className={`w-full text-left p-3 rounded-md border transition-colors ${mode === "new" ? "border-blue-500 bg-blue-500/5" : "border-border bg-background/40"} hover:bg-white/5`}
                >
                  <div className="text-sm font-medium flex items-center gap-2">
                    <Calendar className="h-4 w-4" />
                    {t("backup.jobs.newScheduledJob")}
                  </div>
                  <div className="text-xs text-muted-foreground mt-1">
                    {t("backup.jobs.newScheduledJobDescription")}
                  </div>
                </button>
                <button
                  type="button"
                  onClick={() => backend !== "borg" && setMode("attach")}
                  disabled={backend === "borg"}
                  className={`w-full text-left p-3 rounded-md border transition-colors ${
                    mode === "attach" && backend !== "borg"
                      ? "border-blue-500 bg-blue-500/5"
                      : "border-border bg-background/40"
                  } ${backend === "borg" ? "opacity-60 cursor-not-allowed" : "hover:bg-white/5"}`}
                >
                  <div className="text-sm font-medium flex items-center gap-2">
                    <Server className="h-4 w-4" />
                    {t("backup.jobs.attachToPveJob")}
                    {backend === "borg" && (
                      <Badge variant="outline" className="text-[10px] text-muted-foreground">
                        {t("backup.jobs.notAvailableForBorg")}
                      </Badge>
                    )}
                  </div>
                  <div className="text-xs text-muted-foreground mt-1">
                    {t("backup.jobs.attachDescriptionBefore")} <code className="font-mono">job-end</code> {t("backup.jobs.attachDescriptionMiddle")} <code className="font-mono">{backend === "pbs" ? "PBS" : backend === "borg" ? "Borg" : t("backup.backends.local")}</code> {t("backup.jobs.attachDescriptionAfter")}
                  </div>
                </button>
              </div>
            </div>
          )}

          {/* ── Step 3: branches by mode ─────────────────── */}
          {step === 3 && mode === "attach" && (
            <div className="space-y-3">
              <div>
                <Label>{t("backup.jobs.pickParentPveJob")}</Label>
                <p className="text-xs text-muted-foreground mb-2">
                  {t("backup.jobs.parentPveJobHelpBefore")} <code className="font-mono">job-end</code> {t("backup.jobs.parentPveJobHelpAfter")}
                </p>
              </div>
              {compatibleJobs.length === 0 ? (
                <div className="p-4 rounded-md border border-amber-500/40 bg-amber-500/5 text-sm">
                  <div className="flex items-center gap-2 text-amber-500 font-medium">
                    <AlertTriangle className="h-4 w-4" />
                    {t("backup.jobs.noCompatiblePveJob")}
                  </div>
                  <p className="text-xs text-muted-foreground mt-1">
                    {t("backup.jobs.noCompatiblePveJobDescriptionBefore")} <span className="font-mono">{backend === "pbs" ? "PBS" : backend === "borg" ? "Borg" : t("backup.backends.local")}</span> {t("backup.jobs.noCompatiblePveJobDescriptionMiddle")} <span className="font-medium">Datacenter → Backup</span> {t("backup.jobs.noCompatiblePveJobDescriptionAfter")}
                  </p>
                </div>
              ) : (
                <div className="space-y-1">
                  {compatibleJobs.map((j) => (
                    <button
                      key={j.id}
                      type="button"
                      onClick={() => setPveJobId(j.id)}
                      className={`w-full text-left p-3 rounded-md border ${pveJobId === j.id ? "border-blue-500 bg-blue-500/5" : "border-border bg-background/40"} hover:bg-white/5 transition-colors`}
                    >
                      <div className="flex items-center justify-between gap-2 flex-wrap">
                        <span className="font-mono text-xs">{j.id}</span>
                        {!j.enabled && (
                          <Badge variant="outline" className="text-[10px] text-amber-500 border-amber-500/40">
                            {t("status.disabled")}
                          </Badge>
                        )}
                      </div>
                      <div className="mt-1 text-xs text-muted-foreground flex items-center gap-x-3 gap-y-1 flex-wrap">
                        <span>{t("backup.fields.storageLabel")} <code className="font-mono">{j.storage}</code></span>
                        <span>{t("backup.fields.scheduleLabel")} <code className="font-mono">{j.schedule || "—"}</code></span>
                        <span>{t("backup.fields.retentionLabel")} <code className="font-mono">{j.prune || "—"}</code></span>
                      </div>
                    </button>
                  ))}
                </div>
              )}
            </div>
          )}

          {step === 3 && mode === "new" && (
            <div className="space-y-4">
              <div>
                <Label>{t("backup.fields.schedule")}</Label>
                <p className="text-xs text-muted-foreground mb-2">
                  {t("backup.schedule.pickFrequency")}
                </p>
                <Select
                  value={scheduleType}
                  onValueChange={(v) => setScheduleType(v as typeof scheduleType)}
                >
                  <SelectTrigger>
                    <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
                    <SelectItem value="daily">{t("backup.schedule.dailyOption")}</SelectItem>
                    <SelectItem value="hourly">{t("backup.schedule.hourlyOption")}</SelectItem>
                    <SelectItem value="weekly">{t("backup.schedule.weeklyOption")}</SelectItem>
                    <SelectItem value="monthly">{t("backup.schedule.monthlyOption")}</SelectItem>
                    <SelectItem value="advanced">{t("backup.schedule.advancedOption")}</SelectItem>
                  </SelectContent>
                </Select>
              </div>

              {scheduleType === "daily" && (
                <div>
                  <Label htmlFor="schedTime">{t("backup.fields.timeOfDay")}</Label>
                  <Input
                    id="schedTime"
                    type="time"
                    value={scheduleTime}
                    onChange={(e) => setScheduleTime(e.target.value)}
                    className="font-mono mt-1 max-w-[140px]"
                  />
                </div>
              )}

              {scheduleType === "hourly" && (
                <div>
                  <Label htmlFor="schedMinute">{t("backup.fields.minuteOfHour")}</Label>
                  <Input
                    id="schedMinute"
                    type="number"
                    min="0"
                    max="59"
                    value={scheduleMinute}
                    onChange={(e) => setScheduleMinute(e.target.value)}
                    className="font-mono mt-1 max-w-[120px]"
                  />
                  <p className="text-xs text-muted-foreground mt-1">
                    {t("backup.schedule.hourlyHelpBefore")} <code className="font-mono">0</code> {t("backup.schedule.hourlyHelpMiddle")} <code className="font-mono">30</code> {t("backup.schedule.hourlyHelpAfter")}
                  </p>
                </div>
              )}

              {scheduleType === "weekly" && (
                <div className="space-y-3">
                  <div>
                    <Label>{t("backup.fields.daysOfWeek")}</Label>
                    <div className="flex flex-wrap gap-1 mt-1">
                      {["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map((d) => {
                        const active = scheduleWeekdays.has(d)
                        return (
                          <button
                            key={d}
                            type="button"
                            onClick={() => {
                              setScheduleWeekdays((prev) => {
                                const next = new Set(prev)
                                if (next.has(d)) next.delete(d)
                                else next.add(d)
                                return next
                              })
                            }}
                            className={`px-3 py-1.5 rounded-md text-xs font-mono border transition-colors ${
                              active
                                ? "border-blue-500 bg-blue-500/10 text-blue-400"
                                : "border-border bg-background/40 text-muted-foreground hover:bg-white/5"
                            }`}
                          >
                            {t(`backup.weekdays.short.${d.toLowerCase()}`)}
                          </button>
                        )
                      })}
                    </div>
                    {scheduleWeekdays.size === 0 && (
                      <p className="text-xs text-amber-500 mt-1">{t("backup.schedule.pickAtLeastOneDay")}</p>
                    )}
                  </div>
                  <div>
                    <Label htmlFor="schedTimeW">{t("backup.fields.timeOfDay")}</Label>
                    <Input
                      id="schedTimeW"
                      type="time"
                      value={scheduleTime}
                      onChange={(e) => setScheduleTime(e.target.value)}
                      className="font-mono mt-1 max-w-[140px]"
                    />
                  </div>
                </div>
              )}

              {scheduleType === "monthly" && (
                <div className="space-y-3">
                  <div>
                    <Label htmlFor="schedDay">{t("backup.fields.dayOfMonth")}</Label>
                    <Input
                      id="schedDay"
                      type="number"
                      min="1"
                      max="31"
                      value={scheduleDayOfMonth}
                      onChange={(e) => setScheduleDayOfMonth(e.target.value)}
                      className="font-mono mt-1 max-w-[120px]"
                    />
                    <p className="text-xs text-muted-foreground mt-1">
                      {t("backup.schedule.monthlySkipHelp")}
                    </p>
                  </div>
                  <div>
                    <Label htmlFor="schedTimeM">{t("backup.fields.timeOfDay")}</Label>
                    <Input
                      id="schedTimeM"
                      type="time"
                      value={scheduleTime}
                      onChange={(e) => setScheduleTime(e.target.value)}
                      className="font-mono mt-1 max-w-[140px]"
                    />
                  </div>
                </div>
              )}

              {scheduleType === "advanced" && (
                <div>
                  <Label htmlFor="schedAdv">{t("backup.fields.onCalendarExpression")}</Label>
                  <Input
                    id="schedAdv"
                    value={scheduleAdvanced}
                    onChange={(e) => setScheduleAdvanced(e.target.value)}
                    className="font-mono mt-1"
                    placeholder={t("backup.placeholders.onCalendar")}
                  />
                  <p className="text-xs text-muted-foreground mt-1">
                    {t("backup.schedule.advancedHelpBefore")} <code className="font-mono">systemd-analyze calendar</code>. {t("backup.schedule.advancedHelpMiddle")} <span className="font-mono">man systemd.time</span> {t("backup.schedule.advancedHelpAfter")}
                  </p>
                </div>
              )}

              {/* Live preview from the backend */}
              <div className="rounded-md border border-border bg-background/40 p-3 space-y-1 text-xs">
                <div className="text-[10px] uppercase tracking-wider text-muted-foreground">{t("backup.schedule.preview")}</div>
                <div className="flex items-baseline gap-2">
                  <span className="text-muted-foreground">{t("backup.fields.expressionLabel")}</span>
                  <code className="font-mono">{onCalendar}</code>
                </div>
                {calendarPreview ? (
                  calendarPreview.valid ? (
                    <>
                      {calendarPreview.normalized && calendarPreview.normalized !== onCalendar && (
                        <div className="flex items-baseline gap-2">
                          <span className="text-muted-foreground">{t("backup.fields.normalizedLabel")}</span>
                          <code className="font-mono">{calendarPreview.normalized}</code>
                        </div>
                      )}
                      {calendarPreview.next_elapse && (
                        <div className="flex items-baseline gap-2">
                          <span className="text-muted-foreground">{t("backup.jobs.nextRunLabel")}</span>
                          <span className="text-emerald-400">{calendarPreview.next_elapse}</span>
                          {calendarPreview.from_now && (
                            <span className="text-muted-foreground">({calendarPreview.from_now})</span>
                          )}
                        </div>
                      )}
                    </>
                  ) : (
                    <div className="text-red-500">
                      <span className="font-medium">{t("backup.validation.invalidLabel")}</span> {calendarPreview.error}
                    </div>
                  )
                ) : (
                  <div className="text-muted-foreground italic">{t("backup.common.checking")}</div>
                )}
              </div>

              <div>
                <Label>{t("backup.retention.title")}</Label>
                <p className="text-xs text-muted-foreground mb-2">{t("backup.retention.zeroDisables")}</p>
                <div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
                  {[
                    { id: "keep-last", lbl: t("backup.retention.keepLast"), v: keepLast, set: setKeepLast },
                    { id: "keep-hourly", lbl: t("backup.retention.keepHourly"), v: keepHourly, set: setKeepHourly },
                    { id: "keep-daily", lbl: t("backup.retention.keepDaily"), v: keepDaily, set: setKeepDaily },
                    { id: "keep-weekly", lbl: t("backup.retention.keepWeekly"), v: keepWeekly, set: setKeepWeekly },
                    { id: "keep-monthly", lbl: t("backup.retention.keepMonthly"), v: keepMonthly, set: setKeepMonthly },
                    { id: "keep-yearly", lbl: t("backup.retention.keepYearly"), v: keepYearly, set: setKeepYearly },
                  ].map((row) => (
                    <div key={row.id}>
                      <Label htmlFor={row.id} className="text-xs">{row.lbl}</Label>
                      <Input
                        id={row.id}
                        type="number"
                        min="0"
                        value={row.v}
                        onChange={(e) => row.set(e.target.value)}
                        className="font-mono mt-1"
                      />
                    </div>
                  ))}
                </div>
              </div>
            </div>
          )}

          {/* ── Step 4: Profile + paths ──────────────────── */}
          {step === 4 && (
            <div className="space-y-4">
              <div>
                <Label>{t("backup.profile.title")}</Label>
                <div className="grid grid-cols-2 gap-2 mt-1">
                  <button
                    type="button"
                    onClick={() => setProfileMode("default")}
                    className={`text-left p-3 rounded-md border ${profileMode === "default" ? "border-blue-500 bg-blue-500/5" : "border-border bg-background/40"} hover:bg-white/5 transition-colors`}
                  >
                    <div className="text-sm font-medium">{t("backup.profile.default")}</div>
                    <div className="text-xs text-muted-foreground mt-1">
                      {t("backup.profile.defaultDescription", { count: defaultPaths.length })}
                    </div>
                  </button>
                  <button
                    type="button"
                    onClick={() => setProfileMode("custom")}
                    className={`text-left p-3 rounded-md border ${profileMode === "custom" ? "border-blue-500 bg-blue-500/5" : "border-border bg-background/40"} hover:bg-white/5 transition-colors`}
                  >
                    <div className="text-sm font-medium">{t("backup.profile.custom")}</div>
                    <div className="text-xs text-muted-foreground mt-1">
                      {t("backup.profile.customDescription")}
                    </div>
                  </button>
                </div>
              </div>
              {profileMode === "custom" && (
                <div>
                  <Label>{t("backup.profile.pathsToInclude", { selected: customPaths.size, total: defaultPaths.length })}</Label>
                  <ScrollArea className="h-64 mt-1 rounded-md border border-border p-2">
                    {defaultPaths.map((p) => (
                      <label
                        key={p}
                        className="flex items-center gap-2 py-1 px-1 hover:bg-white/5 rounded text-xs cursor-pointer"
                      >
                        <Checkbox
                          checked={customPaths.has(p)}
                          onCheckedChange={() => togglePath(p)}
                        />
                        <span className="font-mono">{p}</span>
                      </label>
                    ))}
                  </ScrollArea>
                </div>
              )}
            </div>
          )}

          {/* ── Step 5: Backend-specific + summary ──────── */}
          {step === 5 && (
            <div className="space-y-4">
              {backend === "pbs" && (
                <>
                  <div>
                    <Label>{t("backup.fields.pbsRepository")}</Label>
                    {destResp?.pbs?.length ? (
                      <div className="mt-1 flex items-center gap-2">
                        <Select value={pbsRepository} onValueChange={setPbsRepository}>
                          <SelectTrigger className="flex-1 min-w-0">
                            {/* Manual render: SelectValue's default mode
                                duplicates the item children and cuts
                                them in the middle on mobile. We render
                                "name — repository" ourselves with a
                                proper trailing ellipsis. */}
                            {(() => {
                              const sel = destResp.pbs.find((r) => r.repository === pbsRepository)
                              if (!sel) {
                                return <span className="truncate text-muted-foreground">{t("backup.placeholders.pickPbsRepository")}</span>
                              }
                              return (
                                <span className="truncate font-mono text-left">
                                  {sel.name}
                                  <span className="text-muted-foreground"> — {sel.repository}</span>
                                </span>
                              )
                            })()}
                          </SelectTrigger>
                          <SelectContent>
                            {destResp.pbs.map((r) => (
                              <SelectItem key={`${r.name}-${r.repository}`} value={r.repository}>
                                <span className="font-mono">{r.name}</span>
                                <span className="text-muted-foreground ml-2">— {r.repository}</span>
                              </SelectItem>
                            ))}
                          </SelectContent>
                        </Select>
                        <Button
                          type="button"
                          size="sm"
                          variant="outline"
                          className="shrink-0 bg-blue-500/10 border-blue-500/40 !text-blue-400 hover:bg-blue-500/20 hover:!text-blue-300"
                          onClick={() => setAddingDestType("pbs")}
                          title={t("backup.destinations.saveAnotherPbsTitle")}
                        >
                          <Plus className="h-3.5 w-3.5 mr-1" />
                          {t("backup.actions.addAnother")}
                        </Button>
                      </div>
                    ) : (
                      <div className="mt-1 p-3 rounded-md border border-amber-500/40 bg-amber-500/5 text-xs space-y-2">
                        <div>{t("backup.destinations.noPbsInWizard")}</div>
                        <Button
                          type="button"
                          size="sm"
                          variant="outline"
                          className="bg-blue-500/10 border-blue-500/40 !text-blue-400 hover:bg-blue-500/20 hover:!text-blue-300"
                          onClick={() => setAddingDestType("pbs")}
                        >
                          <Plus className="h-3.5 w-3.5 mr-1" />
                          {t("backup.destinations.addPbsRepository")}
                        </Button>
                      </div>
                    )}
                  </div>
                  <div>
                    <Label htmlFor="pbsBackupId">{t("backup.fields.groupNameInPbs")}</Label>
                    <Input
                      id="pbsBackupId"
                      value={pbsBackupId}
                      onChange={(e) => setPbsBackupId(e.target.value)}
                      placeholder={t("backup.placeholders.leaveBlankForDefault")}
                      className="font-mono mt-1"
                    />
                    <p className="text-xs text-muted-foreground mt-1">
                      {t("backup.jobs.pbsGroupHelp")}
                    </p>
                  </div>
                  {/* PBS client-side encryption — mirrors the shell wizard:
                      step 1 is a plain yes/no, step 2 only appears when
                      no keyfile is installed yet. Once a keyfile exists,
                      every future encrypted job silently reuses it. */}
                  <div className="pt-2 border-t border-border space-y-3">
                    <label className="flex items-start gap-2 cursor-pointer">
                      <Checkbox
                        checked={pbsEncrypt}
                        onCheckedChange={(v) => {
                          const checked = !!v
                          if (!checked) {
                            setPbsEncryptMode("none")
                          } else {
                            // Yes → if the host already has a keyfile, submit
                            // "existing" (reuse it silently). If not, default
                            // to "new" (Generate) — the operator can flip to
                            // "existing" (Import) via the radio below.
                            setPbsEncryptMode(pbsRecoveryStatus?.has_keyfile ? "existing" : "new")
                          }
                        }}
                        className="mt-0.5"
                      />
                      <div className="flex-1">
                        <div className="inline-flex items-center gap-1.5 text-sm">
                          <Lock className="h-3.5 w-3.5 text-emerald-400" />
                          {t("backup.encryption.encryptBackups")}
                        </div>
                        <p className="text-[11px] text-muted-foreground mt-1">
                          {t("backup.encryption.pbsHelpBefore")} <code className="font-mono">--keyfile</code> {t("backup.encryption.pbsHelpMiddle")} <code className="font-mono">proxmox-backup-client backup</code>. {t("backup.encryption.pbsHelpAfter")} <code className="font-mono">/usr/local/share/proxmenux/pbs-key.conf</code> (chmod 0600) {t("backup.encryption.pbsHelpEnd")}
                        </p>
                      </div>
                    </label>


                    {pbsEncrypt && !pbsRecoveryStatus?.has_keyfile && (
                      <div className="space-y-2 rounded-md border border-emerald-500/30 bg-emerald-500/5 p-3">
                        <div className="text-[11px] font-medium">
                          {t("backup.encryption.noKeyStored")}
                        </div>
                        <label className="flex items-start gap-2 cursor-pointer text-[11px]">
                          <input
                            type="radio"
                            name="pbsEncryptSetupCreate"
                            checked={pbsEncryptMode === "new"}
                            onChange={() => setPbsEncryptMode("new")}
                            className="mt-1"
                          />
                          <div className="flex-1">
                            <div className="font-medium">{t("backup.encryption.generateNewKeyfile")}</div>
                            <div className="text-muted-foreground">{t("backup.encryption.generateNewKeyfileDescriptionBefore")} <code className="font-mono">/usr/local/share/proxmenux/pbs-key.conf</code>. {t("backup.encryption.generateNewKeyfileDescriptionAfter")}</div>
                          </div>
                        </label>
                        <label className="flex items-start gap-2 cursor-pointer text-[11px]">
                          <input
                            type="radio"
                            name="pbsEncryptSetupCreate"
                            checked={pbsEncryptMode === "existing"}
                            onChange={() => setPbsEncryptMode("existing")}
                            className="mt-1"
                          />
                          <div className="flex-1">
                            <div className="font-medium">{t("backup.encryption.importExistingKeyfile")}</div>
                            <div className="text-muted-foreground">{t("backup.encryption.importExistingKeyfileDescription")}</div>
                          </div>
                        </label>
                        {pbsEncryptMode === "existing" && pbsPveMatch && (
                          <div className="pt-1 rounded-md border border-emerald-500/40 bg-emerald-500/5 p-2.5 text-[11px] space-y-1">
                            <div className="flex items-start gap-1.5">
                              <CheckCircle2 className="h-3.5 w-3.5 text-emerald-400 mt-0.5 shrink-0" />
                              <div className="flex-1">
                                <div className="font-medium text-foreground">{t("backup.encryption.autoDetectedFromPveStorage", { storage: pbsPveMatch.name })}</div>
                                <div className="text-muted-foreground mt-0.5">
                                  {t("backup.encryption.existingKeyAt")} <code className="font-mono text-[10.5px]">{pbsPveMatch.path}</code>
                                </div>
                                <div className="text-muted-foreground mt-0.5">{t("backup.encryption.pveKeyWillBeCopied")}</div>
                              </div>
                            </div>
                          </div>
                        )}
                        {pbsEncryptMode === "existing" && !pbsPveMatch && (
                          <div className="space-y-2 pt-1">
                            <div className="space-y-1.5">
                              <Label htmlFor="pbsImportFile" className="text-[11px] font-medium">
                                {t("backup.keyfileActions.uploadFromMachine")}
                              </Label>
                              <Input
                                id="pbsImportFile"
                                type="file"
                                accept=".conf,.key,application/json,text/plain"
                                onChange={(e) => setPbsImportFile(e.target.files?.[0] ?? null)}
                                disabled={pbsImportBusy || !!pbsImportPath.trim()}
                                className="h-8 text-[11px]"
                              />
                            </div>
                            <div className="text-[10px] text-center text-muted-foreground">{t("backup.common.or")}</div>
                            <div className="space-y-1.5">
                              <Label htmlFor="pbsImportPath" className="text-[11px] font-medium">
                                {t("backup.fields.absolutePathOnHost")}
                              </Label>
                              <Input
                                id="pbsImportPath"
                                type="text"
                                value={pbsImportPath}
                                onChange={(e) => setPbsImportPath(e.target.value)}
                                disabled={pbsImportBusy || !!pbsImportFile}
                                placeholder={t("backup.placeholders.keyfilePath")}
                                className="h-8 text-[11px] font-mono"
                              />
                              <p className="text-[10px] text-muted-foreground">
                                {t("backup.encryption.importKeyfileHelpBefore")} <code className="font-mono">/usr/local/share/proxmenux/pbs-key.conf</code> {t("backup.encryption.importKeyfileHelpAfter")}
                              </p>
                            </div>
                          </div>
                        )}
                      </div>
                    )}

                    {pbsEncrypt && (
                      <div className="pl-7 space-y-3 rounded-md border border-blue-500/30 bg-blue-500/5 p-3">
                        {pbsRecoveryStatus?.has_keyfile && !pbsRecoveryChange && (
                          <div className="flex items-start gap-2 text-[11px]">
                            <CheckCircle2 className="h-3.5 w-3.5 text-emerald-400 mt-0.5 shrink-0" />
                            <div className="flex-1">
                              <div className="text-foreground">{t("backup.keyfileManagement.keyfileInstalled")}</div>
                              <div className="text-muted-foreground mt-0.5">{t("backup.keyfileManagement.currentPbsUploadStays")}</div>
                            </div>
                            <button
                              type="button"
                              onClick={() => setPbsRecoveryChange(true)}
                              className="text-blue-400 hover:text-blue-300 underline underline-offset-2 shrink-0"
                            >
                              {t("backup.actions.change")}
                            </button>
                          </div>
                        )}
                        {pbsRecoveryStatus?.has_keyfile && pbsRecoveryChange && (
                          <>
                            <KeyfileActionsBar
                              mutateStatus={mutatePbsRecovery}
                              escrowMode={pbsRecoveryStatus?.escrow_mode}
                            />
                            <button
                              type="button"
                              onClick={() => setPbsRecoveryChange(false)}
                              className="text-[11px] text-muted-foreground hover:text-foreground underline underline-offset-2"
                            >
                              {t("backup.keyfileManagement.hideOptions")}
                            </button>
                          </>
                        )}
                        {!pbsRecoveryStatus?.has_keyfile && (
                          <>
                            <div>
                              <div className="text-[11px] font-medium text-foreground mb-2">{t("backup.keyfileManagement.uploadToPbs")}</div>
                              <div className="grid gap-1.5">
                                <label className="flex items-start gap-2 cursor-pointer text-[11px]">
                                  <input
                                    type="radio"
                                    name="pbsUploadToPbsCreate"
                                    checked={!pbsUploadToPbs}
                                    onChange={() => setPbsUploadToPbs(false)}
                                    className="mt-1"
                                  />
                                  <div className="flex-1">
                                    <div className="font-medium">{t("backup.keyfileManagement.keepLocalOnly")}</div>
                                    <div className="text-muted-foreground">{t("backup.keyfileManagement.keepLocalDescriptionBefore")} <code className="font-mono text-[10.5px]">/usr/local/share/proxmenux/pbs-key.conf</code>. {t("backup.keyfileManagement.keepLocalDescriptionAfter")}</div>
                                  </div>
                                </label>
                                <label className="flex items-start gap-2 cursor-pointer text-[11px]">
                                  <input
                                    type="radio"
                                    name="pbsUploadToPbsCreate"
                                    checked={pbsUploadToPbs}
                                    onChange={() => setPbsUploadToPbs(true)}
                                    className="mt-1"
                                  />
                                  <div className="flex-1">
                                    <div className="font-medium">{t("backup.keyfileManagement.yesUpload")}</div>
                                    <div className="text-muted-foreground">{t("backup.keyfileManagement.yesUploadDescription")}</div>
                                  </div>
                                </label>
                              </div>
                            </div>
                            {pbsUploadToPbs ? (
                              <div className="space-y-2 pt-1 border-t border-blue-500/20">
                                <div>
                                  <Label htmlFor="pbsRecPass" className="text-[11px]">{t("backup.fields.recoveryPassphrase")}</Label>
                                  <Input
                                    id="pbsRecPass"
                                    type="password"
                                    value={pbsRecoveryPass}
                                    onChange={(e) => setPbsRecoveryPass(e.target.value)}
                                    placeholder={t("backup.placeholders.recoveryPassphrase")}
                                    className="font-mono mt-1 h-8 text-xs"
                                  />
                                </div>
                                <div>
                                  <Label htmlFor="pbsRecPass2" className="text-[11px]">{t("backup.fields.confirmPassphrase")}</Label>
                                  <Input
                                    id="pbsRecPass2"
                                    type="password"
                                    value={pbsRecoveryPass2}
                                    onChange={(e) => setPbsRecoveryPass2(e.target.value)}
                                    placeholder={t("backup.placeholders.typeItAgain")}
                                    className="font-mono mt-1 h-8 text-xs"
                                  />
                                  {pbsRecoveryPass && pbsRecoveryPass2 && pbsRecoveryPass !== pbsRecoveryPass2 && (
                                    <p className="text-[11px] text-red-400 mt-1">{t("backup.errors.passphrasesDontMatch")}</p>
                                  )}
                                </div>
                              </div>
                            ) : (
                              <div className="pt-1 border-t border-blue-500/20 text-[11px] text-amber-300 bg-amber-500/10 border border-amber-500/30 rounded px-3 py-2 leading-relaxed space-y-1">
                                <div className="font-semibold">{t("backup.keyfileManagement.keepOffsiteCopy")}</div>
                                <div>{t("backup.keyfileManagement.copyAfterCreateBefore")} <code className="font-mono text-[10.5px] bg-amber-500/20 rounded px-1">/usr/local/share/proxmenux/pbs-key.conf</code> {t("backup.keyfileManagement.copyAfterCreateAfter")}</div>
                                <div>{t("backup.keyfileManagement.withoutCopyWarning")}</div>
                              </div>
                            )}
                          </>
                        )}
                      </div>
                    )}
                  </div>
                </>
              )}
              {backend === "local" && (
                <div className="space-y-2">
                  <Label htmlFor="localDest">{t("backup.fields.destinationDirectory")}</Label>
                  {mode === "attach" ? (
                    <>
                      <Input
                        id="localDest"
                        value={localDestDir}
                        onChange={(e) => setLocalDestDir(e.target.value)}
                        placeholder={t("backup.placeholders.autoFromPveStorage")}
                        className="font-mono"
                      />
                      <p className="text-xs text-muted-foreground">
                        {t("backup.destinations.localAttachHelpBefore")} <code className="font-mono">/dump</code> {t("backup.destinations.localAttachHelpMiddle")} <code className="font-mono">/var/lib/vz/dump</code>.
                      </p>
                    </>
                  ) : (
                    <>
                      {(destResp?.local?.entries?.length ?? 0) > 0 && (
                        <div className="flex items-center gap-2">
                          <Select
                            value={localDestDir || (destResp?.local?.effective ?? "")}
                            onValueChange={(v) => setLocalDestDir(v)}
                          >
                            <SelectTrigger className="flex-1">
                              <SelectValue placeholder={t("backup.placeholders.pickLocalDestination")} />
                            </SelectTrigger>
                            <SelectContent>
                              {destResp?.local?.entries?.map((e) => (
                                <SelectItem key={e.path} value={e.path}>
                                  <span className="font-mono">{e.path}</span>
                                  {e.source === "default" && (
                                    <span className="text-muted-foreground ml-2 text-[10px] uppercase">{t("backup.profile.default")}</span>
                                  )}
                                </SelectItem>
                              ))}
                            </SelectContent>
                          </Select>
                          <Button
                            type="button"
                            size="sm"
                            variant="outline"
                            className="shrink-0 bg-blue-500/10 border-blue-500/40 !text-blue-400 hover:bg-blue-500/20 hover:!text-blue-300"
                            onClick={() => setAddingDestType("local")}
                            title={t("backup.destinations.saveAnotherLocalTitle")}
                          >
                            <Plus className="h-3.5 w-3.5 mr-1" />
                            {t("backup.actions.addAnother")}
                          </Button>
                        </div>
                      )}
                      <Input
                        id="localDest"
                        value={localDestDir}
                        onChange={(e) => setLocalDestDir(e.target.value)}
                        placeholder={destResp?.local?.effective || "/var/lib/vz/dump"}
                        className="font-mono"
                      />
                      <p className="text-xs text-muted-foreground">
                        {t("backup.destinations.localPickHelpBefore")} <span className="font-mono">{destResp?.local?.effective || "/var/lib/vz/dump"}</span>.
                      </p>
                    </>
                  )}
                </div>
              )}
              {backend === "borg" && (
                <>
                  <div>
                    <Label>{t("backup.fields.borgRepository")}</Label>
                    {destResp?.borg?.length ? (
                      <div className="mt-1 flex items-center gap-2">
                        <Select value={borgRepoSelected} onValueChange={setBorgRepoSelected}>
                          <SelectTrigger className="flex-1 min-w-0">
                            {(() => {
                              const sel = destResp.borg.find((r) => r.repository === borgRepoSelected)
                              if (!sel) {
                                return <span className="truncate text-muted-foreground">{t("backup.placeholders.pickBorgRepository")}</span>
                              }
                              return (
                                <span className="truncate font-mono text-left">
                                  {sel.name}
                                  <span className="text-muted-foreground"> — {sel.repository}</span>
                                </span>
                              )
                            })()}
                          </SelectTrigger>
                          <SelectContent>
                            {destResp.borg.map((r) => (
                              <SelectItem key={`${r.name}-${r.repository}`} value={r.repository}>
                                <span className="font-mono">{r.name}</span>
                                <span className="text-muted-foreground ml-2">— {r.repository}</span>
                              </SelectItem>
                            ))}
                          </SelectContent>
                        </Select>
                        <Button
                          type="button"
                          size="sm"
                          variant="outline"
                          className="shrink-0 bg-fuchsia-500/10 border-fuchsia-500/40 !text-fuchsia-400 hover:bg-fuchsia-500/20 hover:!text-fuchsia-300"
                          onClick={() => setAddingDestType("borg")}
                          title={t("backup.destinations.saveAnotherBorgTitle")}
                        >
                          <Plus className="h-3.5 w-3.5 mr-1" />
                          {t("backup.actions.addAnother")}
                        </Button>
                      </div>
                    ) : (
                      <div className="mt-1 p-3 rounded-md border border-amber-500/40 bg-amber-500/5 text-xs space-y-2">
                        <div>{t("backup.destinations.noBorgInWizard")}</div>
                        <Button
                          type="button"
                          size="sm"
                          variant="outline"
                          className="bg-fuchsia-500/10 border-fuchsia-500/40 !text-fuchsia-400 hover:bg-fuchsia-500/20 hover:!text-fuchsia-300"
                          onClick={() => setAddingDestType("borg")}
                        >
                          <Plus className="h-3.5 w-3.5 mr-1" />
                          {t("backup.destinations.addBorgRepository")}
                        </Button>
                      </div>
                    )}
                  </div>
                  {/* Encryption + passphrase aren't asked here — they
                      live on the borg destination. The runner reads
                      the saved sidecar at run time. */}
                  {(() => {
                    const dest = destResp?.borg?.find((r) => r.repository === borgRepoSelected)
                    if (!dest) return null
                    const mode = dest.encrypt_mode || "repokey"
                    return (
                      <p className="text-[11px] text-muted-foreground">
                        {t("backup.destinations.borgEncryptionInherited", { mode: mode === "none" ? t("backup.encryption.none") : mode })}
                      </p>
                    )
                  })()}
                </>
              )}

              {/* Summary preview — mirrors the JobDetailModal styling so
                  what the operator sees at Create time is what they'll
                  see when they open the job later. */}
              {(() => {
                const retentionPairs: Array<[string, string]> = [
                  [t("backup.retention.last"), String(keepLast || "")],
                  [t("backup.retention.hourly"), String(keepHourly || "")],
                  [t("backup.retention.daily"), String(keepDaily || "")],
                  [t("backup.retention.weekly"), String(keepWeekly || "")],
                  [t("backup.retention.monthly"), String(keepMonthly || "")],
                  [t("backup.retention.yearly"), String(keepYearly || "")],
                ].filter(([, v]) => Number(v) > 0) as Array<[string, string]>
                return (
                  <div className="rounded-md border border-border bg-background/40 p-3 space-y-2 text-xs">
                    <div className="text-[10px] uppercase tracking-wider text-muted-foreground mb-1">{t("backup.jobs.summary")}</div>
                    <div><span className="text-muted-foreground">{t("backup.fields.nameLabel")}</span> <span className="font-mono text-foreground">{jobId}</span></div>
                    <div className="flex items-center gap-2 flex-wrap">
                      <span className="text-muted-foreground">{t("backup.fields.backendLabel")}</span>
                      <Badge variant="outline" className={`text-[10px] uppercase tracking-wide ${methodBadgeCls(backend)}`}>
                        {backend === "pbs" ? "PBS" : backend === "borg" ? "Borg" : t("backup.backends.local")}
                      </Badge>
                      <Badge variant="outline" className="text-[10px] text-blue-400 border-blue-400/40 bg-blue-500/5">
                        {mode === "attach" ? t("backup.status.attached") : t("backup.status.scheduled")}
                      </Badge>
                    </div>
                    {mode === "attach" && selectedPveJob && (
                      <>
                        <div><span className="text-muted-foreground">{t("backup.fields.pveJobLabel")}</span> <span className="font-mono text-foreground">{selectedPveJob.id}</span></div>
                        <div className="flex items-center gap-1.5 flex-wrap">
                          <Calendar className="h-3 w-3 text-green-500/80" />
                          <span className="text-green-500/90 uppercase tracking-wider text-[10px]">{t("backup.jobs.inheritedScheduleLabel")}</span>
                          <span className="text-foreground">{humanizeOnCalendar(selectedPveJob.schedule, t)}</span>
                        </div>
                        <div className="flex items-center gap-1.5 flex-wrap">
                          <Archive className="h-3 w-3 text-green-500/80" />
                          <span className="text-green-500/90 uppercase tracking-wider text-[10px]">{t("backup.jobs.inheritedRetentionLabel")}</span>
                          <span className="font-mono text-foreground">{selectedPveJob.prune}</span>
                        </div>
                      </>
                    )}
                    {mode === "new" && (
                      <>
                        <div className="flex items-center gap-1.5 flex-wrap">
                          <Calendar className="h-3 w-3 text-green-500/80" />
                          <span className="text-green-500/90 uppercase tracking-wider text-[10px]">{t("backup.fields.whenLabel")}</span>
                          <span className="text-foreground">{humanizeOnCalendar(onCalendar, t)}</span>
                        </div>
                        <div className="flex items-start gap-1.5 flex-wrap">
                          <span className="text-green-500/90 uppercase tracking-wider text-[10px] inline-flex items-center gap-1 pt-0.5">
                            <Archive className="h-3 w-3 text-green-500/80" /> {t("backup.fields.retentionLabel")}
                          </span>
                          {retentionPairs.length === 0 ? (
                            <span className="text-muted-foreground italic">{t("backup.retention.none")}</span>
                          ) : (
                            <div className="flex flex-wrap gap-1.5">
                              {retentionPairs.map(([k, v]) => (
                                <span key={k} className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded border border-border bg-background/60">
                                  <span className="text-[10px] uppercase tracking-wide text-muted-foreground">{k}</span>
                                  <span className="font-mono text-xs text-foreground">{v}</span>
                                </span>
                              ))}
                            </div>
                          )}
                        </div>
                      </>
                    )}
                    <div className="inline-flex items-center gap-1.5">
                      <FileSearch className="h-3 w-3 text-green-500/80" />
                      <span className="text-green-500/90 uppercase tracking-wider text-[10px]">{t("backup.fields.profileLabel")}</span>
                      <span className="text-foreground">{profileMode === "default" ? t("backup.profile.default") : t("backup.profile.custom")}</span>
                      <span className="text-muted-foreground">({t("backup.paths.count", { count: profileMode === "default" ? defaultPaths.length : customPaths.size })})</span>
                    </div>
                  </div>
                )
              })()}

              {error && (
                <div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
                  {error}
                </div>
              )}
            </div>
          )}
        </div>

        {/* Footer navigation */}
        <div className="flex items-center justify-between gap-2 pt-3 border-t border-border">
          <Button
            variant="ghost"
            onClick={onClose}
            disabled={submitting}
          >
            {t("actions.cancel")}
          </Button>
          <div className="flex items-center gap-2">
            {step > 1 && (
              <Button
                variant="outline"
                onClick={() => setStep((s) => (s - 1) as 1 | 2 | 3 | 4 | 5)}
                disabled={submitting}
              >
                <ChevronLeft className="h-4 w-4 mr-1" />
                {t("backup.actions.back")}
              </Button>
            )}
            {step < 5 ? (
              <Button
                onClick={() => setStep((s) => (s + 1) as 1 | 2 | 3 | 4 | 5)}
                disabled={
                  (step === 1 && !canAdvanceFrom1) ||
                  (step === 2 && !canAdvanceFrom2) ||
                  (step === 3 && !canAdvanceFrom3) ||
                  (step === 4 && !canAdvanceFrom4)
                }
              >
                {t("backup.actions.next")}
                <ChevronRight className="h-4 w-4 ml-1" />
              </Button>
            ) : (
              <Button
                onClick={handleCreate}
                disabled={!canSubmit || submitting}
                className="bg-blue-600 hover:bg-blue-700 text-white"
              >
                {submitting ? (
                  <Loader2 className="h-4 w-4 animate-spin mr-2" />
                ) : isEdit ? (
                  <Save className="h-4 w-4 mr-2" />
                ) : (
                  <Plus className="h-4 w-4 mr-2" />
                )}
                {isEdit ? t("backup.actions.saveChanges") : t("backup.jobs.createJob")}
              </Button>
            )}
          </div>
        </div>
      </DialogContent>

      {/* AddDestinationDialog spawned from inside the wizard. When the
          operator saves a new PBS or Borg destination we refresh the
          dest list and queue the freshly-saved repository so the next
          destResp render auto-selects it in the dropdown. */}
      <AddDestinationDialog
        type={addingDestType}
        onClose={() => setAddingDestType(null)}
        onSaved={(repo) => {
          if (repo && (addingDestType === "pbs" || addingDestType === "borg")) {
            setPendingAutoSelectDest({ kind: addingDestType, repository: repo })
          }
          setAddingDestType(null)
          mutateDest()
        }}
      />
    </Dialog>
  )
}

// ──────────────────────────────────────────────────────────────
// Manual backup dialog (Sprint B).
//
// One-shot version of CreateJobDialog: same backends + profile + paths
// + backend-specific config, but NO schedule, NO retention, NO timer.
// Writes a `manual-<ts>` .env with ENABLED=0 + MANUAL_RUN=1 and fires
// the runner in the background. The resulting job appears in the
// scheduler list with a "manual" badge and can be deleted when the
// operator wants to clean up.
// ──────────────────────────────────────────────────────────────

function ManualBackupDialog({
  open,
  onClose,
  onLaunched,
}: {
  open: boolean
  onClose: () => void
  // The job_id of the just-launched manual run. The caller uses it to
  // transition straight into the live-progress modal so the operator
  // never has to chase a banner after pressing Run.
  onLaunched: (jobId: string) => void
}) {
  const t = useT()
  const [step, setStep] = useState<1 | 2>(1)
  const [backend, setBackend] = useState<"pbs" | "local" | "borg">("local")
  const [profileMode, setProfileMode] = useState<"default" | "custom">("default")
  const [customPaths, setCustomPaths] = useState<Set<string>>(new Set())

  const [pbsRepository, setPbsRepository] = useState<string>("")
  const [pbsBackupId, setPbsBackupId] = useState<string>("")
  // Encryption mode. See CreateJobDialog above for the full comment
  // block — the three modes and the import flow are identical here.
  const [pbsEncryptMode, setPbsEncryptMode] = useState<"none" | "new" | "existing">("none")
  const [pbsImportFile, setPbsImportFile] = useState<File | null>(null)
  // Absolute-path alternative to the file upload — mirrors the shell
  // wizard's `_hb_pbs_import_dialog` when there is no PVE .enc match.
  const [pbsImportPath, setPbsImportPath] = useState<string>("")
  const [pbsImportBusy, setPbsImportBusy] = useState<boolean>(false)
  const pbsEncrypt = pbsEncryptMode !== "none"
  const [pbsRecoveryPass, setPbsRecoveryPass] = useState<string>("")
  const [pbsRecoveryPass2, setPbsRecoveryPass2] = useState<string>("")
  // Operator opted to replace an already-configured recovery escrow.
  // Default behavior matches the shell: don't prompt for the
  // passphrase if one is already saved — only show the inputs when
  // there's no escrow yet OR the operator explicitly asks to change.
  const [pbsRecoveryChange, setPbsRecoveryChange] = useState<boolean>(false)
  // Binary "upload key to PBS?" toggle. `true` maps to escrow_mode='full'
  // (passphrase required, envelope uploaded on every backup); `false`
  // maps to escrow_mode='none' (keyfile stays local, operator handles
  // the offsite copy). Defaults to 'No' — the keyfile is always
  // downloadable from the Monitor, so the operator can grab it and
  // save it offsite themselves without leaving a passphrase-wrapped
  // copy on the PBS server.
  const [pbsUploadToPbs, setPbsUploadToPbs] = useState<boolean>(false)
  // Whether the host already has an escrow blob configured. When
  // present, the passphrase input becomes optional ("leave blank to
  // keep saved"). Refreshed after a successful setup call.
  const { data: pbsRecoveryStatus, mutate: mutatePbsRecovery } = useSWR<{
    has_keyfile: boolean; has_recovery: boolean; has_keyfile_passphrase?: boolean; escrow_mode?: "none" | "local" | "full"
  }>(
    open && backend === "pbs" ? "/api/host-backups/pbs-recovery/status" : null,
    fetcher,
  )
  // Auto-discover a PVE-managed keyfile for the currently selected
  // PBS repository. When one is found, the "Use an existing keyfile"
  // radio uses it silently (matches the shell wizard's PVE auto-detect
  // path — the operator doesn't have to upload the same file twice).
  const { data: pbsPveDiscover } = useSWR<{
    entries: Array<{ name: string; server: string; datastore: string; path: string; matches_repository: boolean }>
  }>(
    open && backend === "pbs" && pbsRepository && !pbsRecoveryStatus?.has_keyfile
      ? `/api/host-backups/pbs-encryption/discover-pve-keyfiles?repository=${encodeURIComponent(pbsRepository)}`
      : null,
    fetcher,
  )
  const pbsPveMatch = pbsPveDiscover?.entries?.find((e) => e.matches_repository)
  const [localDestDir, setLocalDestDir] = useState<string>("")
  const [borgRepoSelected, setBorgRepoSelected] = useState<string>("")
  const [borgPassphrase, setBorgPassphrase] = useState<string>("")
  const [borgEncryptMode, setBorgEncryptMode] = useState<"none" | "repokey" | "keyfile" | "authenticated">("repokey")

  const [submitting, setSubmitting] = useState(false)
  const [error, setError] = useState<string | null>(null)

  const { data: destResp, mutate: mutateDest } = useSWR<DestinationsResp>(
    open ? "/api/host-backups/destinations" : null,
    fetcher,
  )
  const { data: pathsResp } = useSWR<{ paths: string[] }>(
    open ? "/api/host-backups/default-paths" : null,
    fetcher,
  )
  // Same inline AddDestinationDialog flow as the scheduled CreateJobDialog.
  const [addingDestType, setAddingDestType] = useState<"pbs" | "borg" | "local" | null>(null)
  const [pendingAutoSelectDest, setPendingAutoSelectDest] = useState<{ kind: "pbs" | "borg"; repository: string } | null>(null)
  useEffect(() => {
    if (!pendingAutoSelectDest || !destResp) return
    if (pendingAutoSelectDest.kind === "pbs") {
      const hit = destResp.pbs?.find((r) => r.repository === pendingAutoSelectDest.repository)
      if (hit) {
        setPbsRepository(hit.repository)
        setPendingAutoSelectDest(null)
      }
    } else {
      const hit = destResp.borg?.find((r) => r.repository === pendingAutoSelectDest.repository)
      if (hit) {
        setBorgRepoSelected(hit.repository)
        setPendingAutoSelectDest(null)
      }
    }
  }, [destResp, pendingAutoSelectDest])

  const selectedPbs = destResp?.pbs?.find((r) => r.repository === pbsRepository) ?? destResp?.pbs?.[0]

  useEffect(() => {
    if (!open) {
      setStep(1)
      setBackend("local")
      setProfileMode("default")
      setCustomPaths(new Set())
      setPbsRepository("")
      setPbsBackupId("")
      setPbsEncryptMode("none")
      setPbsImportFile(null)
      setPbsImportPath("")
      setPbsImportBusy(false)
      setPbsRecoveryPass("")
      setPbsRecoveryPass2("")
      setPbsRecoveryChange(false)
      setPbsUploadToPbs(false)
      setLocalDestDir("")
      setBorgRepoSelected("")
      setBorgPassphrase("")
      setBorgEncryptMode("repokey")
      setError(null)
      setSubmitting(false)
    }
  }, [open])

  useEffect(() => {
    if (backend === "pbs" && !pbsRepository && destResp?.pbs?.length) {
      setPbsRepository(destResp.pbs[0].repository)
    }
    if (backend === "borg" && !borgRepoSelected && destResp?.borg?.length) {
      setBorgRepoSelected(destResp.borg[0].repository)
    }
  }, [backend, destResp, pbsRepository, borgRepoSelected])

  const togglePath = (path: string) => {
    setCustomPaths((prev) => {
      const next = new Set(prev)
      if (next.has(path)) next.delete(path)
      else next.add(path)
      return next
    })
  }

  const defaultPaths = pathsResp?.paths ?? []
  const canAdvanceFrom1 = profileMode === "default" || (profileMode === "custom" && customPaths.size > 0)
  const backendValid =
    backend === "pbs"
      ? !!pbsRepository
      : backend === "local"
        ? true
        // Borg destination carries its own passphrase + encryption.
        // No need to gate on local state — the wizard no longer asks.
        : !!borgRepoSelected
  // Recovery gate for PBS encryption. Choosing "Encrypt" makes the
  // recovery passphrase mandatory — same as the CLI. The "Run backup"
  // button stays disabled until a matching passphrase pair is entered
  // (or an already-saved escrow is reused). No accept-risk escape:
  // encrypting without recovery would leave the keyfile only on this
  // host, and a reinstall would render every encrypted backup
  // unrecoverable.
  // Passphrase requirement only kicks in when: encryption is on AND
  // there's no keyfile installed yet (fresh setup) AND the operator
  // opted to upload the key to PBS. Every other case (reuse existing
  // keyfile, or "no upload" mode) doesn't need a passphrase.
  const pbsRecoveryOk = !(backend === "pbs" && pbsEncrypt) ||
    pbsRecoveryStatus?.has_keyfile ||
    !pbsUploadToPbs ||
    (pbsRecoveryPass !== "" && pbsRecoveryPass === pbsRecoveryPass2)
  const canSubmit = canAdvanceFrom1 && backendValid && pbsRecoveryOk

  async function handleRun() {
    if (!canSubmit) return
    setSubmitting(true)
    setError(null)
    // Unified encryption setup — mirrors CreateJobDialog. One atomic
    // /pbs-encryption/import call covers keyfile install + escrow
    // setup with source picked from what the operator provided.
    const needsInstall =
      backend === "pbs" &&
      pbsEncrypt &&
      !pbsRecoveryStatus?.has_keyfile
    if (needsInstall) {
      if (pbsEncryptMode === "existing" && !pbsPveMatch && !pbsImportFile && !pbsImportPath.trim()) {
        setError(t("backup.errors.pickKeyfile"))
        setSubmitting(false)
        return
      }
      if (pbsUploadToPbs && pbsRecoveryPass !== pbsRecoveryPass2) {
        setError(t("backup.errors.passphrasesDoNotMatch"))
        setSubmitting(false)
        return
      }
      setPbsImportBusy(true)
      try {
        let source: "generate" | "pve-storage" | "content" | "path"
        if (pbsEncryptMode === "new") source = "generate"
        else if (pbsPveMatch) source = "pve-storage"
        else if (pbsImportFile) source = "content"
        else source = "path"
        const body: Record<string, unknown> = {
          source,
          escrow_mode: pbsUploadToPbs ? "full" : "none",
        }
        if (pbsUploadToPbs) body.escrow_passphrase = pbsRecoveryPass
        if (source === "pve-storage") {
          body.pve_storage_name = pbsPveMatch!.name
        } else if (source === "content") {
          const buf = new Uint8Array(await pbsImportFile!.arrayBuffer())
          let bin = ""
          for (let i = 0; i < buf.length; i++) bin += String.fromCharCode(buf[i])
          body.content_b64 = btoa(bin)
        } else if (source === "path") {
          body.keyfile_path = pbsImportPath.trim()
        }
        await fetchApi("/api/host-backups/pbs-encryption/import", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(body),
        })
        mutatePbsRecovery()
      } catch (e) {
        const err = e as Error & { body?: { tool_output?: string; tool_exit_code?: number } }
        const detail = err.body?.tool_output
          ? `${err.message}\n\n${t("backup.errors.proxmoxBackupClientOutput")}:\n${err.body.tool_output}`
          : err.message
        setError(t("backup.errors.encryptionSetupFailed", { detail: detail || String(e) }))
        setPbsImportBusy(false)
        setSubmitting(false)
        return
      }
      setPbsImportBusy(false)
    }
    // NOTE: recovery escrow is now bundled into the /pbs-encryption/import
    // call above. No separate /pbs-recovery/setup pass is needed.
    try {
      const body: Record<string, unknown> = {
        backend,
        profile_mode: profileMode,
      }
      if (profileMode === "custom") {
        body.paths = Array.from(customPaths)
      }
      if (backend === "pbs") {
        body.pbs_repository = pbsRepository
        body.pbs_password = ""
        if (pbsBackupId) body.pbs_backup_id = pbsBackupId
        if (selectedPbs?.fingerprint) body.pbs_fingerprint = selectedPbs.fingerprint
        body.pbs_encrypt_mode = pbsEncryptMode
      } else if (backend === "local") {
        if (localDestDir.trim()) body.local_dest_dir = localDestDir.trim()
      } else if (backend === "borg") {
        // Passphrase + encrypt_mode are inherited from the destination
        // (borg-pass-<name>.txt sidecar + 4th field of borg-targets.txt).
        body.borg_repo = borgRepoSelected
      }
      const resp = await fetchApi<{ status: string; job_id: string }>(
        "/api/host-backups/manual-run",
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(body),
        },
      )
      onLaunched(resp.job_id)
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e))
    } finally {
      setSubmitting(false)
    }
  }

  return (
    <Dialog open={open} onOpenChange={(v) => { if (!v) onClose() }}>
      <DialogContent className="max-w-2xl max-h-[85vh] flex flex-col">
        <DialogHeader>
          <DialogTitle className="flex items-center gap-2">
            <PlayCircle className="h-5 w-5 text-blue-500" />
            {t("backup.manual.runOneShotBackup")}
          </DialogTitle>
          <DialogDescription>
            {t("backup.jobs.stepOf", { step, total: 2 })} · {t("backup.manual.oneShotDescription")}
          </DialogDescription>
        </DialogHeader>

        <div className="flex items-center gap-1">
          {[1, 2].map((n) => (
            <div
              key={n}
              className={`h-1 flex-1 rounded-full ${n <= step ? "bg-blue-500" : "bg-border"}`}
            />
          ))}
        </div>

        <div className="overflow-y-auto pr-2 -mr-2 space-y-4 flex-1">
          {step === 1 && (
            <div className="space-y-4">
              <div>
                <Label>{t("backup.fields.backend")}</Label>
                <div className="grid grid-cols-1 sm:grid-cols-3 gap-2 mt-1">
                  {(["pbs", "local", "borg"] as const).map((b) => {
                    const Icon = b === "pbs" ? Server : b === "local" ? HardDrive : Archive
                    const desc = b === "pbs"
                      ? t("backup.backends.pbsShortDescription")
                      : b === "local"
                        ? t("backup.backends.localShortDescription")
                        : t("backup.backends.borgShortDescription")
                    return (
                      <button
                        key={b}
                        type="button"
                        onClick={() => setBackend(b)}
                        className={`text-left p-3 rounded-md border ${backend === b ? "border-blue-500 bg-blue-500/5" : "border-border bg-background/40"} hover:bg-white/5 transition-colors`}
                      >
                        <div className="flex items-center gap-2 font-medium text-sm">
                          <Icon className="h-4 w-4" />
                          {b === "pbs" ? "PBS" : b === "borg" ? "Borg" : t("backup.backends.local")}
                        </div>
                        <div className="text-xs text-muted-foreground mt-1">{desc}</div>
                      </button>
                    )
                  })}
                </div>
              </div>

              <div>
                <Label>{t("backup.profile.title")}</Label>
                <div className="grid grid-cols-2 gap-2 mt-1">
                  <button
                    type="button"
                    onClick={() => setProfileMode("default")}
                    className={`text-left p-3 rounded-md border ${profileMode === "default" ? "border-blue-500 bg-blue-500/5" : "border-border bg-background/40"} hover:bg-white/5 transition-colors`}
                  >
                    <div className="text-sm font-medium">{t("backup.profile.default")}</div>
                    <div className="text-xs text-muted-foreground mt-1">
                      {t("backup.profile.defaultDescriptionShort", { count: defaultPaths.length })}
                    </div>
                  </button>
                  <button
                    type="button"
                    onClick={() => setProfileMode("custom")}
                    className={`text-left p-3 rounded-md border ${profileMode === "custom" ? "border-blue-500 bg-blue-500/5" : "border-border bg-background/40"} hover:bg-white/5 transition-colors`}
                  >
                    <div className="text-sm font-medium">{t("backup.profile.custom")}</div>
                    <div className="text-xs text-muted-foreground mt-1">
                      {t("backup.profile.pickPaths")}
                    </div>
                  </button>
                </div>
              </div>

              {profileMode === "custom" && (
                <div>
                  <Label>{t("backup.profile.pathsToInclude", { selected: customPaths.size, total: defaultPaths.length })}</Label>
                  <ScrollArea className="h-48 mt-1 rounded-md border border-border p-2">
                    {defaultPaths.map((p) => (
                      <label
                        key={p}
                        className="flex items-center gap-2 py-1 px-1 hover:bg-white/5 rounded text-xs cursor-pointer"
                      >
                        <Checkbox
                          checked={customPaths.has(p)}
                          onCheckedChange={() => togglePath(p)}
                        />
                        <span className="font-mono">{p}</span>
                      </label>
                    ))}
                  </ScrollArea>
                </div>
              )}
            </div>
          )}

          {step === 2 && (
            <div className="space-y-4">
              {backend === "pbs" && (
                <>
                  <div>
                    <Label>{t("backup.fields.pbsRepository")}</Label>
                    {destResp?.pbs?.length ? (
                      <div className="mt-1 flex items-center gap-2">
                        <Select value={pbsRepository} onValueChange={setPbsRepository}>
                          <SelectTrigger className="flex-1 min-w-0">
                            {/* Manual render: SelectValue's default mode
                                duplicates the item children and cuts
                                them in the middle on mobile. We render
                                "name — repository" ourselves with a
                                proper trailing ellipsis. */}
                            {(() => {
                              const sel = destResp.pbs.find((r) => r.repository === pbsRepository)
                              if (!sel) {
                                return <span className="truncate text-muted-foreground">{t("backup.placeholders.pickPbsRepository")}</span>
                              }
                              return (
                                <span className="truncate font-mono text-left">
                                  {sel.name}
                                  <span className="text-muted-foreground"> — {sel.repository}</span>
                                </span>
                              )
                            })()}
                          </SelectTrigger>
                          <SelectContent>
                            {destResp.pbs.map((r) => (
                              <SelectItem key={`${r.name}-${r.repository}`} value={r.repository}>
                                <span className="font-mono">{r.name}</span>
                                <span className="text-muted-foreground ml-2">— {r.repository}</span>
                              </SelectItem>
                            ))}
                          </SelectContent>
                        </Select>
                        <Button
                          type="button"
                          size="sm"
                          variant="outline"
                          className="shrink-0 bg-blue-500/10 border-blue-500/40 !text-blue-400 hover:bg-blue-500/20 hover:!text-blue-300"
                          onClick={() => setAddingDestType("pbs")}
                          title={t("backup.destinations.saveAnotherPbsTitle")}
                        >
                          <Plus className="h-3.5 w-3.5 mr-1" />
                          {t("backup.actions.addAnother")}
                        </Button>
                      </div>
                    ) : (
                      <div className="mt-1 p-3 rounded-md border border-amber-500/40 bg-amber-500/5 text-xs space-y-2">
                        <div>{t("backup.destinations.noPbsInDialog")}</div>
                        <Button
                          type="button"
                          size="sm"
                          variant="outline"
                          className="bg-blue-500/10 border-blue-500/40 !text-blue-400 hover:bg-blue-500/20 hover:!text-blue-300"
                          onClick={() => setAddingDestType("pbs")}
                        >
                          <Plus className="h-3.5 w-3.5 mr-1" />
                          {t("backup.destinations.addPbsRepository")}
                        </Button>
                      </div>
                    )}
                  </div>
                  <div>
                    <Label htmlFor="manualPbsBackupId">{t("backup.fields.groupNameInPbs")}</Label>
                    <Input
                      id="manualPbsBackupId"
                      value={pbsBackupId}
                      onChange={(e) => setPbsBackupId(e.target.value)}
                      placeholder={t("backup.placeholders.leaveBlankForDefault")}
                      className="font-mono mt-1"
                    />
                  </div>
                  <div className="pt-2 border-t border-border space-y-3">
                    <label className="flex items-start gap-2 cursor-pointer">
                      <Checkbox
                        checked={pbsEncrypt}
                        onCheckedChange={(v) => {
                          const checked = !!v
                          if (!checked) {
                            setPbsEncryptMode("none")
                          } else {
                            setPbsEncryptMode(pbsRecoveryStatus?.has_keyfile ? "existing" : "new")
                          }
                        }}
                        className="mt-0.5"
                      />
                      <div className="flex-1">
                        <div className="inline-flex items-center gap-1.5 text-sm">
                          <Lock className="h-3.5 w-3.5 text-emerald-400" />
                          {t("backup.encryption.encryptThisBackup")}
                        </div>
                        <p className="text-[11px] text-muted-foreground mt-1">
                          {t("backup.encryption.usesSharedKeyfileAt")} <code className="font-mono">/usr/local/share/proxmenux/pbs-key.conf</code>.
                        </p>
                      </div>
                    </label>


                    {pbsEncrypt && !pbsRecoveryStatus?.has_keyfile && (
                      <div className="space-y-2 rounded-md border border-emerald-500/30 bg-emerald-500/5 p-3">
                        <div className="text-[11px] font-medium">
                          {t("backup.encryption.noKeyStored")}
                        </div>
                        <label className="flex items-start gap-2 cursor-pointer text-[11px]">
                          <input
                            type="radio"
                            name="manualPbsEncryptSetup"
                            checked={pbsEncryptMode === "new"}
                            onChange={() => setPbsEncryptMode("new")}
                            className="mt-1"
                          />
                          <div className="flex-1">
                            <div className="font-medium">{t("backup.encryption.generateNewKeyfile")}</div>
                            <div className="text-muted-foreground">{t("backup.encryption.generateManualDescription")}</div>
                          </div>
                        </label>
                        <label className="flex items-start gap-2 cursor-pointer text-[11px]">
                          <input
                            type="radio"
                            name="manualPbsEncryptSetup"
                            checked={pbsEncryptMode === "existing"}
                            onChange={() => setPbsEncryptMode("existing")}
                            className="mt-1"
                          />
                          <div className="flex-1">
                            <div className="font-medium">{t("backup.encryption.importExistingKeyfile")}</div>
                            <div className="text-muted-foreground">{t("backup.encryption.importManualDescription")}</div>
                          </div>
                        </label>
                        {pbsEncryptMode === "existing" && pbsPveMatch && (
                          <div className="pt-1 rounded-md border border-emerald-500/40 bg-emerald-500/5 p-2.5 text-[11px] space-y-1">
                            <div className="flex items-start gap-1.5">
                              <CheckCircle2 className="h-3.5 w-3.5 text-emerald-400 mt-0.5 shrink-0" />
                              <div className="flex-1">
                                <div className="font-medium text-foreground">{t("backup.encryption.autoDetectedFromPveStorage", { storage: pbsPveMatch.name })}</div>
                                <div className="text-muted-foreground mt-0.5">
                                  {t("backup.encryption.existingKeyAt")} <code className="font-mono text-[10.5px]">{pbsPveMatch.path}</code>
                                </div>
                                <div className="text-muted-foreground mt-0.5">{t("backup.encryption.pveKeyWillBeCopied")}</div>
                              </div>
                            </div>
                          </div>
                        )}
                        {pbsEncryptMode === "existing" && !pbsPveMatch && (
                          <div className="space-y-2 pt-1">
                            <div className="space-y-1.5">
                              <Label htmlFor="manualPbsImportFile" className="text-[11px] font-medium">
                                {t("backup.keyfileActions.uploadFromMachine")}
                              </Label>
                              <Input
                                id="manualPbsImportFile"
                                type="file"
                                accept=".conf,.key,application/json,text/plain"
                                onChange={(e) => setPbsImportFile(e.target.files?.[0] ?? null)}
                                disabled={pbsImportBusy || !!pbsImportPath.trim()}
                                className="h-8 text-[11px]"
                              />
                            </div>
                            <div className="text-[10px] text-center text-muted-foreground">{t("backup.common.or")}</div>
                            <div className="space-y-1.5">
                              <Label htmlFor="manualPbsImportPath" className="text-[11px] font-medium">
                                {t("backup.fields.absolutePathOnHost")}
                              </Label>
                              <Input
                                id="manualPbsImportPath"
                                type="text"
                                value={pbsImportPath}
                                onChange={(e) => setPbsImportPath(e.target.value)}
                                disabled={pbsImportBusy || !!pbsImportFile}
                                placeholder={t("backup.placeholders.keyfilePath")}
                                className="h-8 text-[11px] font-mono"
                              />
                              <p className="text-[10px] text-muted-foreground">
                                {t("backup.encryption.importKeyfileHelpBefore")} <code className="font-mono">/usr/local/share/proxmenux/pbs-key.conf</code> {t("backup.encryption.importKeyfileHelpAfter")}
                              </p>
                            </div>
                          </div>
                        )}
                      </div>
                    )}

                    {pbsEncrypt && (
                      <div className="pl-7 space-y-3 rounded-md border border-blue-500/30 bg-blue-500/5 p-3">
                        {pbsRecoveryStatus?.has_keyfile && !pbsRecoveryChange && (
                          <div className="flex items-start gap-2 text-[11px]">
                            <CheckCircle2 className="h-3.5 w-3.5 text-emerald-400 mt-0.5 shrink-0" />
                            <div className="flex-1">
                              <div className="text-foreground">{t("backup.keyfileManagement.thisBackupReusesKeyfile")}</div>
                              <div className="text-muted-foreground mt-0.5">{t("backup.keyfileManagement.currentPbsUploadStays")}</div>
                            </div>
                            <button
                              type="button"
                              onClick={() => setPbsRecoveryChange(true)}
                              className="text-blue-400 hover:text-blue-300 underline underline-offset-2 shrink-0"
                            >
                              {t("backup.actions.change")}
                            </button>
                          </div>
                        )}
                        {pbsRecoveryStatus?.has_keyfile && pbsRecoveryChange && (
                          <>
                            <KeyfileActionsBar
                              mutateStatus={mutatePbsRecovery}
                              escrowMode={pbsRecoveryStatus?.escrow_mode}
                            />
                            <button
                              type="button"
                              onClick={() => setPbsRecoveryChange(false)}
                              className="text-[11px] text-muted-foreground hover:text-foreground underline underline-offset-2"
                            >
                              {t("backup.keyfileManagement.hideOptions")}
                            </button>
                          </>
                        )}
                        {!pbsRecoveryStatus?.has_keyfile && (
                          <>
                            <div>
                              <div className="text-[11px] font-medium text-foreground mb-2">{t("backup.keyfileManagement.uploadToPbs")}</div>
                              <div className="grid gap-1.5">
                                <label className="flex items-start gap-2 cursor-pointer text-[11px]">
                                  <input
                                    type="radio"
                                    name="pbsUploadToPbsManual"
                                    checked={!pbsUploadToPbs}
                                    onChange={() => setPbsUploadToPbs(false)}
                                    className="mt-1"
                                  />
                                  <div className="flex-1">
                                    <div className="font-medium">{t("backup.keyfileManagement.keepLocalOnly")}</div>
                                    <div className="text-muted-foreground">{t("backup.keyfileManagement.keepLocalDescriptionBefore")} <code className="font-mono text-[10.5px]">/usr/local/share/proxmenux/pbs-key.conf</code>. {t("backup.keyfileManagement.keepLocalDescriptionAfter")}</div>
                                  </div>
                                </label>
                                <label className="flex items-start gap-2 cursor-pointer text-[11px]">
                                  <input
                                    type="radio"
                                    name="pbsUploadToPbsManual"
                                    checked={pbsUploadToPbs}
                                    onChange={() => setPbsUploadToPbs(true)}
                                    className="mt-1"
                                  />
                                  <div className="flex-1">
                                    <div className="font-medium">{t("backup.keyfileManagement.yesUpload")}</div>
                                    <div className="text-muted-foreground">{t("backup.keyfileManagement.yesUploadDescription")}</div>
                                  </div>
                                </label>
                              </div>
                            </div>
                            {pbsUploadToPbs ? (
                              <div className="space-y-2 pt-1 border-t border-blue-500/20">
                                <div>
                                  <Label htmlFor="manualPbsRecPass" className="text-[11px]">{t("backup.fields.recoveryPassphrase")}</Label>
                                  <Input
                                    id="manualPbsRecPass"
                                    type="password"
                                    value={pbsRecoveryPass}
                                    onChange={(e) => setPbsRecoveryPass(e.target.value)}
                                    placeholder={t("backup.placeholders.recoveryPassphrase")}
                                    className="font-mono mt-1 h-8 text-xs"
                                  />
                                </div>
                                <div>
                                  <Label htmlFor="manualPbsRecPass2" className="text-[11px]">{t("backup.fields.confirmPassphrase")}</Label>
                                  <Input
                                    id="manualPbsRecPass2"
                                    type="password"
                                    value={pbsRecoveryPass2}
                                    onChange={(e) => setPbsRecoveryPass2(e.target.value)}
                                    placeholder={t("backup.placeholders.typeItAgain")}
                                    className="font-mono mt-1 h-8 text-xs"
                                  />
                                  {pbsRecoveryPass && pbsRecoveryPass2 && pbsRecoveryPass !== pbsRecoveryPass2 && (
                                    <p className="text-[11px] text-red-400 mt-1">{t("backup.errors.passphrasesDontMatch")}</p>
                                  )}
                                </div>
                              </div>
                            ) : (
                              <div className="pt-1 border-t border-blue-500/20 text-[11px] text-amber-300 bg-amber-500/10 border border-amber-500/30 rounded px-3 py-2 leading-relaxed space-y-1">
                                <div className="font-semibold">{t("backup.keyfileManagement.keepOffsiteCopy")}</div>
                                <div>{t("backup.keyfileManagement.copyAfterManualBefore")} <code className="font-mono text-[10.5px] bg-amber-500/20 rounded px-1">/usr/local/share/proxmenux/pbs-key.conf</code> {t("backup.keyfileManagement.copyAfterCreateAfter")}</div>
                                <div>{t("backup.keyfileManagement.withoutCopyWarning")}</div>
                              </div>
                            )}
                          </>
                        )}
                      </div>
                    )}
                  </div>
                </>
              )}
              {backend === "local" && (
                <div className="space-y-2">
                  <Label>{t("backup.fields.localDestination")}</Label>
                  {(destResp?.local?.entries?.length ?? 0) > 0 && (
                    <div className="flex items-center gap-2">
                      <Select
                        value={localDestDir || (destResp?.local?.effective ?? "")}
                        onValueChange={(v) => setLocalDestDir(v)}
                      >
                        <SelectTrigger className="flex-1">
                          <SelectValue placeholder={t("backup.placeholders.pickLocalDestination")} />
                        </SelectTrigger>
                        <SelectContent>
                          {destResp?.local?.entries?.map((e) => (
                            <SelectItem key={e.path} value={e.path}>
                              <span className="font-mono">{e.path}</span>
                              {e.source === "default" && (
                                <span className="text-muted-foreground ml-2 text-[10px] uppercase">{t("backup.profile.default")}</span>
                              )}
                            </SelectItem>
                          ))}
                        </SelectContent>
                      </Select>
                      <Button
                        type="button"
                        size="sm"
                        variant="outline"
                        className="shrink-0 bg-blue-500/10 border-blue-500/40 !text-blue-400 hover:bg-blue-500/20 hover:!text-blue-300"
                        onClick={() => setAddingDestType("local")}
                        title={t("backup.destinations.saveAnotherLocalTitle")}
                      >
                        <Plus className="h-3.5 w-3.5 mr-1" />
                        {t("backup.actions.addAnother")}
                      </Button>
                    </div>
                  )}
                  <p className="text-xs text-muted-foreground">
                    {t("backup.destinations.localManualPickHelp")}
                  </p>
                  <Input
                    id="manualLocalDest"
                    value={localDestDir}
                    onChange={(e) => setLocalDestDir(e.target.value)}
                    placeholder={destResp?.local?.effective || "/var/lib/vz/dump"}
                    className="font-mono"
                  />
                </div>
              )}
              {backend === "borg" && (
                <>
                  <div>
                    <Label>{t("backup.fields.borgRepository")}</Label>
                    {destResp?.borg?.length ? (
                      <div className="mt-1 flex items-center gap-2">
                        <Select value={borgRepoSelected} onValueChange={setBorgRepoSelected}>
                          <SelectTrigger className="flex-1 min-w-0">
                            {(() => {
                              const sel = destResp.borg.find((r) => r.repository === borgRepoSelected)
                              if (!sel) {
                                return <span className="truncate text-muted-foreground">{t("backup.placeholders.pickBorgRepository")}</span>
                              }
                              return (
                                <span className="truncate font-mono text-left">
                                  {sel.name}
                                  <span className="text-muted-foreground"> — {sel.repository}</span>
                                </span>
                              )
                            })()}
                          </SelectTrigger>
                          <SelectContent>
                            {destResp.borg.map((r) => (
                              <SelectItem key={`${r.name}-${r.repository}`} value={r.repository}>
                                <span className="font-mono">{r.name}</span>
                                <span className="text-muted-foreground ml-2">— {r.repository}</span>
                              </SelectItem>
                            ))}
                          </SelectContent>
                        </Select>
                        <Button
                          type="button"
                          size="sm"
                          variant="outline"
                          className="shrink-0 bg-fuchsia-500/10 border-fuchsia-500/40 !text-fuchsia-400 hover:bg-fuchsia-500/20 hover:!text-fuchsia-300"
                          onClick={() => setAddingDestType("borg")}
                          title={t("backup.destinations.saveAnotherBorgTitle")}
                        >
                          <Plus className="h-3.5 w-3.5 mr-1" />
                          {t("backup.actions.addAnother")}
                        </Button>
                      </div>
                    ) : (
                      <div className="mt-1 p-3 rounded-md border border-amber-500/40 bg-amber-500/5 text-xs space-y-2">
                        <div>{t("backup.destinations.noBorgInDialog")}</div>
                        <Button
                          type="button"
                          size="sm"
                          variant="outline"
                          className="bg-fuchsia-500/10 border-fuchsia-500/40 !text-fuchsia-400 hover:bg-fuchsia-500/20 hover:!text-fuchsia-300"
                          onClick={() => setAddingDestType("borg")}
                        >
                          <Plus className="h-3.5 w-3.5 mr-1" />
                          {t("backup.destinations.addBorgRepository")}
                        </Button>
                      </div>
                    )}
                  </div>
                  {(() => {
                    const dest = destResp?.borg?.find((r) => r.repository === borgRepoSelected)
                    if (!dest) return null
                    const mode = dest.encrypt_mode || "repokey"
                    return (
                      <p className="text-[11px] text-muted-foreground">
                        {t("backup.destinations.borgEncryptionInherited", { mode: mode === "none" ? t("backup.encryption.none") : mode })}
                      </p>
                    )
                  })()}
                </>
              )}

              {/* Summary — mirrors the styling of the JobDetailModal. */}
              <div className="rounded-md border border-border bg-background/40 p-3 space-y-2 text-xs">
                <div className="text-[10px] uppercase tracking-wider text-muted-foreground mb-1">{t("backup.jobs.summary")}</div>
                <div className="flex items-center gap-2 flex-wrap">
                  <span className="text-muted-foreground">{t("backup.fields.backendLabel")}</span>
                  <Badge variant="outline" className={`text-[10px] uppercase tracking-wide ${methodBadgeCls(backend)}`}>
                    {backend === "pbs" ? "PBS" : backend === "borg" ? "Borg" : t("backup.backends.local")}
                  </Badge>
                  <Badge variant="outline" className="text-[10px] text-purple-400 border-purple-400/40 bg-purple-500/5">
                    {t("backup.status.manualOneShot")}
                  </Badge>
                </div>
                <div className="flex items-center gap-1.5 flex-wrap">
                  <FileSearch className="h-3 w-3 text-green-500/80" />
                  <span className="text-green-500/90 uppercase tracking-wider text-[10px]">{t("backup.fields.profileLabel")}</span>
                  <span className="text-foreground">{profileMode === "default" ? t("backup.profile.default") : t("backup.profile.custom")}</span>
                  <span className="text-muted-foreground">({t("backup.paths.count", { count: profileMode === "default" ? defaultPaths.length : customPaths.size })})</span>
                </div>
              </div>

              {error && (
                <div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
                  {error}
                </div>
              )}
            </div>
          )}
        </div>

        <div className="flex items-center justify-between gap-2 pt-3 border-t border-border">
          <Button variant="ghost" onClick={onClose} disabled={submitting}>
            {t("actions.cancel")}
          </Button>
          <div className="flex items-center gap-2">
            {step > 1 && (
              <Button
                variant="outline"
                onClick={() => setStep((s) => (s - 1) as 1 | 2)}
                disabled={submitting}
              >
                <ChevronLeft className="h-4 w-4 mr-1" />
                {t("backup.actions.back")}
              </Button>
            )}
            {step < 2 ? (
              <Button
                onClick={() => setStep(2)}
                disabled={!canAdvanceFrom1}
              >
                {t("backup.actions.next")}
                <ChevronRight className="h-4 w-4 ml-1" />
              </Button>
            ) : (
              <Button
                onClick={handleRun}
                disabled={!canSubmit || submitting}
                className="bg-purple-500 hover:bg-purple-600 text-white"
              >
                {submitting ? (
                  <Loader2 className="h-4 w-4 animate-spin mr-2" />
                ) : (
                  <PlayCircle className="h-4 w-4 mr-2" />
                )}
                {t("backup.manual.runBackup")}
              </Button>
            )}
          </div>
        </div>
      </DialogContent>

      {/* AddDestinationDialog spawned from inside the manual-backup
          dialog. Same auto-select-on-save behavior as the wizard. */}
      <AddDestinationDialog
        type={addingDestType}
        onClose={() => setAddingDestType(null)}
        onSaved={(repo) => {
          if (repo && (addingDestType === "pbs" || addingDestType === "borg")) {
            setPendingAutoSelectDest({ kind: addingDestType, repository: repo })
          }
          setAddingDestType(null)
          mutateDest()
        }}
      />
    </Dialog>
  )
}

// ──────────────────────────────────────────────────────────────
// Backup destinations CRUD (Sprint D).
//
// Three persistence files live in $HB_STATE_DIR (= /usr/local/share/proxmenux/):
//   - pbs-manual-configs.txt  (manual PBS configs the shell also reads)
//   - borg-targets.txt        (Borg repos saved by the shell)
//   - local-target.conf       (single local target — default or override)
//
// Auto-discovered PBS storages from /etc/pve/storage.cfg show with a
// "proxmox" badge and are NOT deletable here (PVE owns those).
// ──────────────────────────────────────────────────────────────

// One entry in the unified destinations list. PBS / Borg / Local all
// flatten into this shape so the row component doesn't need to know
// which backend a given destination targets — only the color, the
// label and the action set.
type UnifiedDest =
  | { id: string; kind: "local"; path: string; source: "default" | "custom"; removable: boolean; jobsUsing: string[] }
  | {
      id: string; kind: "pbs"; name: string; repository: string;
      source: "proxmox" | "manual"; fingerprint?: string; removable: boolean
      jobsUsing: string[]
    }
  | {
      id: string; kind: "borg"; name: string; repository: string;
      isSsh: boolean; ssh?: { user: string; host: string; remotePath: string };
      sshKeyPath?: string;
      // The destination's encryption mode + whether a passphrase is
      // saved server-side; used to render a Lock / Unlock badge so the
      // operator sees at a glance which Borg repos are encrypted.
      encryptMode?: "none" | "repokey" | "keyfile" | "authenticated"
      hasPassphrase?: boolean
      removable: boolean
      jobsUsing: string[]
    }

interface CapacityInfo {
  id: string
  total?: number
  available?: number
  used?: number
  is_usb?: boolean
  remote?: boolean
  error?: string
}

// Parse a Borg repository string into {user, host, remotePath} when
// it's an SSH URL — used for the capacity probe over ssh.
function parseBorgSsh(repo: string): { user: string; host: string; remotePath: string } | null {
  const m = repo.match(/^ssh:\/\/([^@]+)@([^/]+)\/(.+)$/)
  if (!m) return null
  return { user: m[1], host: m[2], remotePath: `/${m[3]}` }
}

function DestinationsSection({
  destinations,
  onChanged,
}: {
  destinations?: DestinationsResp
  onChanged: () => void
}) {
  const t = useT()
  const [configuring, setConfiguring] = useState<boolean>(false)
  const [editingDest, setEditingDest] = useState<EditingDest | null>(null)
  const [confirmingDest, setConfirmingDest] = useState<UnifiedDest | null>(null)
  const [busyKey, setBusyKey] = useState<string | null>(null)
  const [error, setError] = useState<string | null>(null)

  // Used by the Delete-destination confirm to tell the operator how
  // many backups already live on this target (they're kept after the
  // destination + jobs cascade, so the warning is informational).
  const { data: localArchivesResp } = useSWR<{ archives: BackupArchive[] }>(
    "/api/host-backups/archives",
    fetcher,
    { refreshInterval: 60_000, revalidateOnFocus: false },
  )
  const { data: remoteArchivesResp } = useSWR<RemoteArchivesResp>(
    "/api/host-backups/remote-archives",
    fetcher,
    { refreshInterval: 60_000, revalidateOnFocus: false },
  )
  // Recovery state — only needed when at least one PBS destination
  // exists. Drives the "Recover keyfile" banner and the "PBS encryption
  // key" management block.
  const hasPbs = (destinations?.pbs?.length ?? 0) > 0
  const { data: pbsRecoveryStatus, mutate: mutatePbsRecovery } = useSWR<{
    has_keyfile: boolean; has_recovery: boolean; has_keyfile_passphrase?: boolean; escrow_mode?: "none" | "local" | "full"; keyfile_fingerprint?: string
  }>(hasPbs ? "/api/host-backups/pbs-recovery/status" : null, fetcher)
  const { data: pbsRecoveryAvailable } = useSWR<{
    snapshots: Array<{ repo_name: string; repo_repository: string; backup_id: string; source_host: string; backup_time: number; snapshot: string }>
    errors: Array<{ repo_name: string; error: string }>
  }>(
    hasPbs && pbsRecoveryStatus && !pbsRecoveryStatus.has_keyfile
      ? "/api/host-backups/pbs-recovery/available"
      : null,
    fetcher,
  )
  const [recoveringKeyfile, setRecoveringKeyfile] = useState<boolean>(false)
  const backupsCountFor = (it: UnifiedDest): number => {
    if (it.kind === "local") {
      const root = it.path.replace(/\/+$/, "") + "/"
      return (localArchivesResp?.archives || []).filter(
        (a) => (a.path || "").startsWith(root),
      ).length
    }
    const name = it.kind === "pbs" ? it.name : it.kind === "borg" ? it.name : ""
    return (remoteArchivesResp?.snapshots || []).filter(
      (s) => s.backend === it.kind && s.repo_name === name,
    ).length
  }

  // Flatten PBS + Borg + Local into one list. Order: locals first
  // (default always on top), then PBS, then Borg — matches the
  // mental model of "what's already on this machine" before any
  // remote target.
  const items: UnifiedDest[] = (() => {
    const out: UnifiedDest[] = []
    for (const e of destinations?.local?.entries || []) {
      out.push({
        id: `local:${e.path}`,
        kind: "local",
        path: e.path,
        source: e.source as "default" | "custom",
        removable: e.removable,
        jobsUsing: e.jobs_using || [],
      })
    }
    for (const r of destinations?.pbs || []) {
      out.push({
        id: `pbs:${r.name}:${r.repository}`,
        kind: "pbs",
        name: r.name,
        repository: r.repository,
        source: r.source as "proxmox" | "manual",
        fingerprint: r.fingerprint || undefined,
        removable: r.source === "manual",
        jobsUsing: r.jobs_using || [],
      })
    }
    for (const r of destinations?.borg || []) {
      const ssh = parseBorgSsh(r.repository)
      out.push({
        id: `borg:${r.name}`,
        kind: "borg",
        name: r.name,
        repository: r.repository,
        isSsh: !!ssh,
        ssh: ssh ?? undefined,
        sshKeyPath: (r as { ssh_key_path?: string }).ssh_key_path,
        encryptMode: r.encrypt_mode,
        hasPassphrase: r.has_passphrase,
        removable: true,
        jobsUsing: r.jobs_using || [],
      })
    }
    return out
  })()

  // Build the capacity-probe payload + key for SWR. The key includes
  // every destination's id so SWR re-fetches when the list changes
  // (add / remove). 30 s refresh keeps the bars roughly live without
  // hammering ssh / pbs every render.
  const capacityTargets = items.map((it) => {
    if (it.kind === "local") return { id: it.id, kind: "local", path: it.path }
    if (it.kind === "pbs") return { id: it.id, kind: "pbs", name: it.name, repository: it.repository }
    if (it.isSsh && it.ssh) {
      return {
        id: it.id, kind: "borg-ssh",
        host: it.ssh.host, user: it.ssh.user,
        remote_path: it.ssh.remotePath,
        key_path: it.sshKeyPath || "",
      }
    }
    return { id: it.id, kind: "borg-local", path: it.repository }
  })
  const capacityKey = items.length
    ? `/api/host-backups/destinations/capacity?keys=${items.map((i) => i.id).join(",")}`
    : null
  const { data: capacityResp } = useSWR<{ results: CapacityInfo[] }>(
    capacityKey,
    () =>
      fetchApi("/api/host-backups/destinations/capacity", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ targets: capacityTargets }),
      }),
    { refreshInterval: 30_000, revalidateOnFocus: false },
  )
  const capByEdid = new Map<string, CapacityInfo>(
    (capacityResp?.results || []).map((r) => [r.id, r] as const),
  )

  async function removePbs(name: string, force = false) {
    setBusyKey(`pbs:${name}`)
    setError(null)
    try {
      await fetchApi(`/api/host-backups/destinations/pbs/${encodeURIComponent(name)}${force ? "?force=true" : ""}`, { method: "DELETE" })
      onChanged()
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e))
    } finally {
      setBusyKey(null)
    }
  }
  async function removeBorg(name: string, force = false) {
    setBusyKey(`borg:${name}`)
    setError(null)
    try {
      await fetchApi(`/api/host-backups/destinations/borg/${encodeURIComponent(name)}${force ? "?force=true" : ""}`, { method: "DELETE" })
      onChanged()
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e))
    } finally {
      setBusyKey(null)
    }
  }
  async function removeLocal(path: string, force = false) {
    setBusyKey(`local:${path}`)
    setError(null)
    try {
      const params = new URLSearchParams({ path })
      if (force) params.set("force", "true")
      await fetchApi(
        `/api/host-backups/destinations/local?${params.toString()}`,
        { method: "DELETE" },
      )
      onChanged()
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e))
    } finally {
      setBusyKey(null)
    }
  }
  async function confirmDeleteDest() {
    const it = confirmingDest
    if (!it) return
    const force = it.jobsUsing.length > 0
    setConfirmingDest(null)
    if (it.kind === "pbs") return removePbs(it.name, force)
    if (it.kind === "borg") return removeBorg(it.name, force)
    if (it.kind === "local") return removeLocal(it.path, force)
  }
  // Detach a USB drive — only the filesystem is unmounted; the path
  // stays in local-target.conf so re-plugging the same disk picks it
  // up again without re-adding the destination.
  async function unmountUsb(mountpoint: string) {
    setBusyKey(`umount:${mountpoint}`)
    setError(null)
    try {
      await fetchApi("/api/host-backups/usb-drives/unmount", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ mountpoint }),
      })
      onChanged()
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e))
    } finally {
      setBusyKey(null)
    }
  }

  return (
    <div className="space-y-3">
      <div className="flex justify-end">
        <Button
          size="sm"
          className="h-8 px-3 bg-blue-500 hover:bg-blue-600 text-white"
          onClick={() => setConfiguring(true)}
        >
          <Plus className="h-4 w-4 mr-1" />
          {t("backup.destinations.configureDestination")}
        </Button>
      </div>
      {error && (
        <div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
          {error}
        </div>
      )}
      {/* Recovery banner: only when there's no local keyfile AND at
          least one escrow blob was found on a configured PBS. Click
          opens a passphrase dialog and the keyfile is rebuilt from
          the blob. Equivalent to hb_pbs_try_keyfile_recovery in the
          shell — what runs after a fresh PVE install when the
          operator points the new host at the same PBS. */}
      {hasPbs && pbsRecoveryStatus && !pbsRecoveryStatus.has_keyfile && (pbsRecoveryAvailable?.snapshots?.length ?? 0) > 0 && (
        <button
          type="button"
          onClick={() => setRecoveringKeyfile(true)}
          className="w-full flex items-start gap-2 px-3 py-2.5 rounded-md border border-emerald-500/40 bg-emerald-500/5 hover:bg-emerald-500/10 transition-colors text-left"
          title={t("backup.recovery.copyFoundTitle")}
        >
          <Lock className="h-4 w-4 mt-0.5 text-emerald-400 shrink-0" />
          <div className="min-w-0 flex-1">
            <div className="text-sm font-medium text-foreground">
              {t("backup.recovery.keyfileMissingTitle")}
            </div>
            <div className="text-[11px] text-muted-foreground mt-0.5">
              {t("backup.recovery.encryptedCopyAvailable", { count: pbsRecoveryAvailable!.snapshots.length })}
            </div>
          </div>
          <span className="text-xs text-emerald-300 shrink-0 self-center">{t("backup.recovery.recoverArrow")}</span>
        </button>
      )}

      <div className="space-y-2">
        {items.map((it) => {
          const cap = capByEdid.get(it.id)
          const busy = busyKey === it.id ||
            (it.kind === "pbs" && busyKey === `pbs:${it.name}`) ||
            (it.kind === "borg" && busyKey === `borg:${it.name}`) ||
            (it.kind === "local" && busyKey === `local:${it.path}`)
          // Borg local-mode repos sit on top of a path — the same
          // /mnt/proxmenux-backup-disk-* USB mountpoint a Local target
          // would use. Expose Unmount for them too so the operator
          // doesn't have to remember which destination type is on the
          // disk they want to detach.
          const borgLocalUsbPath = it.kind === "borg" && !it.isSsh && cap?.is_usb
            ? it.repository
            : null
          const umountTarget = it.kind === "local" ? it.path : borgLocalUsbPath
          const unmountBusy = !!umountTarget && busyKey === `umount:${umountTarget}`
          return (
            <DestinationRow
              key={it.id}
              item={it}
              capacity={cap}
              busy={busy}
              unmountBusy={unmountBusy}
              onDelete={() => setConfirmingDest(it)}
              onUnmount={
                umountTarget && cap?.is_usb
                  ? () => unmountUsb(umountTarget)
                  : undefined
              }
              onEdit={
                // Only PBS-manual and Borg are editable from the UI.
                // PBS-proxmox is managed by Datacenter → Storage and
                // local destinations have no fields beyond the path.
                it.kind === "pbs" && it.source === "manual"
                  ? () => {
                      const [user, rest] = it.repository.includes("@")
                        ? [it.repository.split("@").slice(0, -1).join("@"), it.repository.split("@").pop() || ""]
                        : ["root@pam", it.repository]
                      const [server, datastore] = rest.includes(":")
                        ? [rest.split(":").slice(0, -1).join(":"), rest.split(":").pop() || ""]
                        : [rest, ""]
                      setEditingDest({
                        kind: "pbs",
                        name: it.name,
                        username: user,
                        server,
                        datastore,
                        fingerprint: it.fingerprint,
                        has_password: true,
                      })
                    }
                  : it.kind === "borg"
                    ? () => setEditingDest({
                        kind: "borg",
                        name: it.name,
                        repository: it.repository,
                        ssh_key_path: it.sshKeyPath,
                        encrypt_mode: (destinations?.borg?.find((b) => b.name === it.name)?.encrypt_mode) || "repokey",
                        has_passphrase: !!destinations?.borg?.find((b) => b.name === it.name)?.has_passphrase,
                      })
                    : undefined
              }
            />
          )
        })}
      </div>
      <ConfigureDestinationWizard
        open={configuring}
        onClose={() => setConfiguring(false)}
        onSaved={() => {
          setConfiguring(false)
          onChanged()
        }}
      />
      {/* Edit-an-existing-destination dialog. Uses the same form as
          Add, just hydrated with the saved values and with `name`
          locked. The POST endpoint is upsert-by-name on the backend
          so no separate route is needed. */}
      <AddDestinationDialog
        type={editingDest?.kind ?? null}
        editing={editingDest}
        onClose={() => setEditingDest(null)}
        onSaved={() => {
          setEditingDest(null)
          onChanged()
        }}
      />
      <PbsKeyfileRecoveryDialog
        open={recoveringKeyfile}
        snapshots={pbsRecoveryAvailable?.snapshots || []}
        onClose={() => setRecoveringKeyfile(false)}
        onRecovered={() => {
          setRecoveringKeyfile(false)
          mutatePbsRecovery()
        }}
      />
      {/* Delete-destination confirm. Always opens (uniform UX) but
          the body adapts: a plain "Remove?" when no jobs/backups
          depend on the destination, or a warning summarizing the
          cascade (jobs go, backups stay) when there's something at
          stake. The destination + its sidecars are removed; backups
          on disk / on the remote stay untouched so the operator can
          decide what to do with them later. */}
      {confirmingDest && (() => {
        const it = confirmingDest
        const jobs = it.jobsUsing
        const backups = backupsCountFor(it)
        const headline =
          it.kind === "local" ? it.path
          : it.kind === "pbs" ? `${it.name} — ${it.repository}`
          : `${it.name} — ${it.repository}`
        return (
          <Dialog open={true} onOpenChange={(v) => { if (!v) setConfirmingDest(null) }}>
            <DialogContent className="max-w-md bg-card border-border">
              <DialogHeader>
                <DialogTitle className="text-base flex items-center gap-2">
                  <AlertTriangle className={`h-5 w-5 ${jobs.length > 0 ? "text-amber-500" : "text-red-500"}`} />
                  {t("backup.destinations.removeDestination")}
                </DialogTitle>
                <DialogDescription className="text-xs">
                  {jobs.length === 0 && backups === 0
                    ? t("backup.destinations.removeDescriptionPlain")
                    : t("backup.destinations.removeDescriptionWithData")}
                </DialogDescription>
              </DialogHeader>
              <div className="text-sm font-mono px-3 py-2 rounded-md border border-border bg-background/40 break-all">
                {headline}
              </div>
              {(jobs.length > 0 || backups > 0) && (
                <div className="text-xs space-y-1.5 text-foreground">
                  {jobs.length > 0 && (
                    <div className="px-3 py-2 rounded-md border border-amber-500/40 bg-amber-500/5">
                      <div className="font-medium text-amber-400">
                        {t("backup.destinations.jobsWillBeDeleted", { count: jobs.length })}
                      </div>
                      <div className="mt-1 flex flex-wrap gap-1">
                        {jobs.map((j) => (
                          <span key={j} className="font-mono text-[11px] px-1.5 py-0.5 rounded bg-background/60 border border-border">
                            {j}
                          </span>
                        ))}
                      </div>
                    </div>
                  )}
                  {backups > 0 && (
                    <div className="px-3 py-2 rounded-md border border-blue-500/40 bg-blue-500/5">
                      {t("backup.destinations.backupsKeptBefore", { count: backups })} <span className="font-medium">{t("backup.destinations.kept")}</span>.
                    </div>
                  )}
                </div>
              )}
              <div className="flex justify-end gap-2">
                <Button variant="ghost" onClick={() => setConfirmingDest(null)}>
                  {t("actions.cancel")}
                </Button>
                <Button variant="destructive" onClick={confirmDeleteDest}>
                  <Trash2 className="h-4 w-4 mr-2" />
                  {jobs.length > 0 ? t("backup.destinations.removeAndDeleteJobs") : t("backup.destinations.removeDestination")}
                </Button>
              </div>
            </DialogContent>
          </Dialog>
        )
      })()}
    </div>
  )
}

// Single row in the unified destinations list. Layout mirrors the
// disk-card chrome from storage-overview.tsx: icon + headline + all
// badges + actions on ONE top row, capacity bar in storage-page blue,
// stats grid below. No hover effect — these rows are inert (no modal
// behind them), so the contrast change would only confuse the eye.
function DestinationRow({
  item,
  capacity,
  busy,
  unmountBusy,
  onDelete,
  onUnmount,
  onEdit,
}: {
  item: UnifiedDest
  capacity?: CapacityInfo
  busy: boolean
  unmountBusy: boolean
  onDelete: () => void
  onUnmount?: () => void
  onEdit?: () => void
}) {
  const t = useT()
  const accent =
    item.kind === "pbs" ? "bg-purple-500/10 text-purple-400 border-purple-500/20"
    : item.kind === "borg" ? "bg-fuchsia-500/10 text-fuchsia-400 border-fuchsia-500/20"
    : "bg-blue-500/10 text-blue-400 border-blue-500/20"
  const iconColor =
    item.kind === "pbs" ? "text-purple-400"
    : item.kind === "borg" ? "text-fuchsia-400"
    : "text-blue-400"
  const Icon = item.kind === "pbs" ? Server : item.kind === "borg" ? Archive : HardDrive
  const headline =
    item.kind === "pbs" ? item.name
    : item.kind === "borg" ? item.name
    : item.path
  const subline =
    item.kind === "pbs" ? item.repository
    : item.kind === "borg" ? item.repository
    : null
  const isUsb = !!capacity?.is_usb || (item.kind === "borg" && !item.isSsh && /\/(?:mnt|media)\//.test(item.repository))
  const sourceLabel =
    item.kind === "local" ? (item.source === "default" ? t("backup.destinations.builtInDefault") : t("backup.destinations.manuallyAdded"))
    : item.kind === "pbs" ? (item.source === "proxmox" ? t("backup.destinations.datacenterStorage") : t("backup.destinations.manuallyAdded"))
    // Borg targets are always operator-configured (no Datacenter
    // auto-discovery path), so they all share the "manually added"
    // wording for visual consistency with the Local custom row.
    : t("backup.destinations.manuallyAdded")

  const pct = capacity?.total && capacity.available !== undefined
    ? Math.min(100, Math.round(((capacity.total - capacity.available) / capacity.total) * 100))
    : null

  return (
    <div className="rounded-lg border border-white/10 bg-card p-4">
      {/* Top row split in two: a wrappable left side (icon + headline +
          badges) and a fixed right side (Unmount + Remove). The right
          side never falls to a new line even if `headline` is long,
          because the outer container is a non-wrapping flex. */}
      <div className="space-y-2 mb-3">
        <div className="flex items-start gap-2">
          <div className="min-w-0 flex-1 flex items-start gap-2 flex-wrap">
            <Icon className={`h-5 w-5 flex-shrink-0 ${iconColor} mt-0.5`} />
            <h3 className="font-mono font-semibold text-sm break-all">{headline}</h3>
            <Badge variant="outline" className={`text-[10px] uppercase tracking-wide ${accent}`}>
              {item.kind}
            </Badge>
          {item.kind === "local" && item.source === "default" && (
            <Badge variant="outline" className="text-[10px] uppercase tracking-wide border-border text-muted-foreground">
              {t("backup.profile.default")}
            </Badge>
          )}
          {item.kind === "borg" ? (
            // For Borg the sub-type is one of three (mutually exclusive):
            //   ssh    → remote repo
            //   usb    → local repo whose path lives on a USB mount
            //   local  → local repo on an internal disk / regular dir
            // Previously we showed BORG + LOCAL + USB which read like
            // three orthogonal facets and confused the badge with the
            // Local destination kind. Collapsing to a single sub-type
            // badge avoids the overlap.
            <>
              {item.isSsh ? (
                <Badge variant="outline" className="text-[10px] uppercase tracking-wide border-cyan-500/40 text-cyan-400 bg-cyan-500/5">
                  ssh
                </Badge>
              ) : isUsb ? (
                <Badge variant="outline" className="text-[10px] uppercase tracking-wide border-orange-500/40 text-orange-400 bg-orange-500/5 gap-1">
                  <HardDrive className="h-2.5 w-2.5" />
                  USB
                </Badge>
              ) : (
                <Badge variant="outline" className="text-[10px] uppercase tracking-wide border-amber-500/40 text-amber-400 bg-amber-500/5">
                  local
                </Badge>
              )}
              {/* Encryption indicator — icon-only chip for encrypted
                  repos. Plaintext repos get no chip (visually quiet
                  for the common case). The tooltip carries the mode +
                  saved-passphrase state for hover detail. */}
              {item.encryptMode && item.encryptMode !== "none" && (
                <Badge
                  variant="outline"
                  className="text-[10px] uppercase tracking-wide border-emerald-500/40 text-emerald-400 bg-emerald-500/5"
                  title={t("backup.encryption.encryptedModeTitle", { mode: item.encryptMode, passphrase: item.hasPassphrase ? t("backup.encryption.passphraseSaved") : t("backup.encryption.passphraseNotSaved") })}
                >
                  {/* Icon sized to roughly the text-[10px] line-box of
                      the neighbouring text badges (BORG, USB, …) so the
                      pill itself ends up the same height — matching
                      visual weight without adding a label. */}
                  <Lock className="h-3.5 w-3.5" />
                </Badge>
              )}
            </>
          ) : isUsb && (
            // PBS / Local destinations: USB badge is additive (the kind
            // badge alone doesn't say where the path lives).
            <Badge variant="outline" className="text-[10px] uppercase tracking-wide border-orange-500/40 text-orange-400 bg-orange-500/5 gap-1">
              <HardDrive className="h-2.5 w-2.5" />
              USB
            </Badge>
          )}
          </div>
          {/* Action chips — always pinned to the right, never wrap.
              The outer flex container has no flex-wrap so even a very
              long headline keeps these two buttons in place. Chip
              style (square border + tinted bg) makes them read as
              actionable rather than decorative icons. */}
          <div className="shrink-0 flex items-start gap-1.5">
            {onEdit && (
              <button
                type="button"
                disabled={busy || unmountBusy}
                onClick={onEdit}
                title={t("backup.destinations.editTitle")}
                className="inline-flex items-center justify-center h-7 w-7 rounded-md border border-emerald-500/40 bg-emerald-500/10 text-emerald-400 hover:bg-emerald-500/20 hover:text-emerald-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
              >
                <Pencil className="h-3.5 w-3.5" />
              </button>
            )}
            {onUnmount && (
              <button
                type="button"
                disabled={unmountBusy || busy}
                onClick={onUnmount}
                title={t("backup.destinations.unmountTitle")}
                className="inline-flex items-center justify-center h-7 w-7 rounded-md border border-amber-500/40 bg-amber-500/10 text-amber-400 hover:bg-amber-500/20 hover:text-amber-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
              >
                {unmountBusy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Power className="h-3.5 w-3.5" />}
              </button>
            )}
            {item.removable ? (
              <button
                type="button"
                disabled={busy || unmountBusy}
                onClick={onDelete}
                title={t("backup.destinations.removeTitle")}
                className="inline-flex items-center justify-center h-7 w-7 rounded-md border border-red-500/40 bg-red-500/10 text-red-400 hover:bg-red-500/20 hover:text-red-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
              >
                {busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Trash2 className="h-3.5 w-3.5" />}
              </button>
            ) : (
              <span className="text-[10px] text-muted-foreground italic self-center px-2 whitespace-nowrap">
                {item.kind === "local" ? t("backup.destinations.builtIn") : t("backup.destinations.managedByPve")}
              </span>
            )}
          </div>
        </div>
        {subline && (
          <p className="text-xs font-mono text-muted-foreground break-all pl-7" title={subline}>
            {subline}
          </p>
        )}
      </div>

      {/* Capacity bar — matches the height of the <Progress> component
          used on the Storage page (h-2). Bar colour comes from the
          shared storage palette so a 100% PBS datastore flags red
          here too, not just on the Storage page. */}
      {capacity?.total && capacity.available !== undefined && (
        <div className="mb-3 h-2 bg-muted rounded-full overflow-hidden">
          <div
            className={`h-full rounded-full ${getStorageUsageColor(pct ?? 0).bgClass}`}
            style={{ width: `${pct}%` }}
          />
        </div>
      )}

      {/* Stats grid */}
      <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
        {capacity?.total !== undefined && (
          <div>
            <p className="text-sm text-muted-foreground">{t("backup.fields.size")}</p>
            <p className="font-medium">{formatBytes(capacity.total)}</p>
          </div>
        )}
        {capacity?.used !== undefined && capacity.total !== undefined && (
          <div>
            <p className="text-sm text-muted-foreground">{t("backup.fields.used")}</p>
            <p className="font-medium">
              {formatBytes(capacity.used)}
              {pct !== null && <span className="text-muted-foreground"> ({pct}%)</span>}
            </p>
          </div>
        )}
        {capacity?.available !== undefined && (
          <div>
            <p className="text-sm text-muted-foreground">{t("backup.fields.free")}</p>
            <p
              className={`font-medium ${
                pct === null ? "" : getStorageUsageColor(pct).textClass
              }`}
            >
              {formatBytes(capacity.available)}
            </p>
          </div>
        )}
        <div>
          <p className="text-sm text-muted-foreground">{t("backup.fields.source")}</p>
          <p className="font-medium text-xs">{sourceLabel}</p>
        </div>
      </div>

      {capacity?.error && (
        <p className="mt-3 text-[11px] text-muted-foreground italic">
          {t("backup.destinations.capacityUnavailable", { error: capacity.error })}
        </p>
      )}
      {!capacity && (
        <p className="mt-3 text-[11px] text-muted-foreground italic flex items-center gap-1.5">
          <Loader2 className="h-3 w-3 animate-spin" /> {t("backup.destinations.probingCapacity")}
        </p>
      )}
      {/* Keyfile actions live inside each PBS row — the encryption key
          is host-wide but conceptually belongs to PBS usage, so the
          Download / Upload / Delete controls sit next to the
          destination(s) they support. */}
      {item.kind === "pbs" && <PbsKeyfileActions pbsRepository={item.repository} />}
    </div>
  )
}

// Single-button wizard that replaces the old per-backend Add buttons.
// Step 1 lets the operator pick the backend; Step 2 hands off to the
// existing AddDestinationDialog with that backend pre-selected, so we
// don't duplicate the per-backend forms.
function ConfigureDestinationWizard({
  open,
  onClose,
  onSaved,
}: {
  open: boolean
  onClose: () => void
  onSaved: () => void
}) {
  const t = useT()
  const [picked, setPicked] = useState<"pbs" | "local" | "borg" | null>(null)
  useEffect(() => {
    if (!open) setPicked(null)
  }, [open])

  if (picked) {
    // Hand off to the existing form. Closing or saving collapses the
    // whole wizard so the operator lands back in the destinations list.
    return (
      <AddDestinationDialog
        type={picked}
        onClose={() => { setPicked(null); onClose() }}
        onSaved={() => { setPicked(null); onSaved() }}
      />
    )
  }

  return (
    <Dialog open={open} onOpenChange={(v) => { if (!v) onClose() }}>
      <DialogContent className="max-w-2xl bg-card border-border">
        <DialogHeader>
          <DialogTitle className="flex items-center gap-2">
            <Plus className="h-5 w-5 text-blue-500" />
            {t("backup.destinations.configureDestination")}
          </DialogTitle>
          <DialogDescription className="text-xs">
            {t("backup.destinations.configureDescription")}
          </DialogDescription>
        </DialogHeader>
        <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
          {([
            { type: "pbs",   label: "PBS",   accent: "border-purple-500/40 hover:border-purple-400 text-purple-400",   blurb: t("backup.backends.pbsDescription") },
            { type: "local", label: t("backup.backends.local"), accent: "border-blue-500/40 hover:border-blue-400 text-blue-400", blurb: t("backup.backends.localDescription") },
            { type: "borg",  label: "Borg",  accent: "border-fuchsia-500/40 hover:border-fuchsia-400 text-fuchsia-400", blurb: t("backup.backends.borgDescription") },
          ] as const).map((opt) => (
            <button
              key={opt.type}
              type="button"
              onClick={() => setPicked(opt.type)}
              className={`text-left rounded-lg border-2 p-4 transition-colors bg-background/40 hover:bg-white/5 ${opt.accent}`}
            >
              <div className="flex items-center gap-2 mb-1">
                {opt.type === "pbs" ? <Server className="h-4 w-4" /> :
                 opt.type === "local" ? <HardDrive className="h-4 w-4" /> :
                 <Archive className="h-4 w-4" />}
                <span className="font-semibold uppercase">{opt.label}</span>
              </div>
              <p className="text-xs text-muted-foreground">{opt.blurb}</p>
            </button>
          ))}
        </div>
        <div className="flex justify-end">
          <Button variant="ghost" onClick={onClose}>{t("actions.cancel")}</Button>
        </div>
      </DialogContent>
    </Dialog>
  )
}

// One dialog handles all three Add flows — the form is small enough
// that branching by type keeps it cleaner than three separate components.
// Pre-fill payload for editing an existing destination. `name` stays
// fixed (it's the key the sidecar / state files are scoped to);
// everything else can be re-typed. PBS password and Borg passphrase
// can be left blank to keep the saved one.
interface EditingDest {
  kind: "pbs" | "borg"
  name: string
  // PBS fields
  server?: string
  datastore?: string
  username?: string
  fingerprint?: string
  has_password?: boolean
  // Borg fields
  repository?: string
  ssh_key_path?: string
  encrypt_mode?: "none" | "repokey" | "keyfile" | "authenticated"
  has_passphrase?: boolean
}

function AddDestinationDialog({
  type,
  editing,
  onClose,
  onSaved,
}: {
  type: "pbs" | "borg" | "local" | null
  editing?: EditingDest | null
  onClose: () => void
  // The repository string of the just-saved destination, so callers
  // can auto-select it in a dropdown (the wizard does this when an
  // operator adds a PBS / Borg repo inline). Undefined for `local`
  // and for the legacy callers that don't care.
  onSaved: (repository?: string) => void
}) {
  const t = useT()
  const [name, setName] = useState("")
  // PBS fields
  const [server, setServer] = useState("")
  const [datastore, setDatastore] = useState("")
  const [username, setUsername] = useState("root@pam")
  const [password, setPassword] = useState("")
  const [fingerprint, setFingerprint] = useState("")
  // Borg fields (local mode)
  const [borgRepo, setBorgRepo] = useState("")
  // Borg SSH mode
  const [borgMode, setBorgMode] = useState<"local" | "ssh">("local")
  const [borgSshUser, setBorgSshUser] = useState("borg")
  const [borgSshHost, setBorgSshHost] = useState("")
  const [borgSshRemotePath, setBorgSshRemotePath] = useState("")
  const [borgSshKeyPath, setBorgSshKeyPath] = useState("/root/.ssh/proxmenux_borg")
  const [generatedKey, setGeneratedKey] = useState<{ public_key: string; authorized_keys_line: string } | null>(null)
  const [generatingKey, setGeneratingKey] = useState(false)
  // Borg encryption + passphrase live on the destination, not on each
  // job. Two-mode UI: encrypted (repokey, the default) or none. Other
  // borg modes (keyfile / authenticated) stay supported by the backend
  // for legacy shell-created configs.
  const [borgEncryptionEnabled, setBorgEncryptionEnabled] = useState(true)
  const [borgPassphrase, setBorgPassphraseLocal] = useState("")
  const [borgPassphrase2, setBorgPassphrase2] = useState("")
  // Local field
  const [localPath, setLocalPath] = useState("")

  const [submitting, setSubmitting] = useState(false)
  const [error, setError] = useState<string | null>(null)

  // Read the current destinations so the UsbPicker can hide any USB
  // mountpoint already in use as a Local or Borg-local destination —
  // no point offering the same disk twice.
  const { data: existingDest } = useSWR<DestinationsResp>(
    type !== null ? "/api/host-backups/destinations" : null,
    fetcher,
  )
  const usedPaths: string[] = (() => {
    const out: string[] = []
    for (const e of existingDest?.local?.entries || []) {
      if (e.source === "custom") out.push(e.path)
    }
    for (const r of existingDest?.borg || []) {
      // Borg local-mode: the `repository` IS the path. SSH repos
      // start with `ssh://` and are excluded automatically.
      if (!r.repository.startsWith("ssh://")) out.push(r.repository)
    }
    return out
  })()

  // Reset every time the dialog OPENS — not just on close. If we're
  // opening in edit mode, hydrate the form from the saved destination
  // so the operator only has to change what they want. Name stays
  // read-only in edit mode (it's the key for sidecars / state files).
  useEffect(() => {
    setError(null)
    setSubmitting(false)
    setGeneratedKey(null)
    setGeneratingKey(false)
    setPassword("")
    setBorgPassphraseLocal("")
    setBorgPassphrase2("")
    if (editing && editing.kind === "pbs") {
      setName(editing.name)
      setServer(editing.server || "")
      setDatastore(editing.datastore || "")
      setUsername(editing.username || "root@pam")
      setFingerprint(editing.fingerprint || "")
      setBorgRepo(""); setBorgMode("local"); setBorgSshUser("borg")
      setBorgSshHost(""); setBorgSshRemotePath("")
      setBorgSshKeyPath("/root/.ssh/proxmenux_borg")
      setBorgEncryptionEnabled(true); setLocalPath("")
      return
    }
    if (editing && editing.kind === "borg") {
      const repo = editing.repository || ""
      const ssh = repo.match(/^ssh:\/\/([^@]+)@([^/]+)\/(.+)$/)
      setName(editing.name)
      if (ssh) {
        setBorgMode("ssh")
        setBorgSshUser(ssh[1])
        setBorgSshHost(ssh[2])
        setBorgSshRemotePath(`/${ssh[3]}`)
        setBorgSshKeyPath(editing.ssh_key_path || "/root/.ssh/proxmenux_borg")
        setBorgRepo("")
      } else {
        setBorgMode("local")
        setBorgRepo(repo)
        setBorgSshUser("borg"); setBorgSshHost(""); setBorgSshRemotePath("")
        setBorgSshKeyPath("/root/.ssh/proxmenux_borg")
      }
      const mode = editing.encrypt_mode || "repokey"
      setBorgEncryptionEnabled(mode !== "none")
      setServer(""); setDatastore(""); setUsername("root@pam"); setFingerprint("")
      setLocalPath("")
      return
    }
    // Add-new path: full reset.
    setName("")
    setServer("")
    setDatastore("")
    setUsername("root@pam")
    setFingerprint("")
    setBorgRepo("")
    setBorgMode("local")
    setBorgSshUser("borg")
    setBorgSshHost("")
    setBorgSshRemotePath("")
    setBorgSshKeyPath("/root/.ssh/proxmenux_borg")
    setBorgEncryptionEnabled(true)
    setLocalPath("")
  }, [type, editing])

  const isEditing = !!editing
  const nameValid = /^[a-zA-Z0-9_-]+$/.test(name)
  const borgPathValid =
    borgMode === "local"
      ? !!borgRepo.trim()
      : !!(borgSshUser.trim() && borgSshHost.trim() && borgSshRemotePath.trim())
  // In edit mode the saved passphrase / password is reused when the
  // operator leaves the inputs blank, so validation relaxes.
  const borgPassphraseValid =
    !borgEncryptionEnabled
    || (isEditing && !borgPassphrase && editing?.has_passphrase)
    || (borgPassphrase.length > 0 && borgPassphrase === borgPassphrase2)
  const borgValid = borgPathValid && borgPassphraseValid
  const canSubmit =
    type === "pbs"
      ? nameValid && server.trim() && datastore.trim() &&
          (!!password || (isEditing && !!editing?.has_password))
      : type === "borg"
        ? nameValid && borgValid
        : type === "local"
          ? localPath.trim().startsWith("/")
          : false

  async function generateBorgKey() {
    setGeneratingKey(true)
    setError(null)
    try {
      const resp = await fetchApi<{ public_key: string; authorized_keys_line: string }>(
        "/api/host-backups/ssh-keys/generate",
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            key_path: borgSshKeyPath.trim() || "/root/.ssh/proxmenux_borg",
            remote_path: borgSshRemotePath.trim(),
          }),
        }
      )
      setGeneratedKey(resp)
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e))
    } finally {
      setGeneratingKey(false)
    }
  }

  async function handleSave() {
    if (!canSubmit) return
    setSubmitting(true)
    setError(null)
    try {
      let savedRepo: string | undefined
      if (type === "pbs") {
        const resp = await fetchApi<{ repository?: string }>("/api/host-backups/destinations/pbs", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            name,
            server: server.trim(),
            datastore: datastore.trim(),
            username: username.trim() || "root@pam",
            password,
            fingerprint: fingerprint.trim() || undefined,
          }),
        })
        savedRepo = resp?.repository
      } else if (type === "borg") {
        const body: Record<string, unknown> = {
          name,
          mode: borgMode,
          encrypt_mode: borgEncryptionEnabled ? "repokey" : "none",
        }
        if (borgEncryptionEnabled) {
          body.passphrase = borgPassphrase
        }
        if (borgMode === "local") {
          body.repo = borgRepo.trim()
        } else {
          body.ssh_user = borgSshUser.trim()
          body.ssh_host = borgSshHost.trim()
          body.ssh_remote_path = borgSshRemotePath.trim()
          if (borgSshKeyPath.trim()) body.ssh_key_path = borgSshKeyPath.trim()
        }
        const resp = await fetchApi<{ repo?: string }>("/api/host-backups/destinations/borg", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(body),
        })
        savedRepo = resp?.repo
      } else if (type === "local") {
        await fetchApi("/api/host-backups/destinations/local", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ path: localPath.trim() }),
        })
      }
      onSaved(savedRepo)
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e))
    } finally {
      setSubmitting(false)
    }
  }

  const destinationTitle = type === "local"
    ? t(isEditing ? "backup.destinations.editLocalDestinationTitle" : "backup.destinations.addLocalDestinationTitle")
    : t(isEditing ? "backup.destinations.editDestinationTitle" : "backup.destinations.addDestinationTitle", { type: type === "pbs" ? "PBS" : type === "borg" ? "Borg" : t("backup.backends.local") })

  return (
    <Dialog open={type !== null} onOpenChange={(v) => { if (!v) onClose() }}>
      <DialogContent className="max-w-lg max-h-[85vh] flex flex-col">
        <DialogHeader>
          <DialogTitle className="flex items-center gap-2">
            {isEditing ? (
              <Pencil className="h-5 w-5 text-emerald-400" />
            ) : (
              <Plus className="h-5 w-5 text-blue-500" />
            )}
            {destinationTitle}
          </DialogTitle>
        </DialogHeader>
        <div className="overflow-y-auto -mr-2 pr-2 space-y-3">
          {type === "pbs" && (
            <>
              <div>
                <Label htmlFor="pbsName">{t("backup.fields.name")}</Label>
                <Input id="pbsName" value={name} onChange={(e) => setName(e.target.value)} className="font-mono mt-1" placeholder="moje-pbs" disabled={isEditing} />
                <p className="text-xs text-muted-foreground mt-1">
                  {isEditing ? t("backup.destinations.nameCredentialsLocked") : t("backup.destinations.shortIdentifierHelp")}
                </p>
              </div>
              <div>
                <Label htmlFor="pbsServer">{t("backup.fields.serverHostOrIp")}</Label>
                <Input id="pbsServer" value={server} onChange={(e) => setServer(e.target.value)} className="font-mono mt-1" placeholder="192.168.1.10" />
              </div>
              <div>
                <Label htmlFor="pbsDatastore">{t("backup.fields.datastore")}</Label>
                <Input id="pbsDatastore" value={datastore} onChange={(e) => setDatastore(e.target.value)} className="font-mono mt-1" placeholder="pbs" />
              </div>
              <div>
                <Label htmlFor="pbsUser">{t("backup.fields.username")}</Label>
                <Input id="pbsUser" value={username} onChange={(e) => setUsername(e.target.value)} className="font-mono mt-1" />
                <p className="text-xs text-muted-foreground mt-1">{t("backup.destinations.pbsUserHelpBefore")} <code className="font-mono">proxmox-backup-client</code>. {t("backup.destinations.defaultUserBefore")} <code className="font-mono">root@pam</code>.</p>
              </div>
              <div>
                <Label htmlFor="pbsPass">{t("backup.fields.password")}</Label>
                <Input
                  id="pbsPass"
                  type="password"
                  value={password}
                  onChange={(e) => setPassword(e.target.value)}
                  className="font-mono mt-1"
                  placeholder={isEditing && editing?.has_password ? t("backup.placeholders.unchangedTypeToReplace") : ""}
                />
                {isEditing && editing?.has_password && (
                  <p className="text-xs text-muted-foreground mt-1">{t("backup.destinations.leavePasswordBlank")}</p>
                )}
              </div>
              <div>
                <Label htmlFor="pbsFp">{t("backup.fields.fingerprintOptional")}</Label>
                <Input id="pbsFp" value={fingerprint} onChange={(e) => setFingerprint(e.target.value)} className="font-mono mt-1" placeholder="aa:bb:cc:…" />
                <p className="text-xs text-muted-foreground mt-1">{t("backup.destinations.fingerprintHelp")}</p>
              </div>
            </>
          )}
          {type === "borg" && (
            <>
              <div>
                <Label htmlFor="borgName">{t("backup.fields.name")}</Label>
                <Input id="borgName" value={name} onChange={(e) => setName(e.target.value)} className="font-mono mt-1" placeholder="usb-borg" disabled={isEditing} />
                {isEditing && (
                  <p className="text-xs text-muted-foreground mt-1">{t("backup.destinations.namePassphraseLocked")}</p>
                )}
              </div>

              <div>
                <Label>{t("backup.destinations.whereIsBorgRepo")}</Label>
                <div className="grid grid-cols-2 gap-2 mt-1">
                  <button
                    type="button"
                    onClick={() => setBorgMode("local")}
                    className={`text-left p-2.5 rounded-md border text-sm ${borgMode === "local" ? "border-blue-500 bg-blue-500/5" : "border-border bg-background/40"} hover:bg-white/5 transition-colors`}
                  >
                    <div className="font-medium flex items-center gap-1"><HardDrive className="h-3.5 w-3.5" /> {t("backup.destinations.localUsb")}</div>
                    <div className="text-[11px] text-muted-foreground mt-0.5">{t("backup.destinations.localUsbDescription")}</div>
                  </button>
                  <button
                    type="button"
                    onClick={() => setBorgMode("ssh")}
                    className={`text-left p-2.5 rounded-md border text-sm ${borgMode === "ssh" ? "border-blue-500 bg-blue-500/5" : "border-border bg-background/40"} hover:bg-white/5 transition-colors`}
                  >
                    <div className="font-medium flex items-center gap-1"><Server className="h-3.5 w-3.5" /> {t("backup.destinations.remoteSsh")}</div>
                    <div className="text-[11px] text-muted-foreground mt-0.5">{t("backup.destinations.remoteSshDescription")}</div>
                  </button>
                </div>
              </div>

              {borgMode === "local" ? (
                <div className="space-y-3">
                  <div>
                    <Label htmlFor="borgRepoIn">{t("backup.fields.repositoryPath")}</Label>
                    <Input id="borgRepoIn" value={borgRepo} onChange={(e) => setBorgRepo(e.target.value)} className="font-mono mt-1" placeholder="/mnt/usb-disk/borgbackup" />
                    <p className="text-xs text-muted-foreground mt-1">
                      {t("backup.destinations.absolutePathHelp")}
                    </p>
                  </div>
                  <UsbPicker onPick={(p) => setBorgRepo(p)} excludePaths={usedPaths} />
                </div>
              ) : (
                <>
                  <div>
                    <Label htmlFor="borgSshUser">{t("backup.fields.sshUser")}</Label>
                    <Input id="borgSshUser" value={borgSshUser} onChange={(e) => setBorgSshUser(e.target.value)} className="font-mono mt-1" placeholder="borg" />
                    <p className="text-xs text-muted-foreground mt-1">
                      {t("backup.destinations.sshUserHelpBefore")} <code className="font-mono">borg serve</code>. {t("backup.destinations.sshUserHelpAfter")} <code className="font-mono">borg</code>, {t("backup.destinations.not")} <code className="font-mono">root</code>.
                    </p>
                  </div>
                  <div>
                    <Label htmlFor="borgSshHost">{t("backup.fields.sshHostOrIp")}</Label>
                    <Input id="borgSshHost" value={borgSshHost} onChange={(e) => setBorgSshHost(e.target.value)} className="font-mono mt-1" placeholder="backup.example.com" />
                  </div>
                  <div>
                    <Label htmlFor="borgSshPath">{t("backup.fields.remoteRepositoryPath")}</Label>
                    <Input id="borgSshPath" value={borgSshRemotePath} onChange={(e) => setBorgSshRemotePath(e.target.value)} className="font-mono mt-1" placeholder="/backup/borgbackup" />
                  </div>
                  <div>
                    <Label htmlFor="borgKeyPath">{t("backup.fields.sshKeyPath")}</Label>
                    <Input id="borgKeyPath" value={borgSshKeyPath} onChange={(e) => setBorgSshKeyPath(e.target.value)} className="font-mono mt-1" />
                    <p className="text-xs text-muted-foreground mt-1">
                      {t("backup.destinations.sshKeyHelpBefore")}
                      {" "}{t("backup.destinations.sshKeyHelpMiddle")} <span className="font-medium text-foreground">{t("backup.actions.generateKey")}</span> {t("backup.destinations.sshKeyHelpAfter")}
                    </p>
                  </div>

                  <div className="rounded-md border border-border bg-background/40 p-3 space-y-2">
                    <div className="flex items-center justify-between gap-2">
                      <span className="text-xs font-medium">{t("backup.destinations.generateNewSshKey")}</span>
                      <Button
                        type="button"
                        size="sm"
                        variant="outline"
                        className="h-7"
                        disabled={generatingKey || !borgSshKeyPath.trim()}
                        onClick={generateBorgKey}
                      >
                        {generatingKey ? <Loader2 className="h-3.5 w-3.5 animate-spin mr-1" /> : <Plus className="h-3.5 w-3.5 mr-1" />}
                        {generatedKey ? t("backup.actions.regenerate") : t("backup.actions.generateKey")}
                      </Button>
                    </div>
                    {generatedKey ? (
                      <>
                        <p className="text-[11px] text-muted-foreground">
                          {t("backup.destinations.appendAuthorizedKeyBefore")} <code className="font-mono">~{borgSshUser}/.ssh/authorized_keys</code>:
                        </p>
                        <textarea
                          readOnly
                          value={generatedKey.authorized_keys_line}
                          className="w-full text-[11px] font-mono p-2 rounded border border-border bg-background h-24 resize-none"
                          onFocus={(e) => e.currentTarget.select()}
                        />
                      </>
                    ) : (
                      <p className="text-[11px] text-muted-foreground">
                        {t("backup.destinations.createsSshKeyBefore")} <code className="font-mono">borg serve</code> {t("backup.destinations.createsSshKeyAfter")}
                      </p>
                    )}
                  </div>
                </>
              )}

              {/* Encryption + passphrase live on the destination, not on
                  each job. Two-mode UI: encrypted (repokey, the borg
                  recommended default) or none. The other borg modes
                  (keyfile, authenticated) are still honored by the
                  backend for legacy shell-created configs but not
                  exposed here. */}
              <div className="space-y-2 pt-2 border-t border-border">
                <Label>{t("backup.fields.encryption")}</Label>
                <div className="grid grid-cols-2 gap-2">
                  <button
                    type="button"
                    onClick={() => setBorgEncryptionEnabled(true)}
                    className={`text-left p-2.5 rounded-md border text-sm transition-colors ${borgEncryptionEnabled ? "border-fuchsia-500 bg-fuchsia-500/5" : "border-border bg-background/40 hover:bg-white/5"}`}
                  >
                    <div className="font-medium">{t("backup.encryption.encryptedRepokey")}</div>
                    <div className="text-[11px] text-muted-foreground">{t("backup.encryption.encryptedRepokeyDescription")}</div>
                  </button>
                  <button
                    type="button"
                    onClick={() => setBorgEncryptionEnabled(false)}
                    className={`text-left p-2.5 rounded-md border text-sm transition-colors ${!borgEncryptionEnabled ? "border-amber-500 bg-amber-500/5" : "border-border bg-background/40 hover:bg-white/5"}`}
                  >
                    <div className="font-medium">{t("backup.encryption.noEncryption")}</div>
                    <div className="text-[11px] text-muted-foreground">{t("backup.encryption.noEncryptionDescription")}</div>
                  </button>
                </div>
                {borgEncryptionEnabled && (
                  <div className="space-y-2 pt-1">
                    <div>
                      <Label htmlFor="borgPass">{t("backup.fields.passphrase")}</Label>
                      <Input
                        id="borgPass"
                        type="password"
                        value={borgPassphrase}
                        onChange={(e) => setBorgPassphraseLocal(e.target.value)}
                        placeholder={isEditing && editing?.has_passphrase
                          ? t("backup.placeholders.unchangedTypeToReplace")
                          : t("backup.placeholders.longRandomStoreSafe")}
                        className="font-mono mt-1"
                      />
                      {isEditing && editing?.has_passphrase && (
                        <p className="text-[11px] text-muted-foreground mt-1">{t("backup.destinations.leavePassphraseBlank")}</p>
                      )}
                    </div>
                    <div>
                      <Label htmlFor="borgPass2">{t("backup.fields.confirmPassphrase")}</Label>
                      <Input
                        id="borgPass2"
                        type="password"
                        value={borgPassphrase2}
                        onChange={(e) => setBorgPassphrase2(e.target.value)}
                        placeholder={t("backup.placeholders.typeItAgain")}
                        className="font-mono mt-1"
                      />
                      {borgPassphrase && borgPassphrase2 && borgPassphrase !== borgPassphrase2 && (
                        <p className="text-[11px] text-red-400 mt-1">{t("backup.errors.passphrasesDontMatch")}</p>
                      )}
                    </div>
                    <div className="mt-1 rounded-md border border-red-500/40 bg-red-500/10 p-2.5">
                      <div className="flex items-start gap-1.5 text-[11px] text-red-300">
                        <AlertTriangle className="h-3.5 w-3.5 shrink-0 mt-0.5" />
                        <div className="space-y-1">
                          <p>
                            <span className="font-medium">{t("backup.encryption.borgPassphraseOnlyWay")}</span> {t("backup.encryption.borgPassphraseSavedAt")} <code className="font-mono">borg-pass-{name || "&lt;name&gt;"}.txt</code> (chmod 0600) {t("backup.encryption.borgPassphraseSavedAfter")}
                          </p>
                          <p>
                            {t("backup.encryption.borgPassphraseLossBefore")} <span className="font-semibold">{t("backup.encryption.unrecoverable")}</span>.
                          </p>
                          <p className="font-medium">{t("backup.encryption.savePassphraseNow")}</p>
                        </div>
                      </div>
                    </div>
                  </div>
                )}
              </div>
            </>
          )}
          {type === "local" && (
            <div className="space-y-3">
              <div>
                <Label htmlFor="localPath">{t("backup.fields.directoryPath")}</Label>
                <Input id="localPath" value={localPath} onChange={(e) => setLocalPath(e.target.value)} className="font-mono mt-1" placeholder="/var/lib/vz/dump" />
                <p className="text-xs text-muted-foreground mt-1">
                  {t("backup.destinations.localPathHelpBefore")} <code className="font-mono">tar.zst</code> {t("backup.destinations.localPathHelpAfter")}
                </p>
              </div>
              <UsbPicker onPick={(p) => setLocalPath(p)} excludePaths={usedPaths} />
            </div>
          )}
          {error && (
            <div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
              {error}
            </div>
          )}
        </div>
        <div className="flex justify-end gap-2 pt-3 border-t border-border">
          <Button variant="ghost" onClick={onClose} disabled={submitting}>{t("actions.cancel")}</Button>
          <Button
            onClick={handleSave}
            disabled={!canSubmit || submitting}
            className="bg-blue-600 hover:bg-blue-700 text-white"
          >
            {submitting ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : <Save className="h-4 w-4 mr-2" />}
            {t("actions.save")}
          </Button>
        </div>
      </DialogContent>
    </Dialog>
  )
}

// ──────────────────────────────────────────────────────────────
// UsbPicker — inline picker for mounted / unmounted / empty USB
// drives. Used inside AddDestinationDialog for Local and Borg-local
// so the operator can mount + format USB media without leaving the
// "configure destination" flow.
// ──────────────────────────────────────────────────────────────
function UsbPicker({
  onPick,
  excludePaths = [],
}: {
  onPick: (path: string) => void
  // Hide a USB drive ONLY when an existing destination already targets
  // its mountpoint root verbatim. Subdirectory uses don't claim the
  // whole disk — both Local and Borg can share the same USB by
  // pointing at different subdirs (`/mnt/usbX/borgbackup` for Borg,
  // `/mnt/usbX/local-dump` for Local). The previous prefix-based
  // filter was too greedy and made it impossible to add a second
  // destination type once any one of them landed on the disk.
  excludePaths?: string[]
}) {
  const t = useT()
  const { data, mutate, isLoading } = useSWR<{ drives: UsbDrive[] }>(
    "/api/host-backups/usb-drives",
    fetcher,
    { refreshInterval: 0 },
  )
  const normalized = new Set((excludePaths || []).map((p) => p.replace(/\/+$/, "")))
  const drives = (data?.drives ?? []).filter((d) => {
    const mp = (d.path_or_device || "").replace(/\/+$/, "")
    return !mp || !normalized.has(mp)
  })
  const [busyKey, setBusyKey] = useState<string | null>(null)
  const [error, setError] = useState<string | null>(null)
  const [formatTarget, setFormatTarget] = useState<UsbDrive | null>(null)
  const [formatTyped, setFormatTyped] = useState("")

  async function mountAndUse(d: UsbDrive) {
    setBusyKey(d.path_or_device)
    setError(null)
    try {
      const resp = await fetchApi<{ mountpoint: string }>(
        "/api/host-backups/usb-drives/mount",
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            device: d.path_or_device,
            label: d.label,
            uuid: d.uuid,
          }),
        },
      )
      mutate()
      if (resp?.mountpoint) onPick(resp.mountpoint)
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e))
    } finally {
      setBusyKey(null)
    }
  }

  async function doFormat() {
    if (!formatTarget) return
    if (formatTyped !== formatTarget.path_or_device) return
    setBusyKey(formatTarget.path_or_device)
    setError(null)
    try {
      const resp = await fetchApi<{ mountpoint?: string }>(
        "/api/host-backups/usb-drives/format",
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            device: formatTarget.path_or_device,
            confirm_device: formatTyped,
          }),
        },
      )
      mutate()
      setFormatTarget(null)
      setFormatTyped("")
      if (resp?.mountpoint) onPick(resp.mountpoint)
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e))
    } finally {
      setBusyKey(null)
    }
  }

  return (
    <>
      <div className="rounded-md border border-border bg-background/40 p-3 space-y-2">
        <div className="flex items-center justify-between gap-2">
          <div className="flex items-center gap-2 text-xs font-medium">
            <HardDrive className="h-3.5 w-3.5 text-orange-400" />
            {t("backup.usb.drivesDetected", { count: drives.length })}
            {drives.length > 0 && (
              <Badge variant="outline" className="text-[10px]">{drives.length}</Badge>
            )}
          </div>
          <Button
            type="button"
            size="sm"
            variant="ghost"
            className="h-7 text-xs"
            onClick={() => mutate()}
            disabled={isLoading}
          >
            {isLoading ? <Loader2 className="h-3 w-3 animate-spin" /> : <RefreshCw className="h-3 w-3" />}
            <span className="ml-1">{t("actions.refresh")}</span>
          </Button>
        </div>
        {error && (
          <div className="text-xs text-red-500 px-2 py-1 rounded border border-red-500/30 bg-red-500/10">
            {error}
          </div>
        )}
        {isLoading ? (
          <div className="flex items-center gap-2 py-2 text-xs text-muted-foreground">
            <Loader2 className="h-3 w-3 animate-spin" /> {t("backup.usb.scanning")}
          </div>
        ) : drives.length === 0 ? (
          <p className="text-[11px] text-muted-foreground italic">
            {t("backup.usb.noneDetected")}
          </p>
        ) : (
          <div className="space-y-1.5">
            {drives.map((d) => {
              const busy = busyKey === d.path_or_device
              const stateBadge =
                d.state === "mounted"
                  ? { label: t("backup.usb.mounted"), cls: "border-green-500/40 text-green-400 bg-green-500/5" }
                  : d.state === "unmounted"
                    ? { label: t("backup.usb.unmounted"), cls: "border-amber-500/40 text-amber-400 bg-amber-500/5" }
                    : { label: t("backup.usb.unformatted"), cls: "border-red-500/40 text-red-400 bg-red-500/5" }
              return (
                <div
                  key={d.uuid || d.path_or_device}
                  className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 p-2 rounded border border-border bg-background"
                >
                  <div className="min-w-0 flex-1">
                    <div className="flex items-center gap-2 flex-wrap">
                      <code className="font-mono text-xs text-foreground">{d.path_or_device}</code>
                      <Badge variant="outline" className={`text-[10px] uppercase tracking-wide ${stateBadge.cls}`}>
                        {stateBadge.label}
                      </Badge>
                      {d.size && (
                        <span className="text-[10px] text-muted-foreground">{d.size}</span>
                      )}
                      {d.fstype && (
                        <span className="text-[10px] text-muted-foreground font-mono">{d.fstype}</span>
                      )}
                      {d.label && (
                        <span className="text-[10px] text-muted-foreground italic">"{d.label}"</span>
                      )}
                    </div>
                  </div>
                  <div className="shrink-0 flex items-center gap-1">
                    {d.state === "mounted" && (
                      <Button
                        type="button"
                        size="sm"
                        variant="outline"
                        className="h-7 text-xs bg-blue-500/10 border-blue-500/40 !text-blue-400 hover:bg-blue-500/20 hover:!text-blue-300"
                        onClick={() => onPick(d.path_or_device)}
                      >
                        {t("backup.actions.use")}
                      </Button>
                    )}
                    {d.state === "unmounted" && (
                      <Button
                        type="button"
                        size="sm"
                        variant="outline"
                        className="h-7 text-xs bg-blue-500/10 border-blue-500/40 !text-blue-400 hover:bg-blue-500/20 hover:!text-blue-300"
                        disabled={busy}
                        onClick={() => mountAndUse(d)}
                      >
                        {busy ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : null}
                        {t("backup.usb.mountAndUse")}
                      </Button>
                    )}
                    {d.state === "empty" && (
                      <Button
                        type="button"
                        size="sm"
                        variant="outline"
                        className="h-7 text-xs bg-red-500/10 border-red-500/40 !text-red-400 hover:bg-red-500/20 hover:!text-red-300"
                        disabled={busy}
                        onClick={() => { setFormatTarget(d); setFormatTyped("") }}
                      >
                        {t("backup.usb.formatAndUse")}
                      </Button>
                    )}
                  </div>
                </div>
              )
            })}
          </div>
        )}
      </div>

      {/* Type-to-confirm format dialog — destructive op. */}
      <Dialog open={formatTarget !== null} onOpenChange={(v) => { if (!v) { setFormatTarget(null); setFormatTyped("") } }}>
        <DialogContent className="max-w-md bg-card border-border">
          <DialogHeader>
            <DialogTitle className="flex items-center gap-2 text-base">
              <AlertTriangle className="h-5 w-5 text-red-500" />
              {t("backup.usb.formatDrive")}
            </DialogTitle>
            <DialogDescription className="text-xs">
              {t("backup.usb.formatDescription")}
            </DialogDescription>
          </DialogHeader>
          {formatTarget && (
            <div className="space-y-2">
              <div className="text-xs font-mono px-3 py-2 rounded-md border border-border bg-background/40 break-all">
                {formatTarget.path_or_device}
                {formatTarget.size && <span className="text-muted-foreground"> · {formatTarget.size}</span>}
              </div>
              <Label htmlFor="formatConfirmInput" className="text-xs">
                {t("backup.usb.typeDeviceToConfirm")}
              </Label>
              <Input
                id="formatConfirmInput"
                value={formatTyped}
                onChange={(e) => setFormatTyped(e.target.value)}
                placeholder={formatTarget.path_or_device}
                className="font-mono"
              />
            </div>
          )}
          <div className="flex justify-end gap-2">
            <Button
              variant="ghost"
              onClick={() => { setFormatTarget(null); setFormatTyped("") }}
              disabled={busyKey === formatTarget?.path_or_device}
            >
              {t("actions.cancel")}
            </Button>
            <Button
              variant="destructive"
              disabled={formatTyped !== formatTarget?.path_or_device || busyKey === formatTarget?.path_or_device}
              onClick={doFormat}
            >
              {busyKey === formatTarget?.path_or_device ? (
                <Loader2 className="h-4 w-4 mr-2 animate-spin" />
              ) : (
                <AlertTriangle className="h-4 w-4 mr-2" />
              )}
              {t("backup.usb.wipeAndFormat")}
            </Button>
          </div>
        </DialogContent>
      </Dialog>
    </>
  )
}

// ──────────────────────────────────────────────────────────────
// Custom backup paths (Sprint C).
//
// Persisted at $HB_STATE_DIR/backup-extra-paths.txt (one path per line).
// The runner merges this list into every backup — manual or scheduled,
// default or custom profile.
// ──────────────────────────────────────────────────────────────

interface ExtraPathEntry {
  path: string
  exists: boolean
}

function ExtraPathsSection() {
  const t = useT()
  const { data, mutate } = useSWR<{ paths: ExtraPathEntry[] }>(
    "/api/host-backups/extra-paths",
    fetcher,
    { refreshInterval: 60000 },
  )

  const [newPath, setNewPath] = useState("")
  const [busyPath, setBusyPath] = useState<string | null>(null)
  const [error, setError] = useState<string | null>(null)
  const [submitting, setSubmitting] = useState(false)

  const paths = data?.paths ?? []

  async function addPath() {
    if (!newPath.trim()) return
    setSubmitting(true)
    setError(null)
    try {
      await fetchApi("/api/host-backups/extra-paths", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ path: newPath.trim() }),
      })
      setNewPath("")
      mutate()
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e))
    } finally {
      setSubmitting(false)
    }
  }

  async function removePath(p: string) {
    setBusyPath(p)
    setError(null)
    try {
      await fetchApi(`/api/host-backups/extra-paths?path=${encodeURIComponent(p)}`, { method: "DELETE" })
      mutate()
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e))
    } finally {
      setBusyPath(null)
    }
  }

  return (
    <div className="space-y-3">
      <p className="text-xs text-muted-foreground">
        {t("backup.extraPaths.descriptionBefore")} <span className="font-medium text-foreground">{t("backup.extraPaths.everyBackup")}</span> {t("backup.extraPaths.descriptionAfter", { count: paths.length })}
      </p>

        <div className="flex flex-col sm:flex-row gap-2">
          <Input
            value={newPath}
            onChange={(e) => setNewPath(e.target.value)}
            placeholder={t("backup.placeholders.extraPathExamples")}
            className="font-mono"
            onKeyDown={(e) => { if (e.key === "Enter" && newPath.trim()) addPath() }}
          />
          <Button
            onClick={addPath}
            disabled={!newPath.trim() || submitting}
            className="shrink-0 bg-blue-500 hover:bg-blue-600 text-white"
          >
            {submitting ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : <Plus className="h-4 w-4 mr-2" />}
            {t("backup.actions.addPath")}
          </Button>
        </div>
        <p className="text-[11px] text-muted-foreground">
          {t("backup.extraPaths.absolutePathHintBefore")} <code className="font-mono">/hola</code> {t("backup.extraPaths.absolutePathHintMiddle")} <code className="font-mono">/root/hola</code> {t("backup.extraPaths.absolutePathHintAfter")}
        </p>

        {error && (
          <div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
            {error}
          </div>
        )}

        {paths.length === 0 ? (
          <div className="text-xs text-muted-foreground py-3 text-center">
            {t("backup.extraPaths.empty")}
          </div>
        ) : (
          <div className="space-y-1">
            {paths.map((p) => (
              <div
                key={p.path}
                className="flex items-center justify-between gap-3 p-2 rounded-md border border-border bg-background/40"
              >
                <div className="min-w-0 flex-1 flex items-center gap-2">
                  <span className="font-mono text-xs truncate" title={p.path}>{p.path}</span>
                  {!p.exists && (
                    <Badge variant="outline" className="text-[10px] text-amber-500 border-amber-500/40">
                      {t("backup.extraPaths.missingOnDisk")}
                    </Badge>
                  )}
                </div>
                <button
                  type="button"
                  disabled={busyPath === p.path}
                  onClick={() => removePath(p.path)}
                  title={t("backup.extraPaths.removeTitle")}
                  className="shrink-0 inline-flex items-center justify-center h-7 w-7 rounded-md border border-red-500/40 bg-red-500/10 text-red-400 hover:bg-red-500/20 hover:text-red-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
                >
                  {busyPath === p.path ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Trash2 className="h-3.5 w-3.5" />}
                </button>
              </div>
            ))}
          </div>
        )}
    </div>
  )
}

// ──────────────────────────────────────────────────────────────
// Backup configuration wrapper (Sprint D.2.a).
//
// Single card hosting Destinations + Custom paths via top-level tabs.
// Future tabs (USB drives, ...) plug in here too.
// ──────────────────────────────────────────────────────────────
function BackupConfigurationCard({
  destinations,
  onDestChanged,
}: {
  destinations?: DestinationsResp
  onDestChanged: () => void
}) {
  const t = useT()
  return (
    <Card className="bg-card border-border">
      <CardHeader className="space-y-0 pb-3">
        <div className="flex items-center gap-2">
          <Server className="h-5 w-5 text-blue-500" />
          <CardTitle className="text-base font-semibold">{t("backup.configuration.title")}</CardTitle>
        </div>
      </CardHeader>
      <CardContent className="space-y-6">
        {/* Destinations — list + add/remove of PBS / Borg / Local targets.
            USB drives are no longer a separate section: they surface in
            this list when mounted, and the add-destination wizard can
            mount / format them inline. */}
        <section className="space-y-2">
          <h3 className="text-sm font-medium">{t("backup.destinations.title")}</h3>
          <DestinationsSection destinations={destinations} onChanged={onDestChanged} />
        </section>

        <div className="h-px bg-border" />

        {/* Custom paths — extra absolute paths included in every backup */}
        <section className="space-y-2">
          <h3 className="text-sm font-medium">{t("backup.extraPaths.title")}</h3>
          <ExtraPathsSection />
        </section>
      </CardContent>
    </Card>
  )
}

// ──────────────────────────────────────────────────────────────
// USB drives section (Sprint D.2.b).
//
// Thin Monitor UI around the shell helpers hb_list_usb_partitions,
// hb_mount_usb_partition, hb_format_usb_disk. Mount / unmount are
// safe-ish; format is destructive and gated by a typed-confirmation
// dialog (the operator must re-type the device path) so a misclick
// can't wipe an attached disk.
// ──────────────────────────────────────────────────────────────

interface UsbDrive {
  state: "mounted" | "unmounted" | "empty"
  path_or_device: string
  label: string
  size: string
  fstype: string
  uuid: string
}

function UsbDrivesSection() {
  const t = useT()
  const { data, mutate, isLoading } = useSWR<{ drives: UsbDrive[] }>(
    "/api/host-backups/usb-drives",
    fetcher,
    { refreshInterval: 30000 },
  )
  const drives = data?.drives ?? []
  const [busyKey, setBusyKey] = useState<string | null>(null)
  const [error, setError] = useState<string | null>(null)
  const [formatTarget, setFormatTarget] = useState<UsbDrive | null>(null)
  const [formatTyped, setFormatTyped] = useState("")

  async function mountDrive(d: UsbDrive) {
    setBusyKey(d.path_or_device)
    setError(null)
    try {
      await fetchApi("/api/host-backups/usb-drives/mount", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          device: d.path_or_device,
          label: d.label,
          uuid: d.uuid,
        }),
      })
      mutate()
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e))
    } finally {
      setBusyKey(null)
    }
  }

  async function unmountDrive(d: UsbDrive) {
    if (!confirm(t("backup.usb.unmountConfirm", { path: d.path_or_device }))) return
    setBusyKey(d.path_or_device)
    setError(null)
    try {
      await fetchApi("/api/host-backups/usb-drives/unmount", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ mountpoint: d.path_or_device }),
      })
      mutate()
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e))
    } finally {
      setBusyKey(null)
    }
  }

  async function doFormat() {
    if (!formatTarget) return
    if (formatTyped !== formatTarget.path_or_device) return
    setBusyKey(formatTarget.path_or_device)
    setError(null)
    try {
      await fetchApi("/api/host-backups/usb-drives/format", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          device: formatTarget.path_or_device,
          confirm_device: formatTyped,
        }),
      })
      mutate()
      setFormatTarget(null)
      setFormatTyped("")
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e))
    } finally {
      setBusyKey(null)
    }
  }

  return (
    <div className="space-y-3">
      <p className="text-xs text-muted-foreground">
        {t("backup.usb.description")} <code className="font-mono">ext4</code>.
      </p>

      {error && (
        <div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
          {error}
        </div>
      )}

      {isLoading ? (
        <div className="text-xs text-muted-foreground py-3 text-center">
          <Loader2 className="h-4 w-4 animate-spin inline mr-2" />
          {t("backup.usb.scanning")}
        </div>
      ) : drives.length === 0 ? (
        <div className="text-xs text-muted-foreground py-3 text-center">
          {t("backup.usb.empty")}
        </div>
      ) : (
        <div className="space-y-2">
          {drives.map((d) => {
            const isBusy = busyKey === d.path_or_device
            const stateBadge =
              d.state === "mounted"
                ? { label: t("backup.usb.stateMounted"), cls: "text-emerald-400 border-emerald-400/40" }
                : d.state === "unmounted"
                  ? { label: t("backup.usb.stateUnmounted"), cls: "text-amber-500 border-amber-500/40" }
                  : { label: t("backup.usb.stateEmpty"), cls: "text-red-400 border-red-400/40" }
            return (
              <div
                key={`${d.state}-${d.path_or_device}-${d.uuid}`}
                className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 p-3 rounded-md border border-border bg-background/40"
              >
                <div className="min-w-0 flex-1">
                  <div className="flex items-center gap-2 flex-wrap">
                    <span className="font-mono text-sm font-medium">{d.path_or_device}</span>
                    <Badge variant="outline" className={`text-[10px] uppercase tracking-wide ${stateBadge.cls}`}>
                      {stateBadge.label}
                    </Badge>
                  </div>
                  <div className="mt-1 text-[11px] text-muted-foreground flex items-center gap-x-3 gap-y-1 flex-wrap">
                    {d.label && <span><span className="text-muted-foreground/70">{t("backup.fields.label")}:</span> <code className="font-mono">{d.label}</code></span>}
                    {d.size && <span><span className="text-muted-foreground/70">{t("backup.fields.size")}:</span> <code className="font-mono">{d.size}</code></span>}
                    {d.fstype && <span><span className="text-muted-foreground/70">{t("backup.fields.filesystemShort")}:</span> <code className="font-mono">{d.fstype}</code></span>}
                    {d.uuid && <span className="hidden sm:inline" title={t("backup.usb.uuidTitle", { uuid: d.uuid })}><span className="text-muted-foreground/70">uuid:</span> <code className="font-mono">{d.uuid.substring(0, 8)}…</code></span>}
                  </div>
                </div>
                <div className="flex items-center gap-1 shrink-0">
                  {d.state === "mounted" && (
                    <Button
                      size="sm"
                      variant="ghost"
                      className="h-8 px-2"
                      disabled={isBusy}
                      onClick={() => unmountDrive(d)}
                    >
                      {isBusy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <XCircle className="h-3.5 w-3.5" />}
                      <span className="ml-1 text-xs">{t("backup.actions.unmount")}</span>
                    </Button>
                  )}
                  {d.state === "unmounted" && (
                    <Button
                      size="sm"
                      variant="ghost"
                      className="h-8 px-2"
                      disabled={isBusy}
                      onClick={() => mountDrive(d)}
                    >
                      {isBusy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <CheckCircle2 className="h-3.5 w-3.5" />}
                      <span className="ml-1 text-xs">{t("backup.actions.mount")}</span>
                    </Button>
                  )}
                  {d.state === "empty" && (
                    <Button
                      size="sm"
                      variant="ghost"
                      className="h-8 px-2 text-red-500 hover:text-red-400 hover:bg-red-500/10"
                      disabled={isBusy}
                      onClick={() => { setFormatTarget(d); setFormatTyped("") }}
                      title={t("backup.usb.formatTitle")}
                    >
                      <AlertTriangle className="h-3.5 w-3.5" />
                      <span className="ml-1 text-xs">{t("backup.actions.format")}</span>
                    </Button>
                  )}
                </div>
              </div>
            )
          })}
        </div>
      )}

      {/* Format confirmation dialog */}
      <Dialog open={formatTarget !== null} onOpenChange={(v) => { if (!v) { setFormatTarget(null); setFormatTyped("") } }}>
        <DialogContent className="max-w-md">
          <DialogHeader>
            <DialogTitle className="flex items-center gap-2">
              <AlertTriangle className="h-5 w-5 text-red-500" />
              {t("backup.usb.formatDriveQuestion")}
            </DialogTitle>
            <DialogDescription>
              {t("backup.usb.formatWarning")}
            </DialogDescription>
          </DialogHeader>
          {formatTarget && (
            <div className="space-y-3 text-sm">
              <div className="rounded-md border border-red-500/40 bg-red-500/5 p-3 space-y-1">
                <div className="text-[10px] uppercase tracking-wider text-red-400">{t("backup.fields.target")}</div>
                <div className="font-mono text-base">{formatTarget.path_or_device}</div>
                <div className="text-xs text-muted-foreground">{t("backup.fields.size")}: {formatTarget.size || "?"}</div>
              </div>
              <div>
                <Label htmlFor="formatConfirm">{t("backup.usb.typeDeviceExactly")}</Label>
                <Input
                  id="formatConfirm"
                  value={formatTyped}
                  onChange={(e) => setFormatTyped(e.target.value)}
                  className="font-mono mt-1"
                  placeholder={formatTarget.path_or_device}
                />
              </div>
              <p className="text-xs text-muted-foreground">
                {t("backup.usb.repartitionHint")}
              </p>
            </div>
          )}
          <div className="flex justify-end gap-2 mt-2">
            <Button variant="ghost" onClick={() => { setFormatTarget(null); setFormatTyped("") }} disabled={busyKey === formatTarget?.path_or_device}>
              {t("actions.cancel")}
            </Button>
            <Button
              variant="destructive"
              onClick={doFormat}
              disabled={formatTyped !== formatTarget?.path_or_device || busyKey === formatTarget?.path_or_device}
            >
              {busyKey === formatTarget?.path_or_device ? (
                <Loader2 className="h-4 w-4 animate-spin mr-2" />
              ) : (
                <AlertTriangle className="h-4 w-4 mr-2" />
              )}
              {t("backup.usb.wipeAndFormat")}
            </Button>
          </div>
        </DialogContent>
      </Dialog>
    </div>
  )
}

// ──────────────────────────────────────────────────────────────────────
// JobDetailModal
// ──────────────────────────────────────────────────────────────────────
// Opens when the user clicks a job row. Everything about the job lives
// inside this modal: schedule, retention, profile, destination, the tail
// of the most recent log, and the action buttons (Run / Enable·Disable /
// Edit / Delete). Run polls the job detail every 2s so the user sees the
// result of the manual trigger without having to close-reopen.
// ──────────────────────────────────────────────────────────────────────
function JobDetailModal({
  jobId,
  onClose,
  onEdit,
  onRequestDelete,
  onChanged,
}: {
  jobId: string | null
  onClose: () => void
  onEdit: (id: string) => void
  onRequestDelete: (id: string) => void
  onChanged: () => void
}) {
  const t = useT()
  const open = jobId !== null
  const [running, setRunning] = useState(false)
  const [runBaseline, setRunBaseline] = useState<string | null>(null)
  const [runBaselineLogPath, setRunBaselineLogPath] = useState<string | null>(null)
  const [actionError, setActionError] = useState<string | null>(null)
  const [busy, setBusy] = useState<"" | "toggle">("")
  const [showDisableConfirm, setShowDisableConfirm] = useState(false)

  const { data: detail, mutate: refetch, isLoading } = useSWR<JobDetail>(
    open ? `/api/host-backups/jobs/${encodeURIComponent(jobId!)}` : null,
    fetcher,
    { refreshInterval: running ? 2000 : 0 },
  )

  // While the job is actively running, poll the full log every 1.5 s so
  // the modal shows the runner's output in real time — same UX the
  // "Run manual backup" dialog provides. SWR is disabled when not
  // running, so this only fires during an active trigger.
  const { data: liveLog } = useSWR<{ content: string; log_path: string | null; size: number }>(
    open && running ? `/api/host-backups/jobs/${encodeURIComponent(jobId!)}/log` : null,
    fetcher,
    { refreshInterval: 1500 },
  )

  // Auto-scroll the live log <pre> to the bottom as new lines arrive,
  // otherwise the operator has to manually scroll on every poll cycle.
  const liveLogRef = useRef<HTMLPreElement | null>(null)
  useEffect(() => {
    if (running && liveLogRef.current) {
      liveLogRef.current.scrollTop = liveLogRef.current.scrollHeight
    }
  }, [liveLog, running])

  // The runner truncates the .status file at start (writing RUN_AT
  // only) and appends RESULT= at finish. So during a run we'll see:
  //
  //   last_run_at  = NEW timestamp (advanced past baseline)
  //   last_result  = null
  //
  // and only when it really finishes:
  //
  //   last_run_at  = NEW timestamp
  //   last_result  = "ok" | "failed" | ...
  //
  // Requiring BOTH conditions keeps the live log streaming visible
  // for the whole duration of the run — the previous version exited
  // streaming mode as soon as the runner wrote the new RUN_AT.
  useEffect(() => {
    if (!running || !detail) return
    if (
      detail.last_run_at &&
      detail.last_run_at !== runBaseline &&
      detail.last_result
    ) {
      setRunning(false)
      setRunBaseline(null)
      setRunBaselineLogPath(null)
      onChanged()
    }
  }, [detail, running, runBaseline, onChanged])

  // When the modal opens onto a job that's already running (last_run_at
  // set but last_result still null), jump straight into streaming mode
  // so the operator can resume watching from anywhere — including the
  // "Manual backup in progress" banner on the Manual backups card and
  // a click on a scheduled job row that's currently mid-run.
  useEffect(() => {
    if (!open || running || !detail) return
    if (detail.last_run_at && !detail.last_result) {
      // Baseline = empty so the exit condition still fires (any
      // non-empty last_run_at + a non-null last_result will trip it).
      setRunBaseline("")
      setRunBaselineLogPath(detail.last_log_path ?? null)
      setRunning(true)
    }
  }, [open, running, detail])

  // Reset on close.
  useEffect(() => {
    if (!open) {
      setRunning(false)
      setRunBaseline(null)
      setRunBaselineLogPath(null)
      setActionError(null)
      setBusy("")
      setShowDisableConfirm(false)
    }
  }, [open])

  async function handleRun() {
    if (!detail) return
    setActionError(null)
    setRunBaseline(detail.last_run_at ?? "")
    // Remember the previous run's log path so the streaming view can
    // tell "the runner hasn't created its log yet" (we'd see the OLD
    // path) from "the runner is writing now" (path advanced).
    setRunBaselineLogPath(detail.last_log_path ?? null)
    setRunning(true)
    try {
      await fetchApi(`/api/host-backups/jobs/${encodeURIComponent(detail.id)}/run`, { method: "POST" })
      setTimeout(() => refetch(), 800)
    } catch (e) {
      setRunning(false)
      setRunBaseline(null)
      setRunBaselineLogPath(null)
      setActionError(t("backup.errors.runFailed", { error: e instanceof Error ? e.message : String(e) }))
    }
  }

  async function handleToggle() {
    if (!detail) return
    setActionError(null)
    setBusy("toggle")
    setShowDisableConfirm(false)
    try {
      await fetchApi(`/api/host-backups/jobs/${encodeURIComponent(detail.id)}/toggle`, { method: "POST" })
      await refetch()
      onChanged()
    } catch (e) {
      setActionError(t("backup.errors.toggleFailed", { error: e instanceof Error ? e.message : String(e) }))
    } finally {
      setBusy("")
    }
  }

  const lastResult = detail?.last_result ?? null
  const lastRunWhen = formatRunAt(detail?.last_run_at ?? null)
  const resultBadge = running
    ? { label: t("backup.taskStates.running"), cls: "bg-blue-500/10 border-blue-500/40 text-blue-300" }
    : lastResult === "ok"
      ? { label: t("backup.status.ok"), cls: "bg-emerald-500/10 border-emerald-500/40 text-emerald-300" }
      : lastResult
        ? { label: lastResult, cls: "bg-red-500/10 border-red-500/40 text-red-300" }
        : null

  return (
    <>
      <Dialog open={open} onOpenChange={(v) => { if (!v) onClose() }}>
        {/* Sized down to 3xl now that the log tail lives in the
            archive's inspect modal — the job dialog only needs to fit
            the schedule, retention chips, paths grid and destination.
            overflow-hidden + the inner ScrollArea keep any long repo
            or path string from forcing a horizontal scrollbar. */}
        <DialogContent className="max-w-3xl bg-card border-border overflow-hidden">
          <DialogHeader>
            <DialogTitle className="flex items-center gap-2 flex-wrap text-base pr-8">
              <DatabaseBackup className="h-5 w-5 text-blue-500" />
              <span className="font-mono break-all">{detail?.id ?? jobId}</span>
              {detail && (
                <>
                  <Badge variant="outline" className={`text-[10px] uppercase tracking-wide ${methodBadgeCls(detail.method)}`}>
                    {detail.method}
                  </Badge>
                  {/* Encryption badge — mirrors the InspectModal one
                      so the operator sees the lock in both the summary
                      list and the job detail view. */}
                  {(detail.pbs_encrypt || (detail.borg_encrypt_mode && detail.borg_encrypt_mode !== "none")) && (
                    <Badge
                      variant="outline"
                      className="text-emerald-400 border-emerald-500/40 bg-emerald-500/10"
                      title={t("backup.archives.encrypted")}
                    >
                      <Lock className="h-3.5 w-3.5" />
                    </Badge>
                  )}
                  {detail.attached && (
                    <Badge variant="outline" className="text-[10px] text-blue-400 border-blue-400/40 bg-blue-500/5">
                      {t("backup.status.attached")}
                    </Badge>
                  )}
                  {!detail.enabled && (
                    <Badge variant="outline" className="text-[10px] text-amber-500 border-amber-500/40 bg-amber-500/5">
                      {t("backup.status.disabled")}
                    </Badge>
                  )}
                </>
              )}
            </DialogTitle>
            <DialogDescription className="text-xs">
              {t("backup.jobs.detailDescription")}
            </DialogDescription>
          </DialogHeader>

          {actionError && (
            <div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
              {actionError}
            </div>
          )}

          {isLoading || !detail ? (
            <div className="flex items-center gap-2 py-6 text-sm text-muted-foreground">
              <Loader2 className="h-4 w-4 animate-spin" />
              {t("backup.jobs.loadingJob")}
            </div>
          ) : (
            <ScrollArea className="max-h-[60vh] pr-2">
              {(() => {
                // One accent color per section. The destination color is
                // bound to the job's backend (purple/blue/fuchsia) so the
                // section feels like a continuation of the method badge in
                // the title; the other sections use neutral accent colors
                // that match their semantic (time → amber, content → emerald).
                const destAccent =
                  detail.method === "pbs"
                    ? "text-purple-400"
                    : detail.method === "borg"
                      ? "text-fuchsia-400"
                      : "text-blue-400"
                return (
                  <div className="space-y-4 text-sm">
                    {/* ─── Last run ─────────────────────────── */}
                    {/* The job modal shows only the verdict + timestamp.
                        The actual log lives in the archive's inspect
                        modal where it belongs (one log per .tar.zst). */}
                    <section className="space-y-2">
                      <h4 className="text-xs font-semibold uppercase tracking-wide flex items-center gap-1.5 text-green-500">
                        <Clock className="h-3.5 w-3.5" /> {t("backup.jobs.lastRun")}
                      </h4>
                      <div className="flex items-center gap-2 flex-wrap text-xs">
                        {resultBadge ? (
                          <span className={`px-2 py-0.5 rounded text-[10px] uppercase tracking-wide border inline-flex items-center gap-1 ${resultBadge.cls}`}>
                            {running && <Loader2 className="h-3 w-3 animate-spin" />}
                            {resultBadge.label}
                          </span>
                        ) : (
                          <span className="text-muted-foreground">{t("backup.jobs.neverRun")}</span>
                        )}
                        {lastRunWhen && <span className="text-muted-foreground">{lastRunWhen}</span>}
                      </div>
                      {running && (
                        // Live mode — poll /log every 1.5 s and auto-scroll
                        // so the operator watches the runner output as it
                        // happens. The archive isn't on disk yet, so this
                        // is the only place to surface the running log.
                        // Suppressed once the run finishes.
                        (() => {
                          const sameOldLog =
                            !!liveLog?.log_path &&
                            !!runBaselineLogPath &&
                            liveLog.log_path === runBaselineLogPath
                          const showContent = liveLog?.content && !sameOldLog
                          return (
                            <div className="rounded-md border border-blue-500/40 bg-blue-500/5 p-2">
                              <pre
                                ref={liveLogRef}
                                className="text-[11px] font-mono whitespace-pre-wrap break-all max-h-64 overflow-auto text-foreground/90"
                              >
{showContent ? liveLog!.content : t("backup.jobs.waitingForRunner")}
                              </pre>
                              <div className="flex items-center justify-between mt-2">
                                <span className="text-[10px] text-blue-300 inline-flex items-center gap-1.5">
                                  <Loader2 className="h-3 w-3 animate-spin" />
                                  {showContent
                                    ? t("backup.jobs.liveLogSize", { size: formatBytes(liveLog?.size ?? 0) })
                                    : t("backup.jobs.starting")}
                                </span>
                                {showContent && liveLog?.log_path && (
                                  <span className="text-[10px] text-muted-foreground font-mono break-all">
                                    {liveLog.log_path}
                                  </span>
                                )}
                              </div>
                            </div>
                          )
                        })()
                      )}
                    </section>

                    {/* ─── Schedule + retention ─────────────── */}
                    <section className="space-y-2">
                      <h4 className="text-xs font-semibold uppercase tracking-wide flex items-center gap-1.5 text-green-500">
                        <Calendar className="h-3.5 w-3.5" /> {t("backup.schedule.title")}
                      </h4>
                      <Field
                        icon={<Calendar className="h-3 w-3 text-green-500/80" />}
                        label={t("backup.fields.when")}
                        value={humanizeOnCalendar(detail.on_calendar, t)}
                        labelClassName="text-green-500/90"
                      />
                      {detail.next_run && (
                        <Field
                          icon={<Clock className="h-3 w-3 text-green-500/80" />}
                          label={t("backup.fields.nextRun")}
                          value={formatNext(detail.next_run)}
                          labelClassName="text-green-500/90"
                        />
                      )}
                      <RetentionDisplay retention={detail.retention || {}} />
                    </section>

                    {/* ─── Profile + paths ──────────────────── */}
                    <section className="space-y-2">
                      <h4 className="text-xs font-semibold uppercase tracking-wide flex items-center gap-1.5 text-green-500">
                        <FileSearch className="h-3.5 w-3.5" /> {t("backup.profile.title")}
                      </h4>
                      <Field
                        icon={<Server className="h-3 w-3 text-green-500/80" />}
                        label={t("backup.fields.mode")}
                        value={detail.profile_mode || "—"}
                        mono
                        labelClassName="text-green-500/90"
                      />
                      {detail.paths && detail.paths.length > 0 && (
                        <PathsDisplay paths={detail.paths} />
                      )}
                    </section>

                    {/* ─── Destination ──────────────────────── */}
                    <section className="space-y-1">
                      <h4 className={`text-xs font-semibold uppercase tracking-wide flex items-center gap-1.5 ${destAccent}`}>
                        <HardDrive className="h-3.5 w-3.5" /> {t("backup.destinations.single")}
                      </h4>
                      <Field icon={<HardDrive className={`h-3 w-3 ${destAccent} opacity-80`} />} label={t("backup.fields.target")} value={detail.destination || "—"} mono />
                      {detail.method === "pbs" && (
                        <>
                          {detail.pbs_repository && (
                            <Field icon={<Server className={`h-3 w-3 ${destAccent} opacity-80`} />} label={t("backup.fields.repository")} value={detail.pbs_repository} mono />
                          )}
                          {detail.pbs_backup_id && (
                            <Field icon={<Archive className={`h-3 w-3 ${destAccent} opacity-80`} />} label={t("backup.fields.backupId")} value={detail.pbs_backup_id} mono />
                          )}
                        </>
                      )}
                      {detail.method === "borg" && (
                        <>
                          {detail.borg_repo && (
                            <Field icon={<Server className={`h-3 w-3 ${destAccent} opacity-80`} />} label={t("backup.fields.repo")} value={detail.borg_repo} mono />
                          )}
                          <Field icon={<FileSearch className={`h-3 w-3 ${destAccent} opacity-80`} />} label={t("backup.fields.encryption")} value={detail.borg_encrypt_mode} mono />
                        </>
                      )}
                      {detail.method === "local" && detail.local_dest_dir && (
                        <Field icon={<HardDrive className={`h-3 w-3 ${destAccent} opacity-80`} />} label={t("backup.fields.directoryShort")} value={detail.local_dest_dir} mono />
                      )}
                      {detail.pve_storage && (
                        <Field icon={<DatabaseBackup className={`h-3 w-3 ${destAccent} opacity-80`} />} label={t("backup.fields.pveStorage")} value={detail.pve_storage} mono />
                      )}
                    </section>
                  </div>
                )
              })()}
            </ScrollArea>
          )}

          {detail && (
            // Layout mirrors the InspectModal footer: primary action on
            // the left in solid green (Run, matches Restore), secondary
            // Edit next to it in blue outline, then state-changers on
            // the right (Disable / Delete) — both outlined to read as
            // less prominent than Run.
            <div className="border-t border-border pt-3 flex items-center justify-between gap-2 flex-wrap">
              <div className="flex items-center gap-2 flex-wrap">
                <Button
                  size="sm"
                  onClick={handleRun}
                  disabled={running || busy !== ""}
                  className="bg-green-600 hover:bg-green-700 text-white"
                  title={detail.attached
                    ? t("backup.jobs.runAttachedTitle")
                    : t("backup.jobs.runNowTitle")}
                >
                  {running ? (
                    <Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" />
                  ) : (
                    <PlayCircle className="h-3.5 w-3.5 mr-1" />
                  )}
                  {running ? t("backup.jobs.running") : t("backup.actions.runNow")}
                </Button>
                <Button
                  size="sm"
                  variant="outline"
                  disabled={running || busy !== ""}
                  className="bg-blue-500/10 border-blue-500/40 !text-blue-400 hover:bg-blue-500/20 hover:!text-blue-300"
                  onClick={() => onEdit(detail.id)}
                >
                  <Pencil className="h-3.5 w-3.5 mr-1" />
                  {t("backup.actions.edit")}
                </Button>
              </div>
              <div className="flex items-center gap-2 flex-wrap">
                <Button
                  size="sm"
                  variant="outline"
                  disabled={running || busy !== ""}
                  className={detail.enabled
                    ? "bg-amber-500/10 border-amber-500/40 !text-amber-400 hover:bg-amber-500/20 hover:!text-amber-300"
                    : "bg-emerald-500/10 border-emerald-500/40 !text-emerald-400 hover:bg-emerald-500/20 hover:!text-emerald-300"}
                  onClick={() => {
                    if (detail.enabled) setShowDisableConfirm(true)
                    else handleToggle()
                  }}
                >
                  {busy === "toggle" ? (
                    <Loader2 className="h-3.5 w-3.5 mr-1 animate-spin" />
                  ) : detail.enabled ? (
                    <Power className="h-3.5 w-3.5 mr-1" />
                  ) : (
                    <CheckCircle2 className="h-3.5 w-3.5 mr-1" />
                  )}
                  {detail.enabled ? t("backup.actions.disable") : t("backup.actions.enable")}
                </Button>
                <Button
                  size="sm"
                  variant="outline"
                  disabled={running || busy !== ""}
                  className="bg-red-500/10 border-red-500/40 !text-red-400 hover:bg-red-500/20 hover:!text-red-300"
                  onClick={() => onRequestDelete(detail.id)}
                >
                  <Trash2 className="h-3.5 w-3.5 mr-1" />
                  {t("backup.actions.delete")}
                </Button>
              </div>
            </div>
          )}
        </DialogContent>
      </Dialog>

      {/* ─── Confirm Disable ──────────────────────────────────── */}
      <Dialog open={showDisableConfirm} onOpenChange={setShowDisableConfirm}>
        <DialogContent className="max-w-md bg-card border-border">
          <DialogHeader>
            <DialogTitle className="text-base flex items-center gap-2">
              <Power className="h-5 w-5 text-amber-500" />
              {t("backup.jobs.disableJob")}
            </DialogTitle>
            <DialogDescription className="text-xs">
              {t("backup.jobs.disableJobDescription")}
            </DialogDescription>
          </DialogHeader>
          <div className="text-sm font-mono px-3 py-2 rounded-md border border-border bg-background/40 break-all">
            {detail?.id}
          </div>
          <div className="flex justify-end gap-2">
            <Button variant="ghost" onClick={() => setShowDisableConfirm(false)}>
              {t("actions.cancel")}
            </Button>
            <Button
              variant="outline"
              className="bg-amber-500/10 border-amber-500/40 !text-amber-400 hover:bg-amber-500/20 hover:!text-amber-300"
              onClick={handleToggle}
            >
              <Power className="h-3.5 w-3.5 mr-1" />
              {t("backup.actions.disable")}
            </Button>
          </div>
        </DialogContent>
      </Dialog>

    </>
  )
}

// ──────────────────────────────────────────────────────────────
// ManualJobWatchModal
// ──────────────────────────────────────────────────────────────
// Sibling of JobDetailModal but tailored to manual / one-shot jobs.
// A manual job has no schedule, no retention, no profile picker — and
// once it has run it can't be re-run or edited (it's a frozen
// snapshot of one trigger). So the modal collapses to:
//   - the live log when the job is in flight
//   - the destination details (only the fields that matter for the
//     picked backend)
//   - Close + Delete actions (Edit / Run / Disable don't apply)
// ──────────────────────────────────────────────────────────────
function ManualJobWatchModal({
  jobId,
  onClose,
  onChanged,
}: {
  jobId: string | null
  onClose: () => void
  onChanged: () => void
}) {
  const t = useT()
  const open = jobId !== null
  const [running, setRunning] = useState(false)
  const [runBaselineLogPath, setRunBaselineLogPath] = useState<string | null>(null)
  const [actionError, setActionError] = useState<string | null>(null)

  const { data: detail, mutate: refetch, isLoading } = useSWR<JobDetail>(
    open ? `/api/host-backups/jobs/${encodeURIComponent(jobId!)}` : null,
    fetcher,
    { refreshInterval: running ? 2000 : 0 },
  )
  const { data: liveLog } = useSWR<{ content: string; log_path: string | null; size: number }>(
    open && running ? `/api/host-backups/jobs/${encodeURIComponent(jobId!)}/log` : null,
    fetcher,
    { refreshInterval: 1500 },
  )
  const liveLogRef = useRef<HTMLPreElement | null>(null)
  useEffect(() => {
    if (running && liveLogRef.current) {
      liveLogRef.current.scrollTop = liveLogRef.current.scrollHeight
    }
  }, [liveLog, running])

  // Auto-detect in-progress runs (RUN_AT set but RESULT not yet
  // persisted) and enter streaming mode on open.
  useEffect(() => {
    if (!open || running || !detail) return
    if (detail.last_run_at && !detail.last_result) {
      setRunBaselineLogPath(detail.last_log_path ?? null)
      setRunning(true)
    }
  }, [open, running, detail])

  // Exit streaming when both RUN_AT and RESULT are set.
  useEffect(() => {
    if (!running || !detail) return
    if (detail.last_run_at && detail.last_result) {
      setRunning(false)
      setRunBaselineLogPath(null)
      onChanged()
    }
  }, [detail, running, onChanged])

  useEffect(() => {
    if (!open) {
      setRunning(false)
      setRunBaselineLogPath(null)
      setActionError(null)
    }
  }, [open])

  const resultBadge = running
    ? { label: t("backup.taskStates.running"), cls: "bg-blue-500/10 border-blue-500/40 text-blue-300" }
    : detail?.last_result === "ok"
      ? { label: t("backup.status.ok"), cls: "bg-emerald-500/10 border-emerald-500/40 text-emerald-300" }
      : detail?.last_result
        ? { label: detail.last_result, cls: "bg-red-500/10 border-red-500/40 text-red-300" }
        : null
  const lastRunWhen = formatRunAt(detail?.last_run_at ?? null)
  const destAccent =
    detail?.method === "pbs" ? "text-purple-400"
    : detail?.method === "borg" ? "text-fuchsia-400"
    : "text-blue-400"

  return (
    <>
      <Dialog open={open} onOpenChange={(v) => { if (!v) onClose() }}>
        <DialogContent className="max-w-3xl bg-card border-border overflow-hidden">
          <DialogHeader>
            <DialogTitle className="flex items-center gap-2 flex-wrap text-base">
              <PlayCircle className="h-5 w-5 text-purple-400" />
              <span className="font-mono break-all">{detail?.id ?? jobId}</span>
              {detail && (
                <>
                  <Badge variant="outline" className={`text-[10px] uppercase tracking-wide ${methodBadgeCls(detail.method)}`}>
                    {detail.method}
                  </Badge>
                  <Badge variant="outline" className="text-[10px] text-purple-400 border-purple-400/40 bg-purple-500/5">
                    {t("backup.jobs.manualOneShot")}
                  </Badge>
                </>
              )}
            </DialogTitle>
            <DialogDescription className="text-xs">
              {t("backup.jobs.manualOneShotDescription")}
            </DialogDescription>
          </DialogHeader>

          {actionError && (
            <div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
              {actionError}
            </div>
          )}

          {isLoading || !detail ? (
            <div className="flex items-center gap-2 py-6 text-sm text-muted-foreground">
              <Loader2 className="h-4 w-4 animate-spin" />
              {t("backup.jobs.loadingJob")}
            </div>
          ) : (
            <ScrollArea className="max-h-[60vh] pr-2">
              <div className="space-y-4 text-sm">
                {/* Status + live log — manual one-shot has no concept
                    of "last run" (the modal IS the run), so we use a
                    neutral "Status" header here. */}
                <section className="space-y-2">
                  <h4 className="text-xs font-semibold uppercase tracking-wide flex items-center gap-1.5 text-green-500">
                    <Clock className="h-3.5 w-3.5" /> {t("backup.fields.status")}
                  </h4>
                  <div className="flex items-center gap-2 flex-wrap text-xs">
                    {resultBadge ? (
                      <span className={`px-2 py-0.5 rounded text-[10px] uppercase tracking-wide border inline-flex items-center gap-1 ${resultBadge.cls}`}>
                        {running && <Loader2 className="h-3 w-3 animate-spin" />}
                        {resultBadge.label}
                      </span>
                    ) : (
                      <span className="text-muted-foreground">{t("backup.jobs.starting")}</span>
                    )}
                    {lastRunWhen && <span className="text-muted-foreground">{lastRunWhen}</span>}
                  </div>
                  {running ? (
                    (() => {
                      const sameOldLog =
                        !!liveLog?.log_path &&
                        !!runBaselineLogPath &&
                        liveLog.log_path === runBaselineLogPath
                      const showContent = liveLog?.content && !sameOldLog
                      return (
                        <div className="rounded-md border border-blue-500/40 bg-blue-500/5 p-2">
                          <pre
                            ref={liveLogRef}
                            className="text-[11px] font-mono whitespace-pre-wrap break-all max-h-64 overflow-auto text-foreground/90"
                          >
{showContent ? liveLog!.content : t("backup.jobs.waitingForRunner")}
                          </pre>
                          <div className="flex items-center justify-between mt-2">
                            <span className="text-[10px] text-blue-300 inline-flex items-center gap-1.5">
                              <Loader2 className="h-3 w-3 animate-spin" />
                              {showContent ? t("backup.jobs.liveLogSize", { size: formatBytes(liveLog?.size ?? 0) }) : t("backup.jobs.starting")}
                            </span>
                            {showContent && liveLog?.log_path && (
                              <span className="text-[10px] text-muted-foreground font-mono break-all">
                                {liveLog.log_path}
                              </span>
                            )}
                          </div>
                        </div>
                      )
                    })()
                  ) : detail.last_log_tail && detail.last_log_tail.length > 0 ? (
                    <div className="rounded-md border border-border bg-background/60 p-2">
                      <pre className="text-[11px] font-mono whitespace-pre-wrap break-all max-h-48 overflow-auto text-foreground/90">
{detail.last_log_tail.join("\n")}
                      </pre>
                      <div className="text-[10px] text-muted-foreground mt-2">
                        {t("backup.logs.tail")} · {detail.last_log_size > 0 ? formatBytes(detail.last_log_size) : "—"}
                      </div>
                    </div>
                  ) : null}
                </section>

                {/* Destination — only the fields that apply to the
                    picked backend. No Schedule/Retention/Profile here
                    because manual jobs don't carry those. */}
                <section className="space-y-1">
                  <h4 className={`text-xs font-semibold uppercase tracking-wide flex items-center gap-1.5 ${destAccent}`}>
                    <HardDrive className="h-3.5 w-3.5" /> {t("backup.destinations.single")}
                  </h4>
                  {detail.method === "pbs" && (
                    <>
                      {detail.pbs_repository && (
                        <Field icon={<Server className={`h-3 w-3 ${destAccent} opacity-80`} />} label={t("backup.fields.repository")} value={detail.pbs_repository} mono />
                      )}
                      {detail.pbs_backup_id && (
                        <Field icon={<Archive className={`h-3 w-3 ${destAccent} opacity-80`} />} label={t("backup.fields.backupId")} value={detail.pbs_backup_id} mono />
                      )}
                    </>
                  )}
                  {detail.method === "borg" && (
                    <>
                      {detail.borg_repo && (
                        <Field icon={<Server className={`h-3 w-3 ${destAccent} opacity-80`} />} label={t("backup.fields.repo")} value={detail.borg_repo} mono />
                      )}
                      <Field icon={<FileSearch className={`h-3 w-3 ${destAccent} opacity-80`} />} label={t("backup.fields.encryption")} value={detail.borg_encrypt_mode} mono />
                    </>
                  )}
                  {detail.method === "local" && detail.local_dest_dir && (
                    <Field icon={<HardDrive className={`h-3 w-3 ${destAccent} opacity-80`} />} label={t("backup.fields.directoryShort")} value={detail.local_dest_dir} mono />
                  )}
                </section>
              </div>
            </ScrollArea>
          )}

        </DialogContent>
      </Dialog>
    </>
  )
}

// ──────────────────────────────────────────────────────────────
// PbsKeyfileRecoveryDialog
// ──────────────────────────────────────────────────────────────
// Drops the missing PBS keyfile back onto disk by pulling the escrow
// blob from a keyrecovery backup and decrypting it with the operator's
// recovery passphrase. Mirrors the shell's hb_pbs_try_keyfile_recovery
// flow. Triggered from the banner in DestinationsSection when the
// local keyfile is missing but at least one escrow backup is present
// on a configured PBS. The backend endpoint surfaces both the current
// `hostcfg-<host>-keyrecovery` and the legacy
// `proxmenux-keyrecovery-<host>` naming, so pre-1.2.2.2 escrow blobs
// stay recoverable without manual migration.
// ──────────────────────────────────────────────────────────────
function PbsKeyfileRecoveryDialog({
  open,
  snapshots,
  onClose,
  onRecovered,
}: {
  open: boolean
  snapshots: Array<{ repo_name: string; repo_repository: string; backup_id: string; source_host: string; backup_time: number; snapshot: string }>
  onClose: () => void
  onRecovered: () => void
}) {
  const t = useT()
  const [selectedKey, setSelectedKey] = useState<string>("")
  const [passphrase, setPassphrase] = useState<string>("")
  const [busy, setBusy] = useState<boolean>(false)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    if (!open) {
      setSelectedKey("")
      setPassphrase("")
      setBusy(false)
      setError(null)
      return
    }
    if (snapshots.length >= 1 && !selectedKey) {
      setSelectedKey(`${snapshots[0].repo_name}::${snapshots[0].snapshot}`)
    }
  }, [open, snapshots])

  const selected = snapshots.find((s) => `${s.repo_name}::${s.snapshot}` === selectedKey) || null

  async function handleRecover() {
    if (!selected || !passphrase) return
    setBusy(true)
    setError(null)
    try {
      await fetchApi("/api/host-backups/pbs-recovery/restore", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          repo_name: selected.repo_name,
          snapshot: selected.snapshot,
          passphrase,
        }),
      })
      onRecovered()
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e))
    } finally {
      setBusy(false)
    }
  }

  return (
    <Dialog open={open} onOpenChange={(v) => { if (!v && !busy) onClose() }}>
      <DialogContent className="max-w-lg bg-card border-border">
        <DialogHeader>
          <DialogTitle className="flex items-center gap-2 text-base">
            <Lock className="h-5 w-5 text-emerald-400" />
            {t("backup.keyfileRecovery.title")}
          </DialogTitle>
          <DialogDescription className="text-xs">
            {t("backup.keyfileRecovery.descriptionBefore")} <code className="font-mono">/usr/local/share/proxmenux/pbs-key.conf</code>.
          </DialogDescription>
        </DialogHeader>

        {snapshots.length > 1 && (
          <div className="space-y-1">
            <Label className="text-xs">{t("backup.keyfileRecovery.snapshot")}</Label>
            <Select value={selectedKey} onValueChange={setSelectedKey}>
              <SelectTrigger>
                <SelectValue />
              </SelectTrigger>
              <SelectContent>
                {snapshots.map((s) => {
                  let when: string
                  try { when = new Date(s.backup_time * 1000).toLocaleString() }
                  catch { when = String(s.backup_time) }
                  return (
                    <SelectItem key={`${s.repo_name}::${s.snapshot}`} value={`${s.repo_name}::${s.snapshot}`}>
                      <span className="font-mono">{s.source_host}</span>
                      <span className="text-muted-foreground ml-2">— {s.repo_name} · {when}</span>
                    </SelectItem>
                  )
                })}
              </SelectContent>
            </Select>
            <p className="text-[11px] text-muted-foreground">
              {t("backup.keyfileRecovery.pickSourceHost")}
            </p>
          </div>
        )}

        {selected && (
          <div className="text-xs space-y-1 px-3 py-2 rounded-md border border-border bg-background/40">
            <div className="flex items-center gap-2">
              <span className="text-muted-foreground">{t("backup.fields.sourceHost")}:</span>
              <span className="font-mono">{selected.source_host}</span>
            </div>
            <div className="flex items-center gap-2">
              <span className="text-muted-foreground">PBS:</span>
              <span className="font-mono break-all">{selected.repo_repository}</span>
            </div>
          </div>
        )}

        <div>
          <Label htmlFor="recoveryPass" className="text-xs">{t("backup.keyfileRecovery.passphrase")}</Label>
          <Input
            id="recoveryPass"
            type="password"
            value={passphrase}
            onChange={(e) => setPassphrase(e.target.value)}
            placeholder={t("backup.placeholders.keyfileRecoveryPassphrase")}
            className="font-mono mt-1"
            autoFocus
            onKeyDown={(e) => {
              if (e.key === "Enter" && passphrase && !busy) handleRecover()
            }}
          />
        </div>

        {error && (
          <div className="text-xs text-red-500 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10 whitespace-pre-wrap break-words">
            {error}
          </div>
        )}

        <div className="flex justify-end gap-2">
          <Button variant="ghost" onClick={onClose} disabled={busy}>
            {t("actions.cancel")}
          </Button>
          <Button
            className="bg-green-600 hover:bg-green-700 text-white"
            disabled={!selected || !passphrase || busy}
            onClick={handleRecover}
          >
            {busy ? (
              <Loader2 className="h-4 w-4 mr-2 animate-spin" />
            ) : (
              <Lock className="h-4 w-4 mr-2" />
            )}
            {t("backup.actions.recoverKeyfile")}
          </Button>
        </div>
      </DialogContent>
    </Dialog>
  )
}

// ──────────────────────────────────────────────────────────────
// ArchiveContentsModal
// ──────────────────────────────────────────────────────────────
// "View contents" of any backup snapshot. Calls /inspect-archive
// (extract to staging + parse_manifest + run_restore dry-run +
// walk rootfs + cleanup) and renders the JSON as a HTML report.
// Same shape regardless of backend so the operator gets a
// consistent view across PBS, Borg and local.
// ──────────────────────────────────────────────────────────────

interface InspectResponse {
  manifest?: any
  manifest_error?: string
  manifest_missing?: boolean
  plan?: any
  plan_error?: string
  plan_raw_stderr?: string
  files?: Array<{ path: string; size: number }>
  files_truncated?: boolean
  files_total_count?: number
  metadata_files?: Record<string, string>
  rollback_plan?: RollbackPlan
}

interface RollbackPlan {
  backup_time?: string
  vms_in_backup: number[]
  vms_in_host: number[]
  vms_to_remove: number[]
  vms_to_restore: number[]
  lxcs_in_backup: number[]
  lxcs_in_host: number[]
  lxcs_to_remove: number[]
  lxcs_to_restore: number[]
  components_to_uninstall: string[]
  components_to_reinstall: string[]
}

function ArchiveContentsModal({
  open,
  onClose,
  source,
  repo_name,
  snapshot,
  path,
  display_id,
}: {
  open: boolean
  onClose: () => void
  source: "pbs" | "borg" | "local" | null
  repo_name?: string
  snapshot?: string
  path?: string
  display_id?: string
}) {
  const t = useT()
  const [data, setData] = useState<InspectResponse | null>(null)
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    if (!open || !source) {
      setData(null)
      setError(null)
      setLoading(false)
      return
    }
    const body: Record<string, string> = { source }
    if (source === "local") {
      if (!path) { setError(t("backup.errors.localArchivePathMissing")); return }
      body.path = path
    } else {
      if (!repo_name || !snapshot) { setError(t("backup.errors.repoNameAndSnapshotRequired")); return }
      body.repo_name = repo_name
      body.snapshot = snapshot
    }
    setLoading(true)
    setError(null)
    fetchApi("/api/host-backups/inspect-archive", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    })
      .then((r: any) => setData(r))
      .catch((e) => setError(e instanceof Error ? e.message : String(e)))
      .finally(() => setLoading(false))
  }, [open, source, repo_name, snapshot, path])

  const manifest = data?.manifest
  const plan = data?.plan
  const files = data?.files

  return (
    <Dialog open={open} onOpenChange={(v) => { if (!v) onClose() }}>
      <DialogContent className="max-w-4xl bg-card border-border h-[85vh] flex flex-col p-0">
        <DialogHeader className="px-6 pt-6 pb-3 shrink-0">
          <DialogTitle className="flex items-center gap-2 text-base">
            <Eye className="h-5 w-5 text-blue-400" />
            {t("backup.contents.title")}
          </DialogTitle>
          <DialogDescription className="text-xs font-mono break-all">
            {display_id}
          </DialogDescription>
        </DialogHeader>

        {loading && (
          <div className="flex items-center gap-3 py-12 justify-center text-sm text-muted-foreground px-6">
            <Loader2 className="h-5 w-5 animate-spin" />
            {t("backup.contents.extracting")}
          </div>
        )}

        {error && !loading && (() => {
          const kf = parseKeyfileError(error)
          return kf ? (
            <div className="px-6"><KeyfileErrorBlock err={kf} /></div>
          ) : (
            <div className="text-sm text-red-400 px-3 py-3 mx-6 rounded-md border border-red-500/30 bg-red-500/10">
              {error}
            </div>
          )
        })()}

        {data && !loading && (
          <ScrollArea className="flex-1 min-h-0 px-6">
            <div className="space-y-4 pr-2">
              <ContentsSection icon={Info} title={t("backup.contents.manifest")} iconColor="text-blue-400">
                {manifest ? (
                  <ManifestGrid manifest={manifest} />
                ) : data.manifest_missing ? (
                  <div className="text-xs text-muted-foreground italic">
                    {t("backup.contents.manifestMissingBefore")} <code className="font-mono">run_info.env</code>, <code className="font-mono">pveversion.txt</code>, <code className="font-mono">selected_paths.txt</code>.
                  </div>
                ) : data.manifest_error ? (
                  <div className="text-xs text-amber-400">{data.manifest_error}</div>
                ) : (
                  <div className="text-xs text-muted-foreground italic">{t("backup.contents.noManifest")}</div>
                )}
              </ContentsSection>

              {(() => {
                // Only render plan/storage/network/drivers sections
                // when they actually carry information — otherwise
                // they read as "Unknown / No entries reported / No
                // changes planned" walls of noise.
                const blockers = plan?.preflight?.blockers || plan?.blockers || []
                const warnings = plan?.preflight?.warnings || plan?.warnings || []
                const planStatus = plan?.status || plan?.preflight?.status
                const planHasInfo = blockers.length > 0 || warnings.length > 0
                  || (planStatus && planStatus !== "unknown")
                const storageEntries = (plan?.storage?.entries || plan?.storage?.storages || [])
                const networkIfaces = (plan?.network?.interfaces || plan?.network?.remap || [])
                const driverList = (plan?.drivers?.plan || plan?.drivers?.components || plan?.drivers?.entries || [])
                return (
                  <>
                    {planHasInfo && (
                      <ContentsSection icon={ListTree} title={t("backup.contents.restorePlan")} iconColor="text-emerald-400">
                        <PlanSummary plan={plan} />
                      </ContentsSection>
                    )}
                    {storageEntries.length > 0 && (
                      <ContentsSection icon={HardDrive} title={t("backup.contents.storage")} iconColor="text-amber-400">
                        <StorageSection storage={plan.storage} />
                      </ContentsSection>
                    )}
                    {networkIfaces.length > 0 && (
                      <ContentsSection icon={Network} title={t("backup.contents.network")} iconColor="text-purple-400">
                        <NetworkSection network={plan.network} />
                      </ContentsSection>
                    )}
                    {driverList.length > 0 && (
                      <ContentsSection icon={Cpu} title={t("backup.contents.driversToReinstall")} iconColor="text-cyan-400">
                        <DriversSection drivers={plan.drivers} />
                      </ContentsSection>
                    )}
                  </>
                )
              })()}

              {data.rollback_plan && (
                <ContentsSection icon={History} title={t("backup.contents.rollbackPlan")} iconColor="text-blue-400">
                  <RollbackPlanView plan={data.rollback_plan} />
                </ContentsSection>
              )}

              {files && files.length > 0 && (
                <ContentsSection
                  icon={Package}
                  title={t("backup.contents.filesTitle", { count: data.files_total_count ?? files.length, plus: data.files_truncated ? "+" : "" })}
                  iconColor="text-fuchsia-400"
                >
                  <FilesTree files={files} truncated={data.files_truncated} />
                </ContentsSection>
              )}

              {data.metadata_files && Object.keys(data.metadata_files).length > 0 && (
                <ContentsSection icon={FileText} title={t("backup.contents.metadataFiles")} iconColor="text-muted-foreground">
                  <div className="space-y-3">
                    {Object.entries(data.metadata_files).map(([fname, content]) => (
                      <div key={fname}>
                        <div className="text-[10px] uppercase tracking-wider text-muted-foreground mb-1 font-mono">{fname}</div>
                        <pre className="text-[11px] font-mono whitespace-pre-wrap break-all max-h-40 overflow-auto rounded border border-border bg-background/40 p-2 text-foreground/80">
                          {content}
                        </pre>
                      </div>
                    ))}
                  </div>
                </ContentsSection>
              )}

              {data.plan_error && !data.manifest_missing && (
                <div className="text-[11px] text-amber-400 italic">
                  {t("backup.contents.restorePlanUnavailable", { error: data.plan_error })}
                </div>
              )}
            </div>
          </ScrollArea>
        )}
      </DialogContent>
    </Dialog>
  )
}

function ContentsSection({
  icon: Icon,
  iconColor,
  title,
  children,
}: {
  icon: any
  iconColor?: string
  title: string
  children: React.ReactNode
}) {
  return (
    <section className="rounded-md border border-border bg-background/40 p-3 space-y-2">
      <h3 className="text-xs font-semibold uppercase tracking-wider flex items-center gap-1.5">
        <Icon className={`h-3.5 w-3.5 ${iconColor || "text-muted-foreground"}`} />
        {title}
      </h3>
      {children}
    </section>
  )
}

function ManifestGrid({ manifest }: { manifest: any }) {
  const t = useT()
  // The manifest follows the proxmenux_backup_manifest schema:
  //   {source_host: {hostname, pve_version, kernel, boot_mode, cpu_model, memory_kb, roles},
  //    hardware_inventory: {gpu, tpu, nic, wireless},
  //    storage_inventory: {zfs_pools, lvm, physical_disks, pve_storage_cfg, mounts},
  //    proxmenux_installed_components: [...],
  //    kernel_params: {...},
  //    vms_lxcs_at_backup: {vms, lxcs},
  //    backup_metadata: {paths_archived, encrypted, compression, ...},
  //    created_at, created_by}
  // So .source_host is an OBJECT — passing it raw to String() gave
  // the dreaded "[object Object]".
  const src = manifest.source_host || {}
  const meta = manifest.backup_metadata || {}
  const hw = manifest.hardware_inventory || {}
  const guests = manifest.vms_lxcs_at_backup || {}
  const rows: Array<[string, any]> = []
  const push = (k: string, v: any) => {
    if (v === undefined || v === null || v === "") return
    rows.push([k, v])
  }
  push(t("backup.manifest.sourceHost"), src.hostname || manifest.hostname)
  push(t("backup.manifest.createdAt"), manifest.created_at || meta.created_at)
  push("Kernel", src.kernel || manifest.kernel)
  push(t("backup.manifest.proxmoxVersion"), src.pve_version || manifest.pve_version)
  push(t("backup.manifest.bootMode"), src.boot_mode)
  push("CPU", src.cpu_model)
  push(t("backup.manifest.memory"), src.memory_kb ? `${(src.memory_kb / 1024 / 1024).toFixed(1)} GB` : undefined)
  push(t("backup.manifest.roles"), Array.isArray(src.roles) && src.roles.length ? src.roles.join(", ") : undefined)
  push(t("backup.manifest.gpus"), Array.isArray(hw.gpu) && hw.gpu.length ? t("backup.manifest.deviceCount", { count: hw.gpu.length }) : undefined)
  push(t("backup.manifest.nics"), Array.isArray(hw.nic) && hw.nic.length ? t("backup.manifest.interfaceCount", { count: hw.nic.length }) : undefined)
  push(t("backup.manifest.vmsAtBackup"), Array.isArray(guests.vms) ? guests.vms.length : undefined)
  push(t("backup.manifest.lxcsAtBackup"), Array.isArray(guests.lxcs) ? guests.lxcs.length : undefined)
  push(t("backup.manifest.compression"), meta.compression)
  push(t("backup.manifest.encrypted"), meta.encrypted === true ? t("backup.common.yes") : meta.encrypted === false ? t("backup.common.no") : undefined)
  push(t("backup.manifest.pathsArchived"), Array.isArray(meta.paths_archived) ? t("backup.manifest.pathCount", { count: meta.paths_archived.length }) : undefined)
  push(t("backup.manifest.builtBy"), manifest.created_by)
  return (
    <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-1 text-xs">
      {rows.map(([k, v]) => (
        <div key={k}>
          <span className="text-muted-foreground">{k}:</span>{" "}
          <code className="font-mono text-foreground/90 break-all">{String(v)}</code>
        </div>
      ))}
    </div>
  )
}

function PlanSummary({ plan }: { plan: any }) {
  const t = useT()
  const status = plan.status || plan.preflight?.status || "unknown"
  const blockers: any[] = plan.preflight?.blockers || plan.blockers || []
  const warnings: any[] = plan.preflight?.warnings || plan.warnings || []
  const StatusIcon = status === "ok" ? CheckCircle2 : status === "warn" ? AlertTriangle : XCircle
  const statusColor = status === "ok" ? "text-emerald-400" : status === "warn" ? "text-amber-400" : "text-red-400"
  return (
    <div className="space-y-2 text-xs">
      <div className="flex items-center gap-2">
        <StatusIcon className={`h-4 w-4 ${statusColor}`} />
        <span className="font-medium">{t(`backup.plan.status.${status}`)}</span>
      </div>
      {blockers.length > 0 && (
        <div>
          <div className="text-[10px] uppercase tracking-wider text-red-400 mb-1">{t("backup.plan.blockers")}</div>
          <ul className="space-y-0.5 ml-4 list-disc">
            {blockers.map((b, i) => (
              <li key={i} className="text-red-400">{typeof b === "string" ? b : (b.message || JSON.stringify(b))}</li>
            ))}
          </ul>
        </div>
      )}
      {warnings.length > 0 && (
        <div>
          <div className="text-[10px] uppercase tracking-wider text-amber-400 mb-1">{t("backup.plan.warnings")}</div>
          <ul className="space-y-0.5 ml-4 list-disc">
            {warnings.map((w, i) => (
              <li key={i} className="text-amber-400">{typeof w === "string" ? w : (w.message || JSON.stringify(w))}</li>
            ))}
          </ul>
        </div>
      )}
      {blockers.length === 0 && warnings.length === 0 && (
        <div className="text-muted-foreground italic">{t("backup.plan.noIssues")}</div>
      )}
    </div>
  )
}

function StorageSection({ storage }: { storage: any }) {
  const t = useT()
  const entries: any[] = storage.entries || storage.storages || []
  if (entries.length === 0) {
    return <div className="text-xs text-muted-foreground italic">{t("backup.contents.noStorageEntries")}</div>
  }
  return (
    <ul className="space-y-1 text-xs">
      {entries.map((e: any, i: number) => {
        const ok = e.status === "ok" || e.matches
        const Icon = ok ? CheckCircle2 : AlertTriangle
        const color = ok ? "text-emerald-400" : "text-amber-400"
        return (
          <li key={i} className="flex items-start gap-2">
            <Icon className={`h-3.5 w-3.5 mt-0.5 shrink-0 ${color}`} />
            <div className="min-w-0 flex-1">
              <code className="font-mono">{e.name || e.storage || `entry ${i}`}</code>
              {e.type && <span className="text-muted-foreground ml-2">{e.type}</span>}
              {e.message && <div className="text-muted-foreground text-[11px]">{e.message}</div>}
            </div>
          </li>
        )
      })}
    </ul>
  )
}

function NetworkSection({ network }: { network: any }) {
  const t = useT()
  const ifaces: any[] = network.interfaces || network.remap || []
  if (ifaces.length === 0) {
    return <div className="text-xs text-muted-foreground italic">{t("backup.contents.noNetworkChanges")}</div>
  }
  return (
    <ul className="space-y-1 text-xs">
      {ifaces.map((nic: any, i: number) => (
        <li key={i} className="flex items-start gap-2">
          <Network className="h-3.5 w-3.5 mt-0.5 shrink-0 text-purple-400" />
          <div className="min-w-0 flex-1">
            <code className="font-mono">{nic.source || nic.from || "?"}</code>
            <span className="text-muted-foreground mx-2">→</span>
            <code className="font-mono">{nic.target || nic.to || "?"}</code>
            {nic.action && <span className="text-[10px] text-muted-foreground ml-2">{nic.action}</span>}
          </div>
        </li>
      ))}
    </ul>
  )
}

function DriversSection({ drivers }: { drivers: any }) {
  const t = useT()
  const list: any[] = drivers.plan || drivers.components || drivers.entries || []
  if (list.length === 0) {
    return <div className="text-xs text-muted-foreground italic">{t("backup.contents.noDrivers")}</div>
  }
  return (
    <ul className="space-y-1 text-xs">
      {list.map((d: any, i: number) => (
        <li key={i} className="flex items-start gap-2">
          <Cpu className="h-3.5 w-3.5 mt-0.5 shrink-0 text-cyan-400" />
          <div className="min-w-0 flex-1">
            <code className="font-mono">{d.name || d.id || `driver ${i}`}</code>
            {d.action && <span className="text-muted-foreground ml-2">{d.action}</span>}
            {d.detail && <div className="text-muted-foreground text-[11px]">{d.detail}</div>}
          </div>
        </li>
      ))}
    </ul>
  )
}

function FilesTree({ files, truncated }: { files: Array<{ path: string; size: number }>; truncated?: boolean }) {
  const t = useT()
  const [query, setQuery] = useState("")
  const filtered = query
    ? files.filter((f) => f.path.toLowerCase().includes(query.toLowerCase()))
    : files
  return (
    <div className="space-y-2">
      <Input
        type="search"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder={t("backup.placeholders.filterPaths")}
        className="h-8 text-xs"
      />
      <div className="max-h-72 overflow-auto rounded border border-border bg-background/40">
        <ul className="text-[11px] font-mono divide-y divide-border/30">
          {filtered.slice(0, 2000).map((f) => (
            <li key={f.path} className="flex items-center justify-between gap-3 px-2 py-1 hover:bg-white/5">
              <span className="truncate" title={f.path}>{f.path}</span>
              <span className="text-muted-foreground shrink-0">{formatBytes(f.size)}</span>
            </li>
          ))}
        </ul>
      </div>
      <div className="text-[10px] text-muted-foreground flex items-center gap-2">
        {t("backup.contents.filesShowing", {
          shown: Math.min(filtered.length, 2000),
          filtered: filtered.length,
          filteredLabel: query ? t("backup.contents.filtered") : "",
          total: filtered.length !== files.length ? t("backup.contents.totalFiles", { count: files.length }) : "",
        })}
        {truncated && <span className="text-amber-400">{t("backup.contents.filesTruncated")}</span>}
      </div>
    </div>
  )
}

// ──────────────────────────────────────────────────────────────
// RollbackPlanView
// ──────────────────────────────────────────────────────────────
// Visualizes what a "restore to backup state" would do — surfaces
// the irreversible parts (VMs/LXCs to delete, components to
// uninstall) in red, the safe re-apply parts in green. Drives the
// operator confirmation in the Complete restore flow.
// ──────────────────────────────────────────────────────────────
function RollbackPlanView({ plan }: { plan: RollbackPlan }) {
  const t = useT()
  // The destructive "Not in backup — will be deleted on rollback"
  // block was removed: operators read it as an action ProxMenux
  // would take silently. We only show the "Configurations included
  // in backup" section now, which is purely informative.
  const hasRollback = (plan.vms_to_restore?.length || 0) > 0
    || (plan.lxcs_to_restore?.length || 0) > 0
    || (plan.components_to_reinstall?.length || 0) > 0

  if (!hasRollback) {
    return (
      <div className="text-xs text-muted-foreground italic">
        {t("backup.rollback.noDifferences")}
      </div>
    )
  }

  // Pill renders inline with proper flex wrapping so dozens of VM
  // IDs don't overflow the modal on mobile (previous version used
  // raw spaces between pills and any value past the viewport got
  // clipped).
  const Pill = ({ children }: { children: any }) => (
    <code className="font-mono px-1.5 py-0.5 rounded border bg-emerald-500/10 border-emerald-500/30 text-emerald-300 text-[11px]">
      {children}
    </code>
  )
  // Each pill is a direct flex child of the row (no inner wrapper)
  // so flex-wrap can break a long list of IDs mid-row on mobile.
  // The previous nested `<div className="flex flex-wrap">` wouldn't
  // wrap because its width was capped by the parent's flex layout,
  // and the whole block would overflow the modal instead.
  const Row = ({ label, items }: { label: string; items: (string | number)[] }) => (
    <div className="flex flex-wrap items-baseline gap-x-2 gap-y-1.5">
      <span className="text-muted-foreground shrink-0">{label}</span>
      {items.map((id) => <Pill key={id}>{id}</Pill>)}
    </div>
  )

  return (
    <div className="rounded-md border border-emerald-500/40 bg-emerald-500/5 p-3 space-y-2 text-xs">
      <div className="text-[11px] font-semibold uppercase tracking-wider text-emerald-400">
        {t("backup.rollback.configsIncluded")}
      </div>
      {plan.vms_to_restore.length > 0 && (
        <Row label={t("backup.rollback.vmConfigs")} items={plan.vms_to_restore} />
      )}
      {plan.lxcs_to_restore.length > 0 && (
        <Row label={t("backup.rollback.lxcConfigs")} items={plan.lxcs_to_restore} />
      )}
      {plan.components_to_reinstall.length > 0 && (
        <Row label={t("backup.rollback.components")} items={plan.components_to_reinstall} />
      )}
      <div className="text-[10px] text-muted-foreground pt-1">
        {t("backup.rollback.pveOnlyHint")}
      </div>
    </div>
  )
}

// ──────────────────────────────────────────────────────────────
// RestoreOptionsModal
// ──────────────────────────────────────────────────────────────
// Two-step UI for restoring a snapshot:
//   Step 1 — pick Complete restore vs Custom by paths.
//   Step 2 — if Custom, tick the paths to restore.
// `pathsAvailable` is what the backend's list_paths.sh detected
// in this specific backup (default profile + operator extras).
// Perfect parity with the TUI's _rs_select_component_paths after
// the paths-not-components refactor.
// ──────────────────────────────────────────────────────────────

function RestoreOptionsModal({
  open,
  onClose,
  stagingPath,
  pathsAvailable,
  rollbackPlan,
  crossKernel,
  hydration,
  display_id,
  onLaunch,
}: {
  open: boolean
  onClose: () => void
  stagingPath: string
  pathsAvailable: string[]
  rollbackPlan?: RollbackPlan
  crossKernel?: {
    direction: "same" | "bk_older" | "bk_newer"
    backupKernel: string
    targetKernel: string
    blockedPaths: string[]
  }
  hydration?: {
    applies: boolean
    actions: string[]
  }
  display_id?: string
  onLaunch: (mode: "full" | "custom", paths: string[], rollbackExecute?: boolean) => void
}) {
  const t = useT()
  // When direction === "bk_older" the CLI-side filter (RS_SKIP_PATHS)
  // will drop these prefixes from any restore. We mirror that
  // behavior in the picker so the operator can see what will be
  // skipped rather than being surprised at run time.
  const isBkOlder = crossKernel?.direction === "bk_older"
  const blockedPrefixes = isBkOlder ? (crossKernel?.blockedPaths ?? []) : []
  const isPathBlocked = (p: string) =>
    blockedPrefixes.some((b) => p === b || p.startsWith(b.endsWith("/") ? b : `${b}/`))
  const [step, setStep] = useState<"choose" | "custom">("choose")
  const [selected, setSelected] = useState<Set<string>>(new Set())
  const [error, setError] = useState<string | null>(null)
  const [filter, setFilter] = useState<string>("")
  const [destructiveAck, setDestructiveAck] = useState<boolean>(false)

  // A Complete restore on a host that has VMs/LXCs/components NOT
  // present in the backup is destructive (those entries get removed
  // at next boot by apply_cluster_postboot.sh's rollback step). We
  // force the operator to tick an explicit "I understand" checkbox
  // before enabling the launch button.
  const hasDestructive = !!rollbackPlan && (
    (rollbackPlan.vms_to_remove?.length || 0) > 0
    || (rollbackPlan.lxcs_to_remove?.length || 0) > 0
    || (rollbackPlan.components_to_uninstall?.length || 0) > 0
  )

  useEffect(() => {
    if (!open) {
      setStep("choose")
      setSelected(new Set())
      setError(null)
      setFilter("")
      setDestructiveAck(false)
    }
  }, [open])

  const togglePath = (p: string) => {
    if (isPathBlocked(p)) return
    setSelected((prev) => {
      const next = new Set(prev)
      if (next.has(p)) next.delete(p)
      else next.add(p)
      return next
    })
  }

  const toggleAll = () => {
    const selectable = filteredPaths.filter((p) => !isPathBlocked(p))
    if (selected.size === selectable.length) {
      setSelected(new Set())
    } else {
      setSelected(new Set(selectable))
    }
  }

  const filteredPaths = filter
    ? pathsAvailable.filter((p) => p.toLowerCase().includes(filter.toLowerCase()))
    : pathsAvailable
  const selectableCount = filteredPaths.filter((p) => !isPathBlocked(p)).length

  const launch = (mode: "full" | "custom") => {
    if (mode === "custom" && selected.size === 0) {
      setError(t("backup.restore.pickPathError"))
      return
    }
    setError(null)
    onLaunch(mode, Array.from(selected), mode === "full" && hasDestructive && destructiveAck)
  }

  return (
    <Dialog open={open} onOpenChange={(v) => { if (!v && step !== "preparing") onClose() }}>
      <DialogContent className="max-w-2xl bg-card border-border">
        <DialogHeader>
          <DialogTitle className="flex items-center gap-2 text-base">
            <DatabaseBackup className="h-5 w-5 text-emerald-400" />
            {t("backup.restore.chooseModeTitle")}
          </DialogTitle>
          <DialogDescription className="text-xs font-mono break-all">
            {display_id}
          </DialogDescription>
        </DialogHeader>

        {step === "choose" && (
          <div className="space-y-3">
            {/* Post-restore Monitor hint — the Backups tab renders a live
                progress card driven by /var/lib/proxmenux/restore-state.json,
                so the operator has a place to watch component reinstalls,
                sanity warnings, and the rollback delta after the reboot. */}
            <div className="rounded-md border border-blue-500/40 bg-blue-500/5 p-3 text-[11px] text-muted-foreground flex items-start gap-2">
              <History className="h-3.5 w-3.5 text-blue-400 shrink-0 mt-0.5" />
              <div>
                {t("backup.restore.afterRebootHint")}
              </div>
            </div>

            {isBkOlder && (
              <div className="rounded-md border border-amber-500/50 bg-amber-500/5 p-3">
                <div className="text-[11px] font-semibold uppercase tracking-wider text-amber-400 flex items-center gap-1.5">
                  <AlertTriangle className="h-3.5 w-3.5" />
                  {t("backup.restore.crossKernelTitle")}
                </div>
                <div className="text-[11px] text-muted-foreground mt-2 space-y-1.5">
                  <div>
                    {t("backup.restore.backupKernel")} <code className="font-mono">{crossKernel?.backupKernel}</code> {t("backup.restore.olderThanTargetKernel")} <code className="font-mono">{crossKernel?.targetKernel}</code>.
                  </div>
                  <div>
                    {t("backup.restore.crossKernelDescription")}
                  </div>
                  {blockedPrefixes.length > 0 && (
                    <details className="mt-1.5">
                      <summary className="cursor-pointer text-amber-400/90 hover:text-amber-300">
                        {t("backup.restore.pathsSkipped", { count: blockedPrefixes.length })}
                      </summary>
                      <ul className="mt-1.5 pl-3 space-y-0.5 max-h-40 overflow-auto">
                        {blockedPrefixes.map((p) => (
                          <li key={p} className="font-mono text-[10.5px] text-muted-foreground/80 break-all">{p}</li>
                        ))}
                      </ul>
                    </details>
                  )}
                </div>
              </div>
            )}

            {isBkOlder && hydration?.applies && hydration.actions.length > 0 && (
              <div className="rounded-md border border-emerald-500/50 bg-emerald-500/5 p-3">
                <div className="text-[11px] font-semibold uppercase tracking-wider text-emerald-400 flex items-center gap-1.5">
                  <CheckCircle2 className="h-3.5 w-3.5" />
                  {t("backup.restore.operatorConfigTitle")}
                </div>
                <div className="text-[11px] text-muted-foreground mt-2 space-y-1.5">
                  <div>
                    {t("backup.restore.operatorConfigDescription")}
                  </div>
                  <ul className="mt-1.5 pl-3 space-y-0.5 max-h-40 overflow-auto">
                    {hydration.actions.map((a, i) => (
                      <li key={`${a}-${i}`} className="text-[10.5px] text-emerald-300/90 break-all">
                        <span className="text-emerald-500 mr-1">•</span>{a}
                      </li>
                    ))}
                  </ul>
                </div>
              </div>
            )}

            {/* Surface destructive deltas BEFORE the operator picks
                Complete. Custom-by-paths can't trigger these
                (paths-only restore doesn't touch the guest list or
                components_status.json), so the warning lives here. */}
            {rollbackPlan && hasDestructive && (
              <div className="rounded-md border border-red-500/50 bg-red-500/5 p-3 space-y-3">
                <div className="text-[11px] font-semibold uppercase tracking-wider text-red-400 flex items-center gap-1.5">
                  <AlertTriangle className="h-3.5 w-3.5" />
                  {t("backup.restore.destructiveTitle")}
                </div>
                <RollbackPlanView plan={rollbackPlan} />
                <label className="flex items-start gap-2 cursor-pointer text-xs">
                  <Checkbox
                    checked={destructiveAck}
                    onCheckedChange={(v) => setDestructiveAck(!!v)}
                    className="mt-0.5"
                  />
                  <span className="text-foreground">
                    {t("backup.restore.ackBefore")} <strong className="text-red-400">{t("backup.restore.permanentlyDelete")}</strong> {t("backup.restore.ackAfter")}
                  </span>
                </label>
              </div>
            )}

            <button
              type="button"
              onClick={() => launch("full")}
              disabled={hasDestructive && !destructiveAck}
              className="w-full text-left p-4 rounded-md border border-emerald-500/40 bg-emerald-500/5 hover:bg-emerald-500/10 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
            >
              <div className="flex items-start gap-3">
                <CheckCircle2 className="h-5 w-5 text-emerald-400 shrink-0 mt-0.5" />
                <div className="flex-1 min-w-0">
                  <div className="text-sm font-medium">{t("backup.restore.completeTitle")}</div>
                  <div className="text-[11px] text-muted-foreground mt-1">
                    {t("backup.restore.completeDescriptionBefore")} <em>{t("backup.restore.completeTitle")}</em> {t("backup.restore.completeDescriptionAfter")}
                    {hasDestructive && !destructiveAck && (
                      <span className="block mt-1 text-red-400">{t("backup.restore.tickAckToEnable")}</span>
                    )}
                  </div>
                </div>
              </div>
            </button>

            <button
              type="button"
              onClick={() => setStep("custom")}
              disabled={pathsAvailable.length === 0}
              className="w-full text-left p-4 rounded-md border border-blue-500/40 bg-blue-500/5 hover:bg-blue-500/10 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
            >
              <div className="flex items-start gap-3">
                <ListTree className="h-5 w-5 text-blue-400 shrink-0 mt-0.5" />
                <div className="flex-1 min-w-0">
                  <div className="text-sm font-medium">
                    {t("backup.restore.customByPathsTitle")}
                    {pathsAvailable.length > 0 && (
                      <span className="ml-2 text-[10px] text-muted-foreground font-normal">
                        {t("backup.restore.pathsInBackup", { count: pathsAvailable.length })}
                      </span>
                    )}
                  </div>
                  <div className="text-[11px] text-muted-foreground mt-1">
                    {pathsAvailable.length === 0
                      ? t("backup.restore.noRestorablePaths")
                      : t("backup.restore.customByPathsDescription")}
                  </div>
                </div>
              </div>
            </button>

            {error && (
              <div className="text-xs text-red-400 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10">
                {error}
              </div>
            )}
          </div>
        )}

        {step === "custom" && (
          <div className="space-y-2">
            <p className="text-[11px] text-muted-foreground">
              {t("backup.restore.customPickDescriptionBefore")} <code className="font-mono">_rs_run_custom_restore</code> {t("backup.restore.customPickDescriptionMiddle")} <code className="font-mono">HB_PRESELECTED_PATHS</code> {t("backup.restore.customPickDescriptionAfter")}
            </p>
            <div className="flex items-center gap-2">
              <Input
                type="search"
                value={filter}
                onChange={(e) => setFilter(e.target.value)}
                placeholder={t("backup.placeholders.filterPaths")}
                className="h-8 text-xs"
              />
              <Button
                type="button"
                variant="outline"
                size="sm"
                onClick={toggleAll}
                className="h-8 text-xs whitespace-nowrap"
              >
                {selected.size === selectableCount && selectableCount > 0 ? t("backup.actions.clear") : t("backup.actions.all")}
              </Button>
            </div>
            {isBkOlder && (
              <div className="text-[10.5px] text-amber-400/90 px-2 py-1.5 rounded border border-amber-500/30 bg-amber-500/5">
                {t("backup.restore.blockedPathsHint")}
              </div>
            )}
            <div className="rounded-md border border-border bg-background/40 p-1 max-h-72 overflow-auto">
              <ul className="divide-y divide-border/40">
                {filteredPaths.map((p) => {
                  const blocked = isPathBlocked(p)
                  return (
                    <li key={p}>
                      <label
                        className={`flex items-center gap-2 px-2 py-1 ${blocked ? "opacity-40 cursor-not-allowed" : "hover:bg-white/5 cursor-pointer"}`}
                        title={blocked ? t("backup.restore.skippedPathTitle") : undefined}
                      >
                        <Checkbox
                          checked={!blocked && selected.has(p)}
                          disabled={blocked}
                          onCheckedChange={() => togglePath(p)}
                        />
                        <code className={`font-mono text-xs break-all flex-1 ${blocked ? "line-through" : ""}`}>{p}</code>
                        {blocked && (
                          <span className="text-[10px] text-amber-400/80 font-normal shrink-0">{t("backup.restore.skipped")}</span>
                        )}
                      </label>
                    </li>
                  )
                })}
                {filteredPaths.length === 0 && (
                  <li className="text-[11px] text-muted-foreground italic px-2 py-2">{t("backup.restore.noPathsMatch")}</li>
                )}
              </ul>
            </div>
            <div className="text-[11px] text-muted-foreground">
              {t("backup.restore.selectedCount", {
                selected: selected.size,
                total: isBkOlder ? selectableCount : pathsAvailable.length,
                shown: filter && filteredPaths.length !== pathsAvailable.length ? t("backup.restore.shownCount", { count: filteredPaths.length }) : "",
              })}
            </div>
            {error && (
              <div className="text-xs text-red-400 px-3 py-2 rounded-md border border-red-500/30 bg-red-500/10">
                {error}
              </div>
            )}
          </div>
        )}

        <div className="flex justify-end gap-2 pt-2 border-t border-border">
          {step === "custom" && (
            <Button variant="ghost" onClick={() => setStep("choose")}>
              {t("backup.actions.back")}
            </Button>
          )}
          <Button variant="ghost" onClick={onClose}>
            {t("actions.cancel")}
          </Button>
          {step === "custom" && (
            <Button
              onClick={() => launch("custom")}
              disabled={selected.size === 0}
              className="bg-green-600 hover:bg-green-700 text-white"
            >
              <DatabaseBackup className="h-4 w-4 mr-2" />
              {t("backup.actions.restoreSelected")}
            </Button>
          )}
        </div>
      </DialogContent>
    </Dialog>
  )
}
