import type { Person } from './types';

export interface Mention {
  user_id: string;
  text: string;
}

export interface MentionQuery {
  start: number;
  query: string;
}

const MXID = /@[a-z0-9._=\-/+]+:[a-zA-Z0-9.\-]+(?::\d+)?/g;
const MAX_CANDIDATES = 8;

/** The `@word` the caret sits in, when it starts at a word boundary. */
export function mentionQueryAt(text: string, caret: number): MentionQuery | null {
  const before = text.slice(0, caret);
  const at = before.lastIndexOf('@');
  if (at < 0) return null;
  if (at > 0 && !/\s/.test(before[at - 1])) return null;
  const query = before.slice(at + 1);
  if (query.length > 64 || /\s/.test(query)) return null;
  return { start: at, query };
}

export function rankPeople(people: Person[], query: string, exclude: string | null): Person[] {
  const q = query.toLowerCase();
  const scored: { person: Person; score: number }[] = [];
  for (const person of people) {
    if (person.user_id === exclude) continue;
    const name = person.name.toLowerCase();
    const id = person.user_id.toLowerCase();
    let score: number;
    if (!q) score = 3;
    else if (name.startsWith(q) || id.startsWith(`@${q}`) || id.startsWith(q)) score = 0;
    else if (name.split(/\s+/).some((word) => word.startsWith(q))) score = 1;
    else if (name.includes(q) || id.includes(q)) score = 2;
    else continue;
    scored.push({ person, score });
  }
  scored.sort((a, b) => a.score - b.score || a.person.name.localeCompare(b.person.name));
  return scored.slice(0, MAX_CANDIDATES).map((s) => s.person);
}

/** Picked names still present in the body, plus any full matrix ids typed by hand. */
export function resolveMentions(body: string, picked: Mention[]): Mention[] {
  const out: Mention[] = [];
  const seen = new Set<string>();
  for (const m of picked) {
    const key = `${m.user_id}\n${m.text}`;
    if (seen.has(key) || !m.text || !body.includes(m.text)) continue;
    seen.add(key);
    out.push(m);
  }
  for (const match of body.matchAll(MXID)) {
    const key = `${match[0]}\n${match[0]}`;
    if (seen.has(key)) continue;
    seen.add(key);
    out.push({ user_id: match[0], text: match[0] });
  }
  return out;
}

let scratch: HTMLElement | null = null;

/** User pills already in a sent message, so editing it keeps them linked. */
export function mentionsFromHtml(html: string | null): Mention[] {
  if (!html) return [];
  scratch ??= document.implementation.createHTMLDocument('').body;
  scratch.innerHTML = html;
  try {
    const out: Mention[] = [];
    for (const a of scratch.querySelectorAll('a[href^="https://matrix.to/#/@"]')) {
      const userId = decodeURIComponent(a.getAttribute('href')!.slice('https://matrix.to/#/'.length).split('?')[0]);
      const text = a.textContent ?? '';
      if (text) out.push({ user_id: userId, text });
    }
    return out;
  } finally {
    scratch.replaceChildren();
  }
}

const MARKDOWN_PILL = /\[([^\]\n]+)\]\(https:\/\/matrix\.to\/#\/(@[^)\s?]+)[^)\s]*\)/g;

/** Some clients send `[name](https://matrix.to/#/@user)` as the plain body instead of just the name. */
export function stripMarkdownPills(body: string): { text: string; mentions: Mention[] } {
  const mentions: Mention[] = [];
  const text = body.replace(MARKDOWN_PILL, (whole: string, name: string, id: string) => {
    try {
      mentions.push({ user_id: decodeURIComponent(id), text: name });
      return name;
    } catch {
      return whole;
    }
  });
  return { text, mentions };
}

export function mentionedUserIds(mentions: Mention[]): string[] {
  return [...new Set(mentions.map((m) => m.user_id))];
}
