/**
 * Auto-link company names in article body HTML to their manufacturer
 * directory profiles. Mentions are wrapped in
 *   <a href="/manufacturers/<slug>" data-company-slug="<slug>" class="company-mention">Name</a>
 * so a client-side hover-card component can pop a preview on hover.
 *
 * Linking respects the directory_visible flag, only links the FIRST
 * occurrence per company per article (to avoid clutter), and never
 * double-wraps existing <a> tags.
 */

import * as cheerio from "cheerio";
import { createAdminClient } from "./supabase/admin";

export type DirectoryCompany = {
  name: string;
  slug: string;
  /** lower-case name used for matching */
  needle: string;
};

const MAX_LINKS_PER_ARTICLE = 12;

let cached: { rows: DirectoryCompany[]; expires: number } | null = null;
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 min — companies barely change

export async function loadDirectoryCompanies(): Promise<DirectoryCompany[]> {
  if (cached && cached.expires > Date.now()) return cached.rows;
  try {
    const svc = createAdminClient(); // bb schema default
    const { data } = await svc
      .from("tracked_companies")
      .select("company_name, slug")
      .eq("directory_visible", true)
      .not("slug", "is", null)
      .not("company_name", "is", null);
    const rows: DirectoryCompany[] = (data || [])
      .filter((r: any) => r.company_name && r.slug)
      .map((r: any) => ({
        name: String(r.company_name).trim(),
        slug: String(r.slug),
        needle: String(r.company_name).trim().toLowerCase(),
      }))
      .filter((r) => r.needle.length >= 3); // skip 2-char names — too noisy
    // Sort by longest name first so "Sony Pictures" wins over "Sony"
    rows.sort((a, b) => b.needle.length - a.needle.length);
    cached = { rows, expires: Date.now() + CACHE_TTL_MS };
    return rows;
  } catch {
    return cached?.rows || [];
  }
}

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

/**
 * Walk text nodes of the parsed HTML and replace the first occurrence of
 * each company name with a link. Skips text inside <a>, <script>, <style>,
 * <code>, <pre>, headings, and any node carrying data-no-autolink="true".
 */
export function linkifyCompanyMentions(html: string, companies: DirectoryCompany[]): string {
  if (!html || companies.length === 0) return html;

  const $ = cheerio.load(html, { decodeEntities: false }, false);
  const used = new Set<string>(); // slugs already linked
  let total = 0;

  // Pre-scan existing <a> texts so we don't re-link a company already linked
  $("a").each((_, el) => {
    const t = $(el).text().toLowerCase();
    for (const c of companies) {
      if (t.includes(c.needle)) used.add(c.slug);
    }
  });

  const SKIP_TAGS = new Set(["a", "script", "style", "code", "pre", "h1", "h2", "h3", "h4", "h5", "h6"]);

  function processNode(node: any): void {
    if (total >= MAX_LINKS_PER_ARTICLE) return;
    if (!node) return;
    if (node.type === "tag") {
      if (SKIP_TAGS.has(node.name)) return;
      if (node.attribs && node.attribs["data-no-autolink"] === "true") return;
      const kids = node.children ? [...node.children] : [];
      for (const k of kids) processNode(k);
      return;
    }
    if (node.type !== "text") return;
    const text: string = node.data || "";
    if (!text.trim()) return;

    // Find the earliest, longest match among unlinked companies
    let bestStart = -1;
    let bestEnd = -1;
    let bestCompany: DirectoryCompany | null = null;
    for (const c of companies) {
      if (used.has(c.slug)) continue;
      if (total >= MAX_LINKS_PER_ARTICLE) break;
      // Word-boundary, case-insensitive. \b doesn't work great for some
      // names with punctuation (e.g. "Aja.com"); we use a lookaround.
      const re = new RegExp(`(^|[^A-Za-z0-9])(${escapeRegex(c.name)})(?![A-Za-z0-9])`, "i");
      const m = re.exec(text);
      if (!m) continue;
      const matchStart = m.index + m[1].length;
      const matchEnd = matchStart + m[2].length;
      // Prefer earlier match; on tie prefer longer name (companies are sorted
      // longest-first, so first hit wins)
      if (bestStart === -1 || matchStart < bestStart || (matchStart === bestStart && matchEnd - matchStart > bestEnd - bestStart)) {
        bestStart = matchStart;
        bestEnd = matchEnd;
        bestCompany = c;
      }
    }

    if (!bestCompany || bestStart < 0) return;

    const before = text.slice(0, bestStart);
    const matched = text.slice(bestStart, bestEnd);
    const after = text.slice(bestEnd);

    // Replace this text node with: before + <a> + after, then continue
    // processing the "after" portion so additional companies in the same
    // text node can also link.
    const $a = $(
      `<a href="/manufacturers/${bestCompany.slug}" data-company-slug="${bestCompany.slug}" class="company-mention">${$.text([{ type: "text", data: matched } as any])}</a>`,
    );
    const beforeNode = before ? { type: "text", data: before } : null;
    const afterNode = after ? { type: "text", data: after } : null;

    const parent = node.parent;
    if (!parent) return;
    const idx = parent.children.indexOf(node);
    if (idx < 0) return;

    const newChildren: any[] = [];
    if (beforeNode) newChildren.push(beforeNode);
    newChildren.push($a[0]);
    if (afterNode) newChildren.push(afterNode);
    parent.children.splice(idx, 1, ...newChildren);

    used.add(bestCompany.slug);
    total++;

    if (afterNode) processNode(afterNode); // recurse into the leftover text
  }

  $.root().children().each((_, el) => processNode(el));

  return $.html();
}

/** Convenience: load companies + linkify in one call. */
export async function linkifyArticleHtml(html: string): Promise<string> {
  const companies = await loadDirectoryCompanies();
  return linkifyCompanyMentions(html, companies);
}

/**
 * Same as linkifyArticleHtml but also returns the set of company slugs that
 * were matched (in the order they first appeared). Callers use this to
 * render a "Companies in this story" sidebar without re-scanning the HTML.
 */
export async function linkifyAndExtractMentions(
  html: string,
): Promise<{ html: string; mentionedSlugs: string[] }> {
  const companies = await loadDirectoryCompanies();
  if (!html || companies.length === 0) return { html: html || "", mentionedSlugs: [] };

  // Re-run the same matching against the original (unmodified) HTML so we
  // can capture the slug list. We need to dupe the logic a bit because the
  // existing linkifyCompanyMentions function mutates as it goes.
  const cheerio = require("cheerio");
  const $ = cheerio.load(html, { decodeEntities: false }, false);
  const matched: string[] = [];
  const seen = new Set<string>();

  const SKIP_TAGS = new Set(["a", "script", "style", "code", "pre", "h1", "h2", "h3", "h4", "h5", "h6"]);

  function scan(node: any): void {
    if (!node) return;
    if (node.type === "tag") {
      if (SKIP_TAGS.has(node.name)) return;
      if (node.attribs && node.attribs["data-no-autolink"] === "true") return;
      const kids = node.children ? [...node.children] : [];
      for (const k of kids) scan(k);
      return;
    }
    if (node.type !== "text") return;
    const text: string = node.data || "";
    if (!text.trim()) return;
    for (const c of companies) {
      if (seen.has(c.slug)) continue;
      const re = new RegExp(`(^|[^A-Za-z0-9])(${c.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})(?![A-Za-z0-9])`, "i");
      if (re.test(text)) {
        matched.push(c.slug);
        seen.add(c.slug);
        if (matched.length >= MAX_LINKS_PER_ARTICLE) break;
      }
    }
  }

  $.root().children().each((_: number, el: any) => scan(el));

  const linkedHtml = linkifyCompanyMentions(html, companies);
  return { html: linkedHtml, mentionedSlugs: matched };
}
