"use client"

// Leaderboard for en frittstående runde (ADR-033).
//
// FULLSTENDIG OMBYGGET 2026-08-03 etter V0-eksport "zip 28": brukeren ba
// EKSPLISITT om V0 sin visuelle tolkning i FULL BREDDE, ikke bare de
// formatene som var reelt ødelagte/manglet ("Jeg vil ABSOLUTT at du
// implementerer V0 sin tolkning... Jeg vil ikke at du skal gjøre
// personlige endringer eller holde tilbake på noe i det hele tatt").
// Forrige runde samme dag portet kun High-low-high/BBB-Københavner/
// Shamble-Money-Ball (reelle bugs) og lot resten av det gamle,
// HÅNDKODEDE designet (2026-07-26, bygget FØR V0 da brukeren gikk tom
// for credits) stå urørt -- brukeren testet en vanlig slagspill-runde
// (det vanligste tilfellet) og så null visuell endring, med rette.
//
// Datalaget (typer, useLeaderboardData/useFormatResult/
// useFlightTeamStandings, rankEntries brukt av RoundLeaderboardMini) er
// UENDRET fra forrige runde -- kun det visuelle laget er nå V0 sin egen
// kode, portet mot ekte data i stedet for V0 sine mock-arrays (samme
// "aldri blind-kopier en fil"-disiplin som alltid, bare nå anvendt på
// ALLE komponentene eksporten rørte, ikke et utvalg).

import { useEffect, useState } from "react"
import Link from "next/link"
import { ArrowLeft, ChevronDown, ChevronRight, Crown, Flag as FlagIcon, MessageSquare, Trophy } from "lucide-react"
import { cn } from "@/lib/utils"
import { ScrambleSoloResultView, type ScrambleSoloResult } from "@/components/scramble-solo-result"
import { MatchScorecardGrid } from "@/components/round-scorecard"

// --- API-typer ---------------------------------------------------------------

type ApiLeaderboardHole = {
  hole_number: number
  par: number
  stroke_index: number
  played: boolean
  score: number | null
  picked_up: boolean
  strokes_received: number | null
}

type ApiLeaderboardEntry = {
  participant_id: string
  display_name: string
  is_owner: boolean
  holes_played: number
  total_score: number | null
  score_to_par: number | null
  net_score_to_par: number | null
  total_points: number | null
  holes: ApiLeaderboardHole[]
}

type ApiLeaderboard = {
  holes_planned: number
  completed: boolean
  entries: ApiLeaderboardEntry[]
}

type ApiRoundParticipant = {
  id: string
  user_id: string | null
  round_side_id: string | null
  handicap_index_snapshot: number | null
  // Felt under er allerede returnert av samme `${apiPrefix}/{roundId}`-
  // endepunkt (samme JSON som round-scorecard.tsx sin `ApiParticipant`
  // fetcher, se der) -- bare ikke tidligere typet her, siden de ikke
  // trengtes før MatchScorecardGrid-gjenbruken (ny utvid-seksjon i
  // TwoSidedBoard/HighLowBoard). `guest_name` er valgfritt -- fraværende i
  // publicMode (PublicRoundParticipantOut har ikke feltet), defaultes til
  // null der (se toGridRound under) -- ubrukt av MatchScorecardGrid uansett
  // (den leser kun display_name).
  guest_name?: string | null
  display_name: string
  is_owner: boolean
  playing_handicap: number | null
}

type ApiSide = {
  id: string
  label: string | null
  // KUN satt for scramble_solo (migrasjon 056) -- null for alle andre
  // formater, som ikke skiller sidene fra hverandre strukturelt.
  side_role: "team" | "individual" | null
}

type ApiRoundHeader = {
  id: string
  name: string | null
  course_name_snapshot: string
  // Samme "allerede i JSON-en, bare ikke tidligere typet"-begrunnelse som
  // ApiRoundParticipant sine nye felt over.
  tee_name_snapshot: string
  played_at: string
  start_hole: number
  holes_planned: number
  play_format: string
  sides: ApiSide[]
  participants: ApiRoundParticipant[]
}

// To-sidede formater har ingen individuell "rangering" -- to lag/spillere i
// en match har en STATUS (hvem leder, hull for hull), ikke en topp-3-liste.
// Delt-ball-formatene (foursome/greensome/scramble) har i tillegg ALDRI
// noen individuell round_hole-rad å rangere fra (ADR-039 Beslutning C: én
// score PER SIDE) -- den vanlige brutto/netto/poeng-rangeringen under ville
// derfor vist en tom/meningsløs liste for dem uansett.
const TWO_SIDED_FORMATS = [
  "match", "fourball", "foursome", "greensome", "scramble_2", "scramble_4", "chapman",
  // scramble_solo_match (migrasjon 057) -- ekte hull-for-hull matchspill
  // (compute_match_state på backend-siden), samme generiske match_lead/
  // match_holes_played/-remaining/is_closed/is_dormie-felter som resten
  // av denne listen. Asymmetrisk lag-mot-individuell-struktur (IKKE
  // fast antall spillere per side), men TwoSidedBoard/buildSideTeams
  // grener allerede på FAKTISK antall spillere per side, ikke et fast
  // tall -- bekreftet trygt å gjenbruke uendret. scramble_solo (stroke-
  // varianten, migrasjon 056) er BEVISST IKKE her, se SCRAMBLE_SOLO_
  // FORMATS under.
  "scramble_solo_match",
]
// High-low-high er et SÆRTILFELLE av de to-sidede formatene -- IKKE et
// hull-ledelse-konsept ("X UP"), se HighLowBoard.
const HIGH_LOW_FORMATS = new Set(["high_low_high"])
// Shamble/Money Ball -- HELE runden er ETT lag, ikke to sider -- lagets
// EGEN samlede hullscore + flight-gruppe-sammenligning, se TeamFlightBoard.
const TEAM_RESULT_FORMATS = new Set(["shamble", "money_ball"])
// Scramble mot enkeltspiller (migrasjon 056) -- BEVISST IKKE i
// TWO_SIDED_FORMATS: dette er netto SLAGSPILL-sammenligning
// (net_stroke_play_margin), ikke matchspill/hull-ledelse -- egen enkel
// visning, se ScrambleSoloResultView.
const SCRAMBLE_SOLO_FORMATS = new Set(["scramble_solo"])
// Bingo Bango Bongo/Københavner har egne poengsystem, uavhengig av rå
// slagtall -- `ApiLeaderboardEntry.total_points` er ALLTID generisk
// Stableford (backend regner den likt uansett format), FEIL for disse to.
const FORMAT_OWN_POINTS_FIELD: Partial<Record<string, "bbb_points" | "copenhagen_points">> = {
  bbb: "bbb_points",
  copenhagen: "copenhagen_points",
}
// Egen lokal kopi per prosjektets konvensjon (liten duplisering fremfor
// cross-file-import, se samme mønster i round-detail.tsx/round-scorecard.tsx).
const FORMAT_LABELS: Record<string, string> = {
  stroke: "Slagspill",
  stableford: "Stableford",
  match: "Match",
  skins: "Skins",
  fourball: "Fourball",
  foursome: "Foursome",
  greensome: "Greensome",
  scramble_2: "Scramble (2)",
  scramble_4: "Scramble (4)",
  chapman: "Chapman",
  copenhagen: "Københavner",
  bbb: "Bingo Bango Bongo",
  flag: "Flaggturnering",
  shamble: "Shamble",
  money_ball: "Money Ball",
  high_low_high: "High-low-high",
  scramble_solo: "Scramble mot enkeltspiller",
  scramble_solo_match: "Scramble mot enkeltspiller (matchspill)",
}

const dateFormatter = new Intl.DateTimeFormat("no-NO", { day: "numeric", month: "long", year: "numeric" })

// --- Rangering (brukt av RoundLeaderboardMini) ------------------------------

type Mode = "gross" | "net" | "points"

type RankedEntry = ApiLeaderboardEntry & {
  rank: number | null
  tied: boolean
  isSelf: boolean
}

function valueFor(entry: ApiLeaderboardEntry, mode: Mode): number | null {
  if (mode === "gross") return entry.score_to_par
  if (mode === "net") return entry.net_score_to_par
  return entry.total_points
}

function rankEntries(entries: ApiLeaderboardEntry[], mode: Mode, viewerParticipantId: string | null): RankedEntry[] {
  const withValue = entries.filter((e) => valueFor(e, mode) !== null)
  const withoutValue = entries.filter((e) => valueFor(e, mode) === null)
  const direction = mode === "points" ? -1 : 1
  withValue.sort((a, b) => direction * ((valueFor(a, mode) as number) - (valueFor(b, mode) as number)))

  const ranked: RankedEntry[] = []
  let rank = 0
  let prevValue: number | null = null
  for (let i = 0; i < withValue.length; i++) {
    const e = withValue[i]
    const v = valueFor(e, mode) as number
    if (v !== prevValue) {
      rank = i + 1
      prevValue = v
    }
    const tied = withValue.filter((o) => valueFor(o, mode) === v).length > 1
    ranked.push({ ...e, rank, tied, isSelf: e.participant_id === viewerParticipantId })
  }
  for (const e of withoutValue) {
    ranked.push({ ...e, rank: null, tied: false, isSelf: e.participant_id === viewerParticipantId })
  }
  return ranked
}

function formatToPar(value: number): string {
  if (value === 0) return "E"
  return value > 0 ? `+${value}` : `−${Math.abs(value)}`
}

// ===========================================================================
// V0 sitt visuelle vokabular (zip 28, 2026-08-03) -- portet verbatim, kun
// HOLES/TOTAL_HOLES er ikke lenger en hardkodet modul-konstant (V0 sin
// egen mock-bane) men en `holes: Hole[]`/`totalHoles: number`-parameter
// bygget fra RUNDENS EGNE par/indeks (se buildHoles under) -- ellers
// samme kode, samme klassenavn, samme struktur.
// ===========================================================================

type Hole = { holeNumber: number; par: number; index: number }

const SIDE = {
  a: { fill: "bg-primary", text: "text-primary-foreground", ink: "text-primary" },
  b: { fill: "bg-brand-orange", text: "text-brand-orange-foreground", ink: "text-brand-orange" },
} as const

type ScoreClass = "eagle" | "birdie" | "par" | "bogey" | "double"

function classify(gross: number, par: number): ScoreClass {
  const d = gross - par
  if (d <= -2) return "eagle"
  if (d === -1) return "birdie"
  if (d === 0) return "par"
  if (d === 1) return "bogey"
  return "double"
}

function scoreClassLabel(gross: number, par: number): string {
  const d = gross - par
  if (d <= -1) return "under par"
  if (d === 0) return "par"
  return "over par"
}

function ScoreMark({ gross, par, size = "md" }: { gross: number; par: number; size?: "sm" | "md" }) {
  const kind = classify(gross, par)
  const dim = size === "sm" ? "size-8 text-sm" : "size-9 text-base"
  const shared = cn("inline-flex items-center justify-center font-extrabold tabular-nums", dim)
  let cls = ""
  if (kind === "eagle") cls = "rounded-full bg-primary text-primary-foreground"
  else if (kind === "birdie") cls = "rounded-full border-2 border-primary bg-primary/10 text-primary"
  else if (kind === "bogey") cls = "rounded-[6px] border-2 border-brand-orange bg-brand-orange/10 text-brand-orange"
  else if (kind === "double") cls = "rounded-[6px] bg-brand-orange text-brand-orange-foreground"
  else cls = "text-foreground"
  return (
    <span className={cn(shared, cls)}>
      {gross}
      <span className="sr-only"> ({scoreClassLabel(gross, par)})</span>
    </span>
  )
}

function toParLabel(diff: number): string {
  if (diff === 0) return "E"
  return diff > 0 ? `+${diff}` : `${diff}`
}

function toParWords(diff: number): string {
  if (diff === 0) return "likt med par"
  return diff > 0 ? `${diff} over par` : `${Math.abs(diff)} under par`
}

const NF1 = new Intl.NumberFormat("no-NO", { maximumFractionDigits: 1 })

function Badge({ children, tone = "neutral" }: { children: React.ReactNode; tone?: "neutral" | "self" | "owner" }) {
  const map = {
    neutral: "bg-muted text-muted-foreground",
    self: "bg-primary/15 text-primary",
    owner: "bg-gold/25 text-gold-foreground",
  } as const
  return (
    <span className={cn("inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-bold uppercase tracking-wide", map[tone])}>
      {children}
    </span>
  )
}

function MetricPill({
  kind,
  value,
  unit,
  size = "md",
}: {
  kind: "topar" | "points"
  value: number
  unit?: string
  size?: "md" | "lg"
}) {
  const dims = size === "lg" ? "min-h-12 min-w-12 px-3 text-xl" : "min-h-11 min-w-11 px-2.5 text-lg"
  const font = "font-extrabold tabular-nums"
  if (kind === "points") {
    return (
      <span className={cn("inline-flex items-center justify-center gap-0.5 rounded-full bg-primary text-primary-foreground", dims, font)}>
        {value}
        {unit ? <span className="text-xs font-bold">{unit}</span> : null}
      </span>
    )
  }
  const label = toParLabel(value)
  if (value === 0) {
    return (
      <span className={cn("inline-flex items-center justify-center text-foreground", dims, font)}>
        {label}
        <span className="sr-only"> {toParWords(value)}</span>
      </span>
    )
  }
  const under = value < 0
  return (
    <span
      className={cn(
        "inline-flex items-center justify-center",
        dims,
        font,
        under ? "rounded-full bg-primary/15 text-primary" : "rounded-[8px] bg-brand-orange/15 text-brand-orange",
      )}
    >
      {label}
      <span className="sr-only"> {toParWords(value)}</span>
    </span>
  )
}

// ===========================================================================
// 1. INDIVIDUAL leaderboard (stroke / stableford / skins / flag / BBB / KBH)
// ===========================================================================

type MetricKey = "brutto" | "netto" | "stableford" | "points"

type IndividualPlayer = {
  id: string
  name: string
  isSelf?: boolean
  isOwner?: boolean
  // Ekte per-hull mottatte slag (allerede korrekt utregnet av backend,
  // samme allokering som resten av appen) -- IKKE en rå course handicap +
  // en egen klientsidig strokes_received-utregning, som V0 sin mock-
  // versjon gjorde uten et ekte backend å hente fra. `null` for et hull
  // betyr "ingen HCP spores for denne spilleren", ikke "0 slag".
  strokesReceived: (number | null)[]
  hcpIndex: number | null
  finished?: boolean
  gross: (number | null)[]
  holePoints?: (number | null)[]
}

function hasHcp(p: IndividualPlayer): boolean {
  return p.strokesReceived.some((v) => v !== null)
}

const METRIC_LABEL: Record<MetricKey, string> = {
  brutto: "Brutto",
  netto: "Netto",
  stableford: "Stableford",
  points: "Poeng",
}

function playedHoles(p: IndividualPlayer): number {
  return p.gross.filter((g) => g !== null).length
}

function grossToPar(p: IndividualPlayer, holes: Hole[]): number {
  let diff = 0
  p.gross.forEach((g, i) => {
    if (g !== null) diff += g - holes[i].par
  })
  return diff
}

function nettoToPar(p: IndividualPlayer, holes: Hole[]): number | null {
  if (!hasHcp(p)) return null
  let diff = 0
  p.gross.forEach((g, i) => {
    const received = p.strokesReceived[i]
    if (g !== null && received !== null) diff += g - received - holes[i].par
  })
  return diff
}

function stablefordTotal(p: IndividualPlayer, holes: Hole[]): number | null {
  if (!hasHcp(p)) return null
  let pts = 0
  p.gross.forEach((g, i) => {
    const received = p.strokesReceived[i]
    if (g !== null && received !== null) pts += Math.max(0, 2 + (holes[i].par - (g - received)))
  })
  return pts
}

function pointsTotal(p: IndividualPlayer): number {
  return (p.holePoints ?? []).reduce<number>((s, v) => s + (v ?? 0), 0)
}

function evalMetric(
  p: IndividualPlayer,
  metric: MetricKey,
  pointsUnit: string,
  holes: Hole[],
): {
  rankable: boolean
  sortVal: number
  higherWins: boolean
  reason?: string
  pill: React.ReactNode
} {
  if (metric === "brutto") {
    const v = grossToPar(p, holes)
    return { rankable: true, sortVal: v, higherWins: false, pill: <MetricPill kind="topar" value={v} size="lg" /> }
  }
  if (metric === "netto") {
    const v = nettoToPar(p, holes)
    if (v === null) return { rankable: false, sortVal: 0, higherWins: false, reason: "Ingen HCP registrert", pill: <NoData /> }
    return { rankable: true, sortVal: v, higherWins: false, pill: <MetricPill kind="topar" value={v} size="lg" /> }
  }
  if (metric === "stableford") {
    const v = stablefordTotal(p, holes)
    if (v === null) return { rankable: false, sortVal: 0, higherWins: false, reason: "Ingen HCP registrert", pill: <NoData /> }
    return { rankable: true, sortVal: v, higherWins: true, pill: <MetricPill kind="points" value={v} unit="p" size="lg" /> }
  }
  const v = pointsTotal(p)
  return { rankable: true, sortVal: v, higherWins: true, pill: <MetricPill kind="points" value={v} unit={pointsUnit} size="lg" /> }
}

function NoData() {
  return (
    <span className="inline-flex min-h-11 items-center justify-center rounded-full border border-dashed border-border px-3 text-sm font-semibold text-muted-foreground">
      –<span className="sr-only">ingen rangering</span>
    </span>
  )
}

function IndividualBoard({
  players,
  metrics,
  defaultMetric,
  pointsUnit = "p",
  holes,
  commentsHref,
}: {
  players: IndividualPlayer[]
  metrics: MetricKey[]
  defaultMetric: MetricKey
  pointsUnit?: string
  holes: Hole[]
  commentsHref: string
}) {
  const [metric, setMetric] = useState<MetricKey>(defaultMetric)
  const [openId, setOpenId] = useState<string | null>(null)

  const evaluated = players.map((p) => ({ player: p, ...evalMetric(p, metric, pointsUnit, holes) }))
  const rankable = evaluated.filter((e) => e.rankable)
  const unranked = evaluated.filter((e) => !e.rankable)
  rankable.sort((a, b) => (a.higherWins ? b.sortVal - a.sortVal : a.sortVal - b.sortVal))

  const rankInfo: { rank: number; tied: boolean }[] = []
  const counts: Record<number, number> = {}
  rankable.forEach((e, i) => {
    const rank = i > 0 && rankable[i - 1].sortVal === e.sortVal ? rankInfo[i - 1].rank : i + 1
    rankInfo.push({ rank, tied: false })
    counts[rank] = (counts[rank] ?? 0) + 1
  })
  rankInfo.forEach((x) => (x.tied = counts[x.rank] > 1))

  return (
    <div className="flex flex-col gap-3">
      <MetricToggle metrics={metrics} value={metric} onChange={setMetric} pointsUnit={pointsUnit} />

      <ol className="flex flex-col overflow-hidden rounded-2xl border border-border bg-card">
        {rankable.map((e, i) => (
          <IndividualRow
            key={e.player.id}
            player={e.player}
            rank={rankInfo[i].rank}
            tied={rankInfo[i].tied}
            isLeader={i === 0}
            pill={e.pill}
            metric={metric}
            holes={holes}
            open={openId === e.player.id}
            onToggle={() => setOpenId((cur) => (cur === e.player.id ? null : e.player.id))}
            commentsHref={commentsHref}
          />
        ))}
        {unranked.map((e) => (
          <IndividualRow
            key={e.player.id}
            player={e.player}
            rank={null}
            tied={false}
            isLeader={false}
            pill={e.pill}
            reason={e.reason}
            metric={metric}
            holes={holes}
            open={openId === e.player.id}
            onToggle={() => setOpenId((cur) => (cur === e.player.id ? null : e.player.id))}
            commentsHref={commentsHref}
          />
        ))}
      </ol>

      <Legend />
    </div>
  )
}

function MetricToggle({
  metrics,
  value,
  onChange,
  pointsUnit,
}: {
  metrics: MetricKey[]
  value: MetricKey
  onChange: (m: MetricKey) => void
  pointsUnit: string
}) {
  if (metrics.length < 2) return null
  return (
    <div role="group" aria-label="Velg visning" className="flex gap-1 rounded-full border border-border bg-muted p-1">
      {metrics.map((m) => {
        const active = m === value
        const label = m === "points" && pointsUnit !== "p" ? `${METRIC_LABEL[m]}` : METRIC_LABEL[m]
        return (
          <button
            key={m}
            type="button"
            aria-pressed={active}
            onClick={() => onChange(m)}
            className={cn(
              "min-h-11 flex-1 rounded-full px-3 text-sm font-bold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
              active ? "bg-primary text-primary-foreground shadow-sm" : "text-muted-foreground hover:text-foreground",
            )}
          >
            {label}
          </button>
        )
      })}
    </div>
  )
}

function IndividualRow({
  player,
  rank,
  tied,
  isLeader,
  pill,
  reason,
  metric,
  holes,
  open,
  onToggle,
  commentsHref,
}: {
  player: IndividualPlayer
  rank: number | null
  tied: boolean
  isLeader: boolean
  pill: React.ReactNode
  reason?: string
  metric: MetricKey
  holes: Hole[]
  open: boolean
  onToggle: () => void
  commentsHref: string
}) {
  const played = playedHoles(player)
  const totalHoles = holes.length
  const progress = player.finished ? "Ferdig" : `${played}/${totalHoles} hull`
  const panelId = `hbh-${player.id}`
  return (
    <li className={cn("border-b border-border last:border-b-0", isLeader && "bg-primary/5")}>
      <button
        type="button"
        onClick={onToggle}
        aria-expanded={open}
        aria-controls={panelId}
        className="flex min-h-16 w-full items-center gap-3 px-3 py-2.5 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
      >
        <span className="flex w-9 shrink-0 justify-center">
          {rank === null ? (
            <span className="text-sm font-bold text-muted-foreground" aria-hidden="true">–</span>
          ) : (
            <span className={cn("text-base font-extrabold tabular-nums", isLeader ? "text-primary" : "text-foreground")}>
              {tied ? "T" : ""}
              {rank}
            </span>
          )}
          <span className="sr-only">{rank === null ? "Uten rangering" : `${tied ? "Delt " : ""}${rank}. plass`}</span>
        </span>

        <span className="flex min-w-0 flex-1 flex-col gap-0.5">
          <span className="flex items-center gap-2">
            {isLeader && <Crown aria-hidden="true" className="size-4 shrink-0 text-gold-foreground" />}
            <span className="truncate text-base font-bold text-foreground">{player.name}</span>
            {player.isSelf && <Badge tone="self">Deg</Badge>}
            {player.isOwner && <Badge tone="owner">Eier</Badge>}
          </span>
          <span className="truncate text-sm text-muted-foreground">
            {player.hcpIndex !== null ? `Hcp ${NF1.format(player.hcpIndex)} · ` : "Uten HCP · "}
            {reason ?? progress}
          </span>
        </span>

        <span className="shrink-0">{pill}</span>
        <ChevronDown aria-hidden="true" className={cn("size-5 shrink-0 text-muted-foreground transition-transform", open && "rotate-180")} />
      </button>

      {open && (
        <div id={panelId} className="px-3 pb-3">
          <HoleByHole player={player} metric={metric} holes={holes} rank={rank} tied={tied} commentsHref={commentsHref} />
        </div>
      )}
    </li>
  )
}

function HoleByHole({
  player,
  metric,
  holes,
  rank,
  tied,
  commentsHref,
}: {
  player: IndividualPlayer
  metric: MetricKey
  holes: Hole[]
  rank: number | null
  tied: boolean
  commentsHref: string
}) {
  const front = holes.slice(0, 9)
  const back = holes.slice(9)
  const totalPar = holes.reduce((s, h) => s + h.par, 0)
  const totalGross = player.gross.reduce<number>((s, g) => s + (g ?? 0), 0)
  const holesWithScore = player.gross.filter((g) => g !== null).length
  return (
    <div className="flex flex-col gap-3 rounded-xl border border-border bg-background p-3">
      <p className="text-xs font-semibold text-muted-foreground">
        Hull-for-hull · brutto (form) og netto (tall). Sirkel = under par, firkant = over par.
      </p>
      {[front, back].filter((set) => set.length > 0).map((set, si) => {
        const offset = si === 0 ? 0 : front.length
        return (
          <div key={si} className="relative min-w-0 overflow-x-auto">
            <table className="w-full min-w-[34rem] border-collapse text-center">
              <thead>
                <tr className="text-[11px] font-bold uppercase tracking-wide text-muted-foreground">
                  <th scope="row" className="sticky left-0 bg-background pr-2 text-left font-bold">Hull</th>
                  {set.map((h) => (
                    <th key={h.holeNumber} scope="col" className="px-1 font-extrabold text-foreground">{h.holeNumber}</th>
                  ))}
                  <th scope="col" className="px-1">{si === 0 ? "Ut" : "Inn"}</th>
                </tr>
                <tr className="text-[11px] text-muted-foreground">
                  <th scope="row" className="sticky left-0 bg-background pr-2 text-left font-semibold">Par · idx</th>
                  {set.map((h) => (
                    <td key={h.holeNumber} className="px-1">{h.par}·{h.index}</td>
                  ))}
                  <td />
                </tr>
              </thead>
              <tbody>
                <tr>
                  <th scope="row" className="sticky left-0 bg-background pr-2 text-left text-xs font-bold text-muted-foreground">Brutto</th>
                  {set.map((h, i) => {
                    const g = player.gross[offset + i]
                    return (
                      <td key={h.holeNumber} className="px-1 py-1">
                        {g === null ? <span className="text-muted-foreground">–</span> : <ScoreMark gross={g} par={h.par} size="sm" />}
                      </td>
                    )
                  })}
                  <td className="px-1 font-extrabold tabular-nums text-foreground">
                    {set.reduce((s, h, i) => s + (player.gross[offset + i] ?? 0), 0) || "–"}
                  </td>
                </tr>
                {hasHcp(player) && (
                  <tr className="text-sm">
                    <th scope="row" className="sticky left-0 bg-background pr-2 text-left text-xs font-bold text-muted-foreground">Netto</th>
                    {set.map((h, i) => {
                      const g = player.gross[offset + i]
                      const received = player.strokesReceived[offset + i]
                      if (g === null || received === null) return <td key={h.holeNumber} className="px-1 text-muted-foreground">–</td>
                      return <td key={h.holeNumber} className="px-1 font-semibold tabular-nums text-foreground">{g - received}</td>
                    })}
                    <td />
                  </tr>
                )}
                {metric === "points" && player.holePoints && (
                  <tr className="text-sm">
                    <th scope="row" className="sticky left-0 bg-background pr-2 text-left text-xs font-bold text-muted-foreground">Poeng</th>
                    {set.map((h, i) => {
                      const pt = player.holePoints?.[offset + i]
                      return <td key={h.holeNumber} className="px-1 font-bold tabular-nums text-primary">{pt ?? "–"}</td>
                    })}
                    <td className="px-1 font-extrabold tabular-nums text-primary">
                      {set.reduce((s, h, i) => s + (player.holePoints?.[offset + i] ?? 0), 0)}
                    </td>
                  </tr>
                )}
              </tbody>
            </table>
          </div>
        )
      })}

      <div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-1 border-t border-border pt-2">
        <span className="text-xs font-semibold tabular-nums text-muted-foreground">
          Par {totalPar} · Score {holesWithScore > 0 ? totalGross : "–"}
          {rank !== null ? ` · ${tied ? "Delt " : ""}${rank}. plass` : ""}
        </span>
        <Link
          href={commentsHref}
          className="inline-flex h-11 items-center gap-1.5 rounded-lg px-1 text-sm font-bold text-primary hover:underline active:scale-[0.98]"
        >
          <MessageSquare aria-hidden="true" className="size-4" />
          Kommentarer
        </Link>
      </div>
    </div>
  )
}

function Legend() {
  return (
    <div className="flex flex-wrap items-center gap-x-4 gap-y-2 px-1 text-xs text-muted-foreground">
      <span className="flex items-center gap-1.5">
        <span className="inline-flex size-5 items-center justify-center rounded-full border-2 border-primary bg-primary/10 text-[10px] font-bold text-primary">3</span>
        Under par (sirkel)
      </span>
      <span className="flex items-center gap-1.5">
        <span className="inline-flex size-5 items-center justify-center rounded-[4px] border-2 border-brand-orange bg-brand-orange/10 text-[10px] font-bold text-brand-orange">5</span>
        Over par (firkant)
      </span>
      <span className="flex items-center gap-1.5">
        <Crown aria-hidden="true" className="size-4 text-gold-foreground" /> Leder
      </span>
    </div>
  )
}

// ===========================================================================
// 2. TWO-SIDED match status (match / fourball / foursome / greensome / …)
// ===========================================================================

type SideTeamInfo = { id: "a" | "b"; name: string; players: string[] }
type HoleWinner = "a" | "b" | "halved" | null

// Statusteksten/lederen/dormie/avgjort kommer FERDIG REGNET fra backend
// (samme tall som allerede vises andre steder i appen, f.eks.
// round-detail.tsx sin FormatResultPanel) -- V0 sin egen `matchStatus()`
// regnet dette på nytt fra `results` klientsidig; her brukes heller de
// allerede korrekte/testede server-tallene, med SAMME visuelle "tug of
// war"-presentasjon V0 tegnet. Unngår to divergerende utregninger av
// samme ting.
function TwoSidedBoard({
  sideA,
  sideB,
  results,
  totalHoles,
  formatName,
  headline,
  sub,
  leader,
  isDormie,
  isClosed,
  round,
  roundId,
  viewerId,
  refreshKey,
  apiPrefix,
  commentsHref,
}: {
  sideA: SideTeamInfo
  sideB: SideTeamInfo
  results: HoleWinner[]
  totalHoles: number
  formatName: string
  headline: string
  sub: string
  leader: "a" | "b" | null
  isDormie: boolean
  isClosed: boolean
  round: ApiRoundHeader
  roundId: string
  viewerId: string | null
  refreshKey: number
  apiPrefix: string
  commentsHref: string
}) {
  const [showDetail, setShowDetail] = useState(false)
  const holesPlayed = results.filter((r) => r !== null).length
  let aWins = 0
  let bWins = 0
  results.forEach((r) => {
    if (r === "a") aWins++
    else if (r === "b") bWins++
  })
  const diff = Math.abs(aWins - bWins)
  const maxUp = Math.max(3, diff)
  const offsetPct = (diff / maxUp) * 50
  const markerLeft = leader === "a" ? 50 - offsetPct : leader === "b" ? 50 + offsetPct : 50

  return (
    <div className="flex flex-col gap-4">
      <div className="flex flex-col items-center gap-3 rounded-2xl border border-border bg-card p-5">
        <span className="flex items-center gap-2 text-xs font-bold uppercase tracking-wide text-muted-foreground">
          {formatName}
          {isClosed && <Badge tone="neutral">Avgjort</Badge>}
          {isDormie && !isClosed && <Badge tone="neutral">Dormie</Badge>}
        </span>
        <div className="flex w-full items-center justify-between gap-3">
          <SideName side="a" team={sideA} leading={leader === "a"} />
          <div className="flex shrink-0 flex-col items-center">
            <span className="text-2xl font-extrabold text-foreground">{headline}</span>
            <span className="text-xs font-semibold text-muted-foreground">{sub}</span>
          </div>
          <SideName side="b" team={sideB} leading={leader === "b"} align="right" />
        </div>

        <div className="relative h-4 w-full overflow-hidden rounded-full bg-muted" aria-hidden="true">
          <div className="absolute inset-y-0 left-0 w-1/2" style={{ background: "color-mix(in oklch, var(--color-primary) 22%, transparent)" }} />
          <div className="absolute inset-y-0 right-0 w-1/2" style={{ background: "color-mix(in oklch, var(--color-brand-orange) 22%, transparent)" }} />
          <span className="absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-border" />
          {leader && (
            <span
              className={cn("absolute inset-y-0 rounded-full", leader === "a" ? "bg-primary" : "bg-brand-orange")}
              style={leader === "a" ? { left: `${markerLeft}%`, right: "50%" } : { left: "50%", right: `${100 - markerLeft}%` }}
            />
          )}
          <span className="absolute left-1/2 top-1/2 size-3 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-background bg-foreground" />
        </div>
      </div>

      <HoleWinners sideA={sideA} sideB={sideB} results={results} totalHoles={totalHoles} />

      <MatchDetailSection
        showDetail={showDetail}
        onToggle={() => setShowDetail((v) => !v)}
        round={round}
        roundId={roundId}
        viewerId={viewerId}
        refreshKey={refreshKey}
        apiPrefix={apiPrefix}
        commentsHref={commentsHref}
      />
    </div>
  )
}

function SideName({ side, team, leading, align = "left" }: { side: "a" | "b"; team: SideTeamInfo; leading: boolean; align?: "left" | "right" }) {
  return (
    <div className={cn("flex min-w-0 flex-1 flex-col gap-1", align === "right" && "items-end text-right")}>
      <span className="flex min-w-0 items-center gap-1.5">
        <span className={cn("inline-block size-3 shrink-0 rounded-full", side === "a" ? "bg-primary" : "bg-brand-orange")} aria-hidden="true" />
        <span className="min-w-0 truncate text-sm font-extrabold text-foreground">{team.name}</span>
        {leading && <FlagIcon aria-hidden="true" className="size-3.5 shrink-0 text-gold-foreground" />}
      </span>
      <span className="truncate text-xs text-muted-foreground">{team.players.join(" / ")}</span>
      {leading && <span className="sr-only">leder</span>}
    </div>
  )
}

function HoleWinners({ sideA, sideB, results, totalHoles }: { sideA: SideTeamInfo; sideB: SideTeamInfo; results: HoleWinner[]; totalHoles: number }) {
  return (
    <div className="flex flex-col gap-2 rounded-2xl border border-border bg-card p-3">
      <h3 className="px-1 text-sm font-bold text-foreground">Hull-for-hull</h3>
      <div className="relative min-w-0 overflow-x-auto">
        <div className="flex min-w-max gap-1.5">
          {Array.from({ length: totalHoles }, (_, i) => i + 1).map((holeNumber, i) => {
            const r = results[i] ?? null
            const label = r === "a" ? sideA.name : r === "b" ? sideB.name : r === "halved" ? "delt" : "ikke spilt"
            return (
              <div key={holeNumber} className="flex w-9 flex-col items-center gap-1">
                <span className="text-[11px] font-bold tabular-nums text-muted-foreground">{holeNumber}</span>
                <span
                  className={cn(
                    "flex size-9 items-center justify-center rounded-md text-sm font-extrabold",
                    r === "a" && "bg-primary text-primary-foreground",
                    r === "b" && "bg-brand-orange text-brand-orange-foreground",
                    r === "halved" && "border border-border bg-muted text-muted-foreground",
                    r === null && "border border-dashed border-border text-muted-foreground",
                  )}
                >
                  {r === "a" ? "A" : r === "b" ? "B" : r === "halved" ? "=" : ""}
                  <span className="sr-only">Hull {holeNumber}: {label}</span>
                </span>
              </div>
            )
          })}
        </div>
      </div>
      <div className="flex flex-wrap gap-x-4 gap-y-1 px-1 pt-1 text-xs text-muted-foreground">
        <span className="flex items-center gap-1.5"><span className="flex size-5 items-center justify-center rounded bg-primary text-[10px] font-bold text-primary-foreground">A</span>{sideA.name}</span>
        <span className="flex items-center gap-1.5"><span className="flex size-5 items-center justify-center rounded bg-brand-orange text-[10px] font-bold text-brand-orange-foreground">B</span>{sideB.name}</span>
        <span className="flex items-center gap-1.5"><span className="flex size-5 items-center justify-center rounded border border-border bg-muted text-[10px] font-bold">=</span>Delt</span>
      </div>
    </div>
  )
}

// Delt av TwoSidedBoard/HighLowBoard (ADR-047) -- ÉN utvidbar detaljseksjon
// for HELE kampen (ikke et per-rad-mønster, siden formatet kun har 2 sider,
// ikke en spillerliste). Viser faktiske hull-for-hull-SCORER (ikke bare
// hvem som vant hullet, som HoleWinners/DuelRow allerede gjør statisk over)
// via den allerede eksisterende, nå eksporterte MatchScorecardGrid --
// samme scorekort som `/my-rounds/{id}/scorecard` viser, ingen ny visuell
// design. `MatchScorecardGrid` gjør sin egen `useFormatResult`-henting
// internt, så dette henter kun ved faktisk utvidelse, ikke ved mount.
// `MatchScorecardGrid` (round-scorecard.tsx) sin `ApiParticipant.guest_name`
// er required (string|null), men `publicMode` sin `PublicRoundParticipantOut`
// har ikke feltet i det hele tatt (undefined ved kjøretid) -- defaultes til
// null her. Trygt: MatchScorecardGrid leser aldri guest_name (kun
// display_name via participantLabel), så dette endrer ingen visning.
function toGridRound(round: ApiRoundHeader) {
  return {
    ...round,
    participants: round.participants.map((p) => ({ ...p, guest_name: p.guest_name ?? null })),
  }
}

function MatchDetailSection({
  showDetail,
  onToggle,
  round,
  roundId,
  viewerId,
  refreshKey,
  apiPrefix,
  commentsHref,
}: {
  showDetail: boolean
  onToggle: () => void
  round: ApiRoundHeader
  roundId: string
  viewerId: string | null
  refreshKey: number
  apiPrefix: string
  commentsHref: string
}) {
  return (
    <div className="rounded-2xl border border-border bg-card">
      <button
        type="button"
        onClick={onToggle}
        aria-expanded={showDetail}
        className="flex min-h-14 w-full items-center justify-between gap-2 px-4 py-3 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
      >
        <span className="text-sm font-bold text-foreground">Vis hull-for-hull med score</span>
        <ChevronDown aria-hidden="true" className={cn("size-5 text-muted-foreground transition-transform", showDetail && "rotate-180")} />
      </button>
      {showDetail && (
        <div className="flex flex-col gap-3 border-t border-border p-3">
          <MatchScorecardGrid round={toGridRound(round)} roundId={roundId} viewerId={viewerId} refreshKey={refreshKey} apiPrefix={apiPrefix} />
          <Link
            href={commentsHref}
            className="inline-flex h-11 w-fit items-center gap-1.5 rounded-lg px-1 text-sm font-bold text-primary hover:underline active:scale-[0.98]"
          >
            <MessageSquare aria-hidden="true" className="size-4" />
            Kommentarer
          </Link>
        </div>
      )}
    </div>
  )
}

// ===========================================================================
// 3. HIGH-LOW-HIGH -- to duller per hull, poeng per side (IKKE hull-opp)
// ===========================================================================

type Duel = "a" | "b" | "halved" | null
type HoleDuel = { low: Duel; high: Duel }

function HighLowBoard({
  sideA,
  sideB,
  duels,
  totalA,
  totalB,
  formatName,
  round,
  roundId,
  viewerId,
  refreshKey,
  apiPrefix,
  commentsHref,
}: {
  sideA: SideTeamInfo
  sideB: SideTeamInfo
  duels: HoleDuel[]
  totalA: number
  totalB: number
  formatName: string
  round: ApiRoundHeader
  roundId: string
  viewerId: string | null
  refreshKey: number
  apiPrefix: string
  commentsHref: string
}) {
  const [showDetail, setShowDetail] = useState(false)
  const leader = totalA > totalB ? "a" : totalB > totalA ? "b" : null
  const fmt = (n: number) => NF1.format(n)
  return (
    <div className="flex flex-col gap-4">
      <div className="flex flex-col gap-3 rounded-2xl border border-border bg-card p-5">
        <span className="text-center text-xs font-bold uppercase tracking-wide text-muted-foreground">{formatName} · totale poeng</span>
        <div className="grid grid-cols-2 gap-3">
          {(["a", "b"] as const).map((s) => {
            const team = s === "a" ? sideA : sideB
            const val = s === "a" ? totalA : totalB
            const isLead = leader === s
            return (
              <div
                key={s}
                className={cn(
                  "flex flex-col items-center gap-1 rounded-xl border p-4",
                  isLead ? (s === "a" ? "border-primary bg-primary/5" : "border-brand-orange bg-brand-orange/5") : "border-border",
                )}
              >
                <span className="flex items-center gap-1.5">
                  <span className={cn("inline-block size-3 rounded-full", s === "a" ? "bg-primary" : "bg-brand-orange")} aria-hidden="true" />
                  <span className="truncate text-sm font-extrabold text-foreground">{team.name}</span>
                </span>
                <span className={cn("text-4xl font-extrabold tabular-nums", s === "a" ? "text-primary" : "text-brand-orange")}>{fmt(val)}</span>
                <span className="text-xs font-semibold text-muted-foreground">poeng {isLead ? "· leder" : ""}</span>
              </div>
            )
          })}
        </div>
        <p className="text-center text-xs text-muted-foreground">
          To uavhengige dueller per hull: laveste mot laveste og høyeste mot høyeste netto/Stableford-poeng. 0–2 poeng per hull.
        </p>
      </div>

      <div className="flex flex-col gap-2 rounded-2xl border border-border bg-card p-3">
        <h3 className="px-1 text-sm font-bold text-foreground">Hull-for-hull · lav / høy</h3>
        <div className="relative min-w-0 overflow-x-auto">
          <div className="flex min-w-max flex-col gap-1.5">
            <DuelRow label="Hull" cells={duels.map((_, i) => ({ text: String(i + 1), head: true }))} />
            <DuelRow label="Lav" cells={duels.map((d) => duelCell(d.low))} />
            <DuelRow label="Høy" cells={duels.map((d) => duelCell(d.high))} />
          </div>
        </div>
        <div className="flex flex-wrap gap-x-4 gap-y-1 px-1 pt-1 text-xs text-muted-foreground">
          <span className="flex items-center gap-1.5"><span className="flex size-5 items-center justify-center rounded bg-primary text-[10px] font-bold text-primary-foreground">A</span>{sideA.name}</span>
          <span className="flex items-center gap-1.5"><span className="flex size-5 items-center justify-center rounded bg-brand-orange text-[10px] font-bold text-brand-orange-foreground">B</span>{sideB.name}</span>
          <span className="flex items-center gap-1.5"><span className="flex size-5 items-center justify-center rounded border border-border bg-muted text-[10px] font-bold">=</span>Delt (½–½)</span>
        </div>
      </div>

      <MatchDetailSection
        showDetail={showDetail}
        onToggle={() => setShowDetail((v) => !v)}
        round={round}
        roundId={roundId}
        viewerId={viewerId}
        refreshKey={refreshKey}
        apiPrefix={apiPrefix}
        commentsHref={commentsHref}
      />
    </div>
  )
}

function duelCell(d: Duel): { text: string; tone: Duel } {
  return { text: d === "a" ? "A" : d === "b" ? "B" : d === "halved" ? "=" : "–", tone: d }
}

function DuelRow({ label, cells }: { label: string; cells: { text: string; head?: boolean; tone?: Duel }[] }) {
  return (
    <div className="flex items-center gap-1.5">
      <span className="sticky left-0 w-12 shrink-0 bg-card text-xs font-bold text-muted-foreground">{label}</span>
      {cells.map((c, i) => (
        <span
          key={i}
          className={cn(
            "flex size-8 shrink-0 items-center justify-center rounded-md text-sm font-extrabold",
            c.head && "text-muted-foreground",
            c.tone === "a" && "bg-primary text-primary-foreground",
            c.tone === "b" && "bg-brand-orange text-brand-orange-foreground",
            c.tone === "halved" && "border border-border bg-muted text-muted-foreground",
            c.tone === null && !c.head && "border border-dashed border-border text-muted-foreground",
          )}
        >
          {c.text}
        </span>
      ))}
    </div>
  )
}

// Lav/høy-rollen per spiller per hull er ikke tagget eksplisitt av
// backend, men er alltid MIN/MAKS av de to lagkameratenes Stableford-
// poeng den hullet (samme regel som handicap_engine.py sin
// high_low_high_points_for_hole) -- utledes trygt her uten noen
// backend-endring.
function highLowDuelForHole(hole: ApiFormatHole): HoleDuel {
  const aPoints = hole.entries.filter((e) => e.side === "a" && e.points !== null).map((e) => e.points as number)
  const bPoints = hole.entries.filter((e) => e.side === "b" && e.points !== null).map((e) => e.points as number)
  if (aPoints.length < 2 || bPoints.length < 2) return { low: null, high: null }
  const lowA = Math.min(...aPoints)
  const highA = Math.max(...aPoints)
  const lowB = Math.min(...bPoints)
  const highB = Math.max(...bPoints)
  return {
    low: lowA > lowB ? "a" : lowB > lowA ? "b" : "halved",
    high: highA > highB ? "a" : highB > highA ? "b" : "halved",
  }
}

// ===========================================================================
// 4. TEAM / FLIGHT (Shamble / Money Ball)
// ===========================================================================

type FlightTeam = { id: string; name: string; toPar: number; through: number; isThisTeam?: boolean }

function TeamFlightBoard({
  teamName,
  toPar,
  through,
  totalHoles,
  flight,
  members,
  holes,
  formatName,
  commentsHref,
}: {
  teamName: string
  toPar: number | null
  through: number
  totalHoles: number
  flight: FlightTeam[]
  members: IndividualPlayer[]
  holes: Hole[]
  formatName: string
  commentsHref: string
}) {
  const [showRaw, setShowRaw] = useState(false)
  const [openMemberId, setOpenMemberId] = useState<string | null>(null)
  const ranked = [...flight].sort((a, b) => a.toPar - b.toPar)
  const myRank = ranked.findIndex((t) => t.isThisTeam) + 1

  return (
    <div className="flex flex-col gap-4">
      <div className="flex flex-col gap-3 rounded-2xl border border-border bg-card p-5">
        <span className="text-xs font-bold uppercase tracking-wide text-muted-foreground">{formatName} · ditt lag</span>
        <div className="flex items-center justify-between gap-3">
          <div className="flex min-w-0 flex-col">
            <span className="truncate text-lg font-extrabold text-foreground">{teamName}</span>
            <span className="text-sm text-muted-foreground">
              {through === totalHoles ? "Ferdig" : `${through}/${totalHoles} hull`}
              {flight.length > 1 && myRank > 0 ? ` · ${myRank}. i flighten` : ""}
            </span>
          </div>
          {toPar !== null ? <MetricPill kind="topar" value={toPar} size="lg" /> : <span className="text-sm font-semibold text-muted-foreground">Ingen hull ferdig ennå</span>}
        </div>
      </div>

      {ranked.length > 1 && (
        <div className="flex flex-col gap-2 rounded-2xl border border-border bg-card p-3">
          <h3 className="px-1 text-sm font-bold text-foreground">Flight-gruppe</h3>
          <ol className="flex flex-col">
            {ranked.map((t, i) => (
              <li
                key={t.id}
                className={cn(
                  "flex min-h-14 items-center gap-3 rounded-xl px-3 py-2",
                  t.isThisTeam && "bg-primary/5 ring-1 ring-inset ring-primary/30",
                )}
              >
                <span className="w-7 text-center text-base font-extrabold tabular-nums text-foreground">{i + 1}</span>
                <span className="flex min-w-0 flex-1 flex-col">
                  <span className="flex items-center gap-2 truncate text-base font-bold text-foreground">
                    {i === 0 && <Crown aria-hidden="true" className="size-4 shrink-0 text-gold-foreground" />}
                    {t.name}
                    {t.isThisTeam && <Badge tone="self">Ditt</Badge>}
                  </span>
                  <span className="text-sm text-muted-foreground">{t.through === totalHoles ? "Ferdig" : `${t.through}/${totalHoles} hull`}</span>
                </span>
                <MetricPill kind="topar" value={t.toPar} />
              </li>
            ))}
          </ol>
        </div>
      )}

      <div className="rounded-2xl border border-border bg-card">
        <button
          type="button"
          onClick={() => setShowRaw((v) => !v)}
          aria-expanded={showRaw}
          className="flex min-h-14 w-full items-center justify-between gap-2 px-4 py-3 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
        >
          <span className="text-sm font-bold text-foreground">Spillernes råscore</span>
          <ChevronDown aria-hidden="true" className={cn("size-5 text-muted-foreground transition-transform", showRaw && "rotate-180")} />
        </button>
        {showRaw && (
          <ol className="flex flex-col border-t border-border">
            {members.map((m) => (
              <TeamMemberRow
                key={m.id}
                member={m}
                holes={holes}
                totalHoles={totalHoles}
                open={openMemberId === m.id}
                onToggle={() => setOpenMemberId((cur) => (cur === m.id ? null : m.id))}
                commentsHref={commentsHref}
              />
            ))}
          </ol>
        )}
      </div>
    </div>
  )
}

// Utvidbar rad for ETT medlem av flighten (ADR-047) -- samme mønster
// (klikk → hull-for-hull via HoleByHole) som IndividualRow, men uten
// rangeringskolonnen (et lags medlemmer rangeres ikke seg imellom her,
// kun laget som helhet gjør, vist i flight-gruppen over).
function TeamMemberRow({
  member,
  holes,
  totalHoles,
  open,
  onToggle,
  commentsHref,
}: {
  member: IndividualPlayer
  holes: Hole[]
  totalHoles: number
  open: boolean
  onToggle: () => void
  commentsHref: string
}) {
  const panelId = `tfm-${member.id}`
  return (
    <li className="border-b border-border last:border-b-0">
      <button
        type="button"
        onClick={onToggle}
        aria-expanded={open}
        aria-controls={panelId}
        className="flex min-h-14 w-full items-center gap-3 px-4 py-2 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
      >
        <span className="flex min-w-0 flex-1 flex-col">
          <span className="flex items-center gap-2 truncate text-base font-bold text-foreground">
            {member.name}
            {member.isSelf && <Badge tone="self">Deg</Badge>}
          </span>
          <span className="text-sm text-muted-foreground">
            {member.hcpIndex !== null ? `Hcp ${NF1.format(member.hcpIndex)}` : "Uten HCP"} · {playedHoles(member)}/{totalHoles} hull
          </span>
        </span>
        <MetricPill kind="topar" value={grossToPar(member, holes)} />
        <ChevronDown aria-hidden="true" className={cn("size-5 shrink-0 text-muted-foreground transition-transform", open && "rotate-180")} />
      </button>
      {open && (
        <div id={panelId} className="px-4 pb-3">
          <HoleByHole player={member} metric="brutto" holes={holes} rank={null} tied={false} commentsHref={commentsHref} />
        </div>
      )}
    </li>
  )
}

// ===========================================================================
// Datalag: fetch-hooks (uendret fra forrige runde)
// ===========================================================================

type ApiFormatHoleEntry = {
  id: string
  label: string
  side: "a" | "b" | null
  gross: number | null
  net: number | null
  strokes_received: number | null
  counted: boolean
  points: number | null
}

type ApiFormatHole = {
  hole_number: number
  par: number
  stroke_index: number
  entries: ApiFormatHoleEntry[]
  result: string | null
}

type ApiFormatResult = {
  play_format: string
  ready: boolean
  match_lead: number | null
  match_holes_played: number | null
  match_holes_remaining: number | null
  match_is_closed: boolean | null
  match_is_dormie: boolean | null
  match_status_text: string | null
  hole_results: string[] | null
  copenhagen_points: Record<string, number> | null
  bbb_points: Record<string, number> | null
  shamble_team_score: number | null
  money_ball_team_score: number | null
  hlh_points_a: number | null
  hlh_points_b: number | null
  // Scramble mot enkeltspiller (migrasjon 056) -- rene netto-totalsummer,
  // side "a" = laget, side "b" = individualspilleren (fast, se
  // _build_scramble_solo_result). margin > 0 = laget leder.
  scramble_solo_team_net: number | null
  scramble_solo_individual_net: number | null
  scramble_solo_margin: number | null
  holes: ApiFormatHole[] | null
}

function useFormatResult(roundId: string, refreshKey: number, apiPrefix: string = "/rounds"): ApiFormatResult | null {
  const [result, setResult] = useState<ApiFormatResult | null>(null)
  useEffect(() => {
    let cancelled = false
    fetch(`${apiPrefix}/${roundId}/format-result`, { credentials: "include" })
      .then((res) => (res.ok ? res.json() : null))
      .then((data: ApiFormatResult | null) => {
        if (!cancelled) setResult(data)
      })
      .catch(() => {})
    return () => {
      cancelled = true
    }
  }, [roundId, refreshKey, apiPrefix])
  return result
}

// Bygger ScrambleSoloResultView sin props-form fra det generiske
// ApiFormatResult-svaret. Hull-entryen sitt `label`-felt (satt av
// _build_scramble_solo_result til deltakerens/lagets faktiske
// visningsnavn) foretrekkes over sidens EGEN label (som bare er den
// faste "Laget"/"Individuell motstander"-teksten satt ved opprettelse,
// se new-round.tsx sin submitWizard) -- faller tilbake til sidens label
// når ingen hull er spilt ennå.
function buildScrambleSoloResult(round: ApiRoundHeader, formatResult: ApiFormatResult | null): ScrambleSoloResult {
  const teamSide = round.sides.find((s) => s.side_role === "team")
  const individualSide = round.sides.find((s) => s.side_role === "individual")
  const firstHoleEntries = formatResult?.holes?.[0]?.entries ?? []
  const teamLabel = firstHoleEntries.find((e) => e.side === "a")?.label || teamSide?.label || "Laget"
  const individualLabel =
    firstHoleEntries.find((e) => e.side === "b")?.label || individualSide?.label || "Individuell motstander"

  const holes: ScrambleSoloResult["holes"] = (formatResult?.holes ?? []).map((h) => {
    const teamEntry = h.entries.find((e) => e.side === "a")
    const individualEntry = h.entries.find((e) => e.side === "b")
    const result: "a" | "b" | "halved" = h.result === "a" || h.result === "b" ? h.result : "halved"
    return {
      holeNumber: h.hole_number,
      par: h.par,
      strokeIndex: h.stroke_index,
      result,
      team: {
        gross: teamEntry?.gross ?? 0,
        net: teamEntry?.net ?? 0,
        strokesReceived: teamEntry?.strokes_received ?? 0,
      },
      individual: {
        gross: individualEntry?.gross ?? 0,
        net: individualEntry?.net ?? 0,
        strokesReceived: individualEntry?.strokes_received ?? 0,
      },
    }
  })

  return {
    ready: formatResult?.ready ?? false,
    teamLabel,
    individualLabel,
    teamNet: formatResult?.scramble_solo_team_net ?? null,
    individualNet: formatResult?.scramble_solo_individual_net ?? null,
    margin: formatResult?.scramble_solo_margin ?? null,
    holes,
  }
}

type FlightSibling = { round_id: string; name: string | null; isAnchor: boolean; teamScore: number | null; par: number | null; through: number }

function useFlightTeamStandings(
  roundId: string,
  playFormat: string,
  refreshKey: number,
  apiPrefix: string = "/rounds",
) {
  const [standings, setStandings] = useState<FlightSibling[] | null>(null)
  useEffect(() => {
    let cancelled = false
    async function load() {
      const flightRes = await fetch(`${apiPrefix}/${roundId}/flight-group`, { credentials: "include" })
      if (!flightRes.ok) return
      const flight: { flight_group_id: string | null; flights: { round_id: string; name: string | null; is_anchor: boolean }[] } =
        await flightRes.json()
      if (flight.flights.length < 2) {
        if (!cancelled) setStandings(null)
        return
      }
      const results = await Promise.all(
        flight.flights.map(async (f) => {
          const r = await fetch(`${apiPrefix}/${f.round_id}/format-result`, { credentials: "include" })
          const data: ApiFormatResult | null = r.ok ? await r.json() : null
          const teamScore = playFormat === "shamble" ? data?.shamble_team_score ?? null : data?.money_ball_team_score ?? null
          const holes = data?.holes ?? []
          const par = holes.length > 0 ? holes.reduce((s, h) => s + h.par, 0) : null
          return { round_id: f.round_id, name: f.name, isAnchor: f.is_anchor, teamScore, par, through: holes.length }
        }),
      )
      if (!cancelled) setStandings(results)
    }
    void load()
    return () => {
      cancelled = true
    }
  }, [roundId, playFormat, refreshKey, apiPrefix])
  return standings
}

function useLeaderboardData(roundId: string, apiPrefix: string = "/rounds") {
  const [round, setRound] = useState<ApiRoundHeader | null>(null)
  const [leaderboard, setLeaderboard] = useState<ApiLeaderboard | null>(null)
  const [viewerId, setViewerId] = useState<string | null>(null)
  const [error, setError] = useState<string | null>(null)
  const [refreshKey, setRefreshKey] = useState(0)
  const publicMode = apiPrefix !== "/rounds"

  useEffect(() => {
    let cancelled = false
    fetch("/auth/me", { credentials: "include" })
      .then((res) => (res.ok ? res.json() : null))
      .then((data: { id: string } | null) => {
        if (!cancelled && data) setViewerId(data.id)
      })
      .catch(() => {})
    return () => {
      cancelled = true
    }
  }, [])

  useEffect(() => {
    const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"
    const wsPath = publicMode ? "ws/public/rounds" : "ws/rounds"
    const socket = new WebSocket(`${protocol}//${window.location.host}/${wsPath}/${roundId}/live`)
    socket.onmessage = () => setRefreshKey((k) => k + 1)
    return () => socket.close()
  }, [roundId, publicMode])

  useEffect(() => {
    let cancelled = false
    async function load() {
      const [roundRes, leaderboardRes] = await Promise.all([
        fetch(`${apiPrefix}/${roundId}`, { credentials: "include" }),
        fetch(`${apiPrefix}/${roundId}/leaderboard`, { credentials: "include" }),
      ])
      if (roundRes.status === 403 || roundRes.status === 404) {
        if (!cancelled) setError("Denne runden finnes ikke, eller du har ikke tilgang til den.")
        return
      }
      if (!roundRes.ok || !leaderboardRes.ok) {
        if (!cancelled) setError("Klarte ikke å hente leaderboardet. Prøv igjen om litt.")
        return
      }
      const roundData: ApiRoundHeader = await roundRes.json()
      const leaderboardData: ApiLeaderboard = await leaderboardRes.json()
      if (!cancelled) {
        setRound(roundData)
        setLeaderboard(leaderboardData)
      }
    }
    void load()
    return () => {
      cancelled = true
    }
  }, [roundId, refreshKey, apiPrefix])

  const viewerParticipantId = round?.participants.find((p) => p.user_id === viewerId)?.id ?? null

  return { round, leaderboard, viewerParticipantId, error, refreshKey }
}

// ===========================================================================
// Adaptere: ekte API-data -> V0 sine props (ingen mock-data noe sted)
// ===========================================================================

function buildHoleOrder(startHole: number, holesPlanned: number): number[] {
  return Array.from({ length: holesPlanned }, (_, i) => ((startHole - 1 + i) % 18) + 1)
}

function buildHoles(entries: ApiLeaderboardEntry[], holeOrder: number[]): Hole[] {
  const anyEntryWithHoles = entries.find((e) => e.holes.length > 0)
  if (!anyEntryWithHoles) return []
  return holeOrder
    .map((n) => anyEntryWithHoles.holes.find((h) => h.hole_number === n))
    .filter((h): h is ApiLeaderboardHole => h !== undefined)
    .map((h) => ({ holeNumber: h.hole_number, par: h.par, index: h.stroke_index }))
}

function buildIndividualPlayers(
  entries: ApiLeaderboardEntry[],
  participants: ApiRoundParticipant[],
  holeOrder: number[],
  viewerParticipantId: string | null,
  ownPointsField: "bbb_points" | "copenhagen_points" | undefined,
  formatResult: ApiFormatResult | null,
): IndividualPlayer[] {
  const pointsByHoleNumber = new Map<number, Map<string, number>>()
  if (ownPointsField && formatResult?.holes) {
    for (const h of formatResult.holes) {
      const m = new Map<string, number>()
      for (const e of h.entries) {
        if (e.points !== null) m.set(e.id, e.points)
      }
      pointsByHoleNumber.set(h.hole_number, m)
    }
  }
  const hcpByParticipant = new Map(participants.map((p) => [p.id, p.handicap_index_snapshot]))
  return entries.map((e) => {
    const gross = holeOrder.map((n) => {
      const h = e.holes.find((x) => x.hole_number === n)
      return h && h.played && h.score !== null ? h.score : null
    })
    // Ekte per-hull mottatte slag -- allerede korrekt utregnet av backend
    // (samme allokering `allocate_strokes_by_index` som resten av appen
    // bruker), null når HCP ikke spores for deltakeren. IKKE en egen
    // klientsidig course-hcp+stroke-indeks-utregning (V0 sin mock-
    // versjon måtte gjøre det siden den ikke hadde et ekte backend).
    const strokesReceived = holeOrder.map((n) => {
      const h = e.holes.find((x) => x.hole_number === n)
      return h?.strokes_received ?? null
    })
    const holePoints = ownPointsField
      ? holeOrder.map((n) => pointsByHoleNumber.get(n)?.get(e.participant_id) ?? null)
      : undefined
    return {
      id: e.participant_id,
      name: e.display_name,
      isSelf: e.participant_id === viewerParticipantId,
      isOwner: e.is_owner,
      strokesReceived,
      hcpIndex: hcpByParticipant.get(e.participant_id) ?? null,
      finished: false,
      gross,
      holePoints,
    }
  })
}

function buildSideTeams(round: ApiRoundHeader, entries: ApiLeaderboardEntry[]): [SideTeamInfo, SideTeamInfo] {
  const nameByParticipant = new Map(entries.map((e) => [e.participant_id, e.display_name]))
  const sideIds = round.sides.map((s) => s.id)
  const namesFor = (sideId: string) =>
    round.participants.filter((p) => p.round_side_id === sideId).map((p) => nameByParticipant.get(p.id) ?? "Medspiller")
  return [
    { id: "a", name: round.sides[0]?.label?.trim() || "Side A", players: namesFor(sideIds[0]) },
    { id: "b", name: round.sides[1]?.label?.trim() || "Side B", players: namesFor(sideIds[1]) },
  ]
}

function buildHoleWinners(holeResults: string[] | null, totalHoles: number): HoleWinner[] {
  const out: HoleWinner[] = Array.from({ length: totalHoles }, () => null)
  if (!holeResults) return out
  holeResults.forEach((r, i) => {
    if (i < totalHoles && (r === "a" || r === "b" || r === "halved")) out[i] = r
  })
  return out
}

const METRICS_FOR_FORMAT: Record<string, { metrics: MetricKey[]; defaultMetric: MetricKey; pointsUnit: string }> = {
  stroke: { metrics: ["brutto", "netto"], defaultMetric: "brutto", pointsUnit: "p" },
  stableford: { metrics: ["stableford", "netto", "brutto"], defaultMetric: "stableford", pointsUnit: "p" },
  skins: { metrics: ["brutto", "netto"], defaultMetric: "brutto", pointsUnit: "p" },
  flag: { metrics: ["brutto", "netto"], defaultMetric: "brutto", pointsUnit: "p" },
  bbb: { metrics: ["points", "brutto", "netto"], defaultMetric: "points", pointsUnit: "p" },
  copenhagen: { metrics: ["points", "brutto", "netto"], defaultMetric: "points", pointsUnit: "p" },
}

// ===========================================================================
// Full leaderboard-side
// ===========================================================================

export function RoundLeaderboard({
  roundId,
  embedded = false,
  publicMode = false,
}: {
  roundId: string
  embedded?: boolean
  // ADR-044 (tilskuer-visning, 2026-08-06) -- samme mønster som
  // `RoundScorecard` sin `publicMode`: bytter alle `/rounds/*`-kall til
  // `/public/rounds/*` og WS-kanalen til den offentlige. INGEN endring
  // i eiersidens oppførsel når denne er `false` (standard).
  publicMode?: boolean
}) {
  const apiPrefix = publicMode ? "/public/rounds" : "/rounds"
  const { round, leaderboard, viewerParticipantId, error, refreshKey } = useLeaderboardData(roundId, apiPrefix)
  const formatResult = useFormatResult(roundId, refreshKey, apiPrefix)
  const standings = useFlightTeamStandings(roundId, round?.play_format ?? "", refreshKey, apiPrefix)

  if (error) {
    return (
      <div className="flex min-h-dvh flex-col items-center justify-center gap-4 bg-background px-5 text-center">
        <p className="text-base font-medium text-destructive">{error}</p>
        <Link
          href={publicMode ? "/dashboard" : "/my-rounds"}
          className="text-base font-semibold text-primary underline underline-offset-2"
        >
          {publicMode ? "Tilbake til dashbordet" : "Tilbake til egne runder"}
        </Link>
      </div>
    )
  }

  if (!round || !leaderboard) {
    return (
      <div className="flex min-h-dvh flex-col items-center justify-center bg-background">
        <div aria-hidden="true" className="size-10 animate-spin rounded-full border-4 border-primary/20 border-t-primary" />
      </div>
    )
  }

  // Delt kommentartråd (ADR-047) -- round_message-reaksjoner/-kommentarer
  // fester seg til ETT spesifikt innlegg, ikke runden som helhet, så
  // hver utvidet rad/kamp-seksjon lenker til den SAMME, allerede
  // eksisterende kommentarseksjonen på selve rundesiden (anker-id
  // "kommentarer" i round-detail.tsx/watch-round.tsx) i stedet for å late
  // som om det finnes en egen tråd per spiller/kamp.
  const commentsHref = publicMode ? `/watch/${roundId}#kommentarer` : `/my-rounds/${roundId}#kommentarer`

  const holeOrder = buildHoleOrder(round.start_hole, round.holes_planned)
  const holes = buildHoles(leaderboard.entries, holeOrder)
  const isHighLow = HIGH_LOW_FORMATS.has(round.play_format)
  const isTwoSided = TWO_SIDED_FORMATS.includes(round.play_format)
  const isTeamResult = TEAM_RESULT_FORMATS.has(round.play_format)
  const isScrambleSolo = SCRAMBLE_SOLO_FORMATS.has(round.play_format)
  const ownPointsField = FORMAT_OWN_POINTS_FIELD[round.play_format]

  return (
    <div className={cn(!embedded && "min-h-dvh bg-background")}>
      {!embedded && (
        <header className="sticky top-0 z-10 border-b border-border bg-background/95 backdrop-blur">
          <div className="mx-auto flex min-h-14 max-w-xl items-center gap-2 px-4 py-2">
            <Link
              href={publicMode ? `/watch/${roundId}` : `/my-rounds/${roundId}`}
              className="flex min-h-11 items-center gap-1.5 rounded-xl pr-3 text-base font-bold text-foreground transition-colors hover:text-primary"
            >
              <ArrowLeft aria-hidden="true" className="size-6" />
              Leaderboard
            </Link>
          </div>
        </header>
      )}

      <main className="mx-auto flex max-w-xl flex-col gap-4 px-4 py-5 pb-16">
        <div className="flex flex-col gap-1 rounded-3xl border border-border bg-card p-4 shadow-md shadow-black/8">
          <span className="flex flex-wrap items-center gap-2">
            <span className="text-xl font-extrabold tracking-tight text-foreground text-balance">
              {round.name?.trim() || round.course_name_snapshot}
            </span>
            {round.play_format !== "stroke" && (
              <span className="rounded-full bg-muted px-2 py-0.5 text-xs font-bold text-muted-foreground">
                {FORMAT_LABELS[round.play_format] ?? round.play_format}
              </span>
            )}
          </span>
          <span className="text-sm font-semibold text-muted-foreground">
            {round.name?.trim() ? `${round.course_name_snapshot} · ` : ""}
            {round.holes_planned} hull · {dateFormatter.format(new Date(round.played_at))}
          </span>
        </div>

        {isTwoSided || isHighLow ? (
          !formatResult ? null : !formatResult.ready ? (
            <div className="rounded-3xl border border-dashed border-border bg-card p-4 text-sm text-muted-foreground sm:p-5">
              Venter på at begge sider er komplette og har beregnet handicap før stillingen kan vises.
            </div>
          ) : isHighLow ? (
            (() => {
              const [sideA, sideB] = buildSideTeams(round, leaderboard.entries)
              const duels = (formatResult.holes ?? []).map(highLowDuelForHole)
              return (
                <HighLowBoard
                  sideA={sideA}
                  sideB={sideB}
                  duels={duels}
                  totalA={formatResult.hlh_points_a ?? 0}
                  totalB={formatResult.hlh_points_b ?? 0}
                  formatName="High-low-high"
                  round={round}
                  roundId={roundId}
                  viewerId={viewerParticipantId}
                  refreshKey={refreshKey}
                  apiPrefix={apiPrefix}
                  commentsHref={commentsHref}
                />
              )
            })()
          ) : (
            (() => {
              const [sideA, sideB] = buildSideTeams(round, leaderboard.entries)
              const results = buildHoleWinners(formatResult.hole_results, round.holes_planned)
              const lead = formatResult.match_lead ?? 0
              const leader: "a" | "b" | null = lead > 0 ? "a" : lead < 0 ? "b" : null
              const holesPlayed = formatResult.match_holes_played ?? 0
              const holesRemaining = formatResult.match_holes_remaining ?? 0
              const diff = Math.abs(lead)
              // V0 sin egen, korte headline-tekst ("2 opp"/"Dormie 2 opp"),
              // IKKE backend sin `match_status_text` (som bevisst bygger inn
              // side-NAVNET, "2 UP (Lag Bjørk)" -- designet for et sted i
              // appen UTEN separate side-navn ved siden av, ulikt her hvor
              // SideName allerede viser navnet på hver side. Gjenbrukt
              // ordlyd, men mot EKTE tall (match_lead/-holes_played/
              // -remaining/-is_dormie/-is_closed) -- ingen egen klientsidig
              // utregning av selve stillingen.
              const headline =
                holesPlayed === 0
                  ? "Ingen hull spilt ennå"
                  : formatResult.match_is_closed
                    ? `Ferdig ${diff}&${holesRemaining}`
                    : holesRemaining === 0
                      ? leader
                        ? `Vant ${diff} opp`
                        : "Delt"
                      : !leader
                        ? "Alt likt"
                        : formatResult.match_is_dormie
                          ? `Dormie ${diff} opp`
                          : `${diff} opp`
              const sub = holesPlayed > 0 ? `${holesPlayed} hull spilt${holesRemaining > 0 ? `, ${holesRemaining} igjen` : ""}` : ""
              return (
                <TwoSidedBoard
                  sideA={sideA}
                  sideB={sideB}
                  results={results}
                  totalHoles={round.holes_planned}
                  formatName="Matchstatus"
                  headline={headline}
                  sub={sub}
                  leader={leader}
                  isDormie={Boolean(formatResult.match_is_dormie)}
                  isClosed={Boolean(formatResult.match_is_closed)}
                  round={round}
                  roundId={roundId}
                  viewerId={viewerParticipantId}
                  refreshKey={refreshKey}
                  apiPrefix={apiPrefix}
                  commentsHref={commentsHref}
                />
              )
            })()
          )
        ) : isTeamResult ? (
          (() => {
            const teamScore = round.play_format === "shamble" ? formatResult?.shamble_team_score ?? null : formatResult?.money_ball_team_score ?? null
            const resultHoles = formatResult?.holes ?? []
            const par = resultHoles.length > 0 ? resultHoles.reduce((s, h) => s + h.par, 0) : null
            const toPar = teamScore !== null && par !== null ? teamScore - par : null
            const flight: FlightTeam[] = (standings ?? [])
              .filter((s) => s.teamScore !== null && s.par !== null)
              .map((s) => ({
                id: s.round_id,
                name: s.name || round.course_name_snapshot,
                toPar: (s.teamScore as number) - (s.par as number),
                through: s.through,
                isThisTeam: s.round_id === roundId,
              }))
            const members = buildIndividualPlayers(leaderboard.entries, round.participants, holeOrder, viewerParticipantId, undefined, null)
            return (
              <TeamFlightBoard
                teamName={round.name?.trim() || round.course_name_snapshot}
                toPar={toPar}
                through={resultHoles.length}
                totalHoles={round.holes_planned}
                flight={flight}
                members={members}
                holes={holes}
                formatName={round.play_format === "shamble" ? "Shamble" : "Money Ball"}
                commentsHref={commentsHref}
              />
            )
          })()
        ) : isScrambleSolo ? (
          !formatResult ? null : <ScrambleSoloResultView result={buildScrambleSoloResult(round, formatResult)} />
        ) : (
          (() => {
            const players = buildIndividualPlayers(leaderboard.entries, round.participants, holeOrder, viewerParticipantId, ownPointsField, formatResult)
            const config = METRICS_FOR_FORMAT[round.play_format] ?? METRICS_FOR_FORMAT.stroke
            return (
              <>
                <h2 className="text-base font-bold text-foreground">Stilling</h2>
                <IndividualBoard
                  players={players}
                  metrics={config.metrics}
                  defaultMetric={config.defaultMetric}
                  pointsUnit={config.pointsUnit}
                  holes={holes}
                  commentsHref={commentsHref}
                />
              </>
            )
          })()
        )}
      </main>
    </div>
  )
}

// --- Kompakt mini-variant, til rundens egen detaljside --------------------
// Topp 3 (brutto) + "og N til", lenke til hele leaderboardet. Kun relevant
// for runder med mer enn én deltaker.

export function RoundLeaderboardMini({ roundId }: { roundId: string }) {
  const { round, leaderboard, viewerParticipantId } = useLeaderboardData(roundId)

  if (!round || !leaderboard || round.participants.length < 2) return null

  // To-sidede formater (match/fourball/foursome/greensome/scramble/
  // high_low_high/scramble_solo_match) vises allerede av FormatResultPanel
  // rett under denne komponenten sitt kallsted i round-detail.tsx -- ingen
  // individuell brutto-rangering å vise her. Shamble/Money Ball har heller
  // ingen meningsfull individuell topp-3 -- lagets EGEN hullscore-sum er
  // det som teller, vist i TeamFlightBoard på selve leaderboard-siden i
  // stedet for en misvisende råscore-rangering her. scramble_solo
  // (stroke-varianten) har av samme grunn ingen meningsfull individuell
  // rangering -- lag-mot-individuell netto-sammenligning vises av
  // ScrambleSoloResultView i stedet (fikset her, opprinnelig glemt da
  // scramble_solo ble bygget 2026-08-05).
  if (
    TWO_SIDED_FORMATS.includes(round.play_format) ||
    HIGH_LOW_FORMATS.has(round.play_format) ||
    TEAM_RESULT_FORMATS.has(round.play_format) ||
    SCRAMBLE_SOLO_FORMATS.has(round.play_format)
  ) {
    return null
  }

  const ranked = rankEntries(leaderboard.entries, "gross", viewerParticipantId)
  const top = ranked.slice(0, 3)
  const restCount = ranked.length - top.length

  return (
    <Link
      href={`/my-rounds/${roundId}/leaderboard`}
      className="mb-5 flex flex-col gap-2 rounded-2xl border border-border bg-card p-4 transition-colors hover:border-primary/50 hover:bg-accent/30"
    >
      <span className="flex items-center justify-between gap-2">
        <span className="flex items-center gap-1.5 text-sm font-bold text-foreground">
          <Trophy aria-hidden="true" className="size-4 text-primary" />
          Leaderboard
        </span>
        <ChevronRight aria-hidden="true" className="size-5 text-muted-foreground" />
      </span>
      <span className="flex flex-col gap-1">
        {top.map((entry) => (
          <span key={entry.participant_id} className="flex items-center gap-2 text-sm">
            <span className="w-8 shrink-0 font-bold tabular-nums text-muted-foreground">
              {entry.rank !== null ? (entry.tied ? `T-${entry.rank}` : `#${entry.rank}`) : "–"}
            </span>
            <span className="min-w-0 flex-1 truncate font-semibold text-foreground">{entry.display_name}</span>
            <span className="shrink-0 tabular-nums text-muted-foreground">
              {entry.score_to_par !== null ? formatToPar(entry.score_to_par) : "–"}
            </span>
          </span>
        ))}
      </span>
      {restCount > 0 && <span className="text-sm text-muted-foreground">og {restCount} til</span>}
    </Link>
  )
}
