// ==UserScript==
// @name         Refurrer Object Statuses
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Show FA object statuses in Refurrer
// @author       You
// @match        https://www.furaffinity.net/*
// @match        https://twitter.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=furaffinity.net
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @connect      refurrer.com
// @connect      localhost:3001
// @connect      localhost
// @updateURL    https://refurrer.com/us/object_statuses.user.js
// @downloadURL  https://refurrer.com/us/object_statuses.user.js
// ==/UserScript==
// This file is generated from a Typescript file, do not edit the js file directly.
'use strict';

type PostObjectState =
  | 'not_seen'
  | 'ok'
  | 'removed'
  | 'scan_error'
  | 'file_error'
  | 'scanned_post'
  | 'have_file';
type UserObjectState = 'not_seen' | 'ok' | 'account_disabled' | 'error';
type ScanState = { last_at: string; due_for_scan: boolean };

interface ObjectStatusesResponse {
  posts: Record<
    number,
    | {
        state: 'not_seen';
      }
    | {
        state: PostObjectState;
        object_url: string;
        seen_at: string;
        post_scan: ScanState;
        file_scan: ScanState;
      }
  >;
  users: Record<
    string,
    | {
        state: 'not_seen';
      }
    | {
        state: UserObjectState;
        object_url: string;
        created_at: string;
        page_scan: ScanState;
        gallery_scan: ScanState;
        favs_scan: ScanState;
      }
  >;
}

type LiveEntityType = 'post' | 'user' | 'entity_statuses';
interface LiveEntityStatTable {
  type: LiveEntityType;
  stats: {
    name: string;
    value: string;
    sep?: string;
  }[];
}

async function faObjectStatuses() {
  const HOST = 'refurrer.com';
  const LIGHT_BG_COLOR = '#353b45';
  const DARK_BG_COLOR = '#20242a';
  const LIGHT_BORDER_COLOR = '#69697d';

  function isNotNull<T>(val: T | null | undefined): val is T {
    return val != null;
  }

  function setupNavbar(): HTMLElement {
    const navbarCssElem = document.createElement('style');
    const navbarHeight = '60px';
    navbarCssElem.innerHTML = `
      li.refurrer-navbar {
        height: 100%;
        display: flex !important;
      }

      li.refurrer-navbar table {
        height: 100% !important;
      }

      .refurrer-stats-tables {
        display: flex;
      }

      .refurrer-entities-dropdown div {
        font-weight: bold;
      }

      .refurrer-entities-dropdown ul,
      .refurrer-entities-dropdown li,
      .refurrer-entities-dropdown div {
        float: left !important;
      }

      .refurrer-entities-dropdown ul {
        height: auto !important;
      }

      .refurrer-entities-dropdown div,
      .refurrer-entities-dropdown li {
        line-height: 1.8em !important;
        user-select: text !important;
      }

      .refurrer-entities-dropdown li a {
        text-decoration: underline dotted !important;
      }

      .refurrer-entities-dropdown li a:hover {
        text-decoration: solid !important;
      }

      li.refurrer-navbar > div,
      .refurrer-stats-tables > table {
        border-right: 1px solid #d7d7d7;
        padding-right: 5px;
        margin-right: 5px;
      }

      li.refurrer-navbar > div:last-child,
      .refurrer-stats-tables > table:last-child {
        border-right: none;
        padding-right: 0;
        margin-right: 0;
      }

      .refurrer-entities-dropdown-container {
        position: relative;
      }

      .refurrer-entities-dropdown {
        position: absolute;
        background: ${LIGHT_BG_COLOR};
        border: 1px solid ${LIGHT_BORDER_COLOR};
        padding: 10px;
        box-shadow: 0px 3px 10px 5px rgb(0 0 0 / 64%);
        top: 60px;
        max-height: calc(100vh - 100px);
        overflow-y: auto;
      }

      .refurrer-entities-dropdown .dropdown-header {
        background: ${DARK_BG_COLOR};
        border-bottom: 1px solid ${LIGHT_BORDER_COLOR};
      }

      // #ddmenu ul {
      //   height: ${navbarHeight};
      // }
      // #ddmenu li {
      //   line-height: ${navbarHeight};
      // }
      // #searchbox input[type=search] {
      //   height: ${navbarHeight};
      // }
      // #ddmenu .menubar-icon-resize {
      //   max-height: ${navbarHeight} !important;
      // }
    `;
    document.head.appendChild(navbarCssElem);

    const navbarStatusNode = document.createElement('li');
    navbarStatusNode.classList.add('refurrer-navbar');
    navbarStatusNode.classList.add('lileft');

    const faNativeNavbar = document.querySelector('nav');
    if (faNativeNavbar != null) {
      faNativeNavbar.querySelector('ul > div')?.after(navbarStatusNode);
      return navbarStatusNode;
    } else {
      // watch pages, etc don't have a navbar - create one at the top of the page
      const center = document.body.querySelector('div[align=center]');
      center?.prepend(navbarStatusNode);
      return navbarStatusNode;
    }
  }

  function makeLargeStatusNode(
    opts: {
      type?: string;
      smaller?: boolean;
      style?: Record<string, string>;
    } = {},
  ) {
    if (opts.type == null) {
      opts.type = 'span';
    }
    if (opts.smaller == null) {
      opts.smaller = true;
    }
    if (opts.style == null) {
      opts.style = {};
    }

    const statusNode = document.createElement(opts.type);
    statusNode.style.cssText =
      'margin-left: 5px; color: #b7b7b7!important; display: inline';
    if (opts.smaller) {
      statusNode.style.fontSize = '80%';
    }

    for (const [property, value] of Object.entries(opts.style)) {
      statusNode.style.setProperty(property, value);
    }

    statusNode.innerHTML = '(...)';
    return statusNode;
  }

  function galleryFigureElements(): HTMLElement[] {
    function queryAsHtmlElement(query: string): HTMLElement[] {
      return [...document.querySelectorAll(query)].map(
        (elem) => elem as HTMLElement,
      );
    }

    return [
      ...queryAsHtmlElement('#gallery-search-results figure'),
      ...queryAsHtmlElement('#gallery-browse figure'),
      ...queryAsHtmlElement('#gallery-favorites figure'),
      ...queryAsHtmlElement('#gallery-gallery figure'),
      ...queryAsHtmlElement('#gallery-latest-favorites figure'),
      ...queryAsHtmlElement('#gallery-latest-submissions figure'),
      ...queryAsHtmlElement('section.gallery.messagecenter figure'),
    ];
  }

  function gatherPostElements(): { faId: number; statusNode: HTMLElement }[] {
    // if on a gallery page, links to the list of submissions
    const galleryFigures = galleryFigureElements().map((figure) => {
      const faId = parseInt(figure.id.split('-')[1]);
      const statusNode = document.createElement('p');
      statusNode.innerHTML = '(...)';
      const captionLabel = figure.querySelector('figcaption label p');
      const caption = figure.querySelector('figcaption');
      // gallery pages will include a figcaption
      if (captionLabel) {
        figure.style.height = `calc(${figure.style.height} + 20px)`;
        let b = figure.querySelector('b');
        if (b) {
          b.style.height = 'auto';
        }
        statusNode.style.cssText = 'position:relative;bottom:2px;';
        captionLabel.after(statusNode);
      } else if (caption) {
        statusNode.style.cssText = 'position:relative;bottom:2px;';
        caption.appendChild(statusNode);
      } else {
        const figcaption = document.createElement('div');
        figcaption.style.cssText =
          'display: block !important; position: absolute; bottom: 0px; font-size: 10px; width:100%';
        figcaption.appendChild(statusNode);
        figure.appendChild(figcaption);
        figure.style.height = `calc(${figure.style.height} + 20px)`;
      }

      return { faId, statusNode };
    });

    const featuredElem = [
      ...document.querySelectorAll('.userpage-featured-title h2'),
    ]
      .map((elem) => {
        // skip if it's a dynamically changing preview, `submissionDataElems` handles those
        if (elem.classList.contains('preview_title')) {
          return null;
        }
        const link = elem.querySelector('a');
        if (!link) {
          return null;
        }
        const faId = faPostIdFromViewHref(link.href);
        if (!faId) {
          return null;
        }
        const statusNode = makeLargeStatusNode();
        elem.appendChild(statusNode);

        return {
          faId,
          statusNode,
        };
      })
      .filter(isNotNull);

    let submissionDataElems: { faId: number; statusNode: HTMLElement }[] = [];
    if (unsafeWindow.submission_data) {
      submissionDataElems = Object.entries(unsafeWindow.submission_data).map(
        ([fa_id, _]) => {
          const statusNode = makeLargeStatusNode();
          return {
            faId: parseInt(fa_id),
            statusNode,
          };
        },
      );
    }

    // watch for changes to preview user and swap out status node when that happens
    [...document.querySelectorAll('h2.preview_title')].forEach(
      (previewTitleNode) => {
        const swapOutNode = document.createElement('span');
        swapOutNode.classList.add('swapper');
        previewTitleNode.appendChild(swapOutNode);
        const previewTitleLink = previewTitleNode.querySelector('a');
        if (!previewTitleLink) {
          return;
        }

        const observerCb = () => {
          const previewFaId = faPostIdFromViewHref(previewTitleLink.href);
          if (!previewFaId) {
            return;
          }
          swapOutNode.innerHTML = '';
          const currentSubmissionElem = submissionDataElems.find(
            ({ faId }) => faId == previewFaId,
          );
          if (currentSubmissionElem) {
            swapOutNode.appendChild(currentSubmissionElem.statusNode);
          }
        };
        const observer = new MutationObserver(observerCb);
        observer.observe(previewTitleLink, { childList: true, subtree: true });
        observerCb();
      },
    );

    // /view/<faId>/ page elements
    let primaryViewPageElems: { faId: number; statusNode: HTMLElement }[] = [];
    let faId = faPostIdFromViewHref(window.location.href);
    if (faId) {
      primaryViewPageElems = [
        ...document.querySelectorAll('.submission-title h2'),
      ].map((elem) => {
        let p = elem.querySelector('p');
        if (p) {
          p.style.display = 'inline';
        }
        const statusNode = makeLargeStatusNode({ type: 'h3' });
        elem.appendChild(statusNode);
        return {
          faId,
          statusNode,
        };
      });
    }

    return [
      ...galleryFigures,
      ...submissionDataElems,
      ...featuredElem,
      ...primaryViewPageElems,
    ];
  }

  function gatherUserElements(): {
    urlName: string;
    statusNode: HTMLElement;
  }[] {
    // if on a gallery / browse page, the creator links from those posts
    const userSubmissionLinks = galleryFigureElements()
      .map((figure) => {
        const userLinkElem = [...figure.querySelectorAll('figcaption a')]
          .map((e) => e as HTMLAnchorElement)
          .map((elem) => ({
            elem: elem,
            urlName: urlNameFromUserHref(elem.href),
          }))
          .filter(({ urlName }) => urlName != null)[0];

        if (userLinkElem == null || userLinkElem.urlName == null) {
          return null;
        }

        let u = figure.querySelector('u');
        if (u) {
          u.style.cssText = 'display: block';
        }

        const statusNode = document.createElement('span');
        statusNode.style.cssText = 'margin-left: 2px; color: #c1c1c1';
        statusNode.innerHTML = '(...)';
        userLinkElem.elem.after(statusNode);

        return {
          urlName: userLinkElem.urlName,
          statusNode,
        };
      })
      .filter(isNotNull);

    // if on a /user/ page, the primary username element
    const userPageUrlName = urlNameFromUserHref(window.location.href);
    let userPageMain = userPageUrlName
      ? [...document.querySelectorAll('h1 username')].map((elem) => {
          const statusNode = document.createElement('span');
          statusNode.innerHTML = '(...)';
          elem.after(statusNode);
          return {
            urlName: userPageUrlName,
            statusNode,
          };
        })
      : [];

    // comments made by users on posts or user pages
    let userComments = [...document.querySelectorAll('comment-username a')]
      .map((e) => e as HTMLAnchorElement)
      .map((elem) => {
        const statusNode = makeLargeStatusNode();
        elem.after(statusNode);
        const urlName = urlNameFromUserHref(elem.href);
        if (urlName == null) {
          return null;
        }
        return {
          urlName,
          statusNode,
        };
      })
      .filter(isNotNull);

    // users mentioned in post descriptions or user bios
    let iconUsernames = [
      ...document.querySelectorAll('a.iconusername, a.linkusername'),
    ]
      .map((e) => e as HTMLAnchorElement)
      .map((elem) => {
        const statusNode = makeLargeStatusNode();
        elem.after(statusNode);
        const urlName = urlNameFromUserHref(elem.href);
        if (urlName == null) {
          return null;
        }
        return {
          urlName,
          statusNode,
        };
      })
      .filter(isNotNull);

    // watcher / watching lists on user page
    let watchersAndWatchList = [
      ...document.querySelectorAll('a > span.artist_name'),
    ]
      .map((elem) => {
        const link = elem.parentNode as HTMLAnchorElement;
        if (!link) {
          return null;
        }
        const statusNode = makeLargeStatusNode();
        link.after(statusNode);
        const urlName = urlNameFromUserHref(link.href);
        if (urlName == null) {
          return null;
        }
        return {
          urlName,
          statusNode,
        };
      })
      .filter(isNotNull);

    // users mentioned in the "hero" sections on a url page
    let submissionDataElems: { urlName: string; statusNode: HTMLElement }[] =
      [];

    if (unsafeWindow.submission_data) {
      submissionDataElems = Object.entries(unsafeWindow.submission_data).map(
        ([_, { lower }]) => {
          const statusNode = makeLargeStatusNode();
          return {
            urlName: lower,
            statusNode,
          };
        },
      );
    }

    // watch for changes to preview user and swap out status node when that happens
    const previewUser = document.querySelector(
      'a.preview_user',
    ) as HTMLAnchorElement;
    if (previewUser) {
      const swapOutNode = document.createElement('span');
      swapOutNode.classList.add('swapper');
      previewUser.after(swapOutNode);

      const observerCb = () => {
        const previewUrlName = urlNameFromUserHref(previewUser.href);
        if (!previewUrlName) {
          return;
        }
        swapOutNode.innerHTML = '';
        const currentSubmissionElem = submissionDataElems.find(
          ({ urlName }) => urlName == previewUrlName,
        );
        if (currentSubmissionElem) {
          swapOutNode.appendChild(currentSubmissionElem.statusNode);
        }
      };
      const observer = new MutationObserver(observerCb);
      observer.observe(previewUser, { childList: true, subtree: true });
      observerCb();
    }

    // on a /view/ page, the name of the user
    const submissionContainerUserLinks = [
      ...document.querySelectorAll('.submission-id-sub-container a'),
    ]
      .map((e) => e as HTMLAnchorElement)
      .map((elem) => {
        const statusNode = makeLargeStatusNode({ smaller: false });
        elem.after(statusNode);
        const urlName = urlNameFromUserHref(elem.href);
        if (urlName == null) {
          return null;
        }
        return {
          urlName,
          statusNode,
        };
      })
      .filter(isNotNull);

    // on a /watchlist/by/ or /watchlist/to page
    const watchListUserLinks = [
      ...document.querySelectorAll('.watch-list-items.watch-row a'),
    ]
      .map((e) => e as HTMLAnchorElement)
      .map((elem) => {
        const statusNode = makeLargeStatusNode({
          smaller: false,
          style: {
            display: 'block',
            'margin-bottom': '5px',
            'font-size': '75%',
          },
        });
        elem.parentNode?.appendChild(statusNode);
        const urlName = urlNameFromUserHref(elem.href);
        if (urlName == null) {
          return null;
        }
        return {
          urlName,
          statusNode,
        };
      })
      .filter(isNotNull);

    return [
      ...userSubmissionLinks,
      ...userPageMain,
      ...userComments,
      ...iconUsernames,
      ...watchersAndWatchList,
      ...submissionDataElems,
      ...submissionContainerUserLinks,
      ...watchListUserLinks,
    ];
  }

  function urlNameFromUserHref(href: string): string | null {
    const url = new URL(href);
    if (url.host != 'www.furaffinity.net') {
      return null;
    }

    const userPageRegex = /^\/(user|gallery|scraps|favorites|journals)\/.+/;
    const match = url.pathname.match(userPageRegex);
    if (!match) {
      return null;
    }
    return url.pathname.split('/')[2];
  }

  function faPostIdFromViewHref(href: string): number | null {
    const viewPageRegex = /\/(view|full)\/(\d+)/;
    const match = href.match(viewPageRegex);
    const faId = (match && match[2]) || null;
    if (faId) {
      return parseInt(faId);
    }
    return null;
  }

  function faPostIdFromCurrentPage(): number | null {
    return faPostIdFromViewHref(window.location.href);
  }

  function faUserUrlNameFromCurrentPage(): string | null {
    const fromViewHref = urlNameFromUserHref(window.location.href);
    if (fromViewHref) {
      return fromViewHref;
    }

    let userLink = document.querySelector('.submission-id-sub-container a');
    if (userLink && userLink instanceof HTMLAnchorElement) {
      return urlNameFromUserHref(userLink.href);
    }
    return null;
  }

  function renderTable(
    stats: {
      name: string;
      value: string;
      sep?: string;
      nameAlign?: string;
      valueAlign?: string;
    }[],
    styleOpts: Record<string, string> = {},
  ): HTMLElement | null {
    if (stats.length == 0) {
      return null;
    }

    if (styleOpts['border-collapse'] == null) {
      styleOpts['border-collapse'] = 'collapse';
    }

    let table = document.createElement('table');
    for (const [property, value] of Object.entries(styleOpts)) {
      table.style.setProperty(property, value as string);
    }

    let tbody = document.createElement('tbody');
    table.appendChild(tbody);

    stats.forEach(({ name, value, sep, nameAlign, valueAlign }) => {
      if (name == '' && value == '') {
        tbody.innerHTML += `<tr><td>---</td><td>---</td></tr>`;
      } else {
        if (sep == null) {
          sep = ':';
        }
        if (nameAlign == null) {
          nameAlign = 'right';
        }
        if (valueAlign == null) {
          valueAlign = 'right';
        }

        tbody.innerHTML += `<tr>
        <td style="text-align:${nameAlign};padding-right:5px">${name}${sep}</td>
        <td style="text-align:${valueAlign}">${value}</td>
      </tr>`;
      }
    });
    return table;
  }

  function optsForNumRows(numRows: number) {
    switch (numRows) {
      case 0:
      case 1:
      case 2:
      case 3:
        return { 'line-height': '1.0em', 'font-size': '1.0em' };
      case 4:
        return { 'line-height': '0.9em', 'font-size': '0.8em' };
      default:
        return { 'line-height': '0.9em', 'font-size': '0.6em' };
    }
  }

  // gather initial entities on the page
  const userElements = gatherUserElements();
  const urlNames = [...new Set(userElements.map(({ urlName }) => urlName))];

  const postElements = gatherPostElements();
  const faIds = [...new Set(postElements.map(({ faId }) => faId))];

  const navbarNode = setupNavbar();

  function setupNavbarEntitiesOnPageNode() {
    const node = document.createElement('div') as HTMLDivElement;
    node.classList.add('refurrer-entities-dropdown-container');

    const dropdownElem = document.createElement('div');
    dropdownElem.classList.add('refurrer-entities-dropdown');
    dropdownElem.classList.add('hidden');

    const pageOverviewStatsTable = renderTable(
      [
        { name: 'users', value: urlNames.length.toString() },
        { name: 'posts', value: faIds.length.toString() },
      ],
      { ...optsForNumRows(2), width: 'auto' },
    );
    node.appendChild(pageOverviewStatsTable);

    if (urlNames.length > 0) {
      const userHeader = document.createElement('div');
      userHeader.textContent = 'Users:';
      dropdownElem.appendChild(userHeader);

      const userList = document.createElement('ul');
      dropdownElem.appendChild(userList);

      urlNames.forEach((name) => {
        const li = document.createElement('li');
        li.textContent = name;
        userList.appendChild(li);
      });
    }

    if (faIds.length > 0) {
      const postHeader = document.createElement('div');
      postHeader.textContent = 'Post IDs:';
      dropdownElem.appendChild(postHeader);

      const postList = document.createElement('ul');
      dropdownElem.appendChild(postList);

      faIds.forEach((id) => {
        const li = document.createElement('li');
        li.textContent = id.toString();
        postList.appendChild(li);
      });
    }

    node.appendChild(dropdownElem);

    node.addEventListener('mouseenter', () => {
      dropdownElem.classList.remove('hidden');
    });

    dropdownElem.addEventListener('mouseleave', () => {
      dropdownElem.classList.add('hidden');
    });

    return [node, dropdownElem];
  }

  function setupNavbarEntityStatsTablesNode() {
    const node = document.createElement('div') as HTMLDivElement;
    node.classList.add('refurrer-stats-tables');
    node.innerHTML = 'loading...';
    return node;
  }

  // set up entities on page & dropdown
  const [navbarEntitiesOnPageNode, navbarDropdownNode] =
    setupNavbarEntitiesOnPageNode();
  navbarNode.append(navbarEntitiesOnPageNode);

  // set up stats tables
  const navbarLiveEntityStatTablesNode = setupNavbarEntityStatsTablesNode();
  navbarNode.append(navbarLiveEntityStatTablesNode);

  function renderLiveEntityStatTables(
    liveEntityStatTables: LiveEntityStatTable[],
  ) {
    const tables = liveEntityStatTables.map(({ type, stats }) => {
      return renderTable(stats, {
        ...optsForNumRows(stats.length),
        width: 'auto',
      });
    });

    navbarLiveEntityStatTablesNode.innerHTML = '';
    tables.forEach((table) => {
      navbarLiveEntityStatTablesNode.appendChild(table);
    });
  }

  function fetchObjectStatuses(): Promise<ObjectStatusesResponse> {
    return new Promise((resolve, reject) => {
      let url = `http://${HOST}/api/fa/object_statuses`;
      let params = new URLSearchParams();
      faIds.forEach((id) => params.append('fa_ids[]', id.toString()));
      urlNames.forEach((name) => params.append('url_names[]', name));
      if (params.toString() != '') {
        url += '?' + params.toString();
      }
      GM_xmlhttpRequest({
        url,
        method: 'GET',
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/json',
        },
        onabort: () => {
          reject(new Error('aborted fetching object statuses'));
        },
        ontimeout: () => {
          reject(new Error('timeout fetching object statuses'));
        },
        onerror: (response) => {
          reject(
            new Error(
              'failed to fetch object statuses: ' + response.responseText,
            ),
          );
        },
        onload: (response) => {
          if (response.status === 200) {
            const jsonResponse = JSON.parse(
              response.responseText,
            ) as ObjectStatusesResponse;
            resolve(jsonResponse);
          } else {
            reject(
              new Error(
                'failed to fetch object statuses: ' + response.responseText,
              ),
            );
          }
        },
      });
    });
  }

  function handleObjectStatusesResponse(jsonResponse: ObjectStatusesResponse) {
    let numNotSeenPosts = 0;
    let numOkPosts = 0;
    let numScannedPosts = 0;
    let numHaveFile = 0;

    navbarDropdownNode.innerHTML = '';
    const userHeader = document.createElement('div');
    userHeader.textContent = 'Users:';
    navbarDropdownNode.appendChild(userHeader);
    const userList = document.createElement('ul');
    navbarDropdownNode.appendChild(userList);

    const postHeader = document.createElement('div');
    postHeader.textContent = 'Post IDs:';
    navbarDropdownNode.appendChild(postHeader);
    const postList = document.createElement('ul');
    navbarDropdownNode.appendChild(postList);

    function createEntityListElem(
      title: string,
      url: string | null,
    ): HTMLElement {
      const li = document.createElement('li');
      if (url == null) {
        li.textContent = title;
      } else {
        const a = document.createElement('a');
        a.href = url;
        a.target = '_blank';
        a.textContent = title;
        li.appendChild(a);
      }
      return li;
    }

    for (const [gotUrlName, userInfo] of Object.entries(jsonResponse.users)) {
      const li = createEntityListElem(
        gotUrlName,
        userInfo.state == 'not_seen' ? null : userInfo.object_url,
      );
      userList.appendChild(li);

      userElements
        .filter(({ urlName }) => urlName == gotUrlName)
        .forEach(({ statusNode }) => {
          statusNode.innerHTML = userInfo.state;
        });
    }

    for (const [gotFaId, postInfo] of Object.entries(jsonResponse.posts)) {
      const li = createEntityListElem(
        gotFaId,
        postInfo.state == 'not_seen' ? null : postInfo.object_url,
      );
      postList.appendChild(li);

      postElements
        .filter(({ faId }) => faId == parseInt(gotFaId))
        .forEach(({ statusNode }) => {
          statusNode.innerHTML = postInfo.state;

          switch (postInfo.state) {
            case 'not_seen':
              numNotSeenPosts += 1;
              break;
            case 'ok':
              numOkPosts += 1;
              break;
            case 'scanned_post':
              numScannedPosts += 1;
              break;
            case 'have_file':
              numHaveFile += 1;
              break;
          }
        });
    }

    let liveEntities: LiveEntityStatTable[] = [];

    liveEntities.push({
      type: 'entity_statuses',
      stats: [
        { name: 'not seen posts', value: numNotSeenPosts.toString() },
        { name: 'ok posts', value: numOkPosts.toString() },
        { name: 'scanned posts', value: numScannedPosts.toString() },
        { name: 'have file', value: numHaveFile.toString() },
      ],
    });

    const thisPageFaId = faPostIdFromCurrentPage();
    const thisPageUrlName = faUserUrlNameFromCurrentPage();

    const pssCommon = { sep: '', valueAlign: 'left' };
    if (thisPageFaId != null) {
      const postData = jsonResponse.posts[thisPageFaId];
      if (postData.state == 'not_seen') {
        liveEntities.push({
          type: 'post',
          stats: [{ name: 'no entity', value: '', sep: '' }],
        });
      } else {
        liveEntities.push({
          type: 'post',
          stats: [
            {
              name: 'refurrer link',
              value: `<a target="_blank" href="${postData.object_url}" style="text-decoration: underline dotted">${thisPageFaId}</a>`,
              ...pssCommon,
            },
            { name: `seen`, value: postData.seen_at, ...pssCommon },
            {
              name: `scanned`,
              value: postData.post_scan.last_at,
              ...pssCommon,
            },
            {
              name: `downloaded`,
              value: postData.file_scan.last_at,
              ...pssCommon,
            },
          ],
        });
      }
    }

    if (thisPageUrlName != null) {
      const userData = jsonResponse.users[thisPageUrlName];
      if (userData.state == 'not_seen') {
        liveEntities.push({
          type: 'user',
          stats: [{ name: 'no entity', value: '', sep: '' }],
        });
      } else {
        liveEntities.push({
          type: 'user',
          stats: [
            {
              name: 'refurrer link',
              value: `<a target="_blank" href="${userData.object_url}" style="text-decoration: underline dotted">${thisPageUrlName}</a>`,
              ...pssCommon,
            },
            { name: 'first seen', value: userData.created_at, ...pssCommon },
            {
              name: 'page scan',
              value: userData.page_scan.last_at,
              ...pssCommon,
            },
            {
              name: 'gallery scan',
              value: userData.gallery_scan.last_at,
              ...pssCommon,
            },
            {
              name: 'favs scan',
              value: userData.favs_scan.last_at,
              ...pssCommon,
            },
          ],
        });
      }
    }

    renderLiveEntityStatTables(liveEntities);
  }

  await fetchObjectStatuses()
    .then(handleObjectStatusesResponse)
    .catch((err) => {
      console.error('error: ', err);
      navbarLiveEntityStatTablesNode.innerHTML =
        '<div style="color: red;">Error: ' + err.message + '</div>';
    });
}

(async function () {
  if (window.location.hostname == 'www.furaffinity.net') {
    await faObjectStatuses();
  } else {
    console.error('unhandled domain ', window.location.hostname);
  }
})();
