"use client"

// Horisontalt scorekort for en frittstående runde (ADR-033) -- egen,
// dedikert side (ikke en del av round-stats.tsx sin aggregerte statistikk).
// Presentasjon fra V0 (zip 17), datalag skrevet om fra mock til ekte fetch
// mot samme `/rounds/{id}/participants/{id}/holes`-endepunkt som
// round-detail.tsx og round-stats.tsx allerede bruker.
//
// Bevisst forskjell fra V0s eksport: V0s egen `strokesReceived()` var en
// generisk modulo-formel -- byttet ut med backend sin ALLEREDE beregnede
// `strokes_received` per hull (samme `allocate_strokes_by_index()`-
// algoritme som resten av appen), for å unngå to ulike implementasjoner av
// samme HCP-slagfordeling.
//
// Format-bevissthet lagt til 2026-07-28: brukeren rapporterte at siden kun
// noensinne viste brutto/netto/stableford, uansett spilleform -- bekreftet
// ved simulering at (a) foursome/greensome/scramble var HELT BLANKE her
// (delt-ball-formater lagrer score PER SIDE, ikke per deltaker -- se
// ADR-039 -- og denne siden spurte kun mot deltaker-endepunktet), og (b)
// match/fourball/skins viste riktige rå tall, men aldri HVORFOR et hull
// ble avgjort som det ble (brutto vs. netto, hvem sitt netto talte for
// siden i fourball, hvem som vant/hvilket hull som rullet videre i skins).
// Fikset ved å: (1) la delt-ball-formater bruke SIDER som "enhet" i stedet
// for deltakere (samme visuelle ScoreBlock-tabell, bare på
// `/rounds/{id}/sides/{id}/holes`, som fikk et nytt `strokes_received`-felt
// samtidig), og (2) legge til en egen "Matchforløp"/"Skins hull for hull"-
// seksjon drevet av `/rounds/{id}/format-result` sitt nye `holes`-felt
// (brutto/netto/slag mottatt PER enhet PER hull, pluss hvem som vant og
// hvorfor).

import { useEffect, useState } from "react"
import Link from "next/link"
import { ArrowLeft, ArrowUp, ArrowDown, ArrowRight, ArrowUpLeft, ArrowUpRight, Check, Target } from "lucide-react"
import { cn } from "@/lib/utils"

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

type ApiParticipant = {
  id: string
  user_id: string | null
  guest_name: string | null
  display_name: string
  is_owner: boolean
  round_side_id: string | null
  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 ApiRound = {
  name: string | null
  id: string
  course_name_snapshot: string
  tee_name_snapshot: string
  played_at: string
  start_hole: number
  holes_planned: number
  play_format: string
  participants: ApiParticipant[]
  sides: ApiSide[]
}

type ApiHole = {
  hole_number: number
  par: number
  stroke_index: number
  played: boolean
  score: number | null
  picked_up: boolean
  strokes_received: number | null
  // Detaljfelt (2026-08-03) -- allerede returnert av samme
  // RoundHoleOut/`/rounds/.../holes`-endepunkt denne siden lenge har brukt,
  // bare ikke tidligere lest her. Presentert som egne, forkortede rader i
  // ScoreBlock i stedet for kun å ligge tilgjengelig i round-detail.tsx sin
  // "Så langt i runden"-seksjon.
  putts: number | null
  tee_shot_result: "fairway" | "left" | "right" | null
  approach_result: "hit" | "short" | "long" | "left" | "right" | null
  chip_count: number | null
  bunker_shot_count: number | null
  penalty_strokes: number | null
  anyway_strokes: number | null
}

const SHARED_BALL_FORMATS = ["foursome", "greensome", "scramble_2", "scramble_4", "chapman"]
// scramble_solo_match (migrasjon 057) lagt til her -- KUN denne bruken
// (isTwoSided) gater MatchScorecardGrid, som får sin egen dedikerte
// scramble-solo-familie-gren for å unngå dupliserte rader (se der).
// SHARED_BALL_FORMATS/isSharedBall RØRES bevisst ikke -- se
// SCRAMBLE_SOLO_FORMATS under for den egne, riktige klassifiseringen.
const TWO_SIDED_FORMATS = ["match", "fourball", "high_low_high", "scramble_solo_match", ...SHARED_BALL_FORMATS]
// Begge scramble-mot-enkeltspiller-variantene deler NØYAKTIG samme
// asymmetriske side-/enhetsstruktur (laget = side, individualspilleren =
// participant) -- kun selve resultat-sammenligningen skiller dem. Én
// samlet klassifisering brukt for alt UNNTATT selve resultatvisningen.
const SCRAMBLE_SOLO_FORMATS = new Set(["scramble_solo", "scramble_solo_match"])
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" })

// Viewer-relativt (ADR-036 fase 3-utvidelsen, 2026-07-26) -- se samme
// begrunnelse i round-detail.tsx sin playerLabel().
function participantLabel(p: ApiParticipant, viewerId: string | null): string {
  return viewerId !== null && p.user_id === viewerId ? "Deg" : p.display_name
}

// --- Scoring helpers -------------------------------------------------------

function netScore(h: ApiHole): number | null {
  return h.played && h.score !== null && h.strokes_received !== null ? h.score - h.strokes_received : null
}

// Stableford: 2 pts for net par, +/- 1 per stroke, floored at 0.
function stablefordPoints(h: ApiHole): number | null {
  const net = netScore(h)
  if (net === null) return null
  return Math.max(0, h.par - net + 2)
}

// Samme GIR-formel som round-stats.tsx sin isGir/round-detail.tsx sin
// GIR-auto-inferens: slag minus putt garanterer green i regulering når
// resultatet er par-2 eller bedre. Krever kjent slag+putt-tall.
function isGir(h: ApiHole): boolean {
  return h.score !== null && h.putts !== null && h.score - h.putts <= h.par - 2
}

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

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

function sum(values: number[]): number {
  return values.reduce((a, b) => a + b, 0)
}

function signedToPar(score: number, par: number): string {
  const diff = score - par
  if (diff === 0) return "E"
  return diff > 0 ? `+${diff}` : `−${Math.abs(diff)}`
}

// --- Score mark (the visual language for the Score row) --------------------
// Shape encodes direction (circle = under par, square = over par) so meaning
// survives without color; fill encodes magnitude (filled = 2+ strokes off).

function ScoreMark({ gross, par, pickedUp }: { gross: number | null; par: number; pickedUp?: boolean }) {
  const shared =
    "flex size-6 items-center justify-center text-sm font-extrabold tabular-nums sm:size-7"
  // "Plukket opp" (migrasjon 038, Stableford) -- 0 poeng er impliserte,
  // vises som "PU" i stedet for det underliggende Net Double Bogey-tallet.
  if (pickedUp) {
    return (
      <span className={cn(shared, "rounded-[4px] border-2 border-brand-orange bg-brand-orange/10 text-[10px] text-brand-orange")}>
        PU
      </span>
    )
  }
  if (gross === null) {
    return <span className={cn(shared, "text-muted-foreground")}>{"–"}</span>
  }
  const kind = classify(gross, par)
  if (kind === "eagle") {
    return (
      <span className={cn(shared, "rounded-full bg-primary text-primary-foreground")}>{gross}</span>
    )
  }
  if (kind === "birdie") {
    return (
      <span className={cn(shared, "rounded-full border-2 border-primary bg-primary/10 text-primary")}>
        {gross}
      </span>
    )
  }
  if (kind === "bogey") {
    return (
      <span
        className={cn(
          shared,
          "rounded-[4px] border-2 border-brand-orange bg-brand-orange/10 text-brand-orange",
        )}
      >
        {gross}
      </span>
    )
  }
  if (kind === "double") {
    return (
      <span className={cn(shared, "rounded-[4px] bg-brand-orange text-brand-orange-foreground")}>
        {gross}
      </span>
    )
  }
  // par
  return <span className={cn(shared, "font-bold text-foreground")}>{gross}</span>
}

// --- One nine-hole block (a real table for row/column semantics) -----------

// Kompakte retnings-/treff-ikoner for de nye detaljradene (2026-08-03) --
// samme ikonvalg som DirectionCross (round-detail.tsx sin registrerings-
// veiviser) allerede bruker, kun skalert ned for en tett tabellcelle.
// Bøyde piler for Fairway (utslaget DRIVER av til en side, ulikt et
// innspill som bommer rett til siden -- derfor ArrowUpLeft/-Right her,
// ikke de rette ArrowLeft/-Right Innspill-raden bruker).
function FairwayIcon({ result }: { result: ApiHole["tee_shot_result"] }) {
  if (result === null) return null
  if (result === "fairway") return <Target aria-hidden="true" className="mx-auto size-3.5 text-primary" />
  if (result === "left") return <ArrowUpLeft aria-hidden="true" className="mx-auto size-3.5 text-brand-orange" />
  return <ArrowUpRight aria-hidden="true" className="mx-auto size-3.5 text-brand-orange" />
}

function ApproachIcon({ result }: { result: ApiHole["approach_result"] }) {
  if (result === null) return null
  const cls = "mx-auto size-3.5"
  if (result === "hit") return <Target aria-hidden="true" className={cn(cls, "text-primary")} />
  if (result === "long") return <ArrowUp aria-hidden="true" className={cn(cls, "text-brand-orange")} />
  if (result === "short") return <ArrowDown aria-hidden="true" className={cn(cls, "text-brand-orange")} />
  if (result === "left") return <ArrowLeft aria-hidden="true" className={cn(cls, "text-brand-orange")} />
  return <ArrowRight aria-hidden="true" className={cn(cls, "text-brand-orange")} />
}

function ScoreBlock({
  holes,
  summaryLabel,
  showStableford,
  showFairway,
  showPutts,
  showGir,
  showApproach,
  showChip,
  showBunker,
  showPenalty,
  showAnyway,
}: {
  holes: ApiHole[]
  summaryLabel: string
  showStableford: boolean
  showFairway: boolean
  showPutts: boolean
  showGir: boolean
  showApproach: boolean
  showChip: boolean
  showBunker: boolean
  showPenalty: boolean
  showAnyway: boolean
}) {
  const parSum = sum(holes.map((h) => h.par))
  const playedHoles = holes.filter((h) => h.played && h.score !== null)
  const grossSum = playedHoles.length > 0 ? sum(playedHoles.map((h) => h.score as number)) : null
  const nettoValues = holes.map(netScore).filter((n): n is number => n !== null)
  const nettoSum = nettoValues.length > 0 ? sum(nettoValues) : null
  const pointsValues = holes.map(stablefordPoints).filter((n): n is number => n !== null)
  const pointsSum = pointsValues.length > 0 ? sum(pointsValues) : null

  // Fairwaytreff spores kun på ikke-par-3-hull (samme regel som
  // round-detail.tsx sin "Så langt i runden"), summen teller kun "Blink".
  const fairwayEligible = holes.filter((h) => h.par !== 3 && h.tee_shot_result !== null)
  const fairwayHitSum = fairwayEligible.length > 0 ? fairwayEligible.filter((h) => h.tee_shot_result === "fairway").length : null
  const puttsValues = holes.filter((h) => h.putts !== null).map((h) => h.putts as number)
  const puttsSum = puttsValues.length > 0 ? sum(puttsValues) : null
  const girEligible = holes.filter((h) => h.score !== null && h.putts !== null)
  const girSum = girEligible.length > 0 ? girEligible.filter(isGir).length : null
  const chipValues = holes.filter((h) => h.chip_count !== null).map((h) => h.chip_count as number)
  const chipSum = chipValues.length > 0 ? sum(chipValues) : null
  const bunkerValues = holes.filter((h) => h.bunker_shot_count !== null).map((h) => h.bunker_shot_count as number)
  const bunkerSum = bunkerValues.length > 0 ? sum(bunkerValues) : null
  const penaltyValues = holes.filter((h) => h.penalty_strokes !== null).map((h) => h.penalty_strokes as number)
  const penaltySum = penaltyValues.length > 0 ? sum(penaltyValues) : null
  const anywayValues = holes.filter((h) => h.anyway_strokes !== null).map((h) => h.anyway_strokes as number)
  const anywaySum = anywayValues.length > 0 ? sum(anywayValues) : null

  const cell = "border border-border text-center tabular-nums"
  const sumCell = "border border-border bg-muted text-center font-extrabold tabular-nums"
  // whitespace-nowrap + overflow-hidden (2026-08-03, rapportert av bruker --
  // "Score"/"Netto" bløuted ut over kolonnegrensen) -- w-11 gir litt mer
  // rom enn tidligere w-9, men de VIRKELIG korte etikettene (forkortelser)
  // under er den egentlige fiksen, ikke bare bredden alene.
  const rowLabel =
    "border border-border px-1 text-left text-xs font-bold text-muted-foreground whitespace-nowrap overflow-hidden"

  return (
    <table className="w-full table-fixed border-collapse">
      <caption className="sr-only">
        Scorekort hull {holes[0].hole_number} til {holes[holes.length - 1].hole_number}
      </caption>
      <colgroup>
        <col className="w-11" />
        {holes.map((h) => (
          <col key={h.hole_number} />
        ))}
        <col className="w-[11%]" />
      </colgroup>
      <thead>
        <tr>
          <th scope="col" className="border border-border bg-muted px-1 py-1 text-left">
            <span className="sr-only">Rad</span>
            <span aria-hidden="true" className="text-xs font-extrabold text-muted-foreground">
              Hull
            </span>
          </th>
          {holes.map((h) => (
            <th
              key={h.hole_number}
              scope="col"
              className="border border-border bg-foreground py-1 text-center text-xs font-extrabold tabular-nums text-background"
            >
              {h.hole_number}
            </th>
          ))}
          <th
            scope="col"
            className="border border-border bg-primary py-1 text-center text-xs font-extrabold uppercase text-primary-foreground"
          >
            {summaryLabel}
          </th>
        </tr>
      </thead>
      <tbody>
        {/* Hcp (stroke index) */}
        <tr>
          <th scope="row" className={rowLabel}>
            Hcp
          </th>
          {holes.map((h) => (
            <td key={h.hole_number} className={cn(cell, "py-1 text-xs font-semibold text-muted-foreground")}>
              {h.stroke_index}
            </td>
          ))}
          <td className={cn(sumCell, "py-1 text-xs text-muted-foreground")} aria-hidden="true">
            {"–"}
          </td>
        </tr>
        {/* Par */}
        <tr>
          <th scope="row" className={rowLabel}>
            Par
          </th>
          {holes.map((h) => (
            <td key={h.hole_number} className={cn(cell, "py-1 text-xs font-semibold text-muted-foreground")}>
              {h.par}
            </td>
          ))}
          <td className={cn(sumCell, "py-1 text-xs text-foreground")}>{parSum}</td>
        </tr>
        {/* Score — the hero row: highlighted band + shape marks */}
        <tr className="bg-primary/5">
          <th scope="row" className={cn(rowLabel, "text-sm text-foreground")}>
            Score
          </th>
          {holes.map((h) => (
            <td key={h.hole_number} className={cn(cell, "px-0.5 py-1")}>
              <span className="flex items-center justify-center">
                <ScoreMark gross={h.played ? h.score : null} par={h.par} pickedUp={h.picked_up} />
              </span>
            </td>
          ))}
          <td className={cn(sumCell, "py-1 text-base text-foreground")}>{grossSum ?? "–"}</td>
        </tr>
        {/* Netto */}
        <tr>
          <th scope="row" className={cn(rowLabel, "text-foreground")}>
            Netto
          </th>
          {holes.map((h) => (
            <td key={h.hole_number} className={cn(cell, "py-1 text-xs font-bold text-foreground")}>
              {netScore(h) ?? "–"}
            </td>
          ))}
          <td className={cn(sumCell, "py-1 text-xs text-foreground")}>{nettoSum ?? "–"}</td>
        </tr>
        {/* Stableford (only when relevant) */}
        {showStableford && (
          <tr>
            <th scope="row" className={rowLabel} title="Stableford-poeng">
              Poeng
            </th>
            {holes.map((h) => (
              <td key={h.hole_number} className={cn(cell, "py-1 text-xs font-semibold text-muted-foreground")}>
                {stablefordPoints(h) ?? "–"}
              </td>
            ))}
            <td className={cn(sumCell, "py-1 text-xs text-foreground")}>{pointsSum ?? "–"}</td>
          </tr>
        )}
        {/* Fairway -- ikon per hull (bøyd pil = utslaget drev til siden,
            blink = fairwaytreff), sum = antall fairwaytreff. */}
        {showFairway && (
          <tr>
            <th scope="row" className={rowLabel} title="Fairwaytreff">
              Fw
            </th>
            {holes.map((h) => (
              <td key={h.hole_number} className={cn(cell, "py-1")}>
                {h.par !== 3 ? <FairwayIcon result={h.tee_shot_result} /> : <span aria-hidden="true">–</span>}
              </td>
            ))}
            <td className={cn(sumCell, "py-1 text-xs text-foreground")}>{fairwayHitSum ?? "–"}</td>
          </tr>
        )}
        {/* Putter -- antall per hull, summert. */}
        {showPutts && (
          <tr>
            <th scope="row" className={rowLabel} title="Putter">
              Putt
            </th>
            {holes.map((h) => (
              <td key={h.hole_number} className={cn(cell, "py-1 text-xs font-semibold text-muted-foreground")}>
                {h.putts ?? "–"}
              </td>
            ))}
            <td className={cn(sumCell, "py-1 text-xs text-foreground")}>{puttsSum ?? "–"}</td>
          </tr>
        )}
        {/* GIR -- sjekkmerke når hullet er nådd i regulering, summert. */}
        {showGir && (
          <tr>
            <th scope="row" className={rowLabel} title="Green i regulering">
              GIR
            </th>
            {holes.map((h) => (
              <td key={h.hole_number} className={cn(cell, "py-1")}>
                {h.score !== null && h.putts !== null ? (
                  isGir(h) ? (
                    <Check aria-hidden="true" className="mx-auto size-3.5 text-primary" />
                  ) : (
                    <span aria-hidden="true" className="text-muted-foreground">
                      –
                    </span>
                  )
                ) : (
                  <span aria-hidden="true">–</span>
                )}
              </td>
            ))}
            <td className={cn(sumCell, "py-1 text-xs text-foreground")}>{girSum ?? "–"}</td>
          </tr>
        )}
        {/* Innspill -- 5-veis retning, ingen meningsfull sum (retning, ikke
            et tall). */}
        {showApproach && (
          <tr>
            <th scope="row" className={rowLabel} title="Innspill">
              Appr.
            </th>
            {holes.map((h) => (
              <td key={h.hole_number} className={cn(cell, "py-1")}>
                <ApproachIcon result={h.approach_result} />
                {h.approach_result === null && <span aria-hidden="true">–</span>}
              </td>
            ))}
            <td className={cn(sumCell, "py-1 text-xs text-muted-foreground")} aria-hidden="true">
              –
            </td>
          </tr>
        )}
        {/* Chip -- antall per hull, summert. */}
        {showChip && (
          <tr>
            <th scope="row" className={rowLabel} title="Chip">
              Chip
            </th>
            {holes.map((h) => (
              <td key={h.hole_number} className={cn(cell, "py-1 text-xs font-semibold text-muted-foreground")}>
                {h.chip_count ?? "–"}
              </td>
            ))}
            <td className={cn(sumCell, "py-1 text-xs text-foreground")}>{chipSum ?? "–"}</td>
          </tr>
        )}
        {/* Bunkerslag -- antall per hull, summert. */}
        {showBunker && (
          <tr>
            <th scope="row" className={rowLabel} title="Bunkerslag">
              Bnk
            </th>
            {holes.map((h) => (
              <td key={h.hole_number} className={cn(cell, "py-1 text-xs font-semibold text-muted-foreground")}>
                {h.bunker_shot_count ?? "–"}
              </td>
            ))}
            <td className={cn(sumCell, "py-1 text-xs text-foreground")}>{bunkerSum ?? "–"}</td>
          </tr>
        )}
        {/* Straffeslag -- antall per hull, summert. */}
        {showPenalty && (
          <tr>
            <th scope="row" className={rowLabel} title="Straffeslag">
              Str
            </th>
            {holes.map((h) => (
              <td key={h.hole_number} className={cn(cell, "py-1 text-xs font-semibold text-muted-foreground")}>
                {h.penalty_strokes ?? "–"}
              </td>
            ))}
            <td className={cn(sumCell, "py-1 text-xs text-foreground")}>{penaltySum ?? "–"}</td>
          </tr>
        )}
        {/* Anywayslag -- antall per hull, summert. */}
        {showAnyway && (
          <tr>
            <th scope="row" className={rowLabel} title="Anywayslag">
              Any.
            </th>
            {holes.map((h) => (
              <td key={h.hole_number} className={cn(cell, "py-1 text-xs font-semibold text-muted-foreground")}>
                {h.anyway_strokes ?? "–"}
              </td>
            ))}
            <td className={cn(sumCell, "py-1 text-xs text-foreground")}>{anywaySum ?? "–"}</td>
          </tr>
        )}
      </tbody>
    </table>
  )
}

// --- Combined total --------------------------------------------------------

function TotalTile({ label, value, tone }: { label: string; value: string; tone?: "orange" | "green" }) {
  return (
    <div className="flex flex-col items-center gap-0.5 rounded-2xl border border-border bg-card px-2 py-3">
      <span
        className={cn(
          "text-2xl font-extrabold tabular-nums",
          tone === "orange" ? "text-brand-orange" : tone === "green" ? "text-primary" : "text-foreground",
        )}
      >
        {value}
      </span>
      <span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">{label}</span>
    </div>
  )
}

// --- Matchforløp / skins-forløp (2026-07-28) ---------------------------------
// Avledet ved LESING fra GET .../format-result -- viser HVORFOR et hull ble
// avgjort som det ble (brutto vs. netto, slag mottatt, hvem sitt netto
// talte for siden i fourball, hvem som vant/hvilket hull som rullet videre
// i skins) -- ikke bare et vinn/tap-merke.

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

type ApiFormatHoleOut = {
  hole_number: number
  par: number
  stroke_index: number
  entries: ApiFormatHoleEntry[]
  result: string | null
  winner_ids: string[] | null
  carried: boolean | null
}

type ApiFormatResult = {
  play_format: string
  ready: boolean
  match_status_text: string | null
  skins_won: Record<string, number> | null
  holes: ApiFormatHoleOut[] | 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
}

// --- Matchforløp: horisontalt scorekort med løpende stilling ---------------
// Egen visuell utforming (ikke en kopi av noe referansebilde) -- gjenbruker
// appens etablerte vinn/tap-språk (grønn=side A, oransje=side B, nøytral=delt,
// se ScoreMark/ResultChip andre steder) fremfor referansens rød/blå, og
// erstatter den tidligere vertikale hull-for-hull-listen med et horisontalt
// rutenett (Hull/Par/enhet-rader/Stilling) + en identitets-/resultatbanner --
// samme informasjon (hvem vant hvilket hull, AS/X UP/X&Y), annen presentasjon.

type RunningState = {
  lead: number
  label: string
  tone: "a" | "b" | "neutral"
  isClosed: boolean
}

// Speiler handicap_engine.py sin compute_match_state()/describe() presist,
// men regner ett PREFIKS om gangen slik at hvert hull får sin egen løpende
// stilling (backend cacher i dag kun SLUTT-tilstanden -- se ADR-039-notatet).
function computeRunning(results: ("a" | "b" | "halved")[], totalHoles: number): RunningState[] {
  const out: RunningState[] = []
  let lead = 0
  for (let i = 0; i < results.length; i++) {
    lead += results[i] === "a" ? 1 : results[i] === "b" ? -1 : 0
    const holesPlayed = i + 1
    const holesRemaining = totalHoles - holesPlayed
    const isClosed = Math.abs(lead) > holesRemaining
    const isDormie = !isClosed && holesRemaining > 0 && Math.abs(lead) === holesRemaining
    const margin = Math.abs(lead)
    let label: string
    if (isClosed) {
      label = holesRemaining === 0 && margin > 0 ? `${margin} UP` : `${margin}&${holesRemaining}`
    } else if (lead === 0) {
      label = "AS"
    } else {
      label = isDormie ? `Dormie ${margin}` : `${margin} UP`
    }
    out.push({ lead, label, tone: lead > 0 ? "a" : lead < 0 ? "b" : "neutral", isClosed })
  }
  return out
}

type HeaderIdentity = { name: string; hcp: number | null }

// Territorium-bar (2026-07-29, brukerens eget forslag: den ledende sidens
// sone strekker seg proporsjonalt forbi midtlinjen inn på motstanderens
// halvdel, i stedet for en statisk 50/50-boks med tekst i midten). Ved AS
// eller ingen hull avgjort ennå er begge soner like store og nøytralt
// farget. Egen lokal kopi av samme idé som round-detail.tsx sin
// FormatResultPanel, per prosjektets "én fil, én kopi"-konvensjon.
function leadZoneFraction(lead: number, totalHoles: number): number {
  if (totalHoles <= 0) return 0.5
  const fraction = 0.5 + (lead / totalHoles) * 0.5
  return Math.min(0.82, Math.max(0.18, fraction))
}

function LeadZone({ identity, side, dominant }: { identity: HeaderIdentity; side: "a" | "b"; dominant: boolean }) {
  const textTone = dominant ? (side === "a" ? "text-primary-foreground" : "text-brand-orange-foreground") : "text-foreground"
  const subTone = dominant
    ? side === "a"
      ? "text-primary-foreground/75"
      : "text-brand-orange-foreground/75"
    : "text-muted-foreground"
  // Boksen STREKKER SEG (ingen items-start/items-end) -- ellers sizes den
  // etter innholdets egen bredde, ikke sonens faktiske tildelte bredde, og
  // kan visuelt lekke inn i midt-kolonnen på smale skjermer. Justering skjer
  // med justify-start/justify-end + text-align inni en boks som alltid har
  // full, korrekt bredde -- truncate virker da presist.
  return (
    <div className="flex h-full min-w-0 w-full flex-col justify-center gap-0.5 px-3">
      <div
        className={cn(
          "flex min-w-0 items-center gap-1.5",
          side === "a" ? "justify-start" : "justify-end",
          side === "b" && "flex-row-reverse",
        )}
      >
        {!dominant && (
          <span
            aria-hidden="true"
            className={cn("size-2 shrink-0 rounded-full", side === "a" ? "bg-primary" : "bg-brand-orange")}
          />
        )}
        <span className={cn("min-w-0 truncate text-sm font-extrabold", textTone)}>{identity.name}</span>
      </div>
      {identity.hcp !== null && (
        <span
          className={cn(
            "truncate text-xs font-bold tabular-nums",
            subTone,
            side === "a" ? "text-left" : "text-right",
          )}
        >
          HCP {identity.hcp}
        </span>
      )}
    </div>
  )
}

function LeadBar({
  identityA,
  identityB,
  state,
  holesPlayed,
  totalHoles,
}: {
  identityA: HeaderIdentity
  identityB: HeaderIdentity
  state: RunningState | null
  holesPlayed: number
  totalHoles: number
}) {
  const lead = state?.lead ?? 0
  const tone = state?.tone ?? "neutral"
  const widthA = state ? leadZoneFraction(lead, totalHoles) * 100 : 50
  const widthB = 100 - widthA
  const zoneAClass = tone === "a" ? "bg-primary" : tone === "b" ? "bg-primary/12" : "bg-muted"
  const zoneBClass = tone === "b" ? "bg-brand-orange" : tone === "a" ? "bg-brand-orange/12" : "bg-muted"
  return (
    // CSS Grid, ikke absolutt overlegg (2026-07-29-fiks, se round-detail.tsx
    // sin FormatResultPanel for full begrunnelse) -- midt-kolonnen (status)
    // er `auto`-bredde, de to sonene `minmax(0, X fr)` -- reserverer alltid
    // nøyaktig plass til statusboksen, kan aldri overlappe et langt navn.
    <div
      className="grid h-[76px] w-full overflow-hidden rounded-2xl sm:h-20"
      style={{ gridTemplateColumns: `minmax(0, ${widthA}fr) auto minmax(0, ${widthB}fr)` }}
    >
      <div className={cn("h-full min-w-0 transition-[grid-template-columns] duration-300", zoneAClass)}>
        <LeadZone identity={identityA} side="a" dominant={tone === "a"} />
      </div>
      <div className="flex h-full items-center justify-center px-2">
        <div className="flex flex-col items-center rounded-xl border border-border/60 bg-card/95 px-3 py-1.5 shadow-sm shadow-black/10 backdrop-blur-sm">
          {state ? (
            <>
              <span
                className={cn(
                  "text-xl font-extrabold leading-none tabular-nums sm:text-2xl",
                  tone === "a" ? "text-primary" : tone === "b" ? "text-brand-orange" : "text-foreground",
                )}
              >
                {state.label}
              </span>
              {state.isClosed ? (
                <span className="mt-1 rounded-full bg-muted px-2 py-0.5 text-[10px] font-extrabold uppercase tracking-wide text-muted-foreground">
                  Ferdig
                </span>
              ) : (
                <span className="mt-1 text-[11px] font-semibold text-muted-foreground">{holesPlayed} hull spilt</span>
              )}
            </>
          ) : (
            <span className="text-xs font-bold text-muted-foreground text-pretty">Ingen hull spilt ennå</span>
          )}
        </div>
      </div>
      <div className={cn("h-full min-w-0 transition-[grid-template-columns] duration-300", zoneBClass)}>
        <LeadZone identity={identityB} side="b" dominant={tone === "b"} />
      </div>
    </div>
  )
}

type MatchRow = { id: string; side: "a" | "b"; label: string; hcp: number | null }

export function MatchScorecardGrid({
  round,
  roundId,
  viewerId,
  refreshKey,
  apiPrefix = "/rounds",
}: {
  round: ApiRound
  roundId: string
  viewerId: string | null
  refreshKey: number
  apiPrefix?: string
}) {
  const result = useFormatResult(roundId, refreshKey, apiPrefix)
  if (!result) return null

  const isSharedBall = SHARED_BALL_FORMATS.includes(round.play_format)
  // scramble_solo_match (migrasjon 057) -- ÉN rad per SIDE (som
  // delt-ball-grenen under), IKKE én per deltaker, siden laget kan ha
  // vilkårlig mange medlemmer som alle deler samme TeamAverage-verdi --
  // uten denne egne grenen ville "else"-grenen under vist N dupliserte
  // rader for laget. Individualsidens rad viser spillerens EGET navn
  // (ikke bare sidens "Individuell motstander"-label) siden den siden
  // alltid har nøyaktig én deltaker.
  const isScrambleSoloMatch = SCRAMBLE_SOLO_FORMATS.has(round.play_format)

  const rows: MatchRow[] = isScrambleSoloMatch
    ? round.sides.map((s, idx) => {
        // side="a"/"b" er POSISJONELL (idx===0 -> "a"), samme sorterte-
        // etter-id-konvensjon som backend sin _build_scramble_solo_match_
        // result bruker (round.sides hentes ALLTID `ORDER BY id`, samme
        // sortering backend legger til grunn for "a"/"b"-tildelingen --
        // IKKE en fast lag=a/individuell=b-tildeling, som ville gitt feil
        // ledelsesfarge når laget ikke er den lavest-sorterte UUID-en).
        // row.id må for individualsiden være DELTAKERENS id (ikke sidens),
        // siden FormatHoleEntry.id for den siden er individual_
        // participant["id"] på backend-siden -- ellers finner
        // h.entries.find(e => e.id === row.id) under aldri en match.
        const sideParticipants = round.participants.filter((p) => p.round_side_id === s.id)
        const isTeam = s.side_role === "team"
        const label =
          !isTeam && sideParticipants.length === 1
            ? participantLabel(sideParticipants[0], viewerId)
            : s.label?.trim() || (isTeam ? "Laget" : "Individuell motstander")
        return {
          id: isTeam ? s.id : (sideParticipants[0]?.id ?? s.id),
          side: idx === 0 ? "a" : ("b" as const),
          label,
          hcp: sideParticipants[0]?.playing_handicap ?? null,
        }
      })
    : isSharedBall
      ? round.sides.map((s, idx) => ({
          id: s.id,
          side: idx === 0 ? "a" : ("b" as const),
          label: s.label?.trim() || (idx === 0 ? "Side A" : "Side B"),
          hcp: round.participants.find((p) => p.round_side_id === s.id)?.playing_handicap ?? null,
        }))
      : round.participants
          .filter((p) => p.round_side_id)
          .map((p) => ({
            id: p.id,
            side: (p.round_side_id === round.sides[0]?.id ? "a" : "b") as "a" | "b",
            label: participantLabel(p, viewerId),
            hcp: p.playing_handicap,
          }))

  // Lagvis gruppering (2026-08-02, "det store grepet") -- individuell-ball-
  // formater (match/fourball) bygde tidligere rader i ren
  // innsettingsrekkefølge (round.participants sin egen orden), ikke lagvis.
  // Stabil sortering på side holder original rekkefølge INNAD i hver
  // gruppe. Delt-ball-formater har allerede nøyaktig én rad per side
  // (rows[0]="a", rows[1]="b" fra konstruksjonen over) -- no-op der.
  rows.sort((a, b) => (a.side === b.side ? 0 : a.side === "a" ? -1 : 1))

  function headerIdentity(side: "a" | "b"): HeaderIdentity {
    const sideRows = rows.filter((r) => r.side === side)
    if (sideRows.length === 1) return { name: sideRows[0].label, hcp: sideRows[0].hcp }
    const idx = side === "a" ? 0 : 1
    return { name: round.sides[idx]?.label?.trim() || (side === "a" ? "Side A" : "Side B"), hcp: null }
  }

  if (!result.ready || !result.holes) {
    return (
      <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 matchforløpet kan vises.
      </div>
    )
  }

  const decidedHoles = result.holes
  const running = computeRunning(decidedHoles.map((h) => h.result as "a" | "b" | "halved"), round.holes_planned)
  const lastState = running.length > 0 ? running[running.length - 1] : null

  return (
    <div className="flex flex-col gap-3 rounded-3xl border border-border bg-card p-3 shadow-md shadow-black/8 sm:p-4">
      <LeadBar
        identityA={headerIdentity("a")}
        identityB={headerIdentity("b")}
        state={lastState}
        holesPlayed={decidedHoles.length}
        totalHoles={round.holes_planned}
      />

      {decidedHoles.length === 0 ? (
        <p className="text-sm text-muted-foreground">
          Ingen hull er avgjort ennå. Matchforløpet vises her hull for hull etter hvert som begge sider har
          registrert.
        </p>
      ) : (
        <div className="-mx-3 overflow-x-auto px-3 sm:-mx-4 sm:px-4">
          <table
            className="w-full border-collapse text-sm"
            style={{ minWidth: `${64 + decidedHoles.length * 40}px` }}
          >
            <colgroup>
              <col className="w-16" />
              {decidedHoles.map((h) => (
                <col key={h.hole_number} className="w-10" />
              ))}
            </colgroup>
            <thead>
              <tr>
                <th
                  scope="col"
                  className="border border-border bg-muted px-2 py-1.5 text-left text-xs font-extrabold text-muted-foreground"
                >
                  Hull
                </th>
                {decidedHoles.map((h) => (
                  <th
                    key={h.hole_number}
                    scope="col"
                    className="border border-border bg-foreground py-1.5 text-center text-xs font-extrabold tabular-nums text-background"
                  >
                    {h.hole_number}
                  </th>
                ))}
              </tr>
            </thead>
            <tbody>
              <tr>
                <th
                  scope="row"
                  className="border border-border px-2 py-1.5 text-left text-xs font-bold text-muted-foreground"
                >
                  Par
                </th>
                {decidedHoles.map((h) => (
                  <td
                    key={h.hole_number}
                    className="border border-border py-1.5 text-center text-xs font-semibold text-muted-foreground"
                  >
                    {h.par}
                  </td>
                ))}
              </tr>
              {rows.map((row) => (
                <tr key={row.id}>
                  <th scope="row" className="border border-border px-2 py-1.5 text-left">
                    <span className="flex min-w-0 items-center gap-1.5">
                      <span
                        aria-hidden="true"
                        className={cn(
                          "size-2 shrink-0 rounded-full",
                          row.side === "a" ? "bg-primary" : "bg-brand-orange",
                        )}
                      />
                      <span className="min-w-0 truncate text-xs font-bold text-foreground">{row.label}</span>
                    </span>
                  </th>
                  {decidedHoles.map((h) => {
                    const entry = h.entries.find((e) => e.id === row.id)
                    const won = !!entry && h.result === row.side && entry.counted !== false
                    const shown = entry ? (entry.net ?? entry.gross) : null
                    return (
                      <td key={h.hole_number} className="border border-border p-0.5 text-center">
                        <span
                          aria-label={
                            entry
                              ? `${row.label}: ${shown} netto${entry.gross !== null && entry.gross !== shown ? `, ${entry.gross} brutto` : ""}${won ? " -- vant hullet" : ""}`
                              : undefined
                          }
                          className={cn(
                            "mx-auto flex size-7 items-center justify-center rounded-full text-xs font-extrabold tabular-nums",
                            won
                              ? row.side === "a"
                                ? "bg-primary text-primary-foreground"
                                : "bg-brand-orange text-brand-orange-foreground"
                              : entry && entry.counted === false
                                ? "text-muted-foreground/50"
                                : "text-foreground",
                          )}
                        >
                          {shown ?? "–"}
                        </span>
                      </td>
                    )
                  })}
                </tr>
              ))}
              <tr className="bg-muted/30">
                <th scope="row" className="border border-border px-2 py-1.5 text-left text-xs font-extrabold text-foreground">
                  Stilling
                </th>
                {running.map((s, i) => (
                  <td
                    key={decidedHoles[i].hole_number}
                    className={cn(
                      "border border-border py-1.5 text-center text-xs font-extrabold tabular-nums",
                      s.tone === "a" ? "text-primary" : s.tone === "b" ? "text-brand-orange" : "text-muted-foreground",
                    )}
                  >
                    {s.label}
                  </td>
                ))}
              </tr>
            </tbody>
          </table>
        </div>
      )}
      <p className="text-xs text-muted-foreground">
        Fylt sirkel = vunnet hull (grønn = {headerIdentity("a").name}, oransje = {headerIdentity("b").name}). Nedtonet
        tall talte ikke for siden det hullet (laveste netto teller, gjelder fourball).
      </p>
    </div>
  )
}

function SkinsProgressTable({
  roundId,
  refreshKey,
  apiPrefix = "/rounds",
}: {
  roundId: string
  refreshKey: number
  apiPrefix?: string
}) {
  const result = useFormatResult(roundId, refreshKey, apiPrefix)
  if (!result || !result.holes || result.holes.length === 0) return null

  const hasNet = result.holes.some((h) => h.entries.some((e) => e.net !== null))

  return (
    <div className="flex flex-col gap-3 rounded-3xl border border-border bg-card p-3 shadow-md shadow-black/8 sm:p-4">
      <h2 className="text-base font-bold text-foreground">Skins, hull for hull</h2>
      <div className="flex flex-col divide-y divide-border overflow-hidden rounded-2xl border border-border">
        {result.holes.map((h) => (
          <div key={h.hole_number} className="flex flex-col gap-1.5 px-3 py-2.5">
            <div className="flex flex-wrap items-center justify-between gap-2">
              <span className="text-xs font-bold text-muted-foreground">
                Hull {h.hole_number} · Par {h.par}
              </span>
              {h.carried ? (
                <span className="rounded-full border border-border bg-muted px-2 py-0.5 text-[11px] font-bold text-muted-foreground">
                  Uavgjort — rullet videre
                </span>
              ) : h.winner_ids && h.winner_ids.length > 0 ? (
                <span className="rounded-full bg-primary px-2 py-0.5 text-[11px] font-bold text-primary-foreground">
                  {h.winner_ids.length === 1
                    ? `${h.entries.find((e) => e.id === h.winner_ids?.[0])?.label ?? "?"} vant skinnet`
                    : `Delt: ${h.winner_ids.map((id) => h.entries.find((e) => e.id === id)?.label ?? "?").join(", ")}`}
                </span>
              ) : null}
            </div>
            <div className="flex flex-wrap gap-x-4 gap-y-1">
              {h.entries.map((e) => {
                const isWinner = h.winner_ids?.includes(e.id) ?? false
                const shown = hasNet && e.net !== null ? e.net : e.gross
                return (
                  <span
                    key={e.id}
                    className={cn(
                      "flex items-baseline gap-1 text-sm",
                      isWinner ? "font-extrabold text-primary" : "text-foreground",
                    )}
                  >
                    {e.label}: <span className="tabular-nums">{shown ?? "–"}</span>
                    {hasNet && e.net !== null && e.gross !== null && e.net !== e.gross && (
                      <span className="text-xs font-normal text-muted-foreground">({e.gross} brutto)</span>
                    )}
                  </span>
                )
              })}
            </div>
          </div>
        ))}
      </div>
      <p className="text-xs text-muted-foreground">
        {hasNet ? "Skins avgjøres på netto (brutto vist i parentes)." : "Skins avgjøres på brutto."} Et uavgjort hull
        ruller potten videre til neste hull.
      </p>
    </div>
  )
}

// --- Screen ----------------------------------------------------------------

export function RoundScorecard({
  roundId,
  embedded = false,
  publicMode = false,
}: {
  roundId: string
  // ADR-040 Beslutning D ("det store grepet") -- Scorekort+Statistikk
  // slått sammen til ÉN side. `embedded` dropper denne komponentens egen
  // sticky header (den delte `RoundHeader`, wired inn av
  // `RoundPageShell`, dekker det nå) når den rendres som øverste halvdel
  // av den slåtte sammen siden, i stedet for standalone.
  embedded?: boolean
  // ADR-044 (tilskuer-visning, 2026-08-06) -- gjenbruker DENNE komponenten
  // uendret for `/watch/{id}/scorecard`, kun med annen datakilde: alle
  // `/rounds/*`-kall byttes til `/public/rounds/*` (samme respons-form,
  // `_can_view_round`-autorisert i stedet for eier/medspiller-only), og
  // WS-kanalen byttes til den offentlige. INGEN visuell/atferdsmessig
  // endring når `publicMode` er `false` (standard) -- eiersiden er
  // uberørt.
  publicMode?: boolean
}) {
  const apiPrefix = publicMode ? "/public/rounds" : "/rounds"
  const [round, setRound] = useState<ApiRound | null>(null)
  const [unitId, setUnitId] = useState<string | null>(null)
  const [holes, setHoles] = useState<ApiHole[] | null>(null)
  const [error, setError] = useState<string | null>(null)
  const [viewerId, setViewerId] = useState<string | null>(null)
  // Sanntid (2026-07-26, oppfølging av ADR-027): et rent "noe endret seg"-
  // signal over WebSocket -- bumper denne, som er med i begge fetch-
  // effektene under, slik at de henter på nytt akkurat som ved
  // førstegangslasting (samme mønster som public-live.tsx).
  const [refreshKey, setRefreshKey] = useState(0)

  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 res = await fetch(`${apiPrefix}/${roundId}`, { credentials: "include" })
      if (res.status === 403 || res.status === 404) {
        if (!cancelled) setError("Denne runden finnes ikke, eller du har ikke tilgang til den.")
        return
      }
      if (!res.ok) {
        if (!cancelled) setError("Klarte ikke å hente runden. Prøv igjen om litt.")
        return
      }
      const data: ApiRound = await res.json()
      if (cancelled) return
      setRound(data)
      const isSharedBall = SHARED_BALL_FORMATS.includes(data.play_format)
      setUnitId((prev) => {
        if (prev) return prev
        // scramble_solo-familien (migrasjon 056/057) -- blandet enhetstype
        // (laget = en SIDE, individualspilleren = en PARTICIPANT, se
        // units-bygging under). Standardvalg: individualspillerens egen
        // rad hvis DET er den spørrende brukeren, ellers lagsiden.
        if (SCRAMBLE_SOLO_FORMATS.has(data.play_format)) {
          const individualSide = data.sides.find((s) => s.side_role === "individual")
          const individualParticipant = individualSide
            ? data.participants.find((p) => p.round_side_id === individualSide.id)
            : undefined
          if (viewerId && individualParticipant?.user_id === viewerId) return individualParticipant.id
          const teamSide = data.sides.find((s) => s.side_role === "team")
          return teamSide?.id ?? data.sides[0]?.id ?? null
        }
        if (isSharedBall) {
          const viewerParticipant = viewerId ? data.participants.find((p) => p.user_id === viewerId) : undefined
          return viewerParticipant?.round_side_id ?? data.sides[0]?.id ?? null
        }
        // Standardvalg: SIN EGEN rad hvis man er en lenket medspiller,
        // ellers eieren (samme som før ADR-036 fase 3-utvidelsen).
        return (
          (viewerId && data.participants.find((p) => p.user_id === viewerId)?.id) ??
          data.participants.find((p) => p.is_owner)?.id ??
          data.participants[0]?.id ??
          null
        )
      })
    }
    void load()
    return () => {
      cancelled = true
    }
  }, [roundId, viewerId, refreshKey, apiPrefix])

  useEffect(() => {
    if (!unitId || !round) return
    // scramble_solo (migrasjon 056) har BLANDEDE enhetstyper i samme
    // fane-liste (laget = side, individualspilleren = participant) -- slå
    // opp faktisk type fra selve unitId i stedet for et format-bredt
    // isSharedBall-flagg, som ikke kan uttrykke en blanding. Denne
    // oppslags-tilnærmingen er samtidig korrekt uendret for de rene
    // delt-ball-/individuell-ball-formatene (alle enheter er da alltid
    // den ene eller den andre typen).
    const isSideUnit = round.sides.some((s) => s.id === unitId)
    const endpoint = isSideUnit
      ? `${apiPrefix}/${roundId}/sides/${unitId}/holes`
      : `${apiPrefix}/${roundId}/participants/${unitId}/holes`
    let cancelled = false
    async function loadHoles() {
      const res = await fetch(endpoint, { credentials: "include" })
      if (!res.ok) return
      const data: ApiHole[] = await res.json()
      if (!cancelled) setHoles(data)
    }
    void loadHoles()
    return () => {
      cancelled = true
    }
  }, [roundId, unitId, 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) {
    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>
    )
  }

  const isSharedBall = SHARED_BALL_FORMATS.includes(round.play_format)
  const isTwoSided = TWO_SIDED_FORMATS.includes(round.play_format)
  const isSkins = round.play_format === "skins"
  const isScrambleSolo = SCRAMBLE_SOLO_FORMATS.has(round.play_format)
  const formatLabel = FORMAT_LABELS[round.play_format] ?? round.play_format

  // Delt-ball-formater OG scramble_solo trenger sidene opprettet (via
  // runde-siden sin "Spillere og runde"-fane) FØR noe scorekort gir
  // mening her -- vis en forklarende melding i stedet for en evig
  // lastespinner.
  if ((isSharedBall || isScrambleSolo) && round.sides.length < 2) {
    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" />
                Scorekort
              </Link>
            </div>
          </header>
        )}
        <main className="mx-auto flex max-w-xl flex-col gap-4 px-4 py-5">
          <div className="rounded-3xl border border-dashed border-border bg-card p-4 text-sm text-muted-foreground">
            {isScrambleSolo
              ? `${formatLabel} krever at både laget og individualspilleren er satt opp som egne sider. Opprett dem på selve runde-siden (fanen "Spillere og runde") før scorekortet kan vises her.`
              : `${formatLabel} spilles med to sider (ett felles slagtall per side). Opprett sidene på selve runde-siden (fanen "Spillere og runde") før scorekortet kan vises her.`}
          </div>
        </main>
      </div>
    )
  }

  // scramble_solo (migrasjon 056) -- blandet enhetsliste: laget er en
  // SIDE-fane (delt ball), individualspilleren er en PARTICIPANT-fane
  // (egen ball) -- IKKE lagt til SHARED_BALL_FORMATS, det ville feilaktig
  // behandlet BEGGE fanene som sider (se hull-hente-effekten over, som
  // slår opp faktisk type fra unitId).
  const units: { id: string; label: string }[] = isScrambleSolo
    ? (() => {
        const teamSide = round.sides.find((s) => s.side_role === "team")
        const individualSide = round.sides.find((s) => s.side_role === "individual")
        const individualParticipant = individualSide
          ? round.participants.find((p) => p.round_side_id === individualSide.id)
          : undefined
        const list: { id: string; label: string }[] = []
        if (teamSide) list.push({ id: teamSide.id, label: teamSide.label?.trim() || "Laget" })
        if (individualParticipant) list.push({ id: individualParticipant.id, label: participantLabel(individualParticipant, viewerId) })
        return list
      })()
    : isSharedBall
      ? round.sides.map((s, idx) => ({ id: s.id, label: s.label?.trim() || (idx === 0 ? "Side A" : "Side B") }))
      : round.participants.map((p) => ({ id: p.id, label: participantLabel(p, viewerId) }))

  if (!holes) {
    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>
    )
  }

  // Samme sirkulære start_hole-rekkefølge som round-detail.tsx/round-stats.tsx
  // -- "Ut"/"Inn" er dermed første/andre halvdel AV SPILLEREKKEFØLGEN, ikke
  // nødvendigvis fysisk hull 1-9/10-18 (relevant kun for en 18-hulls runde
  // med et annet starthull enn 1, uvanlig men strukturelt tillatt).
  const holeOrder = Array.from(
    { length: round.holes_planned },
    (_, i) => ((round.start_hole - 1 + i) % 18) + 1,
  )
  const orderedHoles = holeOrder
    .map((n) => holes.find((h) => h.hole_number === n))
    .filter((h): h is ApiHole => h !== undefined)
  const front = orderedHoles.slice(0, 9)
  const back = orderedHoles.slice(9, 18)

  const showStableford = holes.some((h) => h.strokes_received !== null)
  // Detaljrader (2026-08-03) -- vis kun rader som faktisk har data et sted
  // i runden (samme "vis kun det som faktisk finnes"-prinsipp som resten
  // av appen), regnet ut fra HELE runden (ikke bare front/back for seg)
  // slik at begge blokkene konsekvent viser/skjuler de samme radene.
  const showFairway = holes.some((h) => h.par !== 3 && h.tee_shot_result !== null)
  const showPutts = holes.some((h) => h.putts !== null)
  const showGir = holes.some((h) => h.score !== null && h.putts !== null)
  const showApproach = holes.some((h) => h.approach_result !== null)
  const showChip = holes.some((h) => h.chip_count !== null)
  const showBunker = holes.some((h) => h.bunker_shot_count !== null)
  const showPenalty = holes.some((h) => h.penalty_strokes !== null)
  const showAnyway = holes.some((h) => h.anyway_strokes !== null)
  const playedHoles = orderedHoles.filter((h) => h.played && h.score !== null)
  // Full rundens par (statisk referanse, f.eks. "Par 72") -- ALDRI brukt til
  // en til-par-utregning, kun til "Par"-feltet nederst som viser hele
  // runden uansett hvor langt man har kommet.
  const totalPar = sum(orderedHoles.map((h) => h.par))
  // Til-par MÅ regnes mot paret for kun de SPILTE hullene -- ellers blir
  // "til par" meningsløst midt i runden (f.eks. 19 slag på 4 hull ville vist
  // "−53 til par" mot en 72-par bane i stedet for korrekt "+2", funnet ved
  // faktisk nettleser-testing 2026-07-27).
  const playedPar = playedHoles.length > 0 ? sum(playedHoles.map((h) => h.par)) : 0
  const totalGross = playedHoles.length > 0 ? sum(playedHoles.map((h) => h.score as number)) : null
  const totalPointsValues = orderedHoles.map(stablefordPoints).filter((n): n is number => n !== null)
  const totalPoints = totalPointsValues.length > 0 ? sum(totalPointsValues) : null

  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" />
              Scorekort
            </Link>
          </div>
        </header>
      )}

      <main className="mx-auto flex max-w-xl flex-col gap-4 px-4 py-5 pb-16">
        {/* Round summary strip */}
        <div className="flex flex-wrap items-baseline justify-between gap-2 rounded-3xl border border-border bg-card p-4 shadow-md shadow-black/8">
          <div className="flex flex-col">
            <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">
                  {formatLabel}
                </span>
              )}
            </span>
            <span className="text-sm font-semibold text-muted-foreground">
              {round.name?.trim() ? `${round.course_name_snapshot} · ` : ""}
              {round.tee_name_snapshot} {"·"} {round.holes_planned} hull {"·"}{" "}
              {dateFormatter.format(new Date(round.played_at))}
            </span>
          </div>
          {totalGross !== null && (
            <div className="flex flex-col items-end">
              <span className="text-3xl font-extrabold tabular-nums text-foreground">{totalGross}</span>
              <span className="text-sm font-bold text-brand-orange">{signedToPar(totalGross, playedPar)} til par</span>
            </div>
          )}
        </div>

        {/* Enhets-velger (spillere, eller SIDER for delt-ball-formater) */}
        {units.length > 1 && (
          <div role="tablist" aria-label={isScrambleSolo ? "Velg lag eller motstander" : isSharedBall ? "Velg side" : "Velg spiller"} className="-mx-4 overflow-x-auto px-4">
            <div className="flex gap-2">
              {units.map((u) => {
                const active = u.id === unitId
                return (
                  <button
                    key={u.id}
                    type="button"
                    role="tab"
                    aria-selected={active}
                    onClick={() => {
                      setHoles(null)
                      setUnitId(u.id)
                    }}
                    className={cn(
                      "flex min-h-11 shrink-0 items-center rounded-2xl border px-4 text-base font-bold transition-colors",
                      active
                        ? "border-primary bg-primary text-primary-foreground"
                        : "border-border bg-card text-foreground hover:bg-accent/50",
                    )}
                  >
                    {u.label}
                  </button>
                )
              })}
            </div>
          </div>
        )}

        {/* Scorecard blocks */}
        <div className="flex flex-col gap-3 overflow-x-auto rounded-3xl border border-border bg-card p-3 shadow-md shadow-black/8 sm:p-4">
          {front.length > 0 && (
            <ScoreBlock
              holes={front}
              summaryLabel={back.length === 0 ? "Tot" : "Ut"}
              showStableford={showStableford}
              showFairway={showFairway}
              showPutts={showPutts}
              showGir={showGir}
              showApproach={showApproach}
              showChip={showChip}
              showBunker={showBunker}
              showPenalty={showPenalty}
              showAnyway={showAnyway}
            />
          )}
          {back.length > 0 && (
            <ScoreBlock
              holes={back}
              summaryLabel="Inn"
              showStableford={showStableford}
              showFairway={showFairway}
              showPutts={showPutts}
              showGir={showGir}
              showApproach={showApproach}
              showChip={showChip}
              showBunker={showBunker}
              showPenalty={showPenalty}
              showAnyway={showAnyway}
            />
          )}
        </div>

        {/* Combined total */}
        <div className={cn("grid gap-2", showStableford ? "grid-cols-4" : "grid-cols-3")}>
          <TotalTile label="Par" value={String(totalPar)} />
          <TotalTile label="Score" value={totalGross !== null ? String(totalGross) : "–"} />
          <TotalTile
            label="Til par"
            value={totalGross !== null ? signedToPar(totalGross, playedPar) : "–"}
            tone="orange"
          />
          {showStableford && <TotalTile label="Poeng" value={totalPoints !== null ? String(totalPoints) : "–"} tone="green" />}
        </div>

        {/* Matchforløp / skins-forløp -- HVORFOR ble resultatet som det ble
            (brutto/netto/slag mottatt per hull), ikke bare et sluttall. */}
        {isTwoSided && round.sides.length === 2 && (
          <MatchScorecardGrid round={round} roundId={roundId} viewerId={viewerId} refreshKey={refreshKey} apiPrefix={apiPrefix} />
        )}
        {isSkins && <SkinsProgressTable roundId={roundId} refreshKey={refreshKey} apiPrefix={apiPrefix} />}
      </main>
    </div>
  )
}
