/* LinkedIn connection DOM helpers shared by the content script and tests. */
(function initLinkedInConnectDOM(global) {
  function clean(value) {
    return String(value || "")
      .replace(/[\u00a0\u202f]/g, " ")
      .replace(/\s+/g, " ")
      .trim();
  }

  function normalizeName(value) {
    return clean(value)
      .replace(/\s*[\u2022\u00b7]\s*(?:1st|2nd|3rd\+?|degree connection).*$/i, "")
      .replace(/\s+LinkedIn.*$/i, "")
      .replace(/[^\p{L}\p{N}\s.'-]+/gu, " ")
      .replace(/\s+/g, " ")
      .trim()
      .toLowerCase();
  }

  function normalizeSlug(value) {
    const raw = clean(value);
    if (!raw) return "";
    try {
      const url = new URL(raw, "https://www.linkedin.com");
      const match = url.pathname.match(/^\/in\/([^/?#]+)/i);
      return match
        ? decodeURIComponent(match[1]).toLowerCase()
        : raw
            .replace(/^\/?in\//i, "")
            .replace(/\/$/, "")
            .toLowerCase();
    } catch {
      return raw
        .replace(/^\/?in\//i, "")
        .replace(/\/$/, "")
        .toLowerCase();
    }
  }

  function canonicalProfileId(person) {
    const values = [
      person?.entityUrn,
      person?.trackingUrn,
      person?.targetUrn,
      person?.publicIdentifier,
      person?.navigationUrl,
      person?.linkedinUrl,
    ];
    for (const value of values) {
      const text = String(value || "");
      const fsd = text.match(/fsd_profile:([A-Za-z0-9_-]+)/);
      if (fsd) return fsd[1];
      const member = text.match(/member:(\d+)/);
      if (member) return member[1];
    }
    return normalizeSlug(
      person?.publicIdentifier || person?.navigationUrl || person?.linkedinUrl || "",
    );
  }

  function publicProfileSlug(person) {
    for (const value of [person?.navigationUrl, person?.linkedinUrl, person?.profileUrl]) {
      const match = String(value || "").match(/\/in\/([^/?#]+)/i);
      if (match) {
        const slug = normalizeSlug(match[1]);
        if (slug && !/^acoaa/i.test(slug)) return slug;
      }
    }
    const slug = normalizeSlug(person?.publicIdentifier || "");
    return slug && !/^acoaa/i.test(slug) ? slug : "";
  }

  function isVisible(element) {
    if (!element) return false;
    if (element.hidden || element.getAttribute?.("aria-hidden") === "true") return false;
    return element.offsetParent !== null || typeof element.offsetParent === "undefined";
  }

  function labelOf(element) {
    return clean(element?.textContent || element?.getAttribute?.("aria-label"));
  }

  function actionCluster(main) {
    const controls = Array.from(main?.querySelectorAll("button, a, [role='button']") || []).filter(
      isVisible,
    );
    const moreButtons = controls.filter((element) =>
      /^more(?:\s+actions)?(?:\s+for\b.*)?$/i.test(labelOf(element)),
    );
    for (const more of moreButtons) {
      let node = more.parentElement;
      for (
        let depth = 0;
        node && node !== main && depth < 8;
        depth += 1, node = node.parentElement
      ) {
        const local = Array.from(
          node.querySelectorAll?.("button, a, [role='button']") || [],
        ).filter(isVisible);
        if (local.length < 2 || local.length > 12) continue;
        const labels = local.map(labelOf);
        const hasMore = labels.some((label) => /^more(?:\s+actions)?(?:\s+for\b.*)?$/i.test(label));
        const hasPeerAction = labels.some((label) =>
          /^(?:connect|follow|message|pending)(?:\b|$)/i.test(label),
        );
        if (hasMore && hasPeerAction) return node;
      }
    }
    return null;
  }

  function profileRoot(doc, expectedName) {
    const main = doc.querySelector("main") || doc.body || doc.documentElement;
    const heading = main?.querySelector("h1, .text-heading-xlarge, [class*='heading-xlarge']");
    const expected = normalizeName(expectedName);
    const ownerAction = Array.from(main?.querySelectorAll("button, a, [role='button']") || []).find(
      (element) => {
        if (!isVisible(element)) return false;
        const aria = normalizeName(element.getAttribute("aria-label"));
        return (
          expected &&
          aria.includes(expected) &&
          /\b(invite|connect|pending|more|follow|message)\b/i.test(labelOf(element))
        );
      },
    );
    return (
      actionCluster(main) ||
      ownerAction?.closest(
        "[class*='profile-actions'], [class*='top-card'], .pv-top-card, section",
      ) ||
      heading?.closest("[class*='top-card'], .pv-top-card, section") ||
      main
    );
  }

  function profileState(doc, expectedName) {
    const root = profileRoot(doc, expectedName);
    const controls = Array.from(
      root?.querySelectorAll("button, a, [role='button'], [role='menuitem']") || [],
    ).filter(isVisible);
    const labels = controls.map((element) => labelOf(element).toLowerCase());
    const connect =
      controls.find((element) => {
        const label = labelOf(element);
        const aria = clean(element.getAttribute?.("aria-label"));
        return (
          /^connect$/i.test(label) ||
          /^connect\b/i.test(label) ||
          /invite .* to connect/i.test(aria)
        );
      }) || null;
    const pending =
      controls.find((element) => /(^|\b)pending(\b|$)/i.test(labelOf(element))) || null;
    const more =
      controls.find((element) => /^more(?:\s+actions)?$/i.test(labelOf(element))) || null;
    const main = doc.querySelector("main") || root;
    const heading = main?.querySelector?.("h1, .text-heading-xlarge, [class*='heading-xlarge']");
    const badgeRoot = heading?.closest?.("[class*='top-card'], .pv-top-card, section") || root;
    const rootText = clean(badgeRoot?.textContent || root?.textContent || "");
    return {
      root,
      connect,
      pending,
      more,
      alreadyConnected: !connect && /\b1st degree connection\b/i.test(rootText),
      labels: labels.slice(0, 30),
    };
  }

  function menuSurface(element) {
    const menuTerms =
      /(?:send profile in a message|save to pdf|follow|report\s*\/\s*block|about this member|connect)/gi;
    let node = element;
    for (let depth = 0; node && depth < 8; depth += 1, node = node.parentElement) {
      const role = clean(node.getAttribute?.("role")).toLowerCase();
      const className = String(node.className || "").toLowerCase();
      if (/^(?:menu|dialog|listbox)$/.test(role) || className.includes("dropdown__content"))
        return node;
      const text = clean(node.textContent || "");
      const terms = text.match(menuTerms) || [];
      if (text.length <= 1600 && new Set(terms.map((term) => term.toLowerCase())).size >= 2)
        return node;
    }
    return null;
  }

  function findOpenMenuConnect(doc, profileActionRoot = null) {
    const raw = Array.from(
      doc.querySelectorAll(
        "button, a, [role='button'], [role='menuitem'], [role='option'], li, div, span",
      ),
    ).filter((element) => isVisible(element) && /^connect$/i.test(labelOf(element)));
    const candidates = [];
    for (const element of raw) {
      const clickable =
        element.closest?.("button, a, [role='button'], [role='menuitem'], [role='option'], li") ||
        element;
      if (
        !candidates.includes(clickable) &&
        isVisible(clickable) &&
        /^connect$/i.test(labelOf(clickable))
      ) {
        candidates.push(clickable);
      }
    }
    candidates.sort((left, right) => {
      const score = (element) =>
        (menuSurface(element) ? 100 : 0) + (profileActionRoot?.contains?.(element) ? 0 : 10);
      return score(right) - score(left);
    });
    return candidates.find((element) => menuSurface(element)) || candidates[0] || null;
  }

  function findSentInvitation(doc, { publicIdentifier, expectedName } = {}) {
    const wantedSlug = normalizeSlug(publicIdentifier);
    const wantedName = normalizeName(expectedName);
    const links = Array.from(doc.querySelectorAll('a[href*="/in/"]')).filter(isVisible);
    for (const link of links) {
      const slug = normalizeSlug(link.getAttribute("href") || link.href || "");
      const row =
        link.closest("li, article, [data-view-name], [class*='invitation']") || link.parentElement;
      const rowText = clean(row?.textContent || "");
      if (wantedSlug && slug === wantedSlug && /withdraw|pending|sent/i.test(rowText)) {
        return { found: true, matchedBy: "slug", name: labelOf(link), slug };
      }
    }
    if (wantedName) {
      const rows = Array.from(
        doc.querySelectorAll("li, article, [data-view-name], [class*='invitation']"),
      ).filter(isVisible);
      for (const row of rows) {
        const rowText = clean(row.textContent || "");
        if (!/withdraw|pending|sent/i.test(rowText)) continue;
        const link = row.querySelector('a[href*="/in/"]');
        const candidateName = normalizeName(labelOf(link) || rowText.split(/\n/)[0]);
        if (candidateName && candidateName === wantedName) {
          return {
            found: true,
            matchedBy: "name",
            name: labelOf(link),
            slug: normalizeSlug(link?.getAttribute("href") || ""),
          };
        }
      }
    }
    return { found: false };
  }

  function waitForPending(doc, expectedName, timeoutMs = 6000) {
    return new Promise((resolve) => {
      const started = Date.now();
      const check = () => {
        const state = profileState(doc, expectedName);
        if (state.pending) return resolve(true);
        if (Date.now() - started >= timeoutMs) return resolve(false);
        setTimeout(check, 250);
      };
      check();
    });
  }

  global.NexusLinkedInConnectDOM = {
    clean,
    normalizeName,
    normalizeSlug,
    canonicalProfileId,
    publicProfileSlug,
    profileState,
    findOpenMenuConnect,
    findSentInvitation,
    waitForPending,
  };
})(globalThis);
