import { useCallback, useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import PublicLayout from '../../components/PublicLayout.jsx'
import PageHeader from '../../components/PageHeader.jsx'
import { Loading, ErrorState, EmptyState } from '../../components/PageState.jsx'
import { useAsync } from '../../lib/useAsync.js'
import { api } from '../../api/client.js'

// ── The player-vendor marketplace ───────────────────────────────────────────
//
// What every player vendor on the shard is selling, for how much, and where it
// is standing — the same index the in-game Vendor Search gump reads, honouring
// the same per-vendor opt-out, reachable without logging in to the game.
//
// Three things this page must be honest about, all of them consequences of how
// the data is gathered (docs/link/v3.md §8):
//
//   • **The prices are not live.** The shard sweeps vendors round-robin, so a
//     shop can be a full cycle behind. The banner says how far, from `staleAt`.
//     A page that implied live prices would send people across the world to a
//     vendor whose item sold twenty minutes ago.
//   • **A shop can be truncated.** A commodity reseller with thousands of stacks
//     publishes only the first N, and saying so beats presenting a partial shop
//     as complete.
//   • **An item may have no name.** On a shard whose operator has not converted
//     a cliloc table, `displayName` is null and the honest render is the item id
//     — not an invented name.
//
// There is deliberately no live feed here. The market feature's SSE stream ships
// disabled: a firehose of whole vendor inventories would be the site's single
// biggest bandwidth consumer, and nothing on this page needs it.

const PAGE = 50

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

const SORTS = [
  { key: 'price_asc', label: 'Cheapest' },
  { key: 'price_desc', label: 'Priciest' },
  { key: 'recent', label: 'Recently seen' },
]

// How old the index may be, in words. `staleAt` is the OLDEST vendor row, so
// this is a worst case rather than an average — which is the number worth
// showing, because the one stale shop is the one that wastes a trip.
function staleness(staleAt) {
  if (!staleAt) return null
  const ms = Date.now() - new Date(staleAt).getTime()
  if (!Number.isFinite(ms) || ms < 0) return null
  const mins = Math.round(ms / 60000)
  if (mins < 1) return 'just now'
  if (mins < 60) return `${mins} minute${mins === 1 ? '' : 's'} ago`
  const hours = Math.round(mins / 60)
  if (hours < 48) return `${hours} hour${hours === 1 ? '' : 's'} ago`
  return `${Math.round(hours / 24)} days ago`
}

// The item's name, or an honest statement that we do not have one. Never a
// fabricated label — "Item 3922" would be indistinguishable from a real name.
const itemLabel = (l) => l.displayName || l.name || `id ${l.itemId}`

function Chip({ active, onClick, children }) {
  return (
    <button
      type="button"
      onClick={onClick}
      className="sans"
      style={{
        fontSize: '0.78rem',
        padding: '5px 12px',
        borderRadius: 999,
        cursor: 'pointer',
        color: active ? 'var(--bg-deep)' : 'var(--muted)',
        background: active ? 'var(--accent)' : 'transparent',
        border: `1px solid ${active ? 'var(--accent)' : 'var(--line)'}`,
      }}
    >
      {children}
    </button>
  )
}

function ListingRow({ listing }) {
  const v = listing.vendor || {}
  // `location` is one field the admin can gate away wholesale, so everything
  // that reads from it has to tolerate its absence rather than assuming a map.
  const loc = v.location || null
  const where = loc ? [loc.region, loc.map].filter(Boolean).join(', ') : null

  return (
    <div className="panel" style={{ padding: '13px 15px', display: 'flex', gap: 14, alignItems: 'center' }}>
      <div style={{ minWidth: 0, flex: 1 }}>
        <div
          className="display"
          style={{ fontSize: '0.98rem', color: 'var(--head)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
        >
          {listing.amount > 1 ? `${num(listing.amount)} × ` : ''}
          {itemLabel(listing)}
        </div>
        <div className="sans dim" style={{ fontSize: '0.74rem', marginTop: 3 }}>
          {v.serial ? (
            <Link to={`/site/market/vendors/${encodeURIComponent(v.serial)}`} style={{ color: 'inherit' }}>
              {v.shopName || 'an unnamed shop'}
            </Link>
          ) : (
            v.shopName || 'an unnamed shop'
          )}
          {v.ownerName ? ` · ${v.ownerName}` : ''}
          {where ? ` · ${where}` : ''}
          {/* Priced by the container it sits in, exactly as the in-game search
              reports it — the price buys the whole container, not this item. */}
          {listing.child ? ' · sold with its container' : ''}
        </div>
      </div>
      <div className="sans" style={{ flex: 'none', textAlign: 'right' }}>
        <div style={{ color: 'var(--head)', fontSize: '0.92rem' }}>{num(listing.price)}</div>
        <div className="dim" style={{ fontSize: '0.68rem', letterSpacing: '0.05em' }}>gold</div>
      </div>
    </div>
  )
}

export default function Market() {
  const [input, setInput] = useState('')
  const [q, setQ] = useState('')
  const [map, setMap] = useState('')
  const [region, setRegion] = useState('')
  const [sort, setSort] = useState('price_asc')
  const [minPrice, setMinPrice] = useState('')
  const [maxPrice, setMaxPrice] = useState('')
  // Applied prices are separate from the typed ones so the search fires when the
  // user is done, not on every digit of "250000".
  const [prices, setPrices] = useState({ min: '', max: '' })

  const [state, setState] = useState({ loading: true, error: null, listings: [], total: 0, staleAt: null })
  const [more, setMore] = useState(false)

  const meta = useAsync(() => api.shard.marketMeta())

  // Debounced: typing "vanquishing" should be one request, not eleven — and the
  // endpoint is rate-limited, so an undebounced box would 429 a fast typist.
  useEffect(() => {
    const timer = setTimeout(() => setQ(input.trim()), 300)
    return () => clearTimeout(timer)
  }, [input])

  useEffect(() => {
    const timer = setTimeout(() => setPrices({ min: minPrice, max: maxPrice }), 500)
    return () => clearTimeout(timer)
  }, [minPrice, maxPrice])

  const load = useCallback(
    (offset) =>
      api.shard.market({
        q,
        map,
        region,
        sort,
        minPrice: prices.min,
        maxPrice: prices.max,
        limit: PAGE,
        offset,
      }),
    [q, map, region, sort, prices],
  )

  useEffect(() => {
    let alive = true
    setState({ loading: true, error: null, listings: [], total: 0, staleAt: null })
    load(0)
      .then((page) => {
        if (!alive) return
        setState({
          loading: false,
          error: null,
          listings: page.listings || [],
          total: page.total || 0,
          staleAt: page.staleAt || null,
        })
      })
      .catch((error) => alive && setState({ loading: false, error, listings: [], total: 0, staleAt: null }))
    return () => {
      alive = false
    }
  }, [load])

  const loadMore = async () => {
    setMore(true)
    try {
      const page = await load(state.listings.length)
      setState((s) => ({
        ...s,
        listings: [...s.listings, ...(page.listings || [])],
        total: page.total ?? s.total,
        staleAt: page.staleAt ?? s.staleAt,
      }))
    } catch {
      // A failed "load more" leaves what is on screen alone; the button stays
      // available to retry.
    } finally {
      setMore(false)
    }
  }

  const maps = meta.data?.maps || []
  const regions = meta.data?.regions || []
  const age = staleness(state.staleAt)

  return (
    <PublicLayout section="website">
      <div className="shell-narrow page-body">
        <PageHeader
          eyebrow="Marketplace"
          title="Player vendors"
          lead="Every shop on the shard, searchable from here — the same index the in-game vendor search reads, and it honours the same per-vendor opt-out."
        />

        {/* Not decoration. The sweep is round-robin, so the index is inherently
            up to one full cycle old and the page has to say so. */}
        {age && (
          <p className="sans dim" style={{ fontSize: '0.76rem', margin: '-12px 0 18px' }}>
            Prices last refreshed {age}
            {meta.data?.vendors ? ` · ${num(meta.data.vendors)} shops` : ''}
            {meta.data?.items ? ` · ${num(meta.data.items)} listings` : ''}
          </p>
        )}

        <input
          className="input"
          type="search"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Search listings…"
          style={{ width: '100%', marginBottom: 10 }}
        />

        <div style={{ display: 'flex', gap: 8, marginBottom: 12, flexWrap: 'wrap' }}>
          <input
            className="input"
            type="number"
            min="0"
            value={minPrice}
            onChange={(e) => setMinPrice(e.target.value)}
            placeholder="Min price"
            style={{ maxWidth: 140 }}
          />
          <input
            className="input"
            type="number"
            min="0"
            value={maxPrice}
            onChange={(e) => setMaxPrice(e.target.value)}
            placeholder="Max price"
            style={{ maxWidth: 140 }}
          />
        </div>

        <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 10 }}>
          {SORTS.map((s) => (
            <Chip key={s.key} active={sort === s.key} onClick={() => setSort(s.key)}>
              {s.label}
            </Chip>
          ))}
        </div>

        {/* Facet and region names come from the shard's own data, never a list in
            this file — a shard running custom maps gets its own names here with
            no code change (docs/link/v3.md §6.1 R2). */}
        {maps.length > 0 && (
          <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 10 }}>
            <Chip active={map === ''} onClick={() => setMap('')}>All facets</Chip>
            {maps.map((m) => (
              <Chip key={m} active={map === m} onClick={() => setMap(m)}>{m}</Chip>
            ))}
          </div>
        )}

        {regions.length > 0 && (
          <select
            className="input"
            value={region}
            onChange={(e) => setRegion(e.target.value)}
            style={{ width: '100%', marginBottom: 18 }}
          >
            <option value="">Anywhere</option>
            {regions.map((r) => (
              <option key={r} value={r}>{r}</option>
            ))}
          </select>
        )}

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

        {!state.loading && !state.error && state.listings.length === 0 && (
          <EmptyState>
            {meta.data?.vendors
              ? 'Nothing on the shard matches that.'
              : 'No player vendors have been indexed yet.'}
          </EmptyState>
        )}

        {!state.loading && !state.error && state.listings.length > 0 && (
          <>
            <p className="sans dim" style={{ fontSize: '0.78rem', margin: '0 0 12px' }}>
              Showing {num(state.listings.length)} of {num(state.total)}
            </p>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
              {state.listings.map((l) => (
                <ListingRow key={`${l.vendor?.serial}:${l.serial}`} listing={l} />
              ))}
            </div>
            {state.listings.length < state.total && (
              <div style={{ textAlign: 'center', marginTop: 16 }}>
                <button type="button" className="btn" onClick={loadMore} disabled={more}>
                  {more ? 'Loading…' : 'Load more'}
                </button>
              </div>
            )}
          </>
        )}
      </div>
    </PublicLayout>
  )
}
