"use client"

import type React from "react"
import { useEffect, useMemo, useState } from "react"
import Link from "next/link"
import {
  ArrowLeft,
  Check,
  ChevronLeft,
  ChevronRight,
  ChevronsUpDown,
  RefreshCw,
  Trophy,
  WifiOff,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import { HoleTargetDistance } from "@/components/hole-target-distance"
import { cn } from "@/lib/utils"
import { enqueueWrite, flushQueue, queueCount } from "@/lib/offline-queue"

// --- Types (matcher API-kontrakten i app/routers/tournaments.py/matches.py/scoring.py/courses.py)

// Fire nye org-lagturnering-formater (2026-07-30) -- se tournament-program.tsx.
type Format =
  | "foursome"
  | "greensome"
  | "scramble_2"
  | "scramble_4"
  | "fourball"
  | "singles"
  | "chapman"
  | "shamble"
  | "money_ball"
  | "high_low_high"
type ScoringMode = "stroke" | "hole_result"
type HoleConfig = "full_18" | "front_9" | "back_9"

// Individuell-ball -- hver spiller sin egen hole_score-rad (ikke shamble/
// money_ball/high_low_high sin egen aggregering, se app/routers/scoring.py
// sin _INDIVIDUAL_FORMATS). Chapman er delt-ball (som foursome), IKKE med her.
const INDIVIDUAL_FORMATS: Format[] = ["singles", "fourball", "shamble", "money_ball", "high_low_high"]

const FORMAT_LABELS: Record<Format, string> = {
  foursome: "Foursome",
  greensome: "Greensome",
  scramble_2: "Scramble (2)",
  scramble_4: "Scramble (4)",
  fourball: "Fourball",
  singles: "Singel",
  chapman: "Chapman",
  shamble: "Shamble",
  money_ball: "Money Ball",
  high_low_high: "High-low-high",
}
// To-sidede formater (ADR-039-lignende, org-lag) -- gater Nassau-panelet
// (ren tilleggsvisning av 18-hulls to-sidet data, ingen egen play_format).
const TWO_SIDED_FORMATS: Format[] = [
  "singles", "fourball", "foursome", "greensome", "scramble_2", "scramble_4", "chapman", "high_low_high",
]

type ApiTeam = { id: string; name: string; color: string | null }

type ApiSession = {
  id: string
  name: string | null
  format: Format
  hole_config: HoleConfig
  course_id: string
  scoring_mode: ScoringMode
}

type ApiParticipant = {
  id: string
  team_side: "a" | "b"
  team_roster_id: string
  player_name: string
  tee_id: string
  tee_name: string
  playing_handicap: number | null
}

type ApiMatch = {
  id: string
  sequence: number
  team_a_id: string
  team_b_id: string
  participants: ApiParticipant[]
}

type ApiHole = { hole_number: number; par: number; stroke_index: number }

type ApiHoleScore = {
  hole_number: number
  team_side: "a" | "b"
  match_participant_id: string | null
  gross_strokes: number
  // Scramble/greensome: hvem sitt utslag ble valgt (migrasjon 039, samme
  // funksjon som frittstående runder fikk 2026-07-25). Kun satt for
  // delt-ball-rader (match_participant_id null).
  selected_participant_id: string | null
  // Optimistisk versjonssjekk (ADR-060). Alltid satt av serveren.
  version: number
}

type ApiHoleResult = { hole_number: number; winning_side: "a" | "b" | null; version: number }

type ApiScorecard = {
  match_id: string
  status_text: string | null
  points_side_a: number | null
  points_side_b: number | null
  holes: { hole_number: number; result: "a" | "b" | "halved" }[]
  stroke_entries: ApiHoleScore[] | null
  hole_result_entries: ApiHoleResult[] | null
}

// Enhet som scores: én per SPILLER for individuelle formater (singles/
// fourball), én per SIDE for delt-ball-formater (foursome/greensome/
// scramble) -- se app/routers/scoring.py sin is_individual-sjekk.
type Unit = {
  id: string
  side: "a" | "b"
  label: string
  matchParticipantId: string | null
  playingHandicap: number | null
}

// Nøkkel for det lokale offline-overlayet (ADR-028) -- entydig per
// hull+side+enhet, samme identitet som backend bruker for upsert. Brukes
// OGSÅ som offline-kø-ens resourceKey (ADR-060) -- /hole-scores har samme
// URL for alle hull i matchen, så uten en egen ressursnøkkel ville
// versjons-kjedingen i flushQueue blandet sammen ulike hull.
function strokeKey(holeNumber: number, unit: Unit): string {
  return `${holeNumber}:${unit.side}:${unit.matchParticipantId ?? ""}`
}

// Samme begrunnelse som strokeKey, for /hole-results (som ikke har
// side/enhet -- kun ett resultat per hull).
function resultKey(holeNumber: number): string {
  return `result:${holeNumber}`
}

function playedHoleNumbers(holeConfig: HoleConfig): number[] {
  if (holeConfig === "front_9") return Array.from({ length: 9 }, (_, i) => i + 1)
  if (holeConfig === "back_9") return Array.from({ length: 9 }, (_, i) => i + 10)
  return Array.from({ length: 18 }, (_, i) => i + 1)
}

export function SessionScorecard({
  organizationId,
  tournamentId,
  sessionId,
  matchId,
  tournamentName,
}: {
  organizationId: string
  tournamentId: string
  sessionId: string
  matchId: string
  tournamentName: string
}) {
  const [session, setSession] = useState<ApiSession | null>(null)
  const [teams, setTeams] = useState<[ApiTeam, ApiTeam] | null>(null)
  const [match, setMatch] = useState<ApiMatch | null>(null)
  const [holes, setHoles] = useState<ApiHole[]>([])
  const [scorecard, setScorecard] = useState<ApiScorecard | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)
  const [currentHole, setCurrentHole] = useState<number | null>(null)
  const [showSummary, setShowSummary] = useState(false)
  const [showConcede, setShowConcede] = useState(false)
  const [concedeTeamIndex, setConcedeTeamIndex] = useState<0 | 1 | null>(null)

  // --- Offline scoreregistrering (ADR-028) -----------------------------
  // isOnline styrer kun banner-teksten (navigator.onLine er allerede
  // sjekket direkte der det faktisk betyr noe, i submitStroke/
  // submitHoleResult) -- pendingStrokes/pendingResults er et lokalt
  // overlay som viser verdier som er lagret i IndexedDB-køen, men ikke
  // bekreftet mot serveren ennå.
  const [isOnline, setIsOnline] = useState(true)
  const [pendingCount, setPendingCount] = useState(0)
  const [syncing, setSyncing] = useState(false)
  const [pendingStrokes, setPendingStrokes] = useState<Map<string, number>>(new Map())
  const [pendingResults, setPendingResults] = useState<Map<number, "a" | "b" | null>>(new Map())

  async function flushPending() {
    if (syncing) return
    setSyncing(true)
    try {
      const outcomes = await flushQueue(matchId)
      if (outcomes.length === 0) return
      setPendingStrokes((prev) => {
        const next = new Map(prev)
        for (const o of outcomes) {
          if (o.entry.url.endsWith("/hole-scores")) {
            const b = o.entry.body as {
              hole_number: number
              team_side: "a" | "b"
              match_participant_id: string | null
            }
            next.delete(`${b.hole_number}:${b.team_side}:${b.match_participant_id ?? ""}`)
          }
        }
        return next
      })
      setPendingResults((prev) => {
        const next = new Map(prev)
        for (const o of outcomes) {
          if (o.entry.url.endsWith("/hole-results")) {
            const b = o.entry.body as { hole_number: number }
            next.delete(b.hole_number)
          }
        }
        return next
      })
      const failed = outcomes.filter((o) => !o.ok)
      if (failed.length > 0) {
        setError(
          `${failed.length} lagret ${failed.length === 1 ? "endring" : "endringer"} kunne ikke synkroniseres: ${failed[0].message}`,
        )
      }
      setPendingCount(await queueCount(matchId))
      await refetchScorecard()
    } finally {
      setSyncing(false)
    }
  }

  useEffect(() => {
    setIsOnline(navigator.onLine)
    function handleOnline() {
      setIsOnline(true)
      void flushPending()
    }
    function handleOffline() {
      setIsOnline(false)
    }
    window.addEventListener("online", handleOnline)
    window.addEventListener("offline", handleOffline)
    return () => {
      window.removeEventListener("online", handleOnline)
      window.removeEventListener("offline", handleOffline)
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [matchId])

  async function queueStroke(
    holeNumber: number,
    unit: Unit,
    grossStrokes: number,
    selectedParticipantId: string | null,
  ) {
    const url = `/orgs/${organizationId}/matches/${matchId}/hole-scores`
    const body = {
      team_side: unit.side,
      match_participant_id: unit.matchParticipantId,
      hole_number: holeNumber,
      gross_strokes: grossStrokes,
      selected_participant_id: selectedParticipantId,
      expected_version: strokeVersionFor(holeNumber, unit),
    }
    await enqueueWrite({ url, method: "POST", body, matchId, resourceKey: strokeKey(holeNumber, unit) })
    setPendingStrokes((prev) => new Map(prev).set(strokeKey(holeNumber, unit), grossStrokes))
    setPendingCount((c) => c + 1)
  }

  async function queueHoleResult(holeNumber: number, winningSide: "a" | "b" | null) {
    const url = `/orgs/${organizationId}/matches/${matchId}/hole-results`
    const body = {
      hole_number: holeNumber,
      winning_side: winningSide,
      expected_version: resultVersionFor(holeNumber),
    }
    await enqueueWrite({ url, method: "POST", body, matchId, resourceKey: resultKey(holeNumber) })
    setPendingResults((prev) => new Map(prev).set(holeNumber, winningSide))
    setPendingCount((c) => c + 1)
  }

  async function refetchScorecard() {
    const res = await fetch(`/orgs/${organizationId}/matches/${matchId}/scorecard`, {
      credentials: "include",
    })
    if (res.ok) setScorecard(await res.json())
  }

  useEffect(() => {
    let cancelled = false
    async function load() {
      try {
        const [sessionsRes, teamsRes, matchesRes] = await Promise.all([
          fetch(`/orgs/${organizationId}/tournaments/${tournamentId}/sessions`, { credentials: "include" }),
          fetch(`/orgs/${organizationId}/tournaments/${tournamentId}/teams`, { credentials: "include" }),
          fetch(`/orgs/${organizationId}/sessions/${sessionId}/matches`, { credentials: "include" }),
        ])
        if (!sessionsRes.ok || !teamsRes.ok || !matchesRes.ok) throw new Error("load failed")
        const sessionsData: ApiSession[] = await sessionsRes.json()
        const foundSession = sessionsData.find((s) => s.id === sessionId)
        if (!foundSession) throw new Error("session not found")
        const teamsData: ApiTeam[] = await teamsRes.json()
        if (teamsData.length !== 2) throw new Error("expected 2 teams")
        const matchesData: ApiMatch[] = await matchesRes.json()
        const foundMatch = matchesData.find((m) => m.id === matchId)
        if (!foundMatch) throw new Error("match not found")

        const [holesRes, scorecardRes] = await Promise.all([
          fetch(`/orgs/${organizationId}/courses/${foundSession.course_id}/holes`, { credentials: "include" }),
          fetch(`/orgs/${organizationId}/matches/${matchId}/scorecard`, { credentials: "include" }),
        ])
        if (!holesRes.ok || !scorecardRes.ok) throw new Error("load failed")
        const holesData: ApiHole[] = await holesRes.json()
        const scorecardData: ApiScorecard = await scorecardRes.json()

        if (cancelled) return
        setSession(foundSession)
        setTeams([teamsData[0], teamsData[1]])
        setMatch(foundMatch)
        setHoles(holesData)
        setScorecard(scorecardData)
        const played = playedHoleNumbers(foundSession.hole_config)
        const nextUnregistered = played[scorecardData.holes.length] ?? played[played.length - 1]
        setCurrentHole(nextUnregistered)

        const count = await queueCount(matchId).catch(() => 0)
        if (!cancelled) setPendingCount(count)
        if (count > 0 && navigator.onLine) void flushPending()
      } catch {
        if (!cancelled) setError("Klarte ikke å laste scorekortet. Prøv å laste siden på nytt.")
      } finally {
        if (!cancelled) setLoading(false)
      }
    }
    void load()
    return () => {
      cancelled = true
    }
  }, [organizationId, tournamentId, sessionId, matchId])

  const units: Unit[] = useMemo(() => {
    if (!session || !match || !teams) return []
    const isIndividual = INDIVIDUAL_FORMATS.includes(session.format)
    if (isIndividual) {
      return match.participants.map((p) => ({
        id: p.id,
        side: p.team_side,
        label: p.player_name,
        matchParticipantId: p.id,
        playingHandicap: p.playing_handicap,
      }))
    }
    // Delt-ball (foursome/greensome/scramble): enheten er selve SIDEN, men
    // playing_handicap er den kombinerte side-verdien lagret på HVER av
    // sidens deltakere (ADR-039) -- plukk den fra en av dem.
    return [
      {
        id: "a",
        side: "a" as const,
        label: teams[0].name,
        matchParticipantId: null,
        playingHandicap: match.participants.find((p) => p.team_side === "a")?.playing_handicap ?? null,
      },
      {
        id: "b",
        side: "b" as const,
        label: teams[1].name,
        matchParticipantId: null,
        playingHandicap: match.participants.find((p) => p.team_side === "b")?.playing_handicap ?? null,
      },
    ]
  }, [session, match, teams])

  const decided = scorecard?.points_side_a !== null && scorecard?.points_side_b !== null

  // Scramble/greensome: hvem sitt utslag som ble valgt for et allerede
  // registrert hull (migrasjon 039) -- lest fra scorekortet, ikke lagret
  // lokalt separat.
  function selectedDriverFor(holeNumber: number, unit: Unit): string | null {
    const entry = (scorecard?.stroke_entries ?? []).find(
      (e) => e.hole_number === holeNumber && e.team_side === unit.side && e.match_participant_id === unit.matchParticipantId,
    )
    return entry?.selected_participant_id ?? null
  }

  // Sist kjente versjon for et hull (ADR-060) -- undefined/null for et
  // hull som ikke er registrert fra før, som er riktig: serveren bryr seg
  // ikke om expected_version på en helt ny rad (se scoring.py).
  function strokeVersionFor(holeNumber: number, unit: Unit): number | null {
    const entry = (scorecard?.stroke_entries ?? []).find(
      (e) => e.hole_number === holeNumber && e.team_side === unit.side && e.match_participant_id === unit.matchParticipantId,
    )
    return entry?.version ?? null
  }

  function resultVersionFor(holeNumber: number): number | null {
    const entry = (scorecard?.hole_result_entries ?? []).find((e) => e.hole_number === holeNumber)
    return entry?.version ?? null
  }

  // `selectedParticipantId` utelatt = behold gjeldende valg uendret (brukt
  // av selve slagregistreringen); eksplisitt sendt (inkl. `null`) = et nytt
  // utslags-valg (brukt av "hvem sitt utslag ble brukt"-knappene under).
  async function submitStroke(
    holeNumber: number,
    unit: Unit,
    grossStrokes: number,
    selectedParticipantId?: string | null,
  ) {
    const resolvedSelection =
      selectedParticipantId !== undefined ? selectedParticipantId : selectedDriverFor(holeNumber, unit)
    setError(null)
    if (!navigator.onLine) {
      await queueStroke(holeNumber, unit, grossStrokes, resolvedSelection)
      return
    }
    let res: Response
    try {
      res = await fetch(`/orgs/${organizationId}/matches/${matchId}/hole-scores`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "include",
        body: JSON.stringify({
          team_side: unit.side,
          match_participant_id: unit.matchParticipantId,
          hole_number: holeNumber,
          gross_strokes: grossStrokes,
          selected_participant_id: resolvedSelection,
          expected_version: strokeVersionFor(holeNumber, unit),
        }),
      })
    } catch {
      // Ekte nettverksfeil (ikke bare et avvist svar) -- køordne i stedet
      // for å vise en feil, se ADR-028.
      await queueStroke(holeNumber, unit, grossStrokes, resolvedSelection)
      return
    }
    if (!res.ok) {
      if (res.status === 409) {
        const body = await res.json().catch(() => null)
        if (body?.detail?.code === "STALE_VERSION") {
          setError(body.detail.message ?? "Noen andre har endret dette hullet i mellomtiden. Laster inn siste versjon.")
          await refetchScorecard()
          return
        }
      }
      setError("Klarte ikke å registrere slaget. Kanskje matchen allerede er avgjort.")
      return
    }
    await refetchScorecard()
  }

  // Walkover/konsesjon (ADR-024): kun kaptein for laget som GIR SEG (eller
  // org-admin) får lov -- backend håndhever dette, UI-et viser bare begge
  // knappene og lar en 403 melde seg som en vanlig feiltekst, samme mønster
  // som resten av appen (ingen klientside-forhåndsfiltrering på kapteinstatus).
  async function concede(teamId: string) {
    setError(null)
    try {
      const res = await fetch(`/orgs/${organizationId}/matches/${matchId}/concede`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "include",
        body: JSON.stringify({ conceding_team_id: teamId }),
      })
      if (!res.ok) {
        const body = await res.json().catch(() => null)
        throw new Error(body?.detail?.message ?? "Klarte ikke å registrere walkover.")
      }
      setConcedeTeamIndex(null)
      setShowConcede(false)
      await refetchScorecard()
    } catch (e) {
      setError(e instanceof Error ? e.message : "Klarte ikke å registrere walkover.")
    }
  }

  async function submitHoleResult(holeNumber: number, winningSide: "a" | "b" | null) {
    setError(null)
    if (!navigator.onLine) {
      await queueHoleResult(holeNumber, winningSide)
      return
    }
    let res: Response
    try {
      res = await fetch(`/orgs/${organizationId}/matches/${matchId}/hole-results`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "include",
        body: JSON.stringify({
          hole_number: holeNumber,
          winning_side: winningSide,
          expected_version: resultVersionFor(holeNumber),
        }),
      })
    } catch {
      await queueHoleResult(holeNumber, winningSide)
      return
    }
    if (!res.ok) {
      if (res.status === 409) {
        const body = await res.json().catch(() => null)
        if (body?.detail?.code === "STALE_VERSION") {
          setError(body.detail.message ?? "Noen andre har endret dette hullet i mellomtiden. Laster inn siste versjon.")
          await refetchScorecard()
          return
        }
      }
      setError("Klarte ikke å registrere hull-resultatet. Kanskje matchen allerede er avgjort.")
      return
    }
    await refetchScorecard()
  }

  const programHref = `/tournaments/${tournamentId}/program?org=${organizationId}&name=${encodeURIComponent(tournamentName)}`

  if (loading || !session || !teams || !match || !scorecard || currentHole === null) {
    return (
      <div className="flex min-h-[100dvh] flex-col bg-background">
        <header className="sticky top-0 z-10 border-b border-border bg-background/80 backdrop-blur">
          <div className="mx-auto flex w-full max-w-3xl items-center gap-3 px-5 py-4">
            <Link
              href={programHref}
              aria-label="Tilbake til program"
              className="flex size-10 shrink-0 items-center justify-center rounded-xl border border-border bg-card text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
            >
              <ArrowLeft aria-hidden="true" className="size-5" />
            </Link>
            <div className="flex min-w-0 flex-col">
              <span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
                Turnering
              </span>
              <h1 className="truncate text-xl font-extrabold tracking-tight text-foreground">
                {tournamentName}
              </h1>
            </div>
          </div>
        </header>
        <main className="mx-auto flex w-full max-w-3xl flex-1 items-center justify-center px-5 py-6">
          {error ? (
            <p role="alert" className="text-sm font-medium text-destructive">
              {error}
            </p>
          ) : (
            <div
              aria-hidden="true"
              className="size-10 animate-spin rounded-full border-4 border-primary/20 border-t-primary"
            />
          )}
        </main>
      </div>
    )
  }

  const played = playedHoleNumbers(session.hole_config)
  const totalHoles = played.length
  const par = holes.find((h) => h.hole_number === currentHole)?.par ?? 4
  const registeredHoleNumbers = new Set([
    ...scorecard.holes.map((h) => h.hole_number),
    ...Array.from(pendingStrokes.keys(), (k) => Number(k.split(":")[0])),
    ...Array.from(pendingResults.keys()),
  ])

  const statusLabel = scorecard.status_text ?? "AS"

  function strokeValue(holeNumber: number, unit: Unit): number | null {
    const key = strokeKey(holeNumber, unit)
    if (pendingStrokes.has(key)) return pendingStrokes.get(key)!
    const entry = (scorecard!.stroke_entries ?? []).find(
      (e) =>
        e.hole_number === holeNumber &&
        e.team_side === unit.side &&
        e.match_participant_id === unit.matchParticipantId,
    )
    return entry ? entry.gross_strokes : null
  }

  const currentResultEntry = pendingResults.has(currentHole)
    ? { hole_number: currentHole, winning_side: pendingResults.get(currentHole) ?? null }
    : (scorecard.hole_result_entries ?? []).find((e) => e.hole_number === currentHole)

  function goPrev() {
    setCurrentHole((h) => {
      const idx = played.indexOf(h!)
      return played[Math.max(0, idx - 1)]
    })
  }
  function goNext() {
    setCurrentHole((h) => {
      const idx = played.indexOf(h!)
      return played[Math.min(played.length - 1, idx + 1)]
    })
  }

  const isIndividual = INDIVIDUAL_FORMATS.includes(session.format)
  const isLast = played.indexOf(currentHole) >= played.length - 1

  return (
    <div className="flex min-h-[100dvh] flex-col bg-background">
      <header className="sticky top-0 z-20 border-b border-border bg-background/80 backdrop-blur">
        <div className="mx-auto flex w-full max-w-3xl items-center gap-3 px-5 py-4">
          <Link
            href={programHref}
            aria-label="Tilbake til program"
            className="flex size-10 shrink-0 items-center justify-center rounded-xl border border-border bg-card text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
          >
            <ArrowLeft aria-hidden="true" className="size-5" />
          </Link>
          <div className="flex min-w-0 flex-col">
            <span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
              Turnering
            </span>
            <h1 className="truncate text-xl font-extrabold tracking-tight text-foreground">
              {tournamentName}
            </h1>
            <span className="truncate text-sm text-muted-foreground">
              {session.name?.trim() || "Økt"} · {FORMAT_LABELS[session.format]} · Match {match.sequence}
            </span>
          </div>
        </div>

        <div className="border-t border-border bg-card/40">
          <div className="mx-auto grid w-full max-w-3xl grid-cols-[1fr_auto_1fr] items-center gap-2 px-5 py-3 sm:gap-4">
            <TeamTag name={teams[0].name} color={teams[0].color ?? "#64748b"} align="left" />
            {decided ? (
              <div className="flex flex-col items-center px-1">
                <span className="text-lg font-extrabold tracking-tight text-primary">Avgjort</span>
              </div>
            ) : (
              <div className="flex items-center gap-1.5 sm:gap-3">
                <button
                  type="button"
                  onClick={goPrev}
                  disabled={played.indexOf(currentHole) <= 0}
                  aria-label="Forrige hull"
                  className="flex size-11 shrink-0 items-center justify-center rounded-2xl border border-border bg-card text-foreground transition-colors hover:bg-accent/50 disabled:opacity-40"
                >
                  <ChevronLeft aria-hidden="true" className="size-5" />
                </button>
                <div className="flex min-w-[84px] flex-col items-center">
                  <span className="text-2xl font-extrabold leading-none tracking-tight text-foreground tabular-nums">
                    {statusLabel}
                  </span>
                </div>
                <button
                  type="button"
                  onClick={goNext}
                  disabled={isLast}
                  aria-label="Neste hull"
                  className="flex size-11 shrink-0 items-center justify-center rounded-2xl border border-border bg-card text-foreground transition-colors hover:bg-accent/50 disabled:opacity-40"
                >
                  <ChevronRight aria-hidden="true" className="size-5" />
                </button>
              </div>
            )}
            <TeamTag name={teams[1].name} color={teams[1].color ?? "#64748b"} align="right" />
          </div>
        </div>
      </header>

      <main className="mx-auto w-full max-w-3xl flex-1 px-5 py-6 sm:py-8">
        {!isOnline && (
          <div className="mb-4 flex items-center gap-2 rounded-2xl border border-amber-300 bg-amber-50 px-4 py-3 text-sm font-medium text-amber-900 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-200">
            <WifiOff aria-hidden="true" className="size-4 shrink-0" />
            <span>Du er offline. Endringer lagres lokalt og sendes automatisk når du er tilbake på nett.</span>
          </div>
        )}
        {pendingCount > 0 && (
          <div className="mb-4 flex items-center justify-between gap-2 rounded-2xl border border-border bg-card px-4 py-3 text-sm font-medium text-foreground">
            <span>
              {pendingCount} {pendingCount === 1 ? "endring venter" : "endringer venter"} på synkronisering.
            </span>
            {isOnline && (
              <button
                type="button"
                onClick={() => void flushPending()}
                disabled={syncing}
                className="inline-flex shrink-0 items-center gap-1 font-semibold text-primary disabled:opacity-50"
              >
                <RefreshCw aria-hidden="true" className={cn("size-3.5", syncing && "animate-spin")} />
                Synkroniser nå
              </button>
            )}
          </div>
        )}
        {error && (
          <p role="alert" className="mb-4 text-sm font-medium text-destructive">
            {error}
          </p>
        )}

        {decided ? (
          <DecidedView
            teams={teams}
            scorecard={scorecard}
            holes={holes}
            units={units}
            scoringMode={session.scoring_mode}
            holeConfig={session.hole_config}
            format={session.format}
            organizationId={organizationId}
            matchId={matchId}
          />
        ) : (
          <>
            <nav aria-label="Velg hull" className="-mx-5 mb-4 overflow-x-auto px-5">
              <div className="flex gap-2 pb-1">
                {played.map((h) => {
                  const active = h === currentHole
                  const registered = registeredHoleNumbers.has(h)
                  return (
                    <button
                      key={h}
                      type="button"
                      onClick={() => setCurrentHole(h)}
                      aria-current={active ? "true" : undefined}
                      className={cn(
                        "relative flex size-11 shrink-0 flex-col items-center justify-center rounded-2xl border text-sm font-bold tabular-nums transition-colors",
                        active
                          ? "border-primary bg-primary text-primary-foreground"
                          : "border-border bg-card text-foreground hover:bg-accent/50",
                      )}
                    >
                      {h}
                      {registered && (
                        <span
                          aria-hidden="true"
                          className={cn(
                            "absolute -right-0.5 -top-0.5 flex size-4 items-center justify-center rounded-full border-2 border-background",
                            active ? "bg-primary-foreground text-primary" : "bg-primary text-primary-foreground",
                          )}
                        >
                          <Check className="size-2.5" strokeWidth={3} />
                        </span>
                      )}
                    </button>
                  )
                })}
              </div>
            </nav>

            {/* Nassau Match Play (2026-07-30) -- tre parallelle vinduer av et
                EKSISTERENDE to-sidet format, kun for 18-hulls økter. Ren
                tilleggsvisning, ikke et eget format. */}
            {TWO_SIDED_FORMATS.includes(session.format) && session.hole_config === "full_18" && (
              <NassauPanel organizationId={organizationId} matchId={matchId} refreshKey={scorecard.stroke_entries?.length ?? 0} />
            )}

            <div className="mb-4 overflow-hidden rounded-2xl border border-border bg-card">
              <button
                type="button"
                onClick={() => setShowSummary((v) => !v)}
                aria-expanded={showSummary}
                className="flex w-full items-center justify-between gap-2 px-4 py-3 text-left"
              >
                <span className="text-sm font-semibold text-foreground">Vis full oversikt</span>
                <ChevronsUpDown aria-hidden="true" className="size-4 text-muted-foreground" />
              </button>
              {showSummary && (
                <div className="border-t border-border">
                  {session.format === "high_low_high" ? (
                    <HighLowHighPanel
                      organizationId={organizationId}
                      matchId={matchId}
                      teams={teams}
                      refreshKey={scorecard.stroke_entries?.length ?? 0}
                    />
                  ) : (
                    <TournamentMatchGrid
                      teams={teams}
                      units={units}
                      holes={holes}
                      scorecard={scorecard}
                      scoringMode={session.scoring_mode}
                      holeConfig={session.hole_config}
                    />
                  )}
                  {!isIndividual && session.scoring_mode === "stroke" && match && (
                    <SelectedDriverSummary
                      teams={teams}
                      participants={match.participants}
                      strokeEntries={scorecard.stroke_entries ?? []}
                    />
                  )}
                </div>
              )}
            </div>

            <div className="mb-6 overflow-hidden rounded-2xl border border-border bg-card">
              <button
                type="button"
                onClick={() => {
                  setShowConcede((v) => !v)
                  setConcedeTeamIndex(null)
                }}
                aria-expanded={showConcede}
                className="flex w-full items-center justify-between gap-2 px-4 py-3 text-left"
              >
                <span className="text-sm font-semibold text-foreground">Gi opp matchen (walkover)</span>
                <ChevronsUpDown aria-hidden="true" className="size-4 text-muted-foreground" />
              </button>
              {showConcede && (
                <div className="flex flex-col gap-3 border-t border-border p-4">
                  <p className="text-xs leading-relaxed text-muted-foreground text-pretty">
                    Kun kapteinen for laget som gir seg (eller en organisasjonsadministrator) kan
                    gjøre dette. Motstanderen får full poengsum for matchen, uansett hvor mange hull
                    som allerede er registrert.
                  </p>
                  {teams.map((team, i) =>
                    concedeTeamIndex === i ? (
                      <div
                        key={team.id}
                        className="flex flex-col gap-3 rounded-xl bg-destructive/5 p-3 sm:flex-row sm:items-center sm:justify-between"
                      >
                        <span className="text-sm text-foreground text-pretty">
                          Er du sikker på at <span className="font-semibold">{team.name}</span> gir seg?
                        </span>
                        <div className="flex shrink-0 gap-2">
                          <Button
                            type="button"
                            size="sm"
                            variant="destructive"
                            className="rounded-xl"
                            onClick={() => concede(team.id)}
                          >
                            Gi opp
                          </Button>
                          <Button
                            type="button"
                            size="sm"
                            variant="ghost"
                            className="rounded-xl"
                            onClick={() => setConcedeTeamIndex(null)}
                          >
                            Avbryt
                          </Button>
                        </div>
                      </div>
                    ) : (
                      <Button
                        key={team.id}
                        type="button"
                        variant="outline"
                        className="h-11 justify-start rounded-xl text-sm font-semibold"
                        onClick={() => setConcedeTeamIndex(i as 0 | 1)}
                      >
                        <span aria-hidden="true" className="mr-2 size-3 rounded-full" style={{ backgroundColor: team.color ?? "#64748b" }} />
                        {team.name} gir seg
                      </Button>
                    ),
                  )}
                </div>
              )}
            </div>

            {session.scoring_mode === "stroke" ? (
              <section aria-label={`Hull ${currentHole}`}>
                <div className="mb-4 flex items-baseline gap-2">
                  <h2 className="text-2xl font-extrabold tracking-tight text-foreground">Hull {currentHole}</h2>
                  <span className="text-lg font-bold text-muted-foreground">· Par {par}</span>
                </div>

                {/* Rangefinder (migrasjon 079) -- samme selvskjulende
                    komponent som frittstående runder/individuell
                    org-turnering, ingen synlig rad for baner uten
                    koordinatdata. */}
                <div className="mb-4">
                  <HoleTargetDistance
                    baseUrl={`/orgs/${organizationId}/matches/${matchId}`}
                    holeNumber={currentHole}
                    size="full"
                  />
                </div>
                <div className="flex flex-col gap-3">
                  {units.map((unit) => {
                    const teamColor = (unit.side === "a" ? teams[0].color : teams[1].color) ?? "#64748b"
                    const value = strokeValue(currentHole, unit)
                    const isPending = pendingStrokes.has(strokeKey(currentHole, unit))
                    return (
                      <div
                        key={unit.id}
                        className="flex flex-col gap-3 rounded-3xl border border-border bg-card p-4 shadow-md shadow-black/8 sm:p-5"
                        style={{ borderLeftWidth: 6, borderLeftColor: teamColor }}
                      >
                        <div className="flex items-center gap-2.5">
                          <span aria-hidden="true" className="size-3.5 shrink-0 rounded-full" style={{ backgroundColor: teamColor }} />
                          <span className="text-base font-bold tracking-tight text-foreground text-pretty">
                            {unit.label}
                          </span>
                          {!isIndividual && (
                            <span className="text-xs font-medium text-muted-foreground">(delt ball)</span>
                          )}
                        </div>
                        <StrokePicker
                          value={value}
                          par={par}
                          isPending={isPending}
                          onSelect={(n) => void submitStroke(currentHole, unit, n)}
                        />
                        {/* Scramble/greensome: hvem sitt utslag ble valgt
                            (migrasjon 039, samme funksjon som frittstående
                            runder fikk 2026-07-25). Krever et registrert
                            slagtall å knytte valget til -- hole_score-raden
                            finnes ikke før da (ulikt round_hole, som alltid
                            forhåndsopprettes). Helt valgfritt, egen handling. */}
                        {!isIndividual && value !== null && match && (
                          <fieldset className="flex flex-col gap-2">
                            <legend className="text-xs font-bold uppercase tracking-wide text-muted-foreground">
                              Hvem sitt utslag ble brukt?{" "}
                              <span className="font-normal normal-case">(valgfritt)</span>
                            </legend>
                            <div className="flex flex-wrap gap-2">
                              {match.participants
                                .filter((p) => p.team_side === unit.side)
                                .map((p) => {
                                  const active = selectedDriverFor(currentHole, unit) === p.id
                                  return (
                                    <button
                                      key={p.id}
                                      type="button"
                                      onClick={() => void submitStroke(currentHole, unit, value, active ? null : p.id)}
                                      aria-pressed={active}
                                      className={cn(
                                        "inline-flex min-h-11 items-center rounded-xl border px-4 text-sm font-bold transition-colors",
                                        active
                                          ? "border-primary bg-primary text-primary-foreground"
                                          : "border-border bg-card text-foreground hover:bg-accent/50",
                                      )}
                                    >
                                      {p.player_name}
                                    </button>
                                  )
                                })}
                            </div>
                          </fieldset>
                        )}
                      </div>
                    )
                  })}
                </div>
              </section>
            ) : (
              <section aria-label={`Hull ${currentHole}`}>
                <div className="mb-4 flex items-baseline gap-2">
                  <h2 className="text-2xl font-extrabold tracking-tight text-foreground">Hull {currentHole}</h2>
                  <span className="text-lg font-bold text-muted-foreground">· Par {par}</span>
                </div>

                {/* Rangefinder (migrasjon 079) -- samme selvskjulende
                    komponent som frittstående runder/individuell
                    org-turnering, ingen synlig rad for baner uten
                    koordinatdata. */}
                <div className="mb-4">
                  <HoleTargetDistance
                    baseUrl={`/orgs/${organizationId}/matches/${matchId}`}
                    holeNumber={currentHole}
                    size="full"
                  />
                </div>
                <div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
                  {(
                    [
                      { key: "a", label: `${teams[0].name} vant`, color: teams[0].color, side: "a" as const },
                      { key: "h", label: "Delt", color: undefined, side: null },
                      { key: "b", label: `${teams[1].name} vant`, color: teams[1].color, side: "b" as const },
                    ] as const
                  ).map((opt) => {
                    const active = currentResultEntry
                      ? currentResultEntry.winning_side === opt.side
                      : false
                    return (
                      <button
                        key={opt.key}
                        type="button"
                        onClick={() => submitHoleResult(currentHole, opt.side)}
                        aria-pressed={active}
                        className={cn(
                          "flex h-16 items-center justify-center gap-2 rounded-2xl border text-base font-bold tracking-tight transition-colors",
                          active
                            ? opt.color
                              ? "border-transparent text-primary-foreground"
                              : "border-primary bg-primary text-primary-foreground"
                            : "border-border bg-card text-foreground hover:bg-accent/50",
                        )}
                        style={active && opt.color ? { backgroundColor: opt.color } : undefined}
                      >
                        {opt.color && (
                          <span
                            aria-hidden="true"
                            className="size-3.5 rounded-full"
                            style={{ backgroundColor: active ? undefined : opt.color }}
                          />
                        )}
                        {opt.label}
                      </button>
                    )
                  })}
                </div>
                {pendingResults.has(currentHole) && (
                  <p className="mt-2 text-xs font-medium text-muted-foreground">
                    Lagret lokalt · venter på synk
                  </p>
                )}
              </section>
            )}

            <div className="mt-6">
              {!isLast ? (
                <Button type="button" onClick={goNext} className="h-14 w-full rounded-2xl text-base font-bold shadow-sm">
                  Neste hull
                  <ChevronRight aria-hidden="true" className="size-5" />
                </Button>
              ) : (
                <Button
                  type="button"
                  onClick={() => setShowSummary(true)}
                  className="h-14 w-full rounded-2xl text-base font-bold shadow-sm"
                >
                  <Check aria-hidden="true" className="size-5" />
                  Ferdig
                </Button>
              )}
            </div>
          </>
        )}
      </main>
    </div>
  )
}

// --- Small components --------------------------------------------------------

function TeamTag({ name, color, align }: { name: string; color: string; align: "left" | "right" }) {
  const right = align === "right"
  return (
    <div className={cn("flex min-w-0 items-center gap-2", right ? "flex-row-reverse text-right" : "text-left")}>
      <span aria-hidden="true" className="size-3.5 shrink-0 rounded-full" style={{ backgroundColor: color }} />
      <span className="truncate text-sm font-bold tracking-tight text-foreground">{name}</span>
    </div>
  )
}

// Tallvelger for slagregistrering: 1-9 direkte, pluss et "10 eller flere"-
// valg som åpner en ny rad med 10-19. Erstatter en tidligere pluss/minus-
// stepper -- raskere å treffe riktig tall banebruk (uten å måtte klikke seg
// opp/ned ett og ett slag).
function StrokePicker({
  value,
  par,
  isPending,
  onSelect,
}: {
  value: number | null
  par: number
  isPending: boolean
  onSelect: (n: number) => void
}) {
  const [showHigh, setShowHigh] = useState(value !== null && value >= 10)

  useEffect(() => {
    setShowHigh(value !== null && value >= 10)
  }, [value])

  const numbers = showHigh
    ? Array.from({ length: 10 }, (_, i) => i + 10)
    : Array.from({ length: 9 }, (_, i) => i + 1)

  return (
    <div className="flex flex-col gap-2">
      <div className="grid grid-cols-5 gap-2">
        {numbers.map((n) => (
          <button
            key={n}
            type="button"
            onClick={() => onSelect(n)}
            aria-pressed={value === n}
            className={cn(
              "flex h-12 items-center justify-center rounded-xl border text-base font-bold tabular-nums transition-colors active:scale-95",
              value === n
                ? "border-primary bg-primary text-primary-foreground"
                : "border-border bg-card text-foreground hover:bg-accent/50",
            )}
          >
            {n}
          </button>
        ))}
        {!showHigh && (
          <button
            type="button"
            onClick={() => setShowHigh(true)}
            className="flex h-12 items-center justify-center rounded-xl border border-dashed border-border bg-card text-xs font-bold text-muted-foreground transition-colors hover:bg-accent/50"
          >
            10+
          </button>
        )}
      </div>
      <div className="flex items-center justify-between gap-2">
        {showHigh ? (
          <button
            type="button"
            onClick={() => setShowHigh(false)}
            className="text-xs font-semibold text-primary"
          >
            ← Tilbake til 1–9
          </button>
        ) : (
          <span />
        )}
        <span className="text-xs font-medium text-muted-foreground">
          {value === null
            ? "Ikke registrert"
            : isPending
              ? "Lagret lokalt · venter på synk"
              : scoreToParLabel(value, par)}
        </span>
      </div>
    </div>
  )
}

// --- Match-scorekort: horisontalt rutenett med løpende stilling ------------
// Egen visuell utforming (ikke en kopi av noe referansebilde) -- samme
// informasjon som den tidligere vertikale hull-for-hull-listen (hvem vant
// hvilket hull, brutto der relevant), pluss en løpende "Stilling"-rad
// (AS/X UP/X&Y) regnet frem hull for hull i stedet for kun sluttresultatet.
// Bruker lagenes egne farger (samme konvensjon som resten av appens
// turnering-UI, ikke appens generiske primær/oransje-golfscore-språk --
// laget ER identiteten her, ikke over/under par).

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

// Speiler handicap_engine.py sin compute_match_state()/describe() presist,
// regnet ett prefiks om gangen -- backend cacher i dag kun SLUTT-tilstanden.
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
}

function unitGross(scorecard: ApiScorecard, unit: Unit, holeNumber: number): number | null {
  const entry = (scorecard.stroke_entries ?? []).find(
    (e) =>
      e.hole_number === holeNumber &&
      e.team_side === unit.side &&
      e.match_participant_id === unit.matchParticipantId,
  )
  return entry ? entry.gross_strokes : null
}

// Territorium-bar (2026-07-29, brukerens eget forslag: den ledende sidens
// sone strekker seg proporsjonalt forbi midtlinjen inn på motstanderens
// halvdel). Ulikt round-detail.tsx/round-scorecard.tsx sin primary/brand-
// orange-versjon bruker denne lagets FAKTISKE `team.color` (vilkårlig hex,
// samme som SegmentedBar i tournament-leaderboard.tsx) -- dominant sone får
// hvit tekst (samme kontrastvalg som SegmentedBar), lys sone en svak
// gjennomsiktig tone av samme farge med vanlig tema-tekst.
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 IdentityBlock({
  name,
  hcp,
  color,
  side,
  dominant,
}: {
  name: string
  hcp: number | null
  color: string
  side: "a" | "b"
  dominant: boolean
}) {
  // 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="size-2 shrink-0 rounded-full" style={{ backgroundColor: color }} />}
        <span
          className="min-w-0 truncate text-sm font-extrabold"
          style={{ color: dominant ? "#fff" : undefined }}
        >
          {name}
        </span>
      </div>
      {hcp !== null && (
        <span
          className={cn("truncate text-xs font-bold tabular-nums text-muted-foreground", side === "a" ? "text-left" : "text-right")}
          style={dominant ? { color: "rgba(255,255,255,0.75)" } : undefined}
        >
          HCP {hcp}
        </span>
      )}
    </div>
  )
}

// Scramble/greensome: "utslag brukt" per spiller (migrasjon 039, samme
// funksjon som frittstående runder fikk 2026-07-25, se round-stats.tsx sin
// tilsvarende SelectedDriverSummary). Ren klientside-telling fra allerede
// hentet scorecard-data -- vises kun de hullene som faktisk har et
// registrert valg, ingen antakelse om at ALLE hull må ha ett.
function SelectedDriverSummary({
  teams,
  participants,
  strokeEntries,
}: {
  teams: [ApiTeam, ApiTeam]
  participants: ApiParticipant[]
  strokeEntries: ApiHoleScore[]
}) {
  const rows = (["a", "b"] as const).map((side, idx) => {
    const sideEntries = strokeEntries.filter((e) => e.team_side === side && e.match_participant_id === null)
    const withSelection = sideEntries.filter((e) => e.selected_participant_id !== null)
    const counts = new Map<string, number>()
    for (const e of withSelection) {
      const pid = e.selected_participant_id as string
      counts.set(pid, (counts.get(pid) ?? 0) + 1)
    }
    const sideParticipants = participants.filter((p) => p.team_side === side)
    return {
      team: teams[idx],
      playedCount: sideEntries.length,
      registeredCount: withSelection.length,
      entries: sideParticipants
        .map((p) => ({ name: p.player_name, count: counts.get(p.id) ?? 0 }))
        .sort((a, b) => b.count - a.count),
    }
  })

  if (rows.every((r) => r.registeredCount === 0)) {
    return (
      <div className="border-t border-border p-4 text-sm text-muted-foreground">
        Ingen har registrert hvem sitt utslag som ble brukt på noe hull ennå -- valgfritt felt i
        scoreregistreringen.
      </div>
    )
  }

  return (
    <div className="flex flex-col gap-4 border-t border-border p-4">
      <span className="text-sm font-bold text-foreground">Utslag brukt</span>
      {rows.map(({ team, playedCount, registeredCount, entries }) => (
        <div key={team.id} className="flex flex-col gap-2">
          <span className="flex items-center gap-1.5 text-sm font-semibold text-muted-foreground">
            <span aria-hidden="true" className="size-2.5 shrink-0 rounded-full" style={{ backgroundColor: team.color ?? "#64748b" }} />
            {team.name}
          </span>
          <ul className="flex flex-col divide-y divide-border overflow-hidden rounded-xl border border-border">
            {entries.map((e) => (
              <li key={e.name} className="flex items-center justify-between gap-3 px-4 py-2.5">
                <span className="text-sm font-semibold text-foreground">{e.name}</span>
                <span className="text-base font-extrabold tabular-nums text-foreground">{e.count} utslag</span>
              </li>
            ))}
          </ul>
          <span className="text-xs text-muted-foreground">
            Registrert på {registeredCount} av {playedCount} spilte hull.
          </span>
        </div>
      ))}
    </div>
  )
}

function TournamentMatchGrid({
  teams,
  units,
  holes,
  scorecard,
  scoringMode,
  holeConfig,
}: {
  teams: [ApiTeam, ApiTeam]
  units: Unit[]
  holes: ApiHole[]
  scorecard: ApiScorecard
  scoringMode: ScoringMode
  holeConfig: HoleConfig
}) {
  if (scorecard.holes.length === 0) {
    return <p className="px-4 py-4 text-sm text-muted-foreground">Ingen hull er registrert ennå.</p>
  }

  const decidedHoles = [...scorecard.holes].sort((a, b) => a.hole_number - b.hole_number)
  const totalHoles = playedHoleNumbers(holeConfig).length
  const running = computeRunning(
    decidedHoles.map((h) => h.result),
    totalHoles,
  )
  const lastState = running.length > 0 ? running[running.length - 1] : null

  function headerIdentity(side: "a" | "b"): { name: string; hcp: number | null; color: string } {
    const team = side === "a" ? teams[0] : teams[1]
    const color = team.color ?? "#64748b"
    const sideUnits = units.filter((u) => u.side === side)
    if (sideUnits.length === 1) return { name: sideUnits[0].label, hcp: sideUnits[0].playingHandicap, color }
    return { name: team.name, hcp: null, color }
  }

  const identityA = headerIdentity("a")
  const identityB = headerIdentity("b")
  const lead = lastState?.lead ?? 0
  const tone = lastState?.tone ?? "neutral"
  const widthA = lastState ? leadZoneFraction(lead, totalHoles) * 100 : 50
  const widthB = 100 - widthA
  const zoneAStyle =
    tone === "a"
      ? { backgroundColor: identityA.color }
      : tone === "b"
        ? { backgroundColor: `color-mix(in srgb, ${identityA.color} 15%, transparent)` }
        : undefined
  const zoneBStyle =
    tone === "b"
      ? { backgroundColor: identityB.color }
      : tone === "a"
        ? { backgroundColor: `color-mix(in srgb, ${identityB.color} 15%, transparent)` }
        : undefined

  return (
    <div className="flex flex-col gap-3 p-3 sm:p-4">
      {/* 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", tone === "neutral" && "bg-muted")}
          style={zoneAStyle}
        >
          <IdentityBlock name={identityA.name} hcp={identityA.hcp} color={identityA.color} 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">
            {lastState ? (
              <>
                <span
                  className="text-xl font-extrabold leading-none tabular-nums sm:text-2xl"
                  style={{
                    color:
                      lastState.tone === "a" ? identityA.color : lastState.tone === "b" ? identityB.color : undefined,
                  }}
                >
                  {lastState.label}
                </span>
                {lastState.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">
                    {decidedHoles.length} 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", tone === "neutral" && "bg-muted")}
          style={zoneBStyle}
        >
          <IdentityBlock name={identityB.name} hcp={identityB.hcp} color={identityB.color} side="b" dominant={tone === "b"} />
        </div>
      </div>

      <div className="-mx-3 overflow-x-auto px-3 sm:mx-0 sm:px-0">
        <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"
                >
                  {holes.find((x) => x.hole_number === h.hole_number)?.par ?? "–"}
                </td>
              ))}
            </tr>
            {units.map((unit) => {
              const color = (unit.side === "a" ? teams[0].color : teams[1].color) ?? "#64748b"
              return (
                <tr key={unit.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="size-2 shrink-0 rounded-full"
                        style={{ backgroundColor: color }}
                      />
                      <span className="min-w-0 truncate text-xs font-bold text-foreground">{unit.label}</span>
                    </span>
                  </th>
                  {decidedHoles.map((h) => {
                    const won = h.result === unit.side
                    const gross = scoringMode === "stroke" ? unitGross(scorecard, unit, h.hole_number) : null
                    return (
                      <td key={h.hole_number} className="border border-border p-0.5 text-center">
                        <span
                          aria-label={won ? `${unit.label} vant hullet` : undefined}
                          className={cn(
                            "mx-auto flex size-7 items-center justify-center rounded-full text-xs font-extrabold tabular-nums",
                            won ? "text-primary-foreground" : "text-foreground",
                          )}
                          style={won ? { backgroundColor: color } : undefined}
                        >
                          {scoringMode === "stroke" ? (gross ?? "–") : won ? "✓" : "–"}
                        </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="border border-border py-1.5 text-center text-xs font-extrabold tabular-nums"
                  style={{ color: s.tone === "a" ? identityA.color : s.tone === "b" ? identityB.color : undefined }}
                >
                  {s.label}
                </td>
              ))}
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  )
}

// --- Nassau Match Play (2026-07-30) -- selvstendig, henter sin egen data ---
// Tre parallelle vinduer (front9/back9/overall) av et eksisterende to-sidet
// format, ingen egen lagring (se app/routers/scoring.py sin get_nassau_result).

type ApiNassauWindow = {
  lead: number
  holes_played: number
  holes_remaining: number
  is_closed: boolean
  is_dormie: boolean
  status_text: string
}
type ApiNassauResult = {
  ready: boolean
  front9: ApiNassauWindow | null
  back9: ApiNassauWindow | null
  overall: ApiNassauWindow | null
}

function NassauWindowCard({ label, window }: { label: string; window: ApiNassauWindow | null }) {
  return (
    <div className="flex flex-col items-center gap-1 rounded-xl border border-border bg-background px-3 py-3">
      <span className="text-xs font-bold uppercase tracking-wide text-muted-foreground">{label}</span>
      {window && window.holes_played > 0 ? (
        <>
          <span className="text-lg font-extrabold tabular-nums text-foreground">{window.status_text}</span>
          <span className="text-[11px] text-muted-foreground">{window.holes_played} hull spilt</span>
        </>
      ) : (
        <span className="text-sm text-muted-foreground">–</span>
      )}
    </div>
  )
}

function NassauPanel({
  organizationId,
  matchId,
  refreshKey,
}: {
  organizationId: string
  matchId: string
  refreshKey: number
}) {
  const [result, setResult] = useState<ApiNassauResult | null>(null)
  useEffect(() => {
    let cancelled = false
    fetch(`/orgs/${organizationId}/matches/${matchId}/nassau`, { credentials: "include" })
      .then((res) => (res.ok ? res.json() : null))
      .then((data: ApiNassauResult | null) => {
        if (!cancelled) setResult(data)
      })
      .catch(() => {})
    return () => {
      cancelled = true
    }
  }, [organizationId, matchId, refreshKey])

  if (!result?.ready) return null

  return (
    <div className="mb-4 flex flex-col gap-3 rounded-2xl border border-border bg-card p-4">
      <span className="text-sm font-bold text-foreground">Nassau</span>
      <div className="grid grid-cols-3 gap-2">
        <NassauWindowCard label="Ut (1-9)" window={result.front9} />
        <NassauWindowCard label="Inn (10-18)" window={result.back9} />
        <NassauWindowCard label="Totalt" window={result.overall} />
      </div>
    </div>
  )
}

// --- High-low-high (2026-07-30) -- egen, dedikert visning ------------------
// Passer IKKE inn i TournamentMatchGrid over: _compute_hole_results returnerer
// bevisst [] for dette formatet (litterale 0-2-poeng per hull, ikke en
// ternær vinn/tap/delt-modell), så scorecard.holes ville alltid vært tom her
// selv om ekte slag er registrert. Egen henting mot det dedikerte
// /high-low-high-endepunktet, samme mønster som round-detail.tsx sin
// FormatResultPanel-gren for dette formatet.

type ApiHighLowHighHole = { hole_number: number; points_a: number; points_b: number }
type ApiHighLowHighResult = {
  ready: boolean
  points_a: number | null
  points_b: number | null
  holes: ApiHighLowHighHole[] | null
}

function HighLowHighPanel({
  organizationId,
  matchId,
  teams,
  refreshKey,
}: {
  organizationId: string
  matchId: string
  teams: [ApiTeam, ApiTeam]
  refreshKey: number
}) {
  const [result, setResult] = useState<ApiHighLowHighResult | null>(null)
  useEffect(() => {
    let cancelled = false
    fetch(`/orgs/${organizationId}/matches/${matchId}/high-low-high`, { credentials: "include" })
      .then((res) => (res.ok ? res.json() : null))
      .then((data: ApiHighLowHighResult | null) => {
        if (!cancelled) setResult(data)
      })
      .catch(() => {})
    return () => {
      cancelled = true
    }
  }, [organizationId, matchId, refreshKey])

  if (!result?.ready) {
    return <p className="px-4 py-4 text-sm text-muted-foreground">Venter på at begge sider er komplette.</p>
  }
  const holes = result.holes ?? []
  if (holes.length === 0) {
    return <p className="px-4 py-4 text-sm text-muted-foreground">Ingen hull er registrert ennå.</p>
  }

  return (
    <div className="flex flex-col gap-3 p-3 sm:p-4">
      <div className="grid grid-cols-3 items-center gap-2 rounded-2xl border border-border bg-background px-4 py-3">
        <div className="flex flex-col items-start gap-0.5">
          <span className="text-xs font-bold text-foreground">{teams[0].name}</span>
          <span className="text-2xl font-extrabold tabular-nums text-foreground">{result.points_a ?? 0}</span>
        </div>
        <div className="text-center text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
          Poeng
        </div>
        <div className="flex flex-col items-end gap-0.5">
          <span className="text-xs font-bold text-foreground">{teams[1].name}</span>
          <span className="text-2xl font-extrabold tabular-nums text-foreground">{result.points_b ?? 0}</span>
        </div>
      </div>
      <div className="overflow-x-auto">
        <table className="w-full border-collapse text-sm">
          <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>
              {holes.map((h) => (
                <th key={h.hole_number} scope="col" className="border border-border bg-muted px-1 py-1.5 text-center text-xs font-extrabold tabular-nums text-muted-foreground">
                  {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-semibold text-muted-foreground">
                {teams[0].name}
              </th>
              {holes.map((h) => (
                <td key={h.hole_number} className="border border-border py-1.5 text-center text-xs font-bold tabular-nums text-foreground">
                  {h.points_a}
                </td>
              ))}
            </tr>
            <tr>
              <th scope="row" className="border border-border px-2 py-1.5 text-left text-xs font-semibold text-muted-foreground">
                {teams[1].name}
              </th>
              {holes.map((h) => (
                <td key={h.hole_number} className="border border-border py-1.5 text-center text-xs font-bold tabular-nums text-foreground">
                  {h.points_b}
                </td>
              ))}
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  )
}

function scoreToParLabel(value: number, par: number): string {
  const diff = value - par
  if (diff === 0) return "Par"
  if (diff === -1) return "Birdie"
  if (diff === -2) return "Eagle"
  if (diff <= -3) return "Albatross"
  if (diff === 1) return "Bogey"
  if (diff === 2) return "Dobbel bogey"
  return `+${diff}`
}

function DecidedView({
  teams,
  scorecard,
  holes,
  units,
  scoringMode,
  holeConfig,
  format,
  organizationId,
  matchId,
}: {
  teams: [ApiTeam, ApiTeam]
  scorecard: ApiScorecard
  holes: ApiHole[]
  units: Unit[]
  scoringMode: ScoringMode
  holeConfig: HoleConfig
  format: Format
  organizationId: string
  matchId: string
}) {
  return (
    <div className="flex flex-col gap-6">
      <div className="flex items-center gap-4 rounded-3xl bg-primary px-5 py-5 text-primary-foreground shadow-sm">
        <div className="flex size-14 shrink-0 items-center justify-center rounded-2xl bg-primary-foreground/15">
          <Trophy aria-hidden="true" className="size-7" />
        </div>
        <div className="flex flex-col">
          <span className="text-lg font-extrabold tracking-tight">Matchen er avgjort</span>
          <span className="text-sm text-primary-foreground/85 text-pretty">{scorecard.status_text ?? ""}</span>
        </div>
      </div>

      <div className="grid grid-cols-3 items-center gap-2 rounded-3xl border border-border bg-card px-5 py-4 shadow-sm">
        <div className="flex flex-col items-start gap-1">
          <div className="flex items-center gap-2">
            <span aria-hidden="true" className="size-3.5 rounded-full" style={{ backgroundColor: teams[0].color ?? "#64748b" }} />
            <span className="text-sm font-bold text-foreground text-pretty">{teams[0].name}</span>
          </div>
          <span className="text-3xl font-extrabold tabular-nums text-foreground">
            {formatPoints(scorecard.points_side_a ?? 0)}
          </span>
        </div>
        <div className="text-center text-xs font-semibold uppercase tracking-wide text-muted-foreground">Poeng</div>
        <div className="flex flex-col items-end gap-1">
          <div className="flex flex-row-reverse items-center gap-2">
            <span aria-hidden="true" className="size-3.5 rounded-full" style={{ backgroundColor: teams[1].color ?? "#64748b" }} />
            <span className="text-sm font-bold text-foreground text-pretty">{teams[1].name}</span>
          </div>
          <span className="text-3xl font-extrabold tabular-nums text-foreground">
            {formatPoints(scorecard.points_side_b ?? 0)}
          </span>
        </div>
      </div>

      <div>
        <h2 className="mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground">Hull for hull</h2>
        <div className="overflow-hidden rounded-3xl border border-border bg-card shadow-md shadow-black/8">
          {format === "high_low_high" ? (
            <HighLowHighPanel
              organizationId={organizationId}
              matchId={matchId}
              teams={teams}
              refreshKey={scorecard.stroke_entries?.length ?? 0}
            />
          ) : (
            <TournamentMatchGrid
              teams={teams}
              units={units}
              holes={holes}
              scorecard={scorecard}
              scoringMode={scoringMode}
              holeConfig={holeConfig}
            />
          )}
        </div>
        <p className="mt-3 text-center text-xs text-muted-foreground text-pretty">
          Resultatet kan ikke endres etter at matchen er avgjort.
        </p>
      </div>
    </div>
  )
}

function formatPoints(points: number) {
  return Number.isInteger(points) ? String(points) : points.toFixed(1).replace(".", ",")
}
