// Pure bracket-layout logic for the tournament detail page.
//
// The TSS API hands us authoritative matches, but `round` is numbered *per
// `type_bracket`* — the winner tree, the loser tree, and the special
// `LooserFinal`/`Semifinal`/`Final` stages each restart at round 0. Grouping by
// raw round therefore collapses unrelated stages into one column (e.g. the loser
// Semifinal landing in loser "Round 1"). This module maps each match to a
// (side, stage, round) coordinate so columns come out in true bracket order, and
// drops structural byes so they never render as "Team vs BYE" nodes.
//
// Kept framework-free so it can be unit-tested with plain node.

function lower(value) {
  return String(value || '').toLowerCase()
}

export function cleanName(value) {
  return String(value || '').trim()
}

// A "structural bye" is a slot that exists only to pad the bracket: a team that
// advanced unopposed, or a placeholder whose feeders were themselves byes. We
// hide these. A match with one known team and NO winner yet is *not* a bye —
// it's a real pending matchup awaiting an upstream result (grand final, loser
// bracket final), so it stays.
export function isStructuralBye(match) {
  const a = cleanName(match.team_a_name)
  const b = cleanName(match.team_b_name)
  if (a && b) return false
  if (!a && !b) return true
  return Boolean(cleanName(match.winner_name)) || match.status === 'bye'
}

// Map a TSS `type_bracket` to a display side + stage. `stage` orders the special
// terminal columns after the numbered rounds within a side:
//   winner:  Winner rounds (0) → grand/championship Final (1)
//   loser:   Looser rounds (0) → LooserFinal (1) → Semifinal/lower final (2)
export function classifyBracket(typeBracket, hasLoserSide) {
  const t = lower(typeBracket)
  if (t.includes('swiss')) return { side: 'swiss', stage: 0 }
  if (t.includes('group')) return { side: 'group', stage: 0 }
  if (t.includes('looserfinal') || t.includes('loserfinal')) return { side: 'loser', stage: 1 }
  if (t.includes('looser') || t.includes('loser')) return { side: 'loser', stage: 0 }
  if (t.includes('semifinal')) {
    // Double-elim: the Semifinal is the lower-bracket final (loser side). Single
    // elim: it's just a normal winner-side stage.
    return hasLoserSide ? { side: 'loser', stage: 2 } : { side: 'winner', stage: 0 }
  }
  if (t === 'final' || t.endsWith('final')) {
    // Terminal column of the winner side (championship / grand final).
    return { side: 'winner', stage: 1 }
  }
  if (t.includes('winner')) return { side: 'winner', stage: 0 }
  return { side: 'winner', stage: 0 }
}

const SIDE_ORDER = { winner: 0, loser: 1, group: 2, swiss: 3, matches: 4 }
const SIDE_LABELS = {
  winner: 'Winner bracket',
  loser: 'Loser bracket',
  group: 'Group matches',
  swiss: 'Swiss matches',
  matches: 'Matches',
}

function asInt(value, fallback) {
  const n = Number(value)
  return Number.isFinite(n) ? n : fallback
}

function compareMatches(a, b) {
  return asInt(a.position, Number.MAX_SAFE_INTEGER) - asInt(b.position, Number.MAX_SAFE_INTEGER)
    || String(a.match_id).localeCompare(String(b.match_id))
}

// Label a bracket column. Numbered rounds read "Round N" by their position in
// the side; the terminal winner stage reads Final / Grand Final.
function columnLabel(side, stage, index, total, hasLoserSide) {
  if (side === 'winner' && stage === 1) {
    return hasLoserSide ? 'Grand Final' : 'Final'
  }
  return `Round ${index + 1}`
}

// Build the ordered bracket model from authoritative match rows.
// Returns { sides: [{ key, label, kind, columns: [{ id, label, matches }] }] }
// where kind is 'bracket' (elimination, drawn as a tree) or 'list' (group/swiss,
// drawn as a flat card list).
export function buildBracket(matches) {
  const cleaned = (matches || []).filter((m) => !isStructuralBye(m))

  const hasLoserSide = cleaned.some((m) => {
    const t = lower(m.type_bracket)
    return t.includes('looser') || t.includes('loser') || lower(m.side) === 'loser'
  })

  // Group by (side, stage, round) → one column each.
  const columnMap = new Map()
  for (const match of cleaned) {
    const { side, stage } = classifyBracket(match.type_bracket, hasLoserSide)
    const round = asInt(match.round, 0)
    const key = `${side}:${stage}:${round}`
    if (!columnMap.has(key)) {
      columnMap.set(key, { side, stage, round, matches: [] })
    }
    columnMap.get(key).matches.push(match)
  }

  // Bucket columns by side, ordered by (stage, round).
  const sideMap = new Map()
  for (const column of columnMap.values()) {
    column.matches.sort(compareMatches)
    if (!sideMap.has(column.side)) sideMap.set(column.side, [])
    sideMap.get(column.side).push(column)
  }

  const sides = [...sideMap.entries()]
    .map(([key, columns]) => {
      columns.sort((a, b) => a.stage - b.stage || a.round - b.round)
      const isList = key === 'group' || key === 'swiss' || key === 'matches'
      return {
        key,
        label: SIDE_LABELS[key] || key,
        kind: isList ? 'list' : 'bracket',
        columns: columns.map((column, index) => ({
          id: `${key}:${column.stage}:${column.round}`,
          label: columnLabel(key, column.stage, index, columns.length, hasLoserSide),
          matches: column.matches,
        })),
      }
    })
    .sort((a, b) => (SIDE_ORDER[a.key] ?? 9) - (SIDE_ORDER[b.key] ?? 9))

  return { sides, hasLoserSide }
}

// Which match in the next column a given match feeds into, by slot position.
// Winner tree and loser "major" rounds halve (position → floor/2); loser "minor"
// rounds (drop-ins, same width) map identically. We infer halving vs identity
// from the column node counts. Returns the parent position, or null.
export function parentPosition(childPosition, currentCount, nextCount) {
  if (childPosition == null) return null
  if (nextCount >= currentCount) return childPosition
  return Math.floor(childPosition / 2)
}

// The match in `columns[columnIndex + 1]` that a given match feeds into. Found by
// slot position (faithful to the real tree even with byes hidden); falls back to
// a proportional index if the exact parent slot was itself a hidden bye.
export function feederParent(match, columnIndex, columns) {
  const cur = columns[columnIndex]
  const next = columns[columnIndex + 1]
  if (!next) return null
  const pPos = parentPosition(Number(match.position), cur.matches.length, next.matches.length)
  const exact = next.matches.find((m) => Number(m.position) === pPos)
  if (exact) return exact
  const order = cur.matches.indexOf(match)
  const idx = Math.min(
    next.matches.length - 1,
    Math.floor((order * next.matches.length) / cur.matches.length),
  )
  return next.matches[idx] || null
}

export function layoutKey(columnIndex, match) {
  return `${columnIndex}:${match.match_id}`
}

// Position every match vertically so each one sits centred between the matches
// that feed into it — the classic bracket look. Pure: takes measured card heights,
// returns absolute `top` offsets keyed by `layoutKey`.
//
// Column 0 stacks top-to-bottom. Later columns centre each match on the mean of
// its feeders' centres. Matches whose feeders were all hidden byes (no anchor)
// are *extrapolated* from the nearest anchored sibling using the column's slot
// pitch — including above y=0 — rather than stacked at the top, which would shove
// the anchored matches away from their feeders and make the connectors fan and
// cross. A final per-side shift pulls any negative offsets back on-screen.
export function computeBracketLayout(columns, heights, gap = 16) {
  const heightOf = (c, m) => heights.get(layoutKey(c, m)) || 92
  const centers = columns.map(() => new Map())

  columns.forEach((column, c) => {
    const matches = column.matches
    const hs = matches.map((m) => heightOf(c, m))
    const pitch = (hs.reduce((a, b) => a + b, 0) / Math.max(1, hs.length)) + gap

    // Anchor = mean of this match's feeders' centres (null when all were byes).
    const childCenters = new Map()
    if (c > 0) {
      for (const child of columns[c - 1].matches) {
        const parent = feederParent(child, c - 1, columns)
        if (!parent) continue
        const center = centers[c - 1].get(child.match_id)
        if (center == null) continue
        if (!childCenters.has(parent.match_id)) childCenters.set(parent.match_id, [])
        childCenters.get(parent.match_id).push(center)
      }
    }
    const center = matches.map((m) => {
      const kids = childCenters.get(m.match_id)
      return kids && kids.length ? kids.reduce((a, b) => a + b, 0) / kids.length : null
    })

    const firstAnchor = center.findIndex((v) => v != null)
    if (firstAnchor === -1) {
      // No anchors (column 0, or a fully-bye column): plain stack.
      let cursor = 0
      matches.forEach((m, i) => {
        center[i] = cursor + hs[i] / 2
        cursor += hs[i] + gap
      })
    } else {
      // Leading byes: extrapolate upward (may go negative — fixed by the shift).
      for (let i = firstAnchor - 1; i >= 0; i -= 1) center[i] = center[i + 1] - pitch
      // Trailing / interior byes: extend down / interpolate between anchors.
      let lastAnchor = firstAnchor
      for (let i = firstAnchor + 1; i < matches.length; i += 1) {
        if (center[i] != null) { lastAnchor = i; continue }
        let next = i
        while (next < matches.length && center[next] == null) next += 1
        if (next >= matches.length) {
          center[i] = center[lastAnchor] + (i - lastAnchor) * pitch
        } else {
          const span = next - lastAnchor
          center[i] = center[lastAnchor] + ((center[next] - center[lastAnchor]) * (i - lastAnchor)) / span
        }
      }
      // Resolve any residual overlap by nudging downward (rare; honours heights).
      for (let i = 1; i < matches.length; i += 1) {
        const minC = center[i - 1] + (hs[i - 1] + hs[i]) / 2 + gap
        if (center[i] < minC) center[i] = minC
      }
    }

    matches.forEach((m, i) => centers[c].set(m.match_id, center[i]))
  })

  // Shift the whole side so the highest match sits at y=0, then size to content.
  let minTop = Infinity
  columns.forEach((column, c) => {
    column.matches.forEach((m) => {
      minTop = Math.min(minTop, centers[c].get(m.match_id) - heightOf(c, m) / 2)
    })
  })
  const shift = Number.isFinite(minTop) && minTop < 0 ? -minTop : 0

  const tops = new Map()
  let height = 0
  columns.forEach((column, c) => {
    column.matches.forEach((m) => {
      const h = heightOf(c, m)
      const top = centers[c].get(m.match_id) - h / 2 + shift
      tops.set(layoutKey(c, m), top)
      height = Math.max(height, top + h)
    })
  })

  return { tops, centers, height }
}
