import { PERP_SYMBOL_OVERRIDES } from './perp-symbol-overrides'

// The one place that decides what a listing is CALLED.
//
// WHY IT IS ITS OWN MODULE. Two universes are built from the same Hyperliquid
// payloads: `normalizePerpsMeta` for the trade app, and Pulse's
// `buildTickerUniverse` for the news and brief pipeline. When only the first
// resolved a display name, the trade app called the Nasdaq-100 listing `QQQ`
// while Pulse briefs called it `USTECH` — one instrument, two names, in the
// same product. Both now call this, so the PRECEDENCE cannot drift. One rule
// still lives outside it and is called out at the bottom of this comment.
//
// A DISPLAY NAME IS NOT AN IDENTITY. It is not unique (`xyz:VIX` is annotated
// `VVV`, and `VVV` is itself a real symbol) and upstream may change it at any
// time. Look one up FROM a symbol to render it; never parse one BACK to decide
// which asset was meant. Database rows, `tickers` arrays and trust checks all
// key on the symbol.
//
// THE TRADE URL IS THE EXCEPTION, and it is not ours to change: `/trade/…`
// resolves the DISPLAY name (`/trade/wtioil`, not `/trade/cl`) — see
// `to-trade-url-symbol.ts`. So an upstream annotation edit does move a route
// key. That predates this module; it is recorded here because "everything uses
// the symbol" is the obvious summary and it is false.
//
// THE SHADOW RULE IS PULSE-SIDE ONLY. `buildTickerUniverse` refuses a name that
// belongs to another symbol; `normalizePerpsMeta` does not, because that would
// change live trade-app labels and route keys. So for a shadowed listing the
// two surfaces still disagree — `xyz:VIX` reads `VIX` in a Pulse brief and
// `VVV` on the trade chip. Sharing this function removes the drift for every
// OTHER listing, which is most of them; it does not remove it for that case.

const toOverride = (key: string): string | undefined =>
  // `Object.hasOwn`, not a bare index. `PERP_SYMBOL_OVERRIDES` is an object
  // literal typed `Record<string, string>`, so a symbol named `constructor`
  // would read a FUNCTION off the prototype chain and `??` would happily
  // return it. The typechecker cannot see this — the index signature types the
  // read as `string`.
  Object.hasOwn(PERP_SYMBOL_OVERRIDES, key)
    ? PERP_SYMBOL_OVERRIDES[key]
    : undefined

// A display name is a MARKET NAME, and the live annotation is free text typed
// by whoever deployed the listing — including HIP-3 dexes we do not operate.
// It reaches an LLM prompt, a Telegram `<code>` block and a trade URL, so the
// shape is constrained here, once, rather than at each of those sinks:
//
//   - single line, no spaces — a newline inside the Telegram snapshot table
//     draws a whole fabricated price row, and a blank-padded name (`'   '`)
//     renders a row with a price and no market at all;
//   - ASCII letters and digits, plus exactly the two punctuation marks the
//     live data uses: `&` (`S&P500`) and `.` (`BTC.D`). Checked against all
//     108 annotated listings — nothing else appears. Restricting to ASCII
//     also stops a Cyrillic homoglyph impersonating another listing's ticker;
//   - bounded length, as a sanity limit on a free-text field.
//
// NO `/`, DELIBERATELY. A display name becomes a trade URL path segment
// (`to-trade-url-symbol.ts`), and `ROUTES.TRADE_SYMBOL` interpolates it
// without encoding — so `a/../../vault` would render an href the browser
// resolves to `/vault`. Dot segments need a slash to be segments at all, so
// refusing `/` removes the whole class rather than trying to spot `..`.
//
// AND NOTE WHY THE ORDER MATTERS: normalization runs FIRST, so the charset
// test sees the normalized string. That is not merely tidy — NFKC MAPS some
// characters INTO the allowed set (U+2105 `℅` becomes `c/o`), so testing the
// raw input would let a rejected character launder itself into a slash.
//
// Anything else is upstream failing to name the market, so the symbol wins.
// Rejecting is always safe: the symbol is what we showed before any of this.
const MAX_DISPLAY_LENGTH = 16
const DISPLAY_SAFE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9&.]*$/

const toValidDisplay = (candidate: string): string | undefined => {
  // NFKC first: it folds the compatibility forms that render like ASCII but
  // are not, so the charset test below sees what a reader would see.
  const normalized = candidate.normalize('NFKC').trim()
  return normalized.length > 0 &&
    normalized.length <= MAX_DISPLAY_LENGTH &&
    DISPLAY_SAFE_PATTERN.test(normalized)
    ? normalized
    : undefined
}

/**
 * Resolve the name to show for a perp listing.
 *
 * Precedence: curated override (by raw Hyperliquid name, then by clean symbol)
 * → the exchange's own live annotation → the clean symbol itself. The result is
 * always a non-empty string, so callers render it unconditionally and compare
 * it against `symbol` when they want to know whether it says anything new.
 *
 * A live annotation that is not a plausible market name is refused and the
 * symbol is used instead — see `toValidDisplay`. Curated overrides skip that
 * check because we write them.
 */
export const toDisplaySymbol = ({
  annotationDisplayName,
  hyperliquidSymbol,
  symbol,
}: {
  /** `perpConciseAnnotations`' `displayName` for this listing, if any. */
  readonly annotationDisplayName?: string | undefined
  /** The raw universe name, dex prefix included (`mkts:USTECH`). */
  readonly hyperliquidSymbol: string
  /** The cleaned, unprefixed symbol (`USTECH`). */
  readonly symbol: string
}): string => {
  const override = toOverride(hyperliquidSymbol) ?? toOverride(symbol)
  if (override !== undefined) {
    return override
  }
  if (annotationDisplayName === undefined) {
    return symbol
  }
  return toValidDisplay(annotationDisplayName) ?? symbol
}
