import { useRef, useCallback, type ReactNode } from 'react';

export const PARAM_TOOLTIPS = {
  // Sampling – Generation
  temperature: 'Scales the probability distribution to control randomness. Lower values (e.g. 0.2) make output more focused and deterministic; higher values (e.g. 1.5) increase variety and creativity. Leave empty to use the model default.',
  top_p: 'Nucleus sampling — samples only from the smallest set of tokens whose cumulative probability reaches this value. Lower values (e.g. 0.5) focus on the most likely tokens; 1.0 effectively disables top-p filtering. Works alongside temperature.',
  top_k: 'Limits sampling to the top K most probable tokens at each step. Lower values (e.g. 10) make output more predictable; higher values allow more variety. Set very high to approximate no limit. Leave empty to use the model default.',
  min_p: 'Filters out tokens whose probability is below this fraction of the top token\'s probability. For example, 0.05 removes tokens less than 5% as likely as the best choice. Higher = stricter filtering. 0 disables.',

  // Sampling – Repetition Control
  repeat_penalty: 'Penalizes tokens that appeared in the recent context window (controlled by Repeat Last N). Values above 1.0 discourage repetition; 1.0 means no penalty. Typical range is 1.0–1.3. Too high can make text incoherent.',
  repeat_last_n: 'How many recent tokens to check when applying the repeat penalty. Larger values look further back for repetitions. To disable repetition penalties, set Repeat Penalty to 1.0 instead.',
  frequency_penalty: 'Reduces the likelihood of a token proportional to how many times it has appeared. Positive values discourage overused tokens; negative values encourage them. Common range: -2.0 to 2.0. 0 disables.',
  presence_penalty: 'Applies a flat penalty to any token that has appeared at all, regardless of how often. Positive values encourage the model to use new tokens; negative values favor staying on existing ones. 0 disables.',

  // Sampling – DRY Sampler
  dry_multiplier: 'Strength of the DRY (Don\'t Repeat Yourself) anti-repetition penalty. Higher values more aggressively penalize repeated n-gram patterns. Leave empty to use the model default.',
  dry_base: 'Base for exponential DRY penalty growth. Higher values make the penalty increase faster for longer repeated sequences. Typical values are 1.5–2.0.',
  dry_allowed_length: 'Minimum n-gram length that DRY penalizes. Repeats of length ≥ this value are penalized; shorter repeats are ignored. Useful for allowing common short phrases. Higher values are more lenient.',
  dry_penalty_last_n: 'How many recent tokens DRY examines when looking for repeated patterns. Larger values detect repetitions from further back. 0 means use the full context.',

  // Sampling – XTC Sampler
  xtc_probability: 'Chance of enabling XTC (eXtreme Token Culling) on each token sampling step. When active, XTC removes very high-probability ("obvious") tokens to increase variety. 0 disables XTC entirely, 1 always applies it.',
  xtc_threshold: 'Probability cutoff for XTC culling. When XTC is active, tokens with probability ≥ this threshold are candidates for removal (with safeguards to keep output coherent). Lower thresholds make XTC more aggressive.',
  xtc_min_keep: 'Minimum number of token candidates to keep after XTC culling, preventing over-aggressive filtering. Ensures at least this many choices remain available.',

  // Sampling – Generation limit
  max_tokens: 'Maximum number of tokens (word-pieces) to generate. Rule of thumb: 1 token ≈ 0.75 words in English. Output may stop earlier on end-of-sequence or when the context window is full. Higher values allow longer answers but take more time.',

  // Sampling – Reasoning
  enable_thinking: 'Enables reasoning/thinking mode in the prompt template (model-dependent). May improve accuracy on complex tasks but increases token usage and latency. Some models ignore this or keep reasoning internal.',
  reasoning_effort: 'Requested reasoning level (model/provider dependent): none, minimal, low, medium, high (default: medium). Higher effort may produce more thorough reasoning but uses more tokens and time. Unsupported models ignore this.',

  // Config sweep
  nbatch: 'Batch tray capacity — maximum tokens processed per decode call (shared across slots during prefill). Larger values speed up prompt evaluation and multi-request batching but increase VRAM usage. Typically keep ≤ context window size.',
  nubatch: 'Micro-batch size for prompt processing. Controls VRAM usage per batch operation. Must be ≤ NBatch. Smaller values reduce peak VRAM usage at the cost of slightly slower processing.',
  contextWindow: 'Maximum number of tokens (input + output combined) the model can handle at once. Larger windows support longer conversations but increase VRAM usage proportionally via the KV cache. Models with RoPE can extend beyond native context using YaRN (up to ~4× recommended).',
  nSeqMax: 'Maximum number of concurrent request slots. Each slot handles one user request simultaneously. More slots = better concurrency, but each slot reserves memory for its KV cache.',
  flashAttention: 'Optimized attention algorithm that reduces VRAM usage and can improve speed. "Enabled" forces it on, "Disabled" forces it off, "Auto" lets the server decide based on model compatibility.',
  cacheType: 'KV cache precision. f16 = full precision (best quality), q8_0 = 8-bit quantized (less VRAM, minimal quality loss), q4_0 = 4-bit quantized (most VRAM savings, slight quality trade-off especially at long context).',
  cacheMode: 'Caching strategy. None = clears KV state after each request. IMC (Incremental Message Cache) = keeps the conversation\'s KV state in a dedicated slot for fast multi-turn follow-ups.',

  // MoE configuration
  moeMode: 'How to distribute expert weights between GPU and CPU. "Recommended" auto-detects the best option for your hardware. "Save GPU Memory" moves experts to CPU (most common for consumer GPUs). "Maximum Speed" keeps everything on GPU (requires very large VRAM; exact need depends on model, quantization, context, and slots). "Balanced" lets you choose how many layers stay on GPU.',
  moeKeepExpertsTopN: 'Slide right for more speed (keeps more expert layers on GPU), slide left to save VRAM (offloads to CPU). The highest-numbered layers stay on GPU first. 0 = all experts on CPU.',
  moeTipBatch: 'For MoE models with CPU experts, NBatch/NUBatch ≥ 4096 is recommended for optimal prompt processing speed.',
  moeTipFlashAttention: 'Flash Attention is strongly recommended for MoE models — it significantly reduces VRAM usage and improves performance.',
  moeTipComputeBuffer: 'Larger NUBatch increases compute buffer VRAM usage. Monitor with the VRAM calculator when tuning MoE batch sizes.',
  availableVRAM: 'Total GPU VRAM available (in GB). When set, config candidates estimated to exceed this are auto-skipped before the sweep runs. Set to 0 or leave empty to disable VRAM filtering.',

  // NUMA / mmap / Op-offload (Phase F2/F3)
  useMMap: 'Controls whether mmap is used for model loading. Disabling mmap (--no-mmap) is recommended for multi-socket NUMA systems running MoE models with CPU experts — tensor data is directly allocated on the appropriate NUMA node instead of being memory-mapped.',
  numa: 'NUMA (Non-Uniform Memory Access) strategy for multi-socket systems. "distribute" spreads memory across NUMA nodes (recommended for MoE CPU-expert setups). "isolate" pins to one node. "numactl" defers to system numactl. "mirror" mirrors across nodes. Leave empty to disable.',
  opOffloadMinBatch: 'Minimum batch size before enabling GPU offload for certain host-side operations during prompt processing. 0 = use server default. For large MoE models with many CPU weights, values of 200–500+ may improve prompt ingestion speed.',

  // ── Shared VRAM / Config tooltips ──────────────────────────────────────────
  // These are used by both the VRAM Calculator and the Model Playground /
  // Chat settings panels. Keep descriptions hardware-neutral and applicable
  // to both contexts.

  // Controls (shared)
  kvCacheOnCPU: 'Moves the KV cache from GPU VRAM to system RAM. Frees GPU memory but may reduce generation speed — significant on discrete GPUs (PCIe bottleneck), minimal on Apple Silicon (unified memory).',
  gpuCount: 'Number of GPUs to distribute the model across. Weights are split between GPUs using tensor parallelism. More GPUs reduce per-GPU VRAM but add inter-GPU communication overhead.',
  tensorSplit: 'Proportional distribution of model weights across GPUs (e.g. "0.6,0.4" puts 60% on GPU 0 and 40% on GPU 1). Leave empty for equal distribution. Useful when GPUs have different VRAM capacities.',
  expertLayersOnGPU: 'For MoE models: how many transformer block expert layers to keep on GPU. More layers on GPU = faster inference but more VRAM. Layers are kept top-down (highest-numbered first). 0 = all experts on CPU.',
  gpuLayers: 'Number of transformer layers offloaded to GPU (-ngl). All layers on GPU gives maximum speed. Reducing layers saves VRAM by moving weights to system RAM at the cost of inference speed — significant on discrete GPUs (PCIe bottleneck), minimal on Apple Silicon (unified memory).',

  // Model header / metadata
  modelSize: 'Total size of the quantized GGUF model file. For dense models this approximates the GPU VRAM needed for weights alone; for MoE models the actual GPU portion depends on how many expert layers are offloaded to CPU.',
  blockCount: 'Number of transformer blocks (layers) in the model. More layers = larger model. KV cache scales linearly with this value.',
  headCountKV: 'Number of key-value attention heads. Along with key/value lengths, determines the per-token KV cache size. Some models use fewer KV heads than query heads (grouped-query attention) to save memory.',
  keyLength: 'Dimension of each attention key vector (in elements). Together with value length and head count, determines the per-token-per-layer KV cache size.',
  valueLength: 'Dimension of each attention value vector (in elements). Together with key length and head count, determines the per-token-per-layer KV cache size.',
  expertCount: 'Total number of expert sub-networks in a Mixture-of-Experts model. Only a subset (top-k) are activated per token, but all must be stored in memory.',
  activeExperts: 'Number of experts activated per token (top-k routing). Fewer active experts = faster inference per token but the full expert set still occupies memory.',
  sharedExperts: 'Whether the model has shared (always-active) experts in addition to routed ones. Shared experts run on every token and their weights are always loaded on GPU.',

  // VRAM breakdown
  modelWeights: 'GPU memory consumed by the model\'s weight tensors. For dense models this equals the model file size; for MoE models it includes only the always-active weights plus any expert layers kept on GPU.',
  alwaysActiveWeights: 'Memory for non-expert weights that are always loaded on GPU: embeddings, attention layers, normalization, output head, and any shared experts.',
  expertWeightsGPU: 'Memory for the expert layers kept on GPU. Increasing "Expert Layers on GPU" moves more expert blocks from CPU to GPU for faster inference at the cost of VRAM.',
  expertWeightsCPU: 'Memory for expert layers kept in system RAM (CPU-resident). Saves VRAM but typically reduces throughput vs keeping those layers on GPU.',
  kvCache: 'Total KV cache memory across all slots. Formula: slots × context_window × block_count × head_count_kv × (key_length + value_length) × bytes_per_element.',
  kvPerSlot: 'KV cache memory for a single inference slot. Each concurrent request gets its own slot with a full KV cache allocation.',
  kvPerTokenPerLayer: 'KV cache memory for one token in one transformer layer. This is the fundamental unit — total KV = this × context_window × block_count × slots.',
  computeBuffer: 'Estimated temporary GPU memory for scratch/intermediate tensors during inference. This calculator uses a heuristic based on model size and embedding dimensions; actual usage may vary with backend and batch settings.',

  // Hero / summary
  totalEstimatedVRAM: 'Sum of model weights on GPU, KV cache (if on GPU), and estimated compute buffer. In multi-GPU setups this is the total across all GPUs — see the per-GPU breakdown for individual allocations.',
  totalEstimatedSystemRAM: 'Estimated system RAM usage from MoE expert weights on CPU and/or KV cache offloaded to system RAM. Does not include OS or application overhead.',

  // ── Model Card / metadata tooltips ────────────────────────────────────────
  modelArchitecture: 'The neural network architecture family (e.g. llama, qwen2, gemma). Determines how the model processes tokens and which optimizations apply.',
  sizeLabel: 'Human-readable size label from the model publisher (e.g. "8B", "70B"). Indicates the approximate parameter count.',
  quantization: 'The GGUF quantization format used to compress model weights. Lower-bit formats (Q4) save memory at some quality cost; higher-bit formats (Q8, F16) preserve more quality.',
  contextLength: 'The native context window the model was trained on. Input + output tokens must fit within this limit unless extended via YaRN/RoPE scaling.',
  embeddingDimension: 'Size of each token\'s hidden-state vector. Larger embeddings capture more information per token but increase memory and compute proportionally.',
  attentionHeadsQ: 'Number of query attention heads. More heads allow the model to attend to different representation subspaces simultaneously. Together with KV heads, determines the attention pattern.',
  ropeDimension: 'Number of dimensions used for Rotary Position Embeddings (RoPE). Controls how positional information is encoded into attention. Typically equals the head dimension.',
  feedForwardLength: 'Size of the feed-forward (MLP) intermediate layer in each transformer block. Larger values increase model capacity but also VRAM usage.',
  expertFFNLength: 'Size of the feed-forward layer inside each MoE expert sub-network. Similar to Feed Forward Length but specific to the routed expert MLPs.',
  sharedExpertFFNLength: 'Size of the feed-forward layer in shared (always-active) experts. These experts run on every token in addition to the top-k routed experts.',
  ssmInnerSize: 'Dimension of the inner state in the SSM (State Space Model) layers. Larger values increase the model\'s recurrent memory capacity.',
  ssmStateSize: 'Size of the discrete state in SSM layers. Controls how much information the recurrent state can carry between tokens.',
  ssmConvKernel: 'Convolution kernel width in SSM layers. Determines how many neighboring tokens are mixed in the local convolution before the SSM recurrence.',
  ssmTimeStepRank: 'Rank of the time-step projection in SSM layers. Controls the expressiveness of the learned discretization step.',
  ssmGroupCount: 'Number of groups in the SSM layers. Groups partition the state dimensions for more efficient computation, similar to grouped convolution.',
  fullAttentionInterval: 'In hybrid models, how often a full attention layer appears among SSM layers (e.g. every N layers). Balances the efficiency of SSM with the global context of attention.',
  tokenizerModel: 'The tokenizer algorithm used (e.g. BPE, SentencePiece, Unigram). Determines how text is split into tokens that the model processes.',
  eosTokenId: 'End-of-sequence token ID. When the model generates this token, it signals that the response is complete.',
  bosTokenId: 'Beginning-of-sequence token ID. Prepended to the input to signal the start of a new sequence to the model.',
  paddingTokenId: 'Padding token ID used to fill sequences to a uniform length in batch processing. Not used during generation.',

  // ── Model detail / config tooltips ──────────────────────────────────────────
  nthreads: 'Number of CPU threads used for inference operations. More threads can speed up CPU-bound work but may cause contention on busy systems. 0 or empty = auto (typically physical core count).',
  nthreadsBatch: 'Number of CPU threads used during prompt (batch) processing. Can differ from inference threads to optimize throughput during the prefill phase. 0 or empty = same as Threads.',
  cacheTypeK: 'Precision format for the key portion of the KV cache. f16 = full precision (best quality), q8_0 = 8-bit quantized (less VRAM, minimal quality loss), q4_0 = 4-bit (most savings).',
  cacheTypeV: 'Precision format for the value portion of the KV cache. Same options as Cache Type K. Some models benefit from asymmetric K/V quantization.',
  cacheMinTokens: 'Minimum token count required before cache reuse kicks in. Higher values avoid caching very short prompts; lower values maximize reuse but can consume more memory for small requests.',
  useDirectIO: 'Uses direct I/O for model file reads, bypassing the OS page cache. Can reduce double-buffering and cache pressure for large model loads, but may be slower or unsupported on some filesystems.',

  offloadKQV: 'Offloads key/query/value attention operations to GPU. Can improve performance on GPU-backed inference but increases VRAM usage.',
  opOffload: 'Allows selected host-side tensor operations to be offloaded to GPU during prompt processing. Can improve throughput for large or CPU-heavy workloads.',
  projOnCpu: 'Forces the multimodal projector (mmproj) onto the CPU regardless of GPU availability. Use this for audio models hit by llama.cpp Metal kernel regressions; the main LLM still runs on GPU.',
  mainGpu: 'Primary GPU index used in multi-GPU configurations. Relevant when using split mode or explicit device placement. Leave empty on single-GPU systems.',
  devices: 'Explicit list of devices for inference (e.g. CUDA0,CUDA1). Leave empty to let the runtime auto-select.',

  ropeFreqScale: 'RoPE frequency scale multiplier for context extension. Usually left at the model default unless reproducing a known long-context configuration.',
  yarnBetaFast: 'YaRN beta-fast parameter for short-range frequency transition behavior. Advanced tuning option; usually leave unset unless matching a known config.',
  yarnBetaSlow: 'YaRN beta-slow parameter for long-range frequency transition behavior. Advanced tuning option; usually leave unset unless matching a known config.',
  draftGpuLayers: 'Number of draft-model layers offloaded to GPU. More GPU layers speed up speculative decoding but use more VRAM.',
  draftDevice: 'Device for running the draft model. Useful in multi-device setups when you want the draft model placed separately from the main model.',
  grammar: 'Grammar constraint to force output into a specific syntax (e.g. JSON or a custom GBNF grammar). Improves structured output reliability but can over-constrain generation if the grammar is too strict.',
  ngpuLayers: 'Number of model layers offloaded to GPU. 0 = all layers on GPU (default). -1 = all layers on CPU. Positive N = first N layers on GPU. Lower values save VRAM but reduce speed.',
  splitMode: 'How model weights are distributed across multiple GPUs. "layer" assigns whole layers per GPU, "row" splits individual tensor rows. "none" uses a single GPU.',
  swaFull: 'Use full-size KV cache for sliding window attention (SWA) layers instead of the compact n_swa-sized cache. Increases VRAM usage but preserves accuracy for models like Gemma 4. Default: on (llama.cpp default).',
  incrementalCache: 'Keeps the full conversation KV state in a dedicated slot between requests. Enables fast multi-turn follow-ups by only processing new tokens instead of the entire history.',
  ropeScaling: 'Type of RoPE (Rotary Position Embedding) scaling used to extend the model\'s context window beyond its native training length. "yarn" is the most common method.',
  yarnOrigCtx: 'The model\'s original (native) context length before YaRN extension. YaRN uses this as the baseline to calculate scaling factors. "auto" reads it from the model metadata.',
  ropeFreqBase: 'Base frequency for RoPE position embeddings. Higher values stretch the positional encoding, allowing longer contexts. Typically set automatically by YaRN configuration.',
  yarnExtFactor: 'YaRN extension factor controlling how aggressively context is extended. -1 = auto-calculate based on context ratio. Higher values extend more but may reduce quality.',
  yarnAttnFactor: 'YaRN attention scaling factor that adjusts attention weight magnitude at extended positions. Fine-tunes quality at long context lengths.',
  draftModel: 'Speculative decoding draft model — a smaller, faster model that proposes candidate tokens which the main model then verifies. Speeds up generation when acceptance rate is high.',
  draftTokens: 'Number of tokens the draft model proposes per speculative decoding step. More tokens = potentially faster generation, but too many reduces acceptance rate.',
  mtpNDraft: 'Starting number of draft tokens per round for the auto-detected MTP head baked into this model. The adaptive throttle scales this ceiling down to 0 as acceptance drops. Leave empty to use the default of 4.',
  draftMainGpu: 'Primary GPU index for the draft model in multi-GPU setups. Leave empty to use the same GPU as the main model.',
  draftTensorSplit: 'Proportional weight distribution for the draft model across GPUs. Same format as the main model tensor-split.',
  tensorBuftOverrides: 'Manual tensor buffer type overrides for specific layers. Advanced option for fine-grained control of where individual tensors are placed.',
  hasProjection: 'Whether the model includes a multi-modal projection file (mmproj). Required for vision or audio input — the projection maps image/audio embeddings into the model\'s token space.',
  isGPT: 'Whether the model uses a GPT-style (causal, decoder-only) architecture. GPT models generate text left-to-right. Non-GPT models may be encoder-decoder or embedding models.',
  validated: 'Whether the model has been validated against the Kronk catalog. Validated models have confirmed-working configurations, templates, and recommended settings.',

  // ── Pool / resource budget tooltips ──────────────────────────────────────
  budgetPercent: 'Percentage of detected GPU VRAM and system RAM the pool is allowed to commit to loaded models. Reservations beyond this percentage trigger eviction of idle models. Default: 80%.',
  budgetHeadroom: 'Per-GPU safety margin subtracted from each device\'s budget after the percentage is applied. Reserves a small cushion so the resman never hands out memory that would just-barely OOM under driver/compute-buffer overhead. Default: 256 MB.',
  budgetDeviceTotal: 'Total physical memory the device reports. For GPUs this is dedicated VRAM; for the System RAM row this is the host\'s total RAM. On Apple Silicon (unified memory), the GPU shares this same pool.',
  budgetDeviceBudget: 'How many bytes the resource manager will allow loaded models to consume on this device. Equals (Total × BudgetPercent / 100) − Headroom.',
  budgetDeviceUsed: 'Currently reserved bytes on this device, summed across all live model reservations.',
  budgetDevicePctOfBudget: 'Used ÷ Budget. Approaches 100% as more models are kept warm; passing 100% would have triggered eviction first.',
  budgetDeviceFree: 'Bytes still available within the budget on this device. New models that exceed this trigger eviction of an idle model before loading.',
  budgetUsageBar: 'Visual scaled to the device\'s physical Total. Filled segment = currently reserved bytes (green < 60% of Budget, amber 60–85%, red > 85%). The vertical tick marks the Budget cap (Total × BudgetPercent − Headroom). The dim tail to the right of the tick is physical memory the resman intentionally won\'t hand out — it\'s reserved for OS/driver overhead and the configured headroom.',
  budgetReservationKey: 'Cache/reservation key. For catalog loads this is the model ID; playground sessions use modelID/playground/<session-id> so each session is tracked separately.',
  budgetReservationTotal: 'Total bytes the resman has charged for this reservation, summed across VRAM and system memory. This is the number that counts against the overall budget.',
  budgetReservationVRAM: 'Bytes charged against discrete GPU VRAM budgets. Zero on systems with no GPU and on Apple Silicon unified memory (where the GPU shares the system pool).',
  budgetReservationSystem: 'Bytes charged against the system-memory budget. On unified-memory systems (Apple Silicon Metal) the entire model footprint is charged here because the GPU and CPU share one physical pool.',
  budgetReservationPerDevice: 'Per-device byte allocation for this reservation. Populated when the reservation was split across multiple GPUs via tensor-split. Empty (—) when the reservation is on a single device or on system memory.',
  budgetReservationUnified: 'Bytes charged against the unified memory budget. On Apple Silicon the GPU (Metal) and CPU share a single physical pool, so the resman tracks one combined number instead of splitting VRAM from system memory (which would double-count the same bytes).',

  // Running models grid tooltips
  runningModelID: 'Cache key for the loaded model. Catalog models use the model ID directly; playground/custom sessions use modelID/playground/<session-id>.',
  runningModelBackend: 'Which pool owns this entry. "kronk" is the llama.cpp pool (chat / embed / rerank / vision / audio LLMs); "bucky" is the whisper.cpp pool (speech-to-text via /v1/audio/transcriptions). Both pools share the same resource manager.',
  runningModelOwner: 'Publisher namespace from the model file path (e.g. unsloth, google, meta-llama).',
  runningModelFamily: 'Model family / repo name as it appears in the catalog.',
  runningModelSize: 'On-disk size of the GGUF file(s) backing this model.',
  runningModelVRAMTotal: 'Predicted total memory footprint computed by CalculateVRAM: model weights + KV cache + estimated compute buffer. Hardware-agnostic — on Apple unified memory this same number lives in system RAM.',
  runningModelKVCache: 'Memory consumed by the KV cache across all slots at the current context window and slot count.',
  runningModelSlots: 'Number of parallel sequences (NSeqMax) configured for this model. Each slot can host one in-flight request.',
  runningModelExpiresAt: 'Wall-clock time at which the pool will idle-evict this model unless it is touched again.',
  runningModelActiveStreams: 'Number of in-flight chat/embed/rerank streams currently using this model. The pool refuses to evict a model with active streams.',
  runningModelStatus: 'Lifecycle stage in the pool. "loaded" means the GGUF is open in llama.cpp and the model can serve requests. "loading" means the resource manager has reserved memory for the load but the GGUF is still being SHA-verified and read from disk; the model is not servable yet but the budget already accounts for it.',

  // Library bundles
  bundleArch: 'Target CPU architecture for this library bundle download (amd64 or arm64). Each bundle lives in its own folder under the libraries root and does not replace the active install.',
  bundleOS: 'Target operating system for this library bundle download (linux, bookworm, trixie, darwin, windows). Each bundle lives in its own folder under the libraries root and does not replace the active install.',
  bundleProcessor: 'Target processor backend for this library bundle download (cpu, cuda, metal, rocm, vulkan). Only combinations published by the upstream llama.cpp build matrix can be selected.',
  bundleRemove: 'Delete this bundle directory. Does not affect the active install unless this bundle is the active one.',
  peerLibsHost: 'Address of another Kronk server on the local network in the form ip:port. The peer must be running with download enabled. Useful in workshop environments where Internet access is slow or unavailable.',
  peerLibsConnect: 'Query the peer Kronk server for the list of library bundles it has installed and is willing to share.',
  peerLibsDownload: 'Download this library bundle from the peer over the local network. The peer builds a zip on demand on first request, sends it with a sha256 digest for integrity verification, and the zip is unpacked into the matching bundle directory on this server.',
  peerKMSHost: 'Address of another Kronk server on the local network in the form ip:port. Connect to list the models that peer has downloaded and pull any of them into this server. The peer must be running with the download endpoint enabled.',

  // Translator
  translatorModel: 'Installed whisper.cpp model to use for this run. Multilingual models (e.g. ggml-large-v3) can transcribe many languages and translate any of them to English. English-only models (filenames containing ".en") only accept English audio and cannot translate.',
  translatorSourceLanguage: 'Hint for the language spoken in the audio. Leave on "Auto-detect" to let whisper identify the language from the first ~30 seconds. Set explicitly for short clips, heavy accents, or when auto-detect picks the wrong language.',
  translatorTranslate: 'When on, whisper translates the source audio to English instead of transcribing in the source language. Whisper can only translate to English — no other target languages are supported by the model. Disabled for English-only models.',
  translatorPrompt: 'Optional text passed to whisper as decoder context. Useful to bias spelling of proper nouns, technical terms, or to provide style/punctuation hints. Keep it short (a sentence or two).',
  translatorRealtimeFactor: 'Audio duration divided by wall-clock time. A value of 5x means the run processed 5 seconds of audio per second of real time. Higher is faster.',
  translatorNoSpeechProb: 'Whisper\'s estimated probability that this segment contains no speech. Values close to 1 typically indicate silence or background noise.',
} as const satisfies Record<string, string>;

export type TooltipKey = keyof typeof PARAM_TOOLTIPS;

type ParamTooltipProps =
  | { tooltipKey: TooltipKey; text?: never }
  | { text: string; tooltipKey?: never };

export function ParamTooltip(props: ParamTooltipProps) {
  const text = props.tooltipKey ? PARAM_TOOLTIPS[props.tooltipKey] : props.text!;
  const wrapperRef = useRef<HTMLSpanElement>(null);
  const tipRef = useRef<HTMLSpanElement>(null);

  const reposition = useCallback(() => {
    const wrapper = wrapperRef.current;
    const tip = tipRef.current;
    if (!wrapper || !tip) return;
    const iconRect = wrapper.getBoundingClientRect();
    const tipWidth = tip.offsetWidth;
    const tipHeight = tip.offsetHeight;

    // Position above the icon using viewport coordinates (fixed positioning).
    const top = iconRect.top - tipHeight - 8;

    // Align left edge of tooltip with the icon, then clamp to viewport.
    let left = iconRect.left;
    const rightOverflow = left + tipWidth - window.innerWidth + 8;
    if (rightOverflow > 0) {
      left -= rightOverflow;
    }
    if (left < 8) {
      left = 8;
    }
    tip.style.left = `${left}px`;
    tip.style.top = `${top}px`;

    // Position the arrow to point at the icon.
    const arrowLeft = Math.max(10, Math.min(tipWidth - 10, iconRect.left - left + iconRect.width / 2));
    tip.style.setProperty('--arrow-left', `${arrowLeft}px`);
  }, []);

  return (
    <span className="param-tooltip-wrapper" ref={wrapperRef} onMouseEnter={reposition}>
      <span className="param-tooltip-icon">ⓘ</span>
      <span className="param-tooltip-text" ref={tipRef}>{text}</span>
    </span>
  );
}

export function labelWithTip(label: string, tooltipKey: TooltipKey): ReactNode {
  return <>{label} <ParamTooltip tooltipKey={tooltipKey} /></>;
}

type FieldLabelProps = React.LabelHTMLAttributes<HTMLLabelElement> & {
  children: ReactNode;
  tooltipKey?: TooltipKey;
  after?: ReactNode;
};

export function FieldLabel({ children, tooltipKey, after, ...props }: FieldLabelProps) {
  return (
    <label {...props}>
      {children}
      {tooltipKey && <ParamTooltip tooltipKey={tooltipKey} />}
      {after}
    </label>
  );
}
