/**
 * Remark plugin: auto-link known references (bug#, !MR, CVE-, …) in
 * markdown text nodes. Skips text inside code/inlineCode/link parents
 * so existing markdown links and code samples render untouched.
 *
 * Multi-pattern: caller passes a pre-compiled list (typically from
 * /api/chat/link-patterns). Patterns whose backing connector isn't
 * configured are already filtered server-side.
 */

import { visit, SKIP } from 'unist-util-visit';
import type { SerializablePattern } from './link-patterns';

interface MdastNode { type: string; [k: string]: any }
interface MdastText extends MdastNode { type: 'text'; value: string }
interface MdastParent extends MdastNode { children: MdastNode[] }

interface ClientPattern {
  id: string;
  regex: RegExp;
  urlTemplate: string;
  labelTemplate: string;
}

/** Hydrate `{ source, flags }` into RegExp objects once per render. */
export function hydratePatterns(patterns: SerializablePattern[]): ClientPattern[] {
  return patterns.map(p => ({
    id: p.id,
    regex: new RegExp(p.source, p.flags),
    urlTemplate: p.url,
    labelTemplate: p.label,
  }));
}

interface Hit {
  start: number;
  end: number;
  matchText: string;
  url: string;
  label: string;
}

/** Expand {0}, {1}, ... in a template string using a RegExp match array. */
function expand(tmpl: string, match: RegExpExecArray): string {
  return tmpl.replace(/\{(\d+)\}/g, (_, idx) => match[Number(idx)] ?? '');
}

/** Find every non-overlapping match across all patterns in one string.
 *  Earlier patterns win in case of overlap. */
function findHits(text: string, patterns: ClientPattern[]): Hit[] {
  const hits: Hit[] = [];
  for (const p of patterns) {
    // Clone — running .exec mutates lastIndex on a shared regex.
    const re = new RegExp(p.regex.source, p.regex.flags);
    let m: RegExpExecArray | null;
    while ((m = re.exec(text)) !== null) {
      const start = m.index;
      const matchText = m[0];
      const end = start + matchText.length;
      // Overlap check against already-accepted hits (first-wins).
      if (hits.some(h => !(end <= h.start || start >= h.end))) continue;
      hits.push({
        start,
        end,
        matchText,
        url: expand(p.urlTemplate, m),
        label: expand(p.labelTemplate, m),
      });
      if (m.index === re.lastIndex) re.lastIndex++;     // zero-width safety
    }
  }
  hits.sort((a, b) => a.start - b.start);
  return hits;
}

/** Splice hits into a text node, returning new mdast children. */
function spliceText(value: string, hits: Hit[]): MdastNode[] {
  const out: MdastNode[] = [];
  let cursor = 0;
  for (const h of hits) {
    if (h.start > cursor) {
      out.push({ type: 'text', value: value.slice(cursor, h.start) });
    }
    out.push({
      type: 'link',
      url: h.url,
      title: h.label,
      children: [{ type: 'text', value: h.matchText }],
    });
    cursor = h.end;
  }
  if (cursor < value.length) {
    out.push({ type: 'text', value: value.slice(cursor) });
  }
  return out;
}

const SKIP_PARENT_TYPES = new Set(['link', 'linkReference', 'code', 'inlineCode']);

export function remarkLinkify(patterns: SerializablePattern[]) {
  if (!patterns.length) return () => {};
  const compiled = hydratePatterns(patterns);

  return (tree: MdastNode) => {
    visit(tree as any, 'text', (node: any, index: number | undefined, parent: any) => {
      try {
        if (!parent || index == null) return;
        const n = node as MdastText;
        const p = parent as MdastParent;
        if (!n || typeof n.value !== 'string') return;
        if (!Array.isArray(p?.children)) return;
        if (SKIP_PARENT_TYPES.has(p.type)) return;

        const hits = findHits(n.value, compiled);
        if (!hits.length) return;

        const replacement = spliceText(n.value, hits);
        // Sanity — every replacement child must be a proper node with a type.
        if (!replacement.every(r => r && typeof (r as any).type === 'string')) return;

        p.children.splice(index, 1, ...replacement);
        return [SKIP, index + replacement.length];
      } catch (e) {
        // Linkify is a nice-to-have — never let it crash the chat render.
        if (typeof console !== 'undefined') {
          console.warn('[remark-linkify] visit failed, skipping node:', (e as Error)?.message);
        }
        return;
      }
    });
  };
}
