import type {
  PluginApiSlashCommandContribution,
  PluginApiSlashCommandOption,
  PluginApiSlashCommandScope
} from '../models/plugin-api.models';

/**
 * The chat surface a composer is rendered on. `server` surfaces expose both
 * global and server-scoped commands; `direct` surfaces (DMs/group chats) only
 * expose global commands.
 */
export type SlashCommandSurface = 'server' | 'direct';

/** A registered slash command together with the owning plugin. */
export interface SlashCommandEntry {
  contribution: PluginApiSlashCommandContribution;
  id: string;
  pluginId: string;
}

export interface ParsedSlashCommandInput {
  name: string;
  rawArgs: string;
}

function resolveScope(contribution: PluginApiSlashCommandContribution): PluginApiSlashCommandScope {
  return contribution.scope === 'server' ? 'server' : 'global';
}

function normalizeName(value: string): string {
  return value.trim().toLowerCase();
}

/**
 * Returns the live query while the user is still typing a command name, e.g.
 * `/gi` -> `gi` and `/` -> ``. Returns `null` once the input no longer looks
 * like an in-progress command (contains whitespace or does not start with `/`).
 */
export function parseSlashCommandQuery(text: string): string | null {
  const match = /^\/(\S*)$/.exec(text);

  return match ? match[1] : null;
}

/**
 * Parses a fully typed command for execution. Returns the command name and the
 * raw remaining argument string, or `null` when the text is not a command.
 */
export function parseSlashCommandInput(text: string): ParsedSlashCommandInput | null {
  if (!text.startsWith('/')) {
    return null;
  }

  const body = text.slice(1);
  const whitespaceIndex = body.search(/\s/);

  if (whitespaceIndex === -1) {
    return body.length > 0 ? { name: body, rawArgs: '' } : null;
  }

  const name = body.slice(0, whitespaceIndex);

  if (!name) {
    return null;
  }

  return { name, rawArgs: body.slice(whitespaceIndex + 1).trim() };
}

/**
 * Filters the registered commands down to the ones available on the given chat
 * surface, sorted alphabetically by name.
 */
export function selectAvailableSlashCommands(entries: readonly SlashCommandEntry[], surface: SlashCommandSurface): SlashCommandEntry[] {
  return entries
    .filter((entry) => resolveScope(entry.contribution) === 'global' || surface === 'server')
    .slice()
    .sort((left, right) => left.contribution.name.localeCompare(right.contribution.name));
}

/**
 * Narrows available commands by the typed query. Commands whose name starts
 * with the query rank above commands that merely contain it.
 */
export function filterSlashCommands(entries: readonly SlashCommandEntry[], query: string): SlashCommandEntry[] {
  const normalizedQuery = normalizeName(query);

  if (!normalizedQuery) {
    return entries.slice();
  }

  const matches = entries.filter((entry) => normalizeName(entry.contribution.name).includes(normalizedQuery));

  return matches.sort((left, right) => {
    const leftStarts = normalizeName(left.contribution.name).startsWith(normalizedQuery);
    const rightStarts = normalizeName(right.contribution.name).startsWith(normalizedQuery);

    if (leftStarts !== rightStarts) {
      return leftStarts ? -1 : 1;
    }

    return left.contribution.name.localeCompare(right.contribution.name);
  });
}

/** Finds the command to execute for an exact (case-insensitive) name match. */
export function findSlashCommand(entries: readonly SlashCommandEntry[], name: string): SlashCommandEntry | null {
  const normalizedName = normalizeName(name);

  return entries.find((entry) => normalizeName(entry.contribution.name) === normalizedName) ?? null;
}

/**
 * Splits a raw argument string into named values according to the command's
 * declared options. A `rest` option captures the remaining text verbatim;
 * commands without options receive an empty record.
 */
export function parseSlashCommandArguments(rawArgs: string, options: readonly PluginApiSlashCommandOption[] = []): Record<string, string> {
  const args: Record<string, string> = {};

  if (options.length === 0) {
    return args;
  }

  let remaining = rawArgs.trim();

  for (const option of options) {
    if (option.type === 'rest') {
      args[option.name] = remaining;
      remaining = '';
      continue;
    }

    const match = /^(\S+)\s*/.exec(remaining);

    args[option.name] = match ? match[1] : '';
    remaining = match ? remaining.slice(match[0].length) : '';
  }

  return args;
}
