/**
 * Mention picker client logic + REST helper.
 *
 * Two responsibilities, deliberately separated so each can be unit
 * tested without rendering React or hitting fetch:
 *
 * 1. `searchMentionUsers(query)` — typed wrapper over the new
 *    `GET /api/cms/users/search` endpoint introduced in Phase 7.
 *    Returns up to 10 `{ id, name, email }` rows matching the query.
 * 2. `detectMentionContext(value, caret)` — pure parser that scans a
 *    text input value backwards from the caret to decide whether the
 *    user is currently typing a `@`-mention, and if so what the
 *    query fragment is. Returns `null` when no active mention.
 *
 * Wire format note: the picker INSERTS `@<userId>` tokens at the
 * cursor (matching the server-side `extractMentionedUserIds` regex
 * in `cms-core/realtime/notifications.ts`). The user sees the chosen
 * user's *name* in the picker but the textarea always holds the id.
 * Rendering names back in the comment view is a separate slice that
 * can ship later without breaking the wire format.
 */
import { cmsApi } from './api.js'

export interface MentionUser {
  id: string
  name: string
  email: string
}

export type MentionSearchOutcome =
  | { ok: true; result: MentionUser[] }
  | { ok: false; error: string; status: number }

export async function searchMentionUsers(query: string): Promise<MentionSearchOutcome> {
  const trimmed = query.trim()
  if (trimmed.length === 0) {
    // Mirror the server's contract: an empty query returns an empty
    // array. We short-circuit here so the picker doesn't spend a
    // round-trip on a guaranteed-empty response.
    return { ok: true, result: [] }
  }
  const res = await cmsApi<MentionUser[]>(`/users/search?q=${encodeURIComponent(trimmed)}`, {
    method: 'GET',
  })
  if (res.error || !res.data) {
    return { ok: false, error: res.error ?? `Request failed (${res.status})`, status: res.status }
  }
  return { ok: true, result: res.data }
}

/**
 * Description of an active `@`-mention being typed in a textarea / input.
 *
 *  - `start` and `end` are character offsets into the field value: the
 *    `@` itself sits at `start`, and `end` is the caret position
 *    (exclusive end of the query slice).
 *  - `query` is the substring between the `@` and the caret — exactly
 *    what we should pass to `searchMentionUsers()`.
 *
 * Callers replace `value.slice(start, end)` with the chosen token
 * (`@<userId> `, including the trailing space) to commit a selection.
 */
export interface MentionContext {
  start: number
  end: number
  query: string
}

/**
 * Find an active mention at the caret. Mirrors the server-side
 * `extractMentionedUserIds` regex
 * (`/(^|\s)@[a-zA-Z0-9_-]{3,64}\b/g`) so the picker's intuition
 * about what counts as a mention can never disagree with the
 * server's notification routing.
 *
 * Returns `null` whenever:
 *   - the caret is at offset 0,
 *   - there is no `@` between the caret and the previous whitespace
 *     boundary,
 *   - the `@` is preceded by a non-whitespace, non-newline character
 *     (so an email address mid-word like `foo@bar` does NOT open
 *     the picker), or
 *   - the candidate query contains a character outside the mention
 *     charset (`[a-zA-Z0-9_-]`).
 *
 * The picker DOES open with a 0–2 character query — that's the
 * "you've started typing a mention" UX. But it can NEVER commit
 * such a token: `insertMentionToken` only fires for a `MentionUser`
 * which always has an id of at least 3 chars, matching the server
 * regex's min-length requirement.
 */
export function detectMentionContext(value: string, caret: number): MentionContext | null {
  if (typeof value !== 'string') return null
  if (caret <= 0 || caret > value.length) return null

  // Walk back from the caret looking for a `@`. Stop early on any
  // character that is neither a valid mention char nor `@`. This
  // means a typed space, newline, punctuation, etc. terminates the
  // mention candidate the user was building.
  let i = caret - 1
  while (i >= 0) {
    const ch = value.charCodeAt(i)
    if (ch === 64 /* @ */) break
    if (!isMentionChar(ch)) return null
    i -= 1
  }
  if (i < 0) return null

  // The `@` must sit at the start of the field or be preceded by
  // whitespace / newline. Otherwise we're mid-word (think `foo@bar`),
  // which is not a mention.
  if (i > 0) {
    const prev = value.charCodeAt(i - 1)
    if (!isBoundary(prev)) return null
  }

  const query = value.slice(i + 1, caret)
  // Server caps queries at 64 chars; the picker simply stops
  // suggesting once the typed query exceeds that — extra typing
  // closes the picker so the user can complete an email or other
  // long token without staring at a dead dropdown.
  if (query.length > 64) return null

  return { start: i, end: caret, query }
}

function isMentionChar(code: number): boolean {
  // [a-zA-Z0-9_-] — keep this in lock-step with the regex in
  // `cms-core/realtime/notifications.ts:extractMentionedUserIds`.
  if (code === 95 /* _ */ || code === 45 /* - */) return true
  if (code >= 48 && code <= 57) return true // 0-9
  if (code >= 65 && code <= 90) return true // A-Z
  if (code >= 97 && code <= 122) return true // a-z
  return false
}

function isBoundary(code: number): boolean {
  // ASCII space, tab, line-feed, or carriage-return. We deliberately
  // do NOT widen this to the full Unicode whitespace class — the
  // server-side `extractMentionedUserIds` regex in
  // `cms-core/realtime/notifications.ts` uses `\s` which DOES match
  // Unicode whitespace, but its `^` anchor means the only realistic
  // mid-string boundaries in practice are these four ASCII chars
  // (a CMS comment body containing U+00A0 NBSP before an `@` would
  // be both unusual and not worth opening the picker for).
  return code === 32 || code === 9 || code === 10 || code === 13
}

/**
 * Apply a mention selection to a text value. Returns the new value
 * plus the caret position where the next keystroke should land.
 *
 * Inserts `@<userId> ` (trailing space) so the user can keep typing
 * without manually moving the cursor past the token. If the caret
 * already sits before a space, we don't insert a duplicate space.
 */
export interface MentionInsertion {
  value: string
  caret: number
}

export function insertMentionToken(
  value: string,
  context: MentionContext,
  userId: string,
): MentionInsertion {
  const hasTrailingSpace = value.charAt(context.end) === ' '
  // If the user already has a space immediately after the mention
  // region, reuse it (don't double-space) but still advance the
  // caret past that space so the next keystroke continues naturally
  // rather than landing back in front of the space.
  const trailingSpace = hasTrailingSpace ? '' : ' '
  const token = `@${userId}${trailingSpace}`
  const before = value.slice(0, context.start)
  const after = value.slice(context.end)
  return {
    value: `${before}${token}${after}`,
    caret: context.start + token.length + (hasTrailingSpace ? 1 : 0),
  }
}
