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

export type ParsedTopicSearch = {
  isActive: boolean;
  freeText: string;
  terms: string[];
  qualifiers: {
    intitle: string[];
    inpost: string[];
    inthread: string[];
    from: string[];
  };
};

export type TopicSearchQualifier = {
  key: keyof ParsedTopicSearch['qualifiers'];
  token: string;
  label: string;
  aliases: string[];
};

const QUALIFIER_RE = /\b(intitle|inpost|inbody|inthread|inreply|from):\s*("([^"]+)"|(\S+))/gi;

export const TOPIC_SEARCH_QUALIFIERS: TopicSearchQualifier[] = [
  { key: 'intitle', token: 'intitle:', label: 'Title', aliases: [] },
  { key: 'inpost', token: 'inpost:', label: 'Post body', aliases: ['inbody'] },
  { key: 'inthread', token: 'inthread:', label: 'In thread', aliases: ['inreply'] },
  { key: 'from', token: 'from:', label: 'Author', aliases: [] },
];

export function readQualifierPartialAt(value: string, cursor: number): string {
  const before = String(value || '').slice(0, Math.max(0, cursor));
  const match = before.match(/(?:^|\s)([^\s:]*)$/);
  return match ? String(match[1] || '').toLowerCase() : '';
}

export function matchTopicSearchQualifiers(partial: string): TopicSearchQualifier[] {
  const prefix = String(partial || '').toLowerCase();
  if (!prefix) {
    return TOPIC_SEARCH_QUALIFIERS.slice();
  }

  return TOPIC_SEARCH_QUALIFIERS.filter((qualifier) => {
    if (qualifier.key.startsWith(prefix)) {
      return true;
    }
    return qualifier.aliases.some((alias) => alias.startsWith(prefix));
  });
}

export type TopicSearchAuthorHint = {
  userId: string;
  displayName: string;
  localpart: string;
};

export type QualifierValueContext = {
  key: keyof ParsedTopicSearch['qualifiers'];
  partial: string;
  valueStart: number;
};

const VALUE_QUALIFIER_KEYS = new Set<keyof ParsedTopicSearch['qualifiers']>([
  'intitle',
  'inpost',
  'inthread',
  'from',
]);

function normalizeQualifierKey(raw: string): keyof ParsedTopicSearch['qualifiers'] | null {
  const key = raw.toLowerCase();
  if (key === 'inbody') return 'inpost';
  if (key === 'inreply') return 'inthread';
  if (key === 'intitle' || key === 'inpost' || key === 'inthread' || key === 'from') {
    return key;
  }
  return null;
}

export function formatSearchValue(value: string): string {
  const trimmed = String(value || '').trim();
  if (!trimmed) return '';
  if (/[\s:"]/.test(trimmed)) {
    return `"${trimmed.replace(/"/g, '')}"`;
  }
  return trimmed;
}

/** Partial value typed after a qualifier token (e.g. `from:ali` or `intitle:"rel`). */
export function readActiveQualifierValueAt(value: string, cursor: number): QualifierValueContext | null {
  const before = String(value || '').slice(0, Math.max(0, cursor));
  const match = before.match(
    /(?:^|\s)(intitle|inpost|inbody|inthread|inreply|from):\s*(?:"([^"]*)"?|(\S*))$/i
  );
  if (!match) {
    return null;
  }

  const key = normalizeQualifierKey(String(match[1] || ''));
  if (!key || !VALUE_QUALIFIER_KEYS.has(key)) {
    return null;
  }

  const partial = match[2] !== undefined ? String(match[2] || '') : String(match[3] || '');
  const matchStart = before.length - match[0].length;
  const qualifierLen = String(match[1] || '').length;
  const afterColon = before.slice(matchStart + qualifierLen + 1);
  const leadingSpace = afterColon.match(/^\s*/)?.[0]?.length ?? 0;
  let valueStart = matchStart + qualifierLen + 1 + leadingSpace;
  if (afterColon.trimStart().startsWith('"')) {
    valueStart += 1;
  }

  return { key, partial, valueStart };
}

export function insertTopicSearchValueAt(
  value: string,
  cursor: number,
  replacement: string
): { value: string; cursor: number } {
  const ctx = readActiveQualifierValueAt(value, cursor);
  if (!ctx) {
    return { value, cursor };
  }

  const formatted = formatSearchValue(replacement);
  const prefix = value.slice(0, ctx.valueStart);
  const suffix = value.slice(cursor);
  const spacer = suffix.length && suffix[0] === ' ' ? '' : ' ';
  const nextValue = `${prefix}${formatted}${spacer}${suffix}`;
  const nextCursor = ctx.valueStart + formatted.length + spacer.length;

  return { value: nextValue, cursor: nextCursor };
}

export function collectAuthorHintsFromThreads(
  threads: Array<{ sender: string; senderDisplayName?: string }>
): TopicSearchAuthorHint[] {
  const map = new Map<string, TopicSearchAuthorHint>();

  for (const thread of threads) {
    const userId = thread.sender;
    if (!userId || map.has(userId)) {
      continue;
    }
    const localpart = userId.startsWith('@') ? userId.slice(1).split(':')[0] : userId;
    const displayName = (thread.senderDisplayName || localpart).trim() || localpart;
    map.set(userId, { userId, displayName, localpart });
  }

  return [...map.values()].sort(
    (a, b) => a.displayName.localeCompare(b.displayName) || a.localpart.localeCompare(b.localpart)
  );
}

export function collectTitleHintsFromThreads(threads: Array<{ title: string }>): string[] {
  const titles = new Set<string>();
  for (const thread of threads) {
    const title = String(thread.title || '').trim();
    if (title) {
      titles.add(title);
    }
  }
  return [...titles].sort((a, b) => a.localeCompare(b));
}

export function matchAuthorHints(
  partial: string,
  hints: TopicSearchAuthorHint[]
): TopicSearchAuthorHint[] {
  const prefix = String(partial || '').trim().toLowerCase();
  return hints
    .filter((hint) => {
      if (!prefix) {
        return true;
      }
      return (
        hint.localpart.toLowerCase().includes(prefix) ||
        hint.displayName.toLowerCase().includes(prefix) ||
        hint.userId.toLowerCase().includes(prefix)
      );
    })
    .slice(0, 12);
}

export function matchTitleHints(partial: string, titles: string[]): string[] {
  const prefix = String(partial || '').trim().toLowerCase();
  return titles
    .filter((title) => !prefix || title.toLowerCase().includes(prefix))
    .slice(0, 12);
}

export function insertTopicSearchQualifierAt(
  value: string,
  cursor: number,
  qualifier: TopicSearchQualifier
): { value: string; cursor: number } {
  const input = String(value || '');
  const caret = Math.max(0, Math.min(cursor, input.length));
  const partial = readQualifierPartialAt(input, caret);
  const tokenStart = caret - partial.length;
  const prefix = input.slice(0, tokenStart);
  const suffix = input.slice(caret);
  const spacer = suffix.length && suffix[0] === ' ' ? '' : ' ';
  const nextValue = `${prefix}${qualifier.token}${spacer}${suffix}`;
  const nextCursor = tokenStart + qualifier.token.length + spacer.length;

  return { value: nextValue, cursor: nextCursor };
}

function escapeRegex(value: string): string {
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function collectTerms(qualifiers: ParsedTopicSearch['qualifiers'], freeText: string): string[] {
  const terms = new Set<string>();

  for (const list of Object.values(qualifiers)) {
    for (const value of list) {
      const trimmed = String(value || '').trim();
      if (trimmed) {
        terms.add(trimmed.toLowerCase());
      }
    }
  }

  if (freeText) {
    const quoted = [...freeText.matchAll(/"([^"]+)"/g)].map((match) => match[1]);
    let scratch = freeText;
    for (const phrase of quoted) {
      if (phrase.trim()) {
        terms.add(phrase.trim().toLowerCase());
      }
      scratch = scratch.replace(`"${phrase}"`, ' ');
    }

    for (const word of scratch.split(/\s+/)) {
      const trimmed = word.trim();
      if (trimmed) {
        terms.add(trimmed.toLowerCase());
      }
    }
  }

  return [...terms];
}

export function parseTopicSearchQuery(raw: string): ParsedTopicSearch {
  const input = String(raw || '').trim();
  const qualifiers: ParsedTopicSearch['qualifiers'] = {
    intitle: [],
    inpost: [],
    inthread: [],
    from: [],
  };

  if (!input) {
    return { isActive: false, qualifiers, freeText: '', terms: [] };
  }

  let remainder = input;
  for (const match of input.matchAll(QUALIFIER_RE)) {
    const key = String(match[1] || '').toLowerCase();
    const value = String(match[3] || match[4] || '').trim();
    if (value) {
      if (key === 'inbody') {
        qualifiers.inpost.push(value);
      } else if (key === 'inreply') {
        qualifiers.inthread.push(value);
      } else if (key === 'intitle') {
        qualifiers.intitle.push(value);
      } else if (key === 'inpost') {
        qualifiers.inpost.push(value);
      } else if (key === 'inthread') {
        qualifiers.inthread.push(value);
      } else if (key === 'from') {
        qualifiers.from.push(value);
      }
    }
    remainder = remainder.replace(match[0], ' ');
  }

  const freeText = remainder.replace(/\s+/g, ' ').trim();
  const terms = collectTerms(qualifiers, freeText);
  const isActive = Boolean(
    freeText ||
      qualifiers.intitle.length ||
      qualifiers.inpost.length ||
      qualifiers.inthread.length ||
      qualifiers.from.length
  );

  return { isActive, qualifiers, freeText, terms };
}

function nodeText(node: ForumPost): string {
  return [node.title || '', node.body || '', node.sender || '', node.senderDisplayName || '']
    .join('\n')
    .toLowerCase();
}

function walkReplyTree(node: ForumPost, visit: (node: ForumPost) => void): void {
  for (const reply of node.replies || []) {
    visit(reply);
    walkReplyTree(reply, visit);
  }
}

function termsMatchAll(terms: string[], haystack: string): boolean {
  const text = String(haystack || '').toLowerCase();
  return terms.every((term) => text.includes(String(term || '').toLowerCase()));
}

function termsMatchAny(terms: string[], haystack: string): boolean {
  const text = String(haystack || '').toLowerCase();
  return terms.some((term) => text.includes(String(term || '').toLowerCase()));
}

export function threadMatchesSearch(post: ForumPost, parsed: ParsedTopicSearch): boolean {
  if (!parsed?.isActive || !post) {
    return true;
  }

  const { qualifiers, freeText, terms } = parsed;
  const titleHay = String(post.title || '').toLowerCase();
  const bodyHay = String(post.body || '').toLowerCase();
  const senderHay = `${post.sender || ''}\n${post.senderDisplayName || ''}`.toLowerCase();
  const fullHay = `${titleHay}\n${bodyHay}\n${senderHay}`;

  if (qualifiers.intitle.length && !termsMatchAll(qualifiers.intitle, titleHay)) {
    return false;
  }

  if (qualifiers.inpost.length && !termsMatchAll(qualifiers.inpost, bodyHay)) {
    return false;
  }

  if (qualifiers.from.length && !termsMatchAll(qualifiers.from, senderHay)) {
    return false;
  }

  if (qualifiers.inthread.length) {
    let replyHit = false;
    walkReplyTree(post, (reply) => {
      if (termsMatchAll(qualifiers.inthread, nodeText(reply))) {
        replyHit = true;
      }
    });
    if (!replyHit) {
      return false;
    }
  }

  if (freeText) {
    let anywhere = termsMatchAny(terms, fullHay);
    if (!anywhere) {
      walkReplyTree(post, (reply) => {
        if (termsMatchAny(terms, nodeText(reply))) {
          anywhere = true;
        }
      });
    }
    if (!anywhere) {
      return false;
    }
  }

  return true;
}

export function filterPostsBySearch(posts: ForumPost[], parsed: ParsedTopicSearch): ForumPost[] {
  if (!parsed?.isActive) {
    return posts || [];
  }

  return (posts || []).filter((post) => threadMatchesSearch(post, parsed));
}

export function applyTopicStatusSort<T extends { lastActivityTs: number; timestamp: number; totalReplies: number; eventId: string }>(
  threads: T[],
  statusFilter: string,
  sortBy: string
): T[] {
  let filtered = threads || [];

  if (statusFilter === 'open') {
    filtered = filtered.filter((thread) => thread.lastActivityTs >= Date.now() - 30 * 86400000);
  } else if (statusFilter === 'quiet') {
    filtered = filtered.filter((thread) => thread.lastActivityTs < Date.now() - 30 * 86400000);
  }

  const sorted = [...filtered];
  if (sortBy === 'top') {
    sorted.sort(
      (a, b) =>
        b.totalReplies - a.totalReplies ||
        b.lastActivityTs - a.lastActivityTs ||
        b.eventId.localeCompare(a.eventId)
    );
  } else if (sortBy === 'new') {
    sorted.sort((a, b) => b.timestamp - a.timestamp || b.eventId.localeCompare(a.eventId));
  } else {
    sorted.sort((a, b) => {
      const hotA = a.totalReplies * 4 + Math.floor((a.lastActivityTs - a.timestamp) / 3600000);
      const hotB = b.totalReplies * 4 + Math.floor((b.lastActivityTs - b.timestamp) / 3600000);
      return hotB - hotA || b.lastActivityTs - a.lastActivityTs || b.eventId.localeCompare(a.eventId);
    });
  }

  return sorted;
}

export function highlightSearchTerms(text: string, terms: string[]): string {
  if (!terms.length) {
    return text;
  }

  const usable = [...new Set(terms.map((term) => String(term || '').trim()).filter(Boolean))].sort(
    (a, b) => b.length - a.length
  );

  let result = text;
  for (const term of usable) {
    const re = new RegExp(`(${escapeRegex(term)})`, 'gi');
    result = result.replace(re, '<mark class="search-hit">$1</mark>');
  }

  return result;
}
