import { useMemo, useState } from 'react'
import PublicLayout from '../../components/PublicLayout.jsx'
import PageHeader from '../../components/PageHeader.jsx'
import { Loading, ErrorState } from '../../components/PageState.jsx'
import { useAsync } from '../../lib/useAsync.js'
import { useShardFeed } from '../../lib/useShardFeed.js'
import { api } from '../../api/client.js'
import { useSite } from '../../contexts/SiteContext.jsx'

// Points / loyalty leaderboards (Protocol 3.0 §7). The shard carries ~25 separate
// point currencies — Queen's Loyalty, Void Pool, Clean Up Britannia, the nine city
// loyalties, the Doom/Khaldun/Kotl treasure systems — every one of them a standing
// players build over months, and none of them visible anywhere but an in-game gump
// until now.
//
// Loaded from /public/shard/points, then kept current from the live feed. Unlike
// the ruleset (one frame = the whole thing), a points.board frame describes ONE
// system, so live frames are merged over the fetched set by system key rather than
// replacing it.
const POINTS_KINDS = new Set(['points.board'])

// A board's display name may arrive as a literal (`nameString`), a cliloc id
// (`nameNumber`), or both — Name is a ServUO TextDefinition. We have no cliloc
// table on the site, so a cliloc-only board falls back to humanising its own
// PointsType key, which is already close to a display name ("CleanUpBritannia" →
// "Clean Up Britannia"). Better than showing a bare number.
const humanise = (key) =>
  String(key || '')
    .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
    .replace(/^./, (c) => c.toUpperCase())

const boardTitle = (b) => b.nameString || humanise(b.system)

const num = (v) => (Number.isFinite(v) ? v.toLocaleString() : '—')

// Merge live frames over the fetched boards. Newest frame per system wins; a
// system that has never appeared in either is simply absent.
function mergeBoards(fetched, events) {
  const bySystem = new Map()
  for (const b of Array.isArray(fetched) ? fetched : []) {
    if (b && b.system) bySystem.set(b.system, b)
  }
  // Events arrive newest-first, so walk backwards and let the newest land last.
  for (let i = events.length - 1; i >= 0; i--) {
    const ev = events[i]
    if (ev && ev.system) bySystem.set(ev.system, ev)
  }
  return [...bySystem.values()].sort((a, b) => boardTitle(a).localeCompare(boardTitle(b)))
}

function Medal({ rank }) {
  // Gold / silver / bronze for the podium, plain for the rest.
  const tone = rank === 1 ? '#c9a24b' : rank === 2 ? '#b6bcc6' : rank === 3 ? '#b3805a' : 'var(--muted)'
  return (
    <span
      className="display"
      style={{
        flex: 'none', width: 26, textAlign: 'right', color: tone,
        fontSize: rank <= 3 ? '1rem' : '0.86rem',
      }}
    >
      {rank}
    </span>
  )
}

// One ranked player. `name` is absent rather than empty when an admin has gated
// the leaderboards `name` field above this viewer's rung — the row still renders,
// because the standing itself is the point.
function Entry({ entry, best }) {
  const pct = best > 0 ? Math.max(2, Math.round((entry.points / best) * 100)) : 0
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '6px 0' }}>
      <Medal rank={entry.rank} />
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 10 }}>
          <span
            className="sans"
            style={{
              color: entry.name ? 'var(--ink)' : 'var(--muted)',
              fontSize: '0.86rem', fontStyle: entry.name ? 'normal' : 'italic',
              overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
            }}
          >
            {entry.name || 'Name hidden'}
          </span>
          <span className="sans" style={{ color: 'var(--head)', fontSize: '0.82rem', flex: 'none' }}>
            {num(entry.points)}
          </span>
        </div>
        <div style={{ height: 4, borderRadius: 999, background: 'var(--line)', overflow: 'hidden', marginTop: 3 }}>
          <div style={{ width: `${pct}%`, height: '100%', background: 'var(--accent)' }} />
        </div>
      </div>
    </div>
  )
}

function Board({ board }) {
  const { siteTitle } = useSite()
  const top = Array.isArray(board.top) ? board.top : []
  // Bars are relative to the board leader, not to maxPoints: most systems have no
  // cap (maxPoints 0), and where there is one the leader is often nowhere near it,
  // which would render every bar as a stub.
  const best = top.reduce((m, e) => Math.max(m, e.points || 0), 0)

  return (
    <section className="panel" style={{ padding: 18, display: 'flex', flexDirection: 'column', gap: 10 }}>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 10 }}>
        <h2 className="display" style={{ margin: 0, fontSize: '1.02rem', color: 'var(--head)' }}>
          {boardTitle(board)}
        </h2>
        {Number.isFinite(board.players) && (
          <span className="sans dim" style={{ fontSize: '0.72rem', flex: 'none' }}>
            {num(board.players)} ranked
          </span>
        )}
      </div>

      {top.length === 0 ? (
        // A board nobody has scored on still gets a row, so the page reads as a set
        // of standings waiting to be filled rather than a stack of blanks. It is
        // deliberately NOT shaped like an Entry — no medal, no bar, an em dash where
        // a score goes — because a placeholder that looked like a real standing would
        // be a fabricated one. The first real entry replaces it.
        <div>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 10, padding: '6px 0' }}>
            <span
              className="sans"
              style={{
                color: 'var(--muted)', fontSize: '0.86rem',
                overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
              }}
            >
              {siteTitle}
            </span>
            <span className="sans dim" style={{ fontSize: '0.82rem', flex: 'none' }}>&mdash;</span>
          </div>
          <p className="sans dim" style={{ margin: 0, fontSize: '0.78rem' }}>
            Nobody has earned points here yet.
          </p>
        </div>
      ) : (
        <div>
          {top.map((entry) => (
            <Entry key={`${board.system}-${entry.rank}-${entry.serial}`} entry={entry} best={best} />
          ))}
        </div>
      )}

      {Number.isFinite(board.maxPoints) && board.maxPoints > 0 && (
        <span className="sans dim" style={{ fontSize: '0.72rem' }}>
          Maximum {num(board.maxPoints)} points
        </span>
      )}
    </section>
  )
}

export default function Leaderboards() {
  const { loading, error, data } = useAsync(() => api.shard.points())
  // Buffer generously: a single sweep can emit a frame for every system at once,
  // and a board dropped from the buffer would silently revert to its fetched copy.
  const { events, connected } = useShardFeed({ filter: POINTS_KINDS, max: 60 })
  const [query, setQuery] = useState('')

  const boards = useMemo(() => mergeBoards(data, events), [data, events])

  const shown = useMemo(() => {
    const q = query.trim().toLowerCase()
    if (!q) return boards
    // Match the board name, the raw system key, or any ranked player on it — the
    // last is what makes the filter useful ("where do I appear?").
    return boards.filter(
      (b) =>
        boardTitle(b).toLowerCase().includes(q) ||
        String(b.system).toLowerCase().includes(q) ||
        (b.top || []).some((e) => e.name && e.name.toLowerCase().includes(q)),
    )
  }, [boards, query])

  return (
    <PublicLayout section="website">
      <div className="shell page-body">
        <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16 }}>
          <PageHeader
            eyebrow="Live"
            title="Leaderboards"
            lead="Loyalty and points standings, straight from the shard — every currency the server tracks, updated as players climb."
          />
          <span
            className="sans"
            style={{
              display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: '0.74rem',
              color: connected ? '#7fd0a4' : 'var(--muted)', flex: 'none', marginTop: 6,
            }}
          >
            <span style={{ width: 8, height: 8, borderRadius: '50%', background: connected ? '#7fd0a4' : 'var(--dim)' }} />
            {connected ? 'Live' : 'Offline'}
          </span>
        </div>

        {loading && <Loading />}
        {error && <ErrorState message="Could not load the leaderboards right now." />}

        {!loading && !error && boards.length === 0 && (
          <section className="panel" style={{ padding: 24, textAlign: 'center' }}>
            <p className="sans dim" style={{ margin: 0 }}>
              The shard has not published any leaderboards yet.
            </p>
          </section>
        )}

        {!loading && !error && boards.length > 0 && (
          <>
            <input
              className="input"
              type="search"
              value={query}
              onChange={(e) => setQuery(e.target.value)}
              placeholder="Filter by board or player name…"
              aria-label="Filter leaderboards"
              style={{ maxWidth: 340, marginBottom: 14 }}
            />

            {shown.length === 0 ? (
              <p className="sans dim">No board or ranked player matches “{query}”.</p>
            ) : (
              <div className="grid-2" style={{ gap: 12, alignItems: 'start' }}>
                {shown.map((board) => (
                  <Board key={board.system} board={board} />
                ))}
              </div>
            )}
          </>
        )}
      </div>
    </PublicLayout>
  )
}
