/**
 * Stable LinkedIn feed discovery for classic MV3 content scripts.
 *
 * LinkedIn's 2026 Style-2 UI uses role=listitem post cards. The card exposes an
 * Open reactions menu chevron; the actual Like action is rendered in a portal
 * only after that menu opens. Both controls are modeled separately here.
 */
(function initLinkedInFeedDom(global) {
  const STYLE2_POST_SELECTOR = 'main [data-testid="mainFeed"] [role="listitem"]';
  const EXPLICIT_POST_SELECTOR = [
    "article",
    '[role="listitem"]',
    '[data-urn*="urn:li:activity:"]',
    '[data-urn*="urn:li:ugcPost:"]',
    '[data-id*="urn:li:activity:"]',
    '[data-id*="urn:li:ugcPost:"]',
    '[data-view-name="feed-full-update"]',
    '[data-view-name="feed-update"]',
    "[data-finite-scroll-hotkey-item]",
  ].join(", ");

  function controlName(control) {
    return String(
      control?.getAttribute?.("aria-label") ||
        control?.getAttribute?.("title") ||
        control?.innerText ||
        control?.textContent ||
        "",
    )
      .trim()
      .replace(/\s+/g, " ");
  }

  /** Return "liked", "unliked", or null when this is not a Like control. */
  function likeState(control) {
    const name = controlName(control).toLowerCase();
    if (/^(?:(?:react\s+)?unlike|unreact(?:\s+like)?|remove reaction)\b/.test(name)) {
      return "liked";
    }
    const namedLike = /^(?:react\s+)?like(?:\b|$)/.test(name);
    if (!namedLike) return null;

    let node = control;
    for (let depth = 0; node && depth < 4; depth += 1, node = node.parentElement) {
      const selected = ["aria-pressed", "aria-checked", "aria-selected"]
        .map((attribute) => node.getAttribute?.(attribute))
        .find((value) => value != null);
      const state = String(
        node.getAttribute?.("data-state") || node.getAttribute?.("data-selected") || "",
      ).toLowerCase();
      if (selected === "true" || /^(?:true|active|selected|on)$/.test(state)) return "liked";
      if (selected === "false" || /^(?:false|inactive|off)$/.test(state)) return "unliked";
    }

    return "unliked";
  }

  function reactionMenuState(control) {
    if (!control) return null;
    const name = controlName(control).toLowerCase();
    if (/\b(?:unlike|remove reaction)\b/.test(name)) return "liked";

    let node = control;
    for (let depth = 0; node && depth < 4; depth += 1, node = node.parentElement) {
      const selected = ["aria-pressed", "aria-checked", "aria-selected"]
        .map((attribute) => node.getAttribute?.(attribute))
        .find((value) => value != null);
      const state = String(
        node.getAttribute?.("data-state") || node.getAttribute?.("data-selected") || "",
      ).toLowerCase();
      if (selected === "true" || /^(?:true|active|selected|on)$/.test(state)) return "liked";
      if (selected === "false" || /^(?:false|inactive|off)$/.test(state)) return "unliked";
    }

    const icon = control.querySelector?.('svg[id^="chevron-up"]');
    const iconId = String(icon?.id || icon?.getAttribute?.("id") || "").toLowerCase();
    return /(?:filled|active|selected)/.test(iconId) ? "liked" : "unliked";
  }

  function findLikeControl(root) {
    if (!root?.querySelectorAll) return null;
    const controls = Array.from(root.querySelectorAll('button, [role="button"], a'));
    return controls.find((control) => likeState(control) !== null) || null;
  }

  function findCommentControl(root) {
    if (!root?.querySelectorAll) return null;
    return (
      Array.from(root.querySelectorAll('button, [role="button"]')).find((control) => {
        if (!isVisible(control) || isControlDisabled(control)) return false;
        return /^comment(?:\s+on\b.*)?$/i.test(controlName(control));
      }) || null
    );
  }

  function findCommentInput(root) {
    if (!root?.querySelector) return null;
    return (
      root.querySelector('[role="textbox"][aria-label*="comment" i]') ||
      root.querySelector('div[contenteditable="true"][role="textbox"]') ||
      root.querySelector('.comments-comment-box__form [contenteditable="true"]') ||
      null
    );
  }

  function findCommentSubmit(root, input, actionControl = null) {
    if (!root?.querySelectorAll) return null;

    // Current LinkedIn renders the editor and its real submit control in the
    // same form. Prefer that structural relationship over the visible label:
    // captured Style-2 feeds call this button simply "Submit", while older
    // variants call it "Comment" or "Post".
    const form = input?.closest?.("form");
    const formSubmit = Array.from(
      form?.querySelectorAll?.('button[type="submit"], input[type="submit"]') || [],
    ).find((control) => isVisible(control) && !isControlDisabled(control));
    if (formSubmit) return formSubmit;

    const candidates = Array.from(root.querySelectorAll('button, [role="button"]')).filter(
      (control) =>
        control !== actionControl &&
        isVisible(control) &&
        !isControlDisabled(control) &&
        /^(?:comment|post|submit|submit comment)$/i.test(controlName(control)),
    );
    if (candidates.length === 0) return null;

    // LinkedIn's Style-2 feed names both the opener and submitter "Comment".
    // The submitter is mounted after the editor, so prefer controls following it.
    if (input?.compareDocumentPosition) {
      const following = candidates.filter(
        (control) => (input.compareDocumentPosition(control) & 4) !== 0,
      );
      if (following.length > 0) return following[following.length - 1];
    }
    return candidates[candidates.length - 1];
  }

  function countExactRenderedText(root, text) {
    if (!root?.querySelectorAll) return 0;
    const expected = String(text || "")
      .trim()
      .replace(/\s+/g, " ");
    if (!expected) return 0;
    return Array.from(root.querySelectorAll("span, p, div")).filter((element) => {
      const value = String(element.innerText || element.textContent || "")
        .trim()
        .replace(/\s+/g, " ");
      if (value !== expected) return false;
      return !Array.from(element.children || []).some((child) => {
        const childValue = String(child.innerText || child.textContent || "")
          .trim()
          .replace(/\s+/g, " ");
        return childValue === expected;
      });
    }).length;
  }

  async function waitForCommentInput(
    root,
    {
      timeoutMs = 5000,
      pollMs = 100,
      sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
    } = {},
  ) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() <= deadline) {
      const input = findCommentInput(root);
      if (input) return input;
      await sleep(pollMs);
    }
    return null;
  }

  async function waitForCommentPosted(
    root,
    _input,
    text,
    {
      baselineCount = countExactRenderedText(root, text),
      timeoutMs = 10000,
      pollMs = 150,
      sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
      resolveInput = () => findCommentInput(root),
    } = {},
  ) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() <= deadline) {
      const currentInput = resolveInput();
      const inputValue = String(currentInput?.innerText || currentInput?.textContent || "").trim();
      // A disconnected original input can mean LinkedIn remounted the card
      // while preserving the draft. Only the currently resolved editor is
      // authoritative.
      const editorCleared = !currentInput || inputValue === "";
      if (editorCleared && countExactRenderedText(root, text) > baselineCount) return true;
      await sleep(pollMs);
    }
    return false;
  }

  function findReactionMenuControl(root) {
    const icon = root?.querySelector?.('svg[id^="chevron-up"]');
    if (!icon) return null;
    return (
      icon.closest?.('button, [role="button"], a') ||
      icon.parentElement?.closest?.('button, [role="button"], a') ||
      null
    );
  }

  function isVisible(control) {
    if (!control || control.hidden || control.getAttribute?.("aria-hidden") === "true")
      return false;
    if (typeof control.getClientRects === "function" && control.getClientRects().length > 0)
      return true;
    return control.offsetParent !== null || typeof control.offsetParent === "undefined";
  }

  function findOpenReactionLike(root, excludedRoot = null) {
    if (!root?.querySelectorAll) return null;
    const controls = Array.from(
      root.querySelectorAll('button, [role="button"], [role="menuitem"], a'),
    ).filter(
      (control) =>
        isVisible(control) && likeState(control) !== null && !excludedRoot?.contains?.(control),
    );
    return (
      controls.find((control) =>
        control.closest?.('ul, [role="menu"], [role="listbox"], [role="dialog"]'),
      ) ||
      controls[0] ||
      null
    );
  }

  function hasAction(control, pattern) {
    return pattern.test(controlName(control));
  }

  function isControlDisabled(control) {
    return Boolean(
      control?.disabled ||
      control?.getAttribute?.("disabled") != null ||
      control?.getAttribute?.("aria-disabled") === "true",
    );
  }

  function matchesAction(control, actionPattern) {
    if (!control || !actionPattern) return false;
    const pattern =
      actionPattern instanceof RegExp
        ? new RegExp(actionPattern.source, actionPattern.flags.replace(/[gy]/g, ""))
        : new RegExp(String(actionPattern), "i");
    return pattern.test(controlName(control));
  }

  function actionControlSettled(control, scope, actionPattern) {
    if (!control || control.isConnected === false || isControlDisabled(control)) return true;
    if (!matchesAction(control, actionPattern)) return true;
    if (scope?.isConnected === false) return true;
    if (!scope?.querySelectorAll) return false;

    const matchingControls = Array.from(scope.querySelectorAll('button, [role="button"]')).filter(
      (candidate) =>
        candidate.isConnected !== false &&
        !isControlDisabled(candidate) &&
        matchesAction(candidate, actionPattern),
    );
    return matchingControls.length === 0;
  }

  async function waitForActionControlSettled(
    control,
    {
      scope = control?.parentElement || null,
      actionPattern,
      timeoutMs = 5000,
      pollMs = 100,
      sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
    } = {},
  ) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() <= deadline) {
      if (actionControlSettled(control, scope, actionPattern)) return true;
      await sleep(pollMs);
    }
    return false;
  }

  function findDialogAction(root, actionName) {
    if (!root?.querySelectorAll) return null;
    const expected = String(actionName || "")
      .trim()
      .toLowerCase();
    if (!expected) return null;
    const dialogs = Array.from(
      root.querySelectorAll('div[role="dialog"], [role="alertdialog"], .artdeco-modal'),
    ).filter(isVisible);
    for (const dialog of dialogs) {
      const controls = Array.from(dialog.querySelectorAll?.('button, [role="button"]') || []);
      const match = controls.find((control) => {
        if (!isVisible(control) || isControlDisabled(control)) return false;
        const name = controlName(control).toLowerCase();
        return name === expected || name.startsWith(`${expected} `);
      });
      if (match) return match;
    }
    return null;
  }

  async function waitForDialogAction(
    root,
    actionName,
    {
      timeoutMs = 3000,
      pollMs = 100,
      sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
    } = {},
  ) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() <= deadline) {
      const action = findDialogAction(root, actionName);
      if (action) return action;
      await sleep(pollMs);
    }
    return null;
  }

  function findPostContainer(likeControl) {
    const explicit = likeControl?.closest?.(EXPLICIT_POST_SELECTOR);
    if (explicit) return explicit;

    let node = likeControl?.parentElement || null;
    let textFallback = null;
    for (let depth = 0; node && depth < 14; depth += 1, node = node.parentElement) {
      if (node.tagName === "MAIN") break;

      const controls = Array.from(node.querySelectorAll?.('button, [role="button"], a') || []);
      const likeCount = controls.filter((control) => likeState(control) !== null).length;
      if (likeCount !== 1) continue;

      const textLength = String(node.innerText || node.textContent || "").trim().length;
      if (!textFallback && textLength >= 80) textFallback = node;

      const hasComment = controls.some((control) => hasAction(control, /^(?:react\s+)?comment\b/i));
      const hasActor = !!node.querySelector?.('a[href*="/in/"], a[href*="/company/"]');
      if (hasComment && (hasActor || textLength >= 80)) return node;
    }

    return textFallback || likeControl?.parentElement || null;
  }

  function extractPostUrn(post) {
    if (!post) return null;
    const values = [];
    let node = post;
    for (let depth = 0; node && depth < 4; depth += 1, node = node.parentElement) {
      values.push(
        node.getAttribute?.("data-urn"),
        node.getAttribute?.("data-id"),
        node.getAttribute?.("componentkey"),
      );
    }
    for (const el of Array.from(
      post.querySelectorAll?.(
        '[data-urn], [data-id], [componentkey], a[href*="urn%3Ali%3A"], a[href*="urn:li:"]',
      ) || [],
    )) {
      values.push(
        el.getAttribute?.("data-urn"),
        el.getAttribute?.("data-id"),
        el.getAttribute?.("componentkey"),
        el.getAttribute?.("href"),
      );
    }

    for (const value of values) {
      if (!value) continue;
      let decoded = String(value);
      try {
        decoded = decodeURIComponent(decoded);
      } catch {
        /* keep the raw value */
      }
      const match = decoded.match(/urn:li:(?:activity|ugcPost):\d+/i);
      if (match) return match[0];
    }
    return null;
  }

  function isPostUrn(value) {
    return /^urn:li:(?:activity|ugcPost|share):\d+$/i.test(String(value || ""));
  }

  function normalizedPostText(value) {
    return String(value || "")
      .replace(/\s+/g, " ")
      .trim();
  }

  function comparablePostText(value) {
    return normalizedPostText(value)
      .replace(/\b(?:unreact\s+like|unlike|like|comment|repost|send)\b/gi, " ")
      .replace(/\s+/g, " ")
      .trim();
  }

  function normalizeActorHref(value) {
    const href = String(value || "").trim();
    if (!href) return null;
    try {
      const parsed = new URL(href, "https://www.linkedin.com");
      return parsed.pathname.replace(/\/+$/, "").toLowerCase();
    } catch {
      return href.split(/[?#]/)[0].replace(/\/+$/, "").toLowerCase();
    }
  }

  function postIdentity(candidateOrPost) {
    const post = candidateOrPost?.post || candidateOrPost;
    if (!post) return null;
    const text = normalizedPostText(
      candidateOrPost?.text || post.innerText || post.textContent || "",
    );
    const actor = post.querySelector?.('a[href*="/in/"], a[href*="/company/"]');
    return {
      urn: candidateOrPost?.urn || extractPostUrn(post),
      actorHref: normalizeActorHref(actor?.href || actor?.getAttribute?.("href")),
      // The beginning contains the actor and post copy and remains stable when
      // LinkedIn changes Like to Unlike or mounts the comment editor below it.
      textPrefix: text.slice(0, 240),
    };
  }

  function findMatchingPost(root, identity) {
    if (!identity) return null;
    const candidates = scan(root);
    if (identity.urn) {
      const urnMatch = candidates.find(
        (candidate) => (candidate.urn || extractPostUrn(candidate.post)) === identity.urn,
      );
      if (urnMatch) return urnMatch.post;
    }

    const prefix = comparablePostText(identity.textPrefix);
    const prefixSample = prefix.slice(0, 120);
    const actorHref = normalizeActorHref(identity.actorHref);
    const fallback = candidates.find((candidate) => {
      const current = postIdentity(candidate);
      if (actorHref && current?.actorHref !== actorHref) return false;
      if (!prefixSample) return Boolean(actorHref);
      const currentText = comparablePostText(candidate.text);
      return currentText.startsWith(prefixSample) || prefix.startsWith(currentText.slice(0, 120));
    });
    return fallback?.post || null;
  }

  function scan(root) {
    if (!root?.querySelectorAll) return [];
    const results = [];
    const seenPosts = new Set();

    const addResult = (post, control, reactionMenuButton = null) => {
      if (!post || !control || seenPosts.has(post)) return;
      const state = reactionMenuButton ? reactionMenuState(reactionMenuButton) : likeState(control);
      if (!state) return;
      seenPosts.add(post);
      results.push({
        post,
        likeButton: control,
        reactionMenuButton,
        liked: state === "liked",
        text: String(post.innerText || post.textContent || "").trim(),
        urn: extractPostUrn(post),
      });
    };

    // Style-2: identify post cards first so comment reaction controls nested in
    // the card cannot be mistaken for the post's own Like action.
    for (const post of Array.from(root.querySelectorAll(STYLE2_POST_SELECTOR))) {
      const likeControl = findLikeControl(post);
      const reactionMenuButton = likeControl ? null : findReactionMenuControl(post);
      addResult(post, likeControl || reactionMenuButton, reactionMenuButton);
    }

    // Profile activity pages use the same Style-2 post card, but do not always
    // mount it below mainFeed. Anchor on the post-level reactions chevron and
    // walk back to its listitem/article instead of broadening to every listitem
    // in <main> (which would also include comments and navigation entries).
    for (const icon of Array.from(root.querySelectorAll('main svg[id^="chevron-up"]'))) {
      const reactionMenuButton =
        icon.closest?.('button, [role="button"], a') ||
        icon.parentElement?.closest?.('button, [role="button"], a') ||
        null;
      if (!reactionMenuButton) continue;
      addResult(findPostContainer(reactionMenuButton), reactionMenuButton, reactionMenuButton);
    }

    // Legacy and profile-activity fallback: start from named controls.
    for (const control of Array.from(root.querySelectorAll('button, [role="button"]'))) {
      const state = likeState(control);
      if (!state) continue;
      addResult(findPostContainer(control), control);
    }
    return results;
  }

  async function waitForLiked(
    post,
    originalControl,
    {
      timeoutMs = 5000,
      pollMs = 100,
      sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
    } = {},
  ) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() <= deadline) {
      const currentControl = findLikeControl(post) || originalControl;
      if (likeState(currentControl) === "liked") return true;
      await sleep(pollMs);
    }
    return false;
  }

  global.NexusLinkedInFeedDom = Object.freeze({
    countExactRenderedText,
    controlName,
    actionControlSettled,
    extractPostUrn,
    findCommentControl,
    findCommentInput,
    findCommentSubmit,
    findDialogAction,
    findLikeControl,
    findOpenReactionLike,
    findMatchingPost,
    findPostContainer,
    findReactionMenuControl,
    isPostUrn,
    likeState,
    postIdentity,
    reactionMenuState,
    scan,
    waitForActionControlSettled,
    waitForCommentInput,
    waitForCommentPosted,
    waitForDialogAction,
    waitForLiked,
  });
})(globalThis);
