"use strict";

const path = require("path");
const fs = require("fs");

const envPaths = require("../server/node_modules/env-paths");
const debug = require("../server/node_modules/debug")("oasis");
const highlightJs = require("../server/node_modules/highlight.js");
const prettyMs = require("../server/node_modules/pretty-ms");
const moment = require('../server/node_modules/moment');
const { renderUrl } = require('../backend/renderUrl');
const ssbClientGUI = require("../client/gui");
const config = require("../server/ssb_config");
const cooler = ssbClientGUI({ offline: config.offline });
const sharedState = require('../configs/shared-state');

let ssb, userId;

const getUserId = async () => {
  if (!ssb) ssb = await cooler.open();
  if (!userId) userId = ssb.id;
  return userId;
};

const { a, article, br, body, button, details, div, em, footer, form, h1, h2, h3, head, header, hr, html, img, input, label, li, link, main, meta, nav, option, p, pre, section, select, span, summary, table, td, textarea, title, tr, ul, strong, video: videoHyperaxe, audio: audioHyperaxe } = require("../server/node_modules/hyperaxe");

const lodash = require("../server/node_modules/lodash");
const markdown = require("./markdown");
const { sanitizeHtml } = require('../backend/sanitizeHtml');

const i18nBase = require("../client/assets/translations/i18n");
let selectedLanguage = "en";
let i18n = {};
Object.assign(i18n, i18nBase[selectedLanguage]);
exports.setLanguage = (language) => {
  selectedLanguage = language;
  const newLang = i18nBase[selectedLanguage] || i18nBase['en'];
  Object.keys(i18n).forEach(k => delete i18n[k]);
  Object.assign(i18n, newLang);
};
exports.i18n = i18n;
Object.defineProperty(exports, 'selectedLanguage', { get: () => selectedLanguage });

// markdown
const markdownUrl = "https://commonmark.org/help/";

const doctypeString = "<!DOCTYPE html>";

const THREAD_PREVIEW_LENGTH = 3;
const toAttributes = (obj) =>
  Object.entries(obj)
    .map(([key, val]) => `${key}=${val}`)
    .join(", ");
    
const nbsp = "\xa0";

const { getConfig } = require('../configs/config-manager.js');

// menu INIT
const readPkg = () => {
  const file = path.resolve(__dirname, "..", "server", "package.json");
  try {
    const txt = fs.readFileSync(file, "utf8");
    const parsed = JSON.parse(txt || "{}");
    return parsed && typeof parsed === "object" ? parsed : {};
  } catch (_) {
    return {};
  }
};

const renderFooter = () => {
  const pkg = readPkg();
  const year = moment().format("YYYY");
  const pkgName = pkg?.name || "@krakenslab/oasis";
  const pkgVersion = pkg?.version || "?";

  let blockchainCycle = {};
  try {
    blockchainCycle = JSON.parse(fs.readFileSync(path.join(__dirname, "../configs/blockchain-cycle.json"), "utf8"));
  } catch (_) {}
  const cycleVal = blockchainCycle.cycle || "?";
  const cycleUrl = blockchainCycle.url || "https://laplaza.solarnethub.com";

  const hcT = sharedState.getCarbonHcT();
  const hcH = sharedState.getCarbonHcH();

  return div(
    { class: "oasis-footer" },
    div(
      { class: "oasis-footer-center" },
      a(
        { href: "/", class: "oasis-footer-logo-link" },
        img({
          class: "oasis-footer-logo",
          src: "/assets/images/snh-oasis.jpg",
          alt: "Oasis"
        })
      ),
      br(),
      a(
        { href: "https://code.03c8.net/krakenslab/oasis", target: "_blank", rel: "noreferrer noopener" },
      span(pkgName),
      ),
      span("["),
         span({ class: "oasis-footer-version" }, pkgVersion),
      span("]"),
      span({ class: "oasis-footer-sep" }, " - "),
      a(
        { href: "https://www.gnu.org/licenses/gpl-3.0.html", target: "_blank", rel: "noreferrer noopener" },
        i18n.footerLicense
      ),
      span({ class: "oasis-footer-sep" }, " - "),
      span({ class: "oasis-footer-year" }, year),
      br(),
      span("BLOCKCHAIN CYCLE: "),
      a({ href: cycleUrl, target: "_blank", rel: "noreferrer noopener" }, String(cycleVal)),
      br(),
      span({ class: "oasis-footer-carbon" },
        span("HcT: "),
        a({ href: "/stats?filter=ALL" }, hcT != null ? String(hcT) : '–'),
        span(" | HcH: "),
        a({ href: "/stats?filter=MINE" }, hcH != null ? String(hcH) : '–')
      )
    )
  );
};

const navLink = ({ href, emoji, text, current, class: extraClass }) =>
  li(
    a(
      {
        href,
        class: [current ? "current" : "", extraClass]
          .filter(Boolean)
          .join(" ")
      },
      span({ class: "emoji" }, emoji),
      nbsp,
      text
    )
  );

const customCSS = (filename) => {
  const customStyleFile = path.join(
    envPaths("oasis", { suffix: "" }).config,
    filename
  );
  try {
    if (fs.existsSync(customStyleFile)) {
      return link({ rel: "stylesheet", href: filename });
    }
  } catch (error) {
    return "";
  }
};

const navGroup = ({ id, emoji, title, defaultOpen = false }, ...items) => {
  const active = items.filter(Boolean);
  if (!active.length) return null;
  return li(
    { class: "oasis-nav-group" },
    input({
      type: "checkbox",
      id: `oasis-nav-group-${id}`,
      class: "oasis-nav-toggle",
      ...(defaultOpen ? { checked: true } : {})
    }),
    label(
      { for: `oasis-nav-group-${id}`, class: "oasis-nav-header" },
      span({ class: "emoji" }, emoji),
      nbsp,
      title,
      span({ class: "oasis-nav-arrow" }, "▾")
    ),
    ul({ class: "oasis-nav-list" }, ...active)
  );
};

const renderPopularLink = () => {
  const popularMod = getConfig().modules.popularMod === "on";
  return popularMod
    ? navLink({
        href: "/public/popular/day",
        emoji: "⌘",
        text: i18n.popular,
        class: "popular-link enabled"
      })
    : "";
};

const renderTopicsLink = () => {
  const topicsMod = getConfig().modules.topicsMod === "on";
  return topicsMod
    ? navLink({
        href: "/public/latest/topics",
        emoji: "ϟ",
        text: i18n.topics,
        class: "topics-link enabled"
      })
    : "";
};

const renderSummariesLink = () => {
  const summariesMod = getConfig().modules.summariesMod === "on";
  if (summariesMod) {
    return [
      navLink({
        href: "/public/latest/summaries",
        emoji: "※",
        text: i18n.summaries,
        class: "summaries-link enabled"
      })
    ];
  }
  return "";
};

const renderLatestLink = () => {
  const latestMod = getConfig().modules.latestMod === "on";
  return latestMod
    ? navLink({
        href: "/public/latest",
        emoji: "☄",
        text: i18n.latest,
        class: "latest-link enabled"
      })
    : "";
};

const renderThreadsLink = () => {
  const threadsMod = getConfig().modules.threadsMod === "on";
  if (threadsMod) {
    return [
      navLink({
        href: "/public/latest/threads",
        emoji: "♺",
        text: i18n.threads,
        class: "threads-link enabled"
      })
    ];
  }
  return "";
};

const renderInvitesLink = () => {
  const invitesMod = getConfig().modules.invitesMod === "on";
  return invitesMod
    ? navLink({
        href: "/invites",
        emoji: "ꔹ",
        text: i18n.invites,
        class: "invites-link enabled"
      })
    : "";
};

const renderWalletLink = () => {
  const walletMod = getConfig().modules.walletMod === "on";
  if (walletMod) {
    return [
      navLink({
        href: "/wallet",
        emoji: "❄",
        text: i18n.wallet,
        class: "wallet-link enabled"
      })
    ];
  }
  return "";
};

const renderLegacyLink = () => {
  const legacyMod = getConfig().modules.legacyMod === "on";
  if (legacyMod) {
    return [
      navLink({
        href: "/legacy",
        emoji: "ꖤ",
        text: i18n.legacy,
        class: "legacy-link enabled"
      })
    ];
  }
  return "";
};

const renderCipherLink = () => {
  const cipherMod = getConfig().modules.cipherMod === "on";
  if (cipherMod) {
    return [
      navLink({
        href: "/cipher",
        emoji: "ꗄ",
        text: i18n.cipher,
        class: "cipher-link enabled"
      })
    ];
  }
  return "";
};

const renderBookmarksLink = () => {
  const bookmarksMod = getConfig().modules.bookmarksMod === "on";
  return bookmarksMod
    ? navLink({
        href: "/bookmarks",
        emoji: "ꔪ",
        text: i18n.bookmarksLabel,
        class: "bookmark-link enabled"
      })
    : "";
};

const renderImagesLink = () => {
  const imagesMod = getConfig().modules.imagesMod === "on";
  if (imagesMod) {
    return [
      navLink({
        href: "/images",
        emoji: "ꕥ",
        text: i18n.imagesLabel,
        class: "images-link enabled"
      })
    ];
  }
  return "";
};

const renderTorrentsLink = () => {
  const torrentsMod = getConfig().modules.torrentsMod === "on";
  if (torrentsMod) {
    return [
      navLink({
        href: "/torrents",
        emoji: "ꖅ",
        text: i18n.torrentsLabel,
        class: "torrents-link enabled"
      })
    ];
  }
  return "";
};

const renderMapsLink = () => {
  const mapsMod = getConfig().modules.mapsMod === "on";
  if (mapsMod) {
    return [
      navLink({
        href: "/maps",
        emoji: "ꔌ",
        text: i18n.mapsLabel,
        class: "maps-link enabled"
      })
    ];
  }
  return "";
};

const renderChatsLink = () => {
  const chatsMod = getConfig().modules.chatsMod === "on";
  if (chatsMod) {
    return [
      navLink({
        href: "/chats",
        emoji: "ꖒ",
        text: i18n.chatsTitle,
        class: "chats-link enabled"
      })
    ];
  }
  return "";
};

const renderVideosLink = () => {
  const videosMod = getConfig().modules.videosMod === "on";
  if (videosMod) {
    return [
      navLink({
        href: "/videos",
        emoji: "ꗟ",
        text: i18n.videosLabel,
        class: "videos-link enabled"
      })
    ];
  }
  return "";
};

const renderAudiosLink = () => {
  const audiosMod = getConfig().modules.audiosMod === "on";
  if (audiosMod) {
    return [
      navLink({
        href: "/audios",
        emoji: "ꔿ",
        text: i18n.audiosLabel,
        class: "audios-link enabled"
      })
    ];
  }
  return "";
};

const renderDocsLink = () => {
  const docsMod = getConfig().modules.docsMod === "on";
  if (docsMod) {
    return [
      navLink({
        href: "/documents",
        emoji: "ꕨ",
        text: i18n.docsLabel,
        class: "docs-link enabled"
      })
    ];
  }
  return "";
};

const renderTagsLink = () => {
  const tagsMod = getConfig().modules.tagsMod === "on";
  return tagsMod
    ? [
        navLink({
          href: "/tags",
          emoji: "ꖶ",
          text: i18n.tagsLabel,
          class: "tags-link enabled"
        })
      ]
    : "";
};

const renderMultiverseLink = () => {
  const multiverseMod = getConfig().modules.multiverseMod === "on";
  return multiverseMod
    ? navLink({
        href: "/public/latest/extended",
        emoji: "∞",
        text: i18n.multiverse,
        class: "multiverse-link enabled"
      })
    : "";
};

const renderMarketLink = () => {
  const marketMod = getConfig().modules.marketMod === "on";
  return marketMod
    ? [
        navLink({
          href: "/market",
          emoji: "ꕻ",
          text: i18n.marketTitle
        })
      ]
    : "";
};

const renderJobsLink = () => {
  const jobsMod = getConfig().modules.jobsMod === "on";
  return jobsMod
    ? [
        navLink({
          href: "/jobs",
          emoji: "ꗒ",
          text: i18n.jobsTitle
        })
      ]
    : "";
};

const renderShopsLink = () => {
  const shopsMod = getConfig().modules.shopsMod === "on";
  return shopsMod
    ? [
        navLink({
          href: "/shops",
          emoji: "ꔜ",
          text: i18n.shopsTitle
        })
      ]
    : "";
};

const renderProjectsLink = () => {
  const projectsMod = getConfig().modules.projectsMod === "on";
  return projectsMod
    ? [
        navLink({
          href: "/projects",
          emoji: "ꕧ",
          text: i18n.projectsTitle
        })
      ]
    : "";
};

const renderBankingLink = () => {
  const bankingMod = getConfig().modules.bankingMod === "on";
  return bankingMod
    ? navLink({
        href: "/banking",
        emoji: "ꗴ",
        text: i18n.bankingTitle
      })
    : "";
};

const renderTribesLink = () => {
  const tribesMod = getConfig().modules.tribesMod === "on";
  return tribesMod
    ? [
        navLink({
          href: "/tribes",
          emoji: "ꖥ",
          text: i18n.tribesTitle,
          class: "tribes-link enabled"
        })
      ]
    : "";
};

const renderParliamentLink = () => {
  const parliamentMod = getConfig().modules.parliamentMod === "on";
  return parliamentMod
    ? [
        navLink({
          href: "/parliament",
          emoji: "ꗞ",
          text: i18n.parliamentTitle,
          class: "parliament-link enabled"
        })
      ]
    : "";
};

const renderCourtsLink = () => {
  const courtsMod = getConfig().modules.courtsMod === "on";
  return courtsMod
    ? navLink({
        href: "/courts",
        emoji: "ꖻ",
        text: i18n.courtsTitle,
        class: "courts-link enabled"
      })
    : "";
};

const renderVotationsLink = () => {
  const votesMod = getConfig().modules.votesMod === "on";
  return votesMod
    ? [
        navLink({
          href: "/votes",
          emoji: "ꔰ",
          text: i18n.votationsTitle,
          class: "votations-link enabled"
        })
      ]
    : "";
};

const renderTrendingLink = () => {
  const trendingMod = getConfig().modules.trendingMod === "on";
  return trendingMod
    ? [
        navLink({
          href: "/trending",
          emoji: "ꗝ",
          text: i18n.trendingLabel,
          class: "trending-link enabled"
        })
      ]
    : "";
};

const renderReportsLink = () => {
  const reportsMod = getConfig().modules.reportsMod === "on";
  return reportsMod
    ? [
        navLink({
          href: "/reports",
          emoji: "ꕥ",
          text: i18n.reportsTitle,
          class: "reports-link enabled"
        })
      ]
    : "";
};

const renderOpinionsLink = () => {
  const opinionsMod = getConfig().modules.opinionsMod === "on";
  return opinionsMod
    ? [
        navLink({
          href: "/opinions",
          emoji: "ꔍ",
          text: i18n.opinionsTitle,
          class: "opinions-link enabled"
        })
      ]
    : "";
};

const renderPadsLink = () => {
  const padsMod = getConfig().modules.padsMod === "on";
  return padsMod
    ? [
        navLink({
          href: "/pads",
          emoji: "ꔗ",
          text: i18n.padsTitle,
          class: "pads-link enabled"
        })
      ]
    : "";
};

const renderTransfersLink = () => {
  const transfersMod = getConfig().modules.transfersMod === "on";
  return transfersMod
    ? [
        navLink({
          href: "/transfers",
          emoji: "ꘉ",
          text: i18n.transfersTitle,
          class: "transfers-link enabled"
        })
      ]
    : "";
};

const renderFeedLink = () => {
  const feedMod = getConfig().modules.feedMod === "on";
  return feedMod
    ? navLink({
        href: "/feed",
        emoji: "ꕿ",
        text: i18n.feedTitle,
        class: "feed-link enabled"
      })
    : "";
};

const renderPixeliaLink = () => {
  const pixeliaMod = getConfig().modules.pixeliaMod === "on";
  return pixeliaMod
    ? [
        navLink({
          href: "/pixelia",
          emoji: "ꔘ",
          text: i18n.pixeliaTitle,
          class: "pixelia-link enabled"
        })
      ]
    : "";
};

const renderGamesLink = () => {
  const gamesMod = getConfig().modules.gamesMod === "on";
  return gamesMod
    ? [navLink({ href: "/games", emoji: "ꕇ", text: i18n.gamesTitle, class: "games-link enabled" })]
    : "";
};

const renderForumLink = () => {
  const forumMod = getConfig().modules.forumMod === "on";
  return forumMod
    ? [
        navLink({
          href: "/forum",
          emoji: "ꕒ",
          text: i18n.forumTitle,
          class: "forum-link enabled"
        })
      ]
    : "";
};

const renderAgendaLink = () => {
  const agendaMod = getConfig().modules.agendaMod === "on";
  return agendaMod
    ? [
        navLink({
          href: "/agenda",
          emoji: "ꗤ",
          text: i18n.agendaTitle,
          class: "agenda-link enabled"
        })
      ]
    : "";
};

const renderFavoritesLink = () => {
  const favoritesMod = getConfig().modules.favoritesMod === "on";
  return favoritesMod
    ? [
        navLink({
          href: "/favorites",
          emoji: "ꘝ",
          text: i18n.favoritesTitle,
          class: "favorites-link enabled"
        })
      ]
    : "";
};

const renderLogsLink = () => {
  const logsMod = getConfig().modules.logsMod === "on";
  return logsMod
    ? [
        navLink({
          href: "/logs",
          emoji: "ꗯ",
          text: i18n.logsTitle || "Logs",
          class: "logs-link enabled"
        })
      ]
    : "";
};

const renderAILink = () => {
  const aiMod = getConfig().modules.aiMod === "on";
  return aiMod
    ? [
        navLink({
          href: "/ai",
          emoji: "ꘜ",
          text: i18n.ai,
          class: "ai-link enabled"
        })
      ]
    : "";
};

const renderEventsLink = () => {
  const eventsMod = getConfig().modules.eventsMod === "on";
  return eventsMod
    ? [
        navLink({
          href: "/events",
          emoji: "ꕆ",
          text: i18n.eventsLabel,
          class: "events-link enabled"
        })
      ]
    : "";
};

const renderCalendarsLink = () => {
  const calendarsMod = getConfig().modules.calendarsMod === "on";
  return calendarsMod
    ? [
        navLink({
          href: "/calendars",
          emoji: "\uA5AF",
          text: i18n.calendarsTitle || "Calendars",
          class: "calendars-link enabled"
        })
      ]
    : "";
};

const renderTasksLink = () => {
  const tasksMod = getConfig().modules.tasksMod === "on";
  return tasksMod
    ? [
        navLink({
          href: "/tasks",
          emoji: "ꖧ",
          text: i18n.tasksTitle,
          class: "tasks-link enabled"
        })
      ]
    : "";
};

const template = (titlePrefix, ...elements) => {
  const currentConfig = getConfig();
  const theme = currentConfig.themes.current || "Dark-SNH";
  const themeLink = link({
    rel: "stylesheet",
    href: `/assets/themes/${theme}.css`
  });
  const nodes = html(
    { lang: "en" },
    head(
      title(titlePrefix, " | Oasis"),
      link({ rel: "stylesheet", href: "/assets/styles/style.css" }),
      themeLink,
      link({ rel: "stylesheet", href: "/assets/styles/mobile.css", media: "(max-width: 768px)" }),
      link({ rel: "icon", href: "/assets/images/favicon.svg" }),
      meta({ charset: "utf-8" }),
      meta({ name: "description", content: i18n.oasisDescription }),
      meta({
        name: "viewport",
        content: toAttributes({
          width: "device-width",
          "initial-scale": 1
        })
      })
    ),
    body(
      div(
        { class: "header" },
        div(
          { class: "top-bar-left" },
          a(
            { class: "logo-icon", href: "/" },
            img({
              class: "logo-icon",
              src: "/assets/images/snh-oasis.jpg",
              alt: "Oasis Logo"
            })
          ),
          nav(
            ul(
              (() => {
                const inboxCount = sharedState.getInboxCount();
                const badge = inboxCount > 0 ? span({ class: 'inbox-badge' }, String(inboxCount)) : '';
                return li(
                  a({ href: "/inbox" },
                    span({ class: "emoji" }, "☂"), nbsp, i18n.inbox, badge
                  )
                );
              })(),
              navLink({
                href: "/pm",
                emoji: "ꕕ",
                text: i18n.privateMessage
              }),
              navLink({ href: "/publish", emoji: "❂", text: i18n.publish })
            )
          )
        ),
        div(
          { class: "top-bar-right" },
          nav(
            ul(
              renderTagsLink(),
              navLink({ href: "/search", emoji: "ꔅ", text: i18n.searchTitle })
            )
          )
        )
      ),
      (() => {
        const updateFlagPath = path.join(__dirname, '../server/.update_required');
        if (fs.existsSync(updateFlagPath)) {
          return div(
            { class: "update-banner" },
            span({ class: "update-banner-icon" }, "⟳"),
            span({ class: "update-banner-text" }, i18n.updateBannerText),
            a({ href: "/settings", class: "update-banner-link" }, i18n.updateBannerAction)
          );
        }
        return null;
      })(),
      div(
        { class: "main-content" },
        div(
          { class: "sidebar-left" },
          nav(
            ul(
              navGroup(
                {
                  id: "personal",
                  emoji: "⚉",
                  title: i18n.menuPersonal
                },
                navLink({
                  href: "/profile",
                  emoji: "⚉",
                  text: i18n.profile
                }),
                navLink({
                  href: "/cv",
                  emoji: "ꕛ",
                  text: i18n.cvTitle
                }),
                renderAgendaLink(),
                renderFavoritesLink(),
                renderLogsLink(),
                renderWalletLink(),
                navLink({
                  href: "/modules",
                  emoji: "ꗣ",
                  text: i18n.modules
                }),
                navLink({
                  href: "/settings",
                  emoji: "⚙",
                  text: i18n.settings
                })
              ),
              navGroup(
                {
                  id: "content",
                  emoji: "✦",
                  title: i18n.menuContent
                },
                navLink({
                  href: "/mentions",
                  emoji: "✺",
                  text: i18n.mentions
                }),
                renderLatestLink(),
                renderThreadsLink(),
                renderTopicsLink(),
                renderSummariesLink(),
                renderPopularLink(),
                renderMultiverseLink()
              ),
              navGroup(
                {
                  id: "governance",
                  emoji: "⚖",
                  title: i18n.menuGovernance
                },
                navLink({
                  href: "/inhabitants",
                  emoji: "ꖘ",
                  text: i18n.inhabitantsLabel
                }),
                renderTribesLink(),
                renderParliamentLink(),
                renderCourtsLink()
              ),
              navGroup(
                {
                  id: "office",
                  emoji: "⌂",
                  title: i18n.menuOffice
                },
                renderVotationsLink(),
                renderEventsLink(),
                renderCalendarsLink(),
                renderTasksLink(),
                renderReportsLink()
              ),
              navGroup(
                {
                  id: "tools",
                  emoji: "⚒",
                  title: i18n.menuTools
                },
                renderAILink(),
                navLink({
                  href: "/stats",
                  emoji: "ꕷ",
                  text: i18n.statistics
                }),
                navLink({
                  href: "/blockexplorer",
                  emoji: "ꖸ",
                  text: i18n.blockchain
                }),
                renderCipherLink(),
                renderLegacyLink()
              )
            )
          )
        ),
        main({ id: "content", class: "main-column" }, elements),
        div(
          { class: "sidebar-right" },
          nav(
            ul(
              navGroup(
                {
                  id: "network",
                  emoji: "☍",
                  title: i18n.menuNetwork
                },
                navLink({
                  href: "/activity",
                  emoji: "ꔙ",
                  text: i18n.activityTitle
                }),
                renderTrendingLink(),
                renderOpinionsLink(),
                renderPadsLink(),
                renderForumLink(),
                renderMapsLink(),
                renderChatsLink(),
                renderInvitesLink(),
                navLink({
                  href: "/peers",
                  emoji: "⧖",
                  text: i18n.peers
                })
              ),
              navGroup(
                {
                  id: "creative",
                  emoji: "✎",
                  title: i18n.menuCreative
                },
                renderFeedLink(),
                renderGamesLink(),
                renderPixeliaLink()
              ),
              navGroup(
                {
                  id: "economy",
                  emoji: "¤",
                  title: i18n.menuEconomy
                },
                renderBankingLink(),
                renderMarketLink(),
                renderProjectsLink(),
                renderJobsLink(),
                renderShopsLink(),
                renderTransfersLink()
              ),
              navGroup(
                {
                  id: "media",
                  emoji: "▤",
                  title: i18n.menuMedia
                },
                renderAudiosLink(),
                renderBookmarksLink(),
                renderDocsLink(),
                renderImagesLink(),
                renderTorrentsLink(),
                renderVideosLink()
              )
            )
          )
        )
      ),
    renderFooter()
    )
  );
  return doctypeString + nodes.outerHTML;
};
// menu END

exports.template = template;

exports.tribeAccessDeniedView = (tribe) => {
  const tribeName = tribe && !tribe.isAnonymous ? tribe.title : "";
  return template(
    i18n.tribeContentAccessDenied,
    div({ class: "div-center" },
      h2(i18n.tribeContentAccessDenied),
      p(i18n.tribeContentAccessDeniedMsg),
      tribeName ? p({ class: "tribe-access-name" }, tribeName) : null,
      div({ class: "visit-btn-centered" },
        a({ href: "/tribes", class: "filter-btn" }, i18n.tribeViewTribes)
      )
    )
  );
};

exports.inviteRequiredView = (kind, tribe) => {
  const msg = kind === 'pad' ? (i18n.tribePadInviteRequired || 'You do not have access to the pad. Ask for an invitation to access the content.')
            : kind === 'chat' ? (i18n.tribeChatInviteRequired || 'You do not have access to the chat. Ask for an invitation to access the content.')
            : (i18n.tribeContentAccessDeniedMsg);
  const backHref = tribe ? `/tribe/${encodeURIComponent(tribe.id)}?section=${kind === 'chat' ? 'chats' : 'pads'}` : (kind === 'chat' ? '/chats' : '/pads');
  return template(
    i18n.tribeContentAccessDenied,
    div({ class: "div-center" },
      h2(i18n.tribeContentAccessDenied),
      p(msg),
      div({ class: "visit-btn-centered" },
        a({ href: backHref, class: "filter-btn" }, i18n.walletBack || "Back")
      )
    )
  );
};

const thread = (messages) => {
  let lookingForTarget = true;
  let shallowest = Infinity;

  for (let i = messages.length - 1; i >= 0; i--) {
    const msg = messages[i];
    const depth = lodash.get(msg, "value.meta.thread.depth", 0);

    if (lookingForTarget) {
      const isThreadTarget = Boolean(
        lodash.get(msg, "value.meta.thread.target", false)
      );
      if (isThreadTarget) {
        lookingForTarget = false;
      }
    } else {
      if (depth < shallowest) {
        lodash.set(msg, "value.meta.thread.ancestorOfTarget", true);
        shallowest = depth;
      }
    }
  }

  const msgList = [];
  for (let i = 0; i < messages.length; i++) {
    const j = i + 1;
    const currentMsg = messages[i];
    const nextMsg = messages[j];

    const depth = (msg) => {
      if (msg === undefined) return 0;
      return lodash.get(msg, "value.meta.thread.depth", 0);
    };

    msgList.push(post({ msg: currentMsg }));

    if (depth(currentMsg) < depth(nextMsg)) {
      const isAncestor = Boolean(
        lodash.get(currentMsg, "value.meta.thread.ancestorOfTarget", false)
      );
      const isBlocked = Boolean(nextMsg.value.meta.blocking);
      const nextAuthor = lodash.get(nextMsg, "value.meta.author.name") || (typeof nextMsg?.value?.author === "string" ? (nextMsg.value.author.startsWith("@") ? nextMsg.value.author.slice(1) : nextMsg.value.author) : "Anonymous");
      const nextSnippet = postSnippet(
        lodash.has(nextMsg, "value.content.contentWarning")
          ? lodash.get(nextMsg, "value.content.contentWarning")
          : lodash.get(nextMsg, "value.content.text")
      );
      msgList.push(
        details(
          isAncestor ? { open: true } : {},
          summary(
            isBlocked
              ? i18n.relationshipBlockingPost
              : `${nextAuthor}: ${nextSnippet}`
          )
        )
      );
    } else if (depth(currentMsg) > depth(nextMsg)) {
      const diffDepth = depth(currentMsg) - depth(nextMsg);
    }
  }

  return div({ class: "thread-container" }, ...msgList);
};

const postSnippet = (text) => {
  const max = 40;

  text = text.trim().split("\n", 3).join("\n");
  text = text.replace(/_|`|\*|#|^\[@.*?]|\[|]|\(\S*?\)/g, "").trim();
  text = text.replace(/:$/, "");
  text = text.trim().split("\n", 1)[0].trim();

  if (text.length > max) {
    text = text.substring(0, max - 1) + "…";
  }

  return text;
};

const continueThreadComponent = (thread, isComment) => {
  const encoded = {
    next: encodeURIComponent(thread[THREAD_PREVIEW_LENGTH + 1].key),
    parent: encodeURIComponent(thread[0].key),
  };
  const left = thread.length - (THREAD_PREVIEW_LENGTH + 1);
  let continueLink;
  if (isComment == false) {
    continueLink = `/thread/${encoded.parent}#${encoded.next}`;
    return a(
      { href: continueLink },
      i18n.continueReading, ` ${left} `, i18n.moreComments+`${left === 1 ? "" : "s"}`
    );
  } else {
    continueLink = `/thread/${encoded.parent}`;
    return a({ href: continueLink }, i18n.readThread);
  }
};

const postAside = ({ key, value }) => {
  const thread = value.meta.thread;
  if (thread == null) return null;

  const isComment = value.meta.postType === "comment";

  let postsToShow;
  if (isComment) {
    const commentPosition = thread.findIndex((msg) => msg.key === key);
    postsToShow = thread.slice(
      commentPosition + 1,
      Math.min(commentPosition + (THREAD_PREVIEW_LENGTH + 1), thread.length)
    );
  } else {
    postsToShow = thread.slice(
      1,
      Math.min(thread.length, THREAD_PREVIEW_LENGTH + 1)
    );
  }

  const fragments = postsToShow.map((p) => post({ msg: p }));

  if (thread.length > THREAD_PREVIEW_LENGTH + 1) {
    fragments.push(section(continueThreadComponent(thread, isComment)));
  }

  return fragments;
};

const post = ({ msg, aside = false, preview = false }) => {
    const encoded = {
        key: encodeURIComponent(msg.key),
        author: encodeURIComponent(msg.value?.author),
        parent: encodeURIComponent(msg.value?.content?.root),
    };

    const url = {
        author: `/author/${encoded.author}`,
        likeForm: `/like/${encoded.key}`,
        link: `/thread/${encoded.key}#${encoded.key}`,
        parent: `/thread/${encoded.parent}#${encoded.parent}`,
        avatar: msg.value?.meta?.author?.avatar?.url || '/assets/images/default-avatar.png',
        json: `/json/${encoded.key}`,
        subtopic: `/subtopic/${encoded.key}`,
        comment: `/comment/${encoded.key}`,
    };

    const isPrivate = Boolean(msg.value?.meta?.private);
    const isBlocked = Boolean(msg.value?.meta?.blocking);
    const isRoot = msg.value?.content?.root == null;
    const isFork = msg.value?.meta?.postType === "subtopic";
    const hasContentWarning = typeof msg.value?.content?.contentWarning === "string";
    const isThreadTarget = Boolean(lodash.get(msg, "value.meta.thread.target", false));

    const authorIdForName = msg.value?.author; 
    const name =
      msg.value?.meta?.author?.name ||
      (typeof authorIdForName === "string"
        ? (authorIdForName.startsWith("@") ? authorIdForName.slice(1) : authorIdForName)
        : "Anonymous");

    const content = msg.value?.content || {};
    const contentType = String(content.type || "");

    const THREAD_ENTITY_TYPES = new Set([
        'bookmark',
        'image',
        'audio',
        'video',
        'document',
        'votes',
        'event',
        'task',
        'report',
        'market',
        'project',
        'job'
    ]);

    const safeUpper = (s) => String(s || '').toUpperCase();
    const safeStr = (v) => (v == null ? '' : String(v));
    const isMsgId = (s) => typeof s === 'string' && (s.startsWith('%') || s.startsWith('&') || s.startsWith('@'));
    const fmtDate = (v) => {
        if (!v) return '';
        const m = moment(v, moment.ISO_8601, true);
        if (m.isValid()) return m.format('YYYY-MM-DD HH:mm:ss');
        const n = typeof v === 'number' ? v : Date.parse(v);
        if (!Number.isFinite(n)) return '';
        return moment(n).format('YYYY-MM-DD HH:mm:ss');
    };

    const renderField = (labelText, valueNode) => {
        if (valueNode == null || valueNode === '') return null;
        return div(
            { class: 'card-field' },
            span({ class: 'card-label' }, labelText),
            span({ class: 'card-value' }, valueNode)
        );
    };

    const entityTitle = (c) => {
        const t = String(c.type || '').toLowerCase();
        if (t === 'votes') return safeStr(c.question || c.title);
        if (t === 'bookmark') return safeStr(c.title || c.name || c.url);
        if (t === 'market') return safeStr(c.title);
        if (t === 'project') return safeStr(c.title);
        if (t === 'job') return safeStr(c.title);
        if (t === 'report') return safeStr(c.title);
        if (t === 'task') return safeStr(c.title);
        if (t === 'event') return safeStr(c.title);
        if (t === 'document') return safeStr(c.title || c.name || c.url);
        if (t === 'image' || t === 'audio' || t === 'video') return safeStr(c.title || c.name || c.url);
        return safeStr(c.title || c.name || c.question || c.url);
    };

    const renderEntityRoot = (c) => {
        const t = String(c.type || '').toLowerCase();
        const header = `[${safeUpper(t)}]`;
        const titleText = entityTitle(c) || '(sin título)';

        const nodes = [];
        nodes.push(
            div(
                { class: 'card-field card-field-mb' },
                span({ class: 'card-label card-label-bold' }, header),
                span({ class: 'card-value card-value-bold' }, titleText)
            )
        );

        if (t === 'votes') {
            const status = safeStr(c.status);
            const deadline = fmtDate(c.deadline);
            const totalVotes = (typeof c.totalVotes !== 'undefined') ? safeStr(c.totalVotes) : '';
            const tags = Array.isArray(c.tags) ? c.tags.filter(Boolean) : [];

            const f1 = renderField((i18n.status || 'Status') + ':', status ? safeUpper(status) : '');
            const f2 = renderField((i18n.deadline || 'Deadline') + ':', deadline);
            const f3 = renderField((i18n.voteTotalVotes || 'Total votes') + ':', totalVotes);
            if (f1) nodes.push(f1);
            if (f2) nodes.push(f2);
            if (f3) nodes.push(f3);

            if (tags.length) {
                nodes.push(
                    div(
                        { class: 'card-tags card-tags-mt' },
                        ...tags.map(tag =>
                            a(
                                { href: `/search?query=%23${encodeURIComponent(tag)}`, class: 'tag-link' },
                                `#${tag}`
                            )
                        )
                    )
                );
            }
        } else if (t === 'report') {
            const status = safeStr(c.status);
            const severity = safeStr(c.severity);
            const r1 = renderField((i18n.status || 'Status') + ':', status ? safeUpper(status) : '');
            const r2 = renderField((i18n.severity || 'Severity') + ':', severity ? safeUpper(severity) : '');
            if (r1) nodes.push(r1);
            if (r2) nodes.push(r2);
        } else if (t === 'task') {
            const status = safeStr(c.status);
            const priority = safeStr(c.priority);
            const startTime = fmtDate(c.startTime);
            const endTime = fmtDate(c.endTime);

            const r1 = renderField((i18n.status || 'Status') + ':', status ? safeUpper(status) : '');
            const r2 = renderField((i18n.priority || 'Priority') + ':', priority ? safeUpper(priority) : '');
            const r3 = renderField((i18n.taskStartTimeLabel || 'Start') + ':', startTime);
            const r4 = renderField((i18n.taskEndTimeLabel || 'End') + ':', endTime);

            if (r1) nodes.push(r1);
            if (r2) nodes.push(r2);
            if (r3) nodes.push(r3);
            if (r4) nodes.push(r4);
        } else if (t === 'event') {
            const dateStr = fmtDate(c.date);
            const location = safeStr(c.location);
            const price = (typeof c.price !== 'undefined') ? safeStr(c.price) : '';

            const r1 = renderField((i18n.date || 'Date') + ':', dateStr);
            const r2 = renderField((i18n.location || 'Location') + ':', location);
            const r3 = renderField((i18n.price || 'Price') + ':', price ? `${price} ECO` : '');

            if (r1) nodes.push(r1);
            if (r2) nodes.push(r2);
            if (r3) nodes.push(r3);
        } else if (t === 'bookmark') {
            const u = safeStr(c.url);
            if (u) {
                nodes.push(
                    renderField((i18n.url || 'URL') + ':', a({ href: u, target: '_blank', rel: 'noopener noreferrer' }, u))
                );
            }
        } else if (t === 'image') {
            const u = safeStr(c.url);
            if (u && isMsgId(u)) {
                nodes.push(
                    div({ class: 'card-field card-field-mt' },
                        img({ src: `/blob/${encodeURIComponent(u)}`, class: 'feed-image img-content' })
                    )
                );
            }
        } else if (t === 'audio') {
            const u = safeStr(c.url);
            if (u && isMsgId(u)) {
                nodes.push(
                    div({ class: 'card-field card-field-mt' },
                        audioHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(u)}` })
                    )
                );
            }
        } else if (t === 'video') {
            const u = safeStr(c.url);
            if (u && isMsgId(u)) {
                nodes.push(
                    div({ class: 'card-field card-field-mt' },
                        videoHyperaxe({ controls: true, src: `/blob/${encodeURIComponent(u)}` })
                    )
                );
            }
	} else if (t === 'document') {
	  const u = safeStr(c.url);
	  if (u && isMsgId(u)) {
	    const safeId = String(msg.key || u).replace(/[^a-zA-Z0-9_-]/g, '');
	    nodes.push(
	      div({ class: 'card-field card-field-mt' },
		div({
		  id: `pdf-container-${safeId}`,
		  class: 'pdf-viewer-container',
		  'data-pdf-url': `/blob/${encodeURIComponent(u)}`
		})
	      )
	    );
	  }

        } else if (t === 'market') {
            const status = safeStr(c.status);
            const price = (typeof c.price !== 'undefined') ? safeStr(c.price) : '';
            const r1 = renderField((i18n.status || 'Status') + ':', status ? safeUpper(status) : '');
            const r2 = renderField((i18n.price || 'Price') + ':', price ? `${price} ECO` : '');
            if (r1) nodes.push(r1);
            if (r2) nodes.push(r2);
        } else if (t === 'project') {
            const status = safeStr(c.status);
            const r1 = renderField((i18n.status || 'Status') + ':', status ? safeUpper(status) : '');
            if (r1) nodes.push(r1);
        } else if (t === 'job') {
            const status = safeStr(c.status);
            const location = safeStr(c.location);
            const salary = (typeof c.salary !== 'undefined') ? safeStr(c.salary) : '';

            const r1 = renderField((i18n.status || 'Status') + ':', status ? safeUpper(status) : '');
            const r2 = renderField((i18n.jobLocation || 'Location') + ':', location ? safeUpper(location) : '');
            const r3 = renderField((i18n.jobSalary || 'Salary') + ':', salary ? `${salary} ECO` : '');

            if (r1) nodes.push(r1);
            if (r2) nodes.push(r2);
            if (r3) nodes.push(r3);
        }

        return article({ class: 'content' }, ...nodes.filter(Boolean));
    };

    const rawText = content.text || "";
    const emptyContent = "<p>undefined</p>\n";

    const isProbablyHtml =
        typeof rawText === "string" &&
        /<\/?[a-z][\s\S]*>/i.test(rawText.trim());

    let articleElement;

    if (contentType !== 'post' && contentType !== 'blog' && THREAD_ENTITY_TYPES.has(contentType)) {
        articleElement = renderEntityRoot(content);
    } else if (rawText === emptyContent) {
        articleElement = article(
            { class: "content" },
            div(
                { class: "card-field card-field-mb" },
                span({ class: "card-label" }, (i18n.invalidPost || 'Invalid content') + ':'),
                span({ class: "card-value" }, (i18n.invalidPostHint || 'This message has invalid/empty text.'))
            ),
            details(
                summary(i18n.viewJson || 'View JSON'),
                pre({
                    innerHTML: highlightJs.highlight(
                        JSON.stringify(msg, null, 2),
                        { language: "json", ignoreIllegals: true }
                    ).value,
                })
            )
        );
    } else if (isProbablyHtml) {
        let html = rawText;
        if (!/<a\b[^>]*>/i.test(html)) {
            html = html.replace(
                /(https?:\/\/[^\s<]+)/g,
                (u) => `<a href="${u}" target="_blank" rel="noopener noreferrer">${u}</a>`
            );
        }
        articleElement = article({ class: "content", innerHTML: sanitizeHtml(html) });
    } else {
        articleElement = article(
            { class: "content" },
            p({ class: "post-text" }, ...renderUrl(rawText))
        );
    }

    if (preview) {
        return section(
            { id: msg.key, class: "post-preview" },
            hasContentWarning
                ? details(summary(msg.value?.content?.contentWarning), articleElement)
                : articleElement
        );
    }

    const ts_received = msg.value?.meta?.timestamp?.received;
    const iso =
        (ts_received && ts_received.iso8601) ||
        (typeof msg.value?.timestamp === 'number' ? new Date(msg.value.timestamp).toISOString() : null) ||
        (content.createdAt ? new Date(content.createdAt).toISOString() : null);

    if (!iso || !moment(iso, moment.ISO_8601, true).isValid()) {
        return null;
    }

    const validTimestamp = moment(iso, moment.ISO_8601);
    const timeAgo = validTimestamp.fromNow();
    const timeAbsolute = validTimestamp.toISOString().split(".")[0].replace("T", " ");

    const likeButton = msg.value?.meta?.voted
        ? { value: 0, class: "liked" }
        : { value: 1, class: null };

    const likeCount = msg.value?.meta?.votes?.length || 0;
    const maxLikedNameLength = 16;
    const maxLikedNames = 16;

    const likedByNames = msg.value?.meta?.votes
        .slice(0, maxLikedNames)
        .map((person) => person.name)
        .map((n) => n.slice(0, maxLikedNameLength))
        .join(", ");

    const additionalLikesMessage =
        likeCount > maxLikedNames ? `+${likeCount - maxLikedNames} more` : ``;

    const likedByMessage =
        likeCount > 0 ? `${likedByNames} ${additionalLikesMessage}` : null;

    const messageClasses = ["post"];
    const recps = [];

    const addRecps = (recpsInfo) => {
        recpsInfo.forEach((recp) => {
            recps.push(
                a(
                    { href: `/author/${encodeURIComponent(recp.feedId)}` },
                    img({ class: "avatar", src: recp.avatarUrl, alt: "" })
                )
            );
        });
    };

    if (isPrivate) {
        messageClasses.push("private");
        addRecps(msg.value?.meta?.recpsInfo || []);
    }

    if (isThreadTarget) {
        messageClasses.push("thread-target");
    }

    if (isBlocked) {
        messageClasses.push("blocked");
        return section(
            {
                id: msg.key,
                class: messageClasses.join(" "),
            },
            i18n.relationshipBlockingPost
        );
    }

    const articleContent = article(
        { class: "content" },
        hasContentWarning ? div({ class: "post-subject" }, msg.value?.content?.contentWarning) : null,
        articleElement
    );

    const fragment = section(
        {
            id: msg.key,
            class: messageClasses.join(" "),
        },
        header(
            div(
                { class: "header-content" },
                a(
                    { href: url.author },
                    img({ class: "avatar-profile", src: url.avatar, alt: "" })
                ),
                span(
                    { class: "created-at" },
                    `${i18n.createdBy} `,
                    a({ href: url.author }, "@", name),
                    ` | ${timeAbsolute} | ${i18n.sendTime} `,
                    a({ href: url.link }, timeAgo)
                ),
                isPrivate ? "🔒" : null,
                isPrivate ? recps : null
            )
        ),
        articleContent,
        footer(
            div(
                form(
                    { action: url.likeForm, method: "post" },
                    button(
                        {
                            name: "voteValue",
                            type: "submit",
                            value: likeButton.value,
                            class: likeButton.class,
                            title: likedByMessage,
                        },
                        `☉ ${likeCount}`
                    )
                ),
                a({ href: url.comment }, i18n.comment),
                isPrivate || isRoot || isFork
                    ? null
                    : a({ href: url.subtopic }, nbsp, i18n.subtopic)
            ),
            br()
        )
    );

    const threadSeparator = [br()];

    if (aside) {
        return [fragment, postAside(msg), isRoot ? threadSeparator : null];
    } else {
        return fragment;
    }
};

exports.editProfileView = ({ name, description }) =>
  template(
    i18n.editProfile,
    section(
      h1(i18n.editProfile),
      p(i18n.editProfileDescription),
      form(
        {
          action: "/profile/edit",
          method: "POST",
          enctype: "multipart/form-data",
        },
        label(
          i18n.profileImage,
          br(),
          input({ type: "file", name: "image", accept: "image/*" })
        ),
        br(),br(),
        label(i18n.profileName,
        br(),
        input({ name: "name", value: name })),
        br(),br(),
        label(
          i18n.profileDescription,
          br(),
          textarea(
            {
              autofocus: true,
              name: "description",
              rows: "6",
            },
            description
          )
        ),
        br(),
        button(
          {
            type: "submit",
          },
          i18n.submit
        )
      )
    )
  );

exports.authorView = ({
  avatarUrl,
  description,
  feedId,
  messages,
  firstPost,
  lastPost,
  name,
  relationship,
  ecoAddress,
  karmaScore = 0,
  estimatedUBI = 0,
  lastClaimedDate = null,
  totalClaimed = 0,
  lastActivityBucket
}) => {
  const linkUrl = `/author/${encodeURIComponent(feedId)}`;

  const mention = `[@${name}](${feedId})`;
  const markdownMention = highlightJs.highlight(mention, { language: "markdown", ignoreIllegals: true }).value;

  const contactForms = [];
  const addForm = ({ action }) =>
    contactForms.push(
      form(
        { action: `/${action}/${encodeURIComponent(feedId)}`, method: "post" },
        button({ type: "submit" }, i18n[action])
      )
    );

  if (relationship.me === false) {
    if (relationship.following) addForm({ action: "unfollow" });
    else if (relationship.blocking) addForm({ action: "unblock" });
    else { addForm({ action: "follow" }); addForm({ action: "block" }) }
  }

  const relationshipMessage = (() => {
    if (relationship.me) return i18n.relationshipYou;
    const following = relationship.following === true;
    const followsMe = relationship.followsMe === true;
    if (following && followsMe) return i18n.relationshipMutuals;
    const messagesArr = [];
    messagesArr.push(following ? i18n.relationshipFollowing : i18n.relationshipNone);
    messagesArr.push(followsMe ? i18n.relationshipTheyFollow : i18n.relationshipNotFollowing);
    return messagesArr.join(". ") + ".";
  })();

  const bucket = lastActivityBucket || 'red';

  const { lastActivityBadge } = require('./inhabitants_view');

  const prefix = section(
    { class: "message" },
    div(
      { class: "profile" },
      div({ class: "avatar-container" },
        img({ class: "inhabitant-photo-details", src: avatarUrl }),
        h1({ class: "name" }, name),
      ),
      pre({ class: "md-mention", innerHTML: sanitizeHtml(markdownMention) }),
      p(a({ class: "user-link", href: `/author/${encodeURIComponent(feedId)}` }, feedId)),
      div({ class: "profile-metrics" },
        ...lastActivityBadge({ lastActivityBucket: bucket }, true),
        div({ class: "inhabitant-karma-ubi" },
          span({ class: "karma-line" }, `${i18n.bankingUserEngagementScore}: `, strong(karmaScore !== undefined ? karmaScore : 0)),
          span({ class: "ubi-line" }, `${i18n.bankUbiThisMonth}: `, strong(`${Number(estimatedUBI || 0).toFixed(6)} ECO`)),
          span({ class: "ubi-line" }, `${i18n.bankUbiLastClaimed}: `,
            lastClaimedDate
              ? a({ href: "/transfers?filter=ubi", class: "user-link" }, new Date(lastClaimedDate).toLocaleDateString())
              : strong(i18n.bankUbiNeverClaimed)
          ),
          span({ class: "ubi-line" }, `${i18n.bankUbiTotalClaimed}: `, strong(`${Number(totalClaimed || 0).toFixed(6)} ECO`))
        ),
        (ecoAddress || relationship.me)
          ? div({ class: "eco-wallet" },
              p(`${i18n.statsEcoWalletLabel || 'ECOin Wallet'}: `,
                a({ href: '/wallet' }, ecoAddress || i18n.statsEcoWalletNotConfigured || 'Not configured!')))
          : null
      )
    ),
    description !== "" ? article({ innerHTML: sanitizeHtml(markdown(description)) }) : null,
    footer(
      div(
        { class: "profile" },
        ...contactForms.map(form => span({ class: "contact-bold" }, form)),
        relationship.me
          ? span({ class: "status you" }, i18n.relationshipYou)
          : div({ class: "relationship-status" },
              relationship.blocking && relationship.blockedBy
                ? span({ class: "status blocked" }, i18n.relationshipMutualBlock)
                : [
                    relationship.blocking ? span({ class: "status blocked" }, i18n.relationshipBlocking) : null,
                    relationship.blockedBy ? span({ class: "status blocked-by" }, i18n.relationshipBlockedBy) : null,
                    relationship.following && relationship.followsMe
                      ? span({ class: "status mutual" }, i18n.relationshipMutuals)
                      : [
                          span({ class: "status supporting" }, relationship.following ? i18n.relationshipFollowing : i18n.relationshipNone),
                          span({ class: "status supported-by" }, relationship.followsMe ? i18n.relationshipTheyFollow : i18n.relationshipNotFollowing)
                        ]
                  ]
            ),
        relationship.me ? a({ href: `/profile/edit`, class: "btn" }, nbsp, i18n.editProfile) : null,
        a({ href: `/likes/${encodeURIComponent(feedId)}`, class: "btn" }, i18n.viewLikes),
        !relationship.me ? a({ href: `/pm?recipients=${encodeURIComponent(feedId)}`, class: "btn" }, i18n.pmCreateButton) : null
      )
    )
  );

  let items = messages.map((msg) => post({ msg }));
  if (items.length === 0) {
    if (lastPost === undefined) {
      items.push(section(div(span(i18n.feedEmpty))));
    } else {
      items.push(
        section(
          div(
            span(i18n.feedRangeEmpty),
            a({ href: `${linkUrl}` }, i18n.seeFullFeed)
          )
        )
      );
    }
  } else {
    const highestSeqNum = messages[0].value.sequence;
    const lowestSeqNum = messages[messages.length - 1].value.sequence;

    const newerPostsLink = a(
      {
        href:
          lastPost !== undefined && highestSeqNum < lastPost.value.sequence
            ? `${linkUrl}?gt=${highestSeqNum}`
            : "#",
        class:
          lastPost !== undefined && highestSeqNum < lastPost.value.sequence
            ? "btn"
            : "btn disabled",
        "aria-disabled":
          lastPost === undefined || highestSeqNum >= lastPost.value.sequence
      },
      i18n.newerPosts
    );

    const olderPostsLink = a(
      {
        href:
          lowestSeqNum > firstPost.value.sequence
            ? `${linkUrl}?lt=${lowestSeqNum}`
            : "#",
        class:
          lowestSeqNum > firstPost.value.sequence
            ? "btn"
            : "btn disabled",
        "aria-disabled": !(lowestSeqNum > firstPost.value.sequence)
      },
      i18n.olderPosts
    );

    const pagination = section(
      { class: "message" },
      footer(div(newerPostsLink, olderPostsLink), br())
    );

    items.unshift(pagination);
    items.push(pagination);
  }

  return template(i18n.profile, prefix, items);
};

exports.previewCommentView = async ({
  previewData,
  messages,
  myFeedId,
  parentMessage,
  contentWarning,
}) => {
  if (!parentMessage || !parentMessage.value) {
    throw new Error("Missing parentMessage or value");
  }

  const publishAction = `/comment/${encodeURIComponent(parentMessage.key)}`;
  const preview = generatePreview({
    previewData,
    contentWarning,
    action: publishAction,
  });
  return exports.commentView(
    { messages, myFeedId, parentMessage },
    preview, 
    previewData.text,
    contentWarning
  );
};

exports.commentView = async (
  { messages, myFeedId, parentMessage },
  preview,
  text,
  contentWarning
) => {
  if (!parentMessage || !parentMessage.value) {
    throw new Error("Missing parentMessage or value");
  }

  const parentKey = parentMessage.key;
  const threadRoot = parentMessage.value?.content?.root || parentKey;

  const messagesInput = Array.isArray(messages) ? messages : [];
  const merged = [parentMessage, ...messagesInput];

  const filtered = merged.filter((m) => {
    if (!m || !m.value) return false;
    return m.key === threadRoot || m.value?.content?.root === threadRoot;
  });

  const seen = new Set();
  const threadMessages = [];
  for (const m of filtered) {
    if (m && m.key && !seen.has(m.key)) {
      seen.add(m.key);
      threadMessages.push(m);
    }
  }

  const tsNum = (m) => {
    const n1 = Number(m?.value?.timestamp);
    if (Number.isFinite(n1) && n1 > 0) return n1;
    const iso = m?.value?.meta?.timestamp?.received?.iso8601;
    const raw = m?.value?.meta?.timestamp?.received;
    const n2 = iso ? Date.parse(iso) : (raw ? Date.parse(raw) : NaN);
    if (Number.isFinite(n2) && n2 > 0) return n2;
    const createdAt = m?.value?.content?.createdAt;
    const n3 = createdAt ? Date.parse(createdAt) : NaN;
    if (Number.isFinite(n3) && n3 > 0) return n3;
    return 0;
  };

  threadMessages.sort((a, b) => tsNum(a) - tsNum(b));

  const authorName = parentMessage.value?.meta?.author?.name || parentMessage.value?.author || "Anonymous";

  let markdownMention = "";
  const parentAuthorFeedId = parentMessage.value?.author;
  const parentAuthorName =
    parentMessage.value?.meta?.author?.name ||
    (typeof parentAuthorFeedId === "string"
      ? (parentAuthorFeedId.startsWith("@") ? parentAuthorFeedId.slice(1) : parentAuthorFeedId)
      : "Anonymous");

  if (parentAuthorFeedId && parentAuthorFeedId !== myFeedId) {
    markdownMention = `[@${parentAuthorName}](${parentAuthorFeedId})\n\n`;
  }

  const messageElements = threadMessages.map((m) => post({ msg: m }));

  const action = `/comment/preview/${encodeURIComponent(parentKey)}`;
  const method = "post";
  const isPrivate = Boolean(parentMessage.value?.meta?.private);

  return template(
    i18n.commentTitle({ authorName }),
    div({ class: "thread-container" }, ...messageElements),
    form(
      { action, method, enctype: "multipart/form-data" },
      i18n.blogSubject,
      br(),
      label(
        i18n.contentWarningLabel,
        input({
          name: "contentWarning",
          type: "text",
          class: "contentWarning",
          value: contentWarning ? contentWarning : "",
          placeholder: i18n.contentWarningPlaceholder
        })
      ),
      br(),
      label({ for: "text" }, i18n.blogMessage),
      br(),
	textarea(
	  {
	    autofocus: true,
	    required: true,
	    name: "text",
	    rows: "6",
	    cols: "50",
	    placeholder: i18n.publishWarningPlaceholder
	  },
	  text ? text : null
	),
      br(),
      label(
        { for: "blob" },
        i18n.blogImage || "Upload media (max-size: 50MB)"
      ),
      input({ type: "file", id: "blob", name: "blob" }),
      br(),
      br(),
      button({ type: "submit" }, i18n.blogPublish)
    ),
    preview ? div({ class: "comment-preview" }, preview) : ""
  );
};

const renderMessage = (msg) => {
  const content = lodash.get(msg, "value.content", {});
  const authorId = msg.value.author || "Anonymous";
  const authorName = lodash.get(msg, "value.meta.author.name") || authorId.slice(0, 10) + '...';
  const createdAt = new Date(msg.value.timestamp).toLocaleString();
  const mentionsText = content.text || '';
  const isTribe = content.type === 'tribe-content';
  const visitUrl = isTribe
    ? `/tribe/${encodeURIComponent(content.tribeId)}`
    : content.root
      ? `/thread/${encodeURIComponent(content.root)}#${encodeURIComponent(msg.key)}`
      : msg.key
        ? `/thread/${encodeURIComponent(msg.key)}#${encodeURIComponent(msg.key)}`
        : null;
  const badge = isTribe && content.tribeName
    ? span({ class: 'tribe-badge' }, content.tribeName)
    : null;

  return div({ class: "mention-item" },
    div({ class: "mention-content" },
      badge,
      ...renderUrl(mentionsText || '[No content]')
    ),
    p(a({ class: 'user-link', href: `/author/${encodeURIComponent(authorId)}` }, authorName)),
    p(`${i18n.createdAtLabel || 'Created at'}: ${createdAt}`),
    visitUrl
      ? form({ method: 'GET', action: visitUrl },
          button({ type: 'submit', class: 'filter-btn' }, i18n.visitContent || 'Visit')
        )
      : null
  );
};

const hasMention = (msg, feedId) => {
  const content = lodash.get(msg, "value.content", {});
  const mentions = content.mentions;
  if (mentions) {
    if (Array.isArray(mentions)) {
      if (mentions.some(m => m.link === feedId || m.feed === feedId)) return true;
    } else if (typeof mentions === 'object') {
      for (const arr of Object.values(mentions)) {
        if (Array.isArray(arr) && arr.some(m => m.link === feedId || m.feed === feedId)) return true;
        if (arr && (arr.link === feedId || arr.feed === feedId)) return true;
      }
    }
  }
  const text = content.text || '';
  if (text.includes(feedId) || text.includes(feedId.slice(1))) return true;
  return false;
};

exports.mentionsView = ({ messages, myFeedId }) => {
  const title = i18n.mentions;
  const description = i18n.mentionsDescription;
  if (!Array.isArray(messages) || messages.length === 0) {
    return template(
      title,
      section(
        div({ class: "tags-header" },
          h2(title),
          p(description)
        )
      ),
      section(
        div({ class: "mentions-list" },
          p({ class: "empty" }, i18n.noMentions)
        )
      )
    );
  }
  const filteredMessages = messages
    .filter(msg => hasMention(msg, myFeedId))
    .sort((a, b) => (b.value.timestamp || 0) - (a.value.timestamp || 0));
  if (filteredMessages.length === 0) {
    return template(
      title,
      section(
        div({ class: "tags-header" },
          h2(title),
          p(description)
        )
      ),
      section(
        div({ class: "mentions-list" },
          p({ class: "empty" }, i18n.noMentions)
        )
      )
    );
  }
  return template(
    title,
    section(
      div({ class: "tags-header" },
        h2(title),
        p(description)
      )
    ),
    section(
      div({ class: "mentions-list" },
        filteredMessages.map(renderMessage) 
      )
    )
  );
};

exports.privateView = async (messagesInput, filter) => {
  const messagesRaw = Array.isArray(messagesInput) ? messagesInput : messagesInput.messages
  const messages = (messagesRaw || []).filter(m => m && m.key && m.value && m.value.content && m.value.content.type === 'post' && m.value.content.private === true)
  const userId = await getUserId()

  const isSent = m => (m?.value?.author === userId) || (m?.value?.content?.from === userId)
  const isToUser = m => Array.isArray(m?.value?.content?.to) && m.value.content.to.includes(userId)

  const linkAuthor = (id) =>
    a({ class: 'user-link', href: `/author/${encodeURIComponent(id)}` }, id)

  const hrefFor = {
    job: (id) => `/jobs/${encodeURIComponent(id)}`,
    project: (id) => `/projects/${encodeURIComponent(id)}`,
    market: (id) => `/market/${encodeURIComponent(id)}`
  }

  const clickableCardProps = (href, extraClass = '') => {
    const props = { class: `pm-card ${extraClass}` }
    if (href) {
      props.onclick = `window.location='${href}'`
      props.tabindex = 0
      props.onkeypress = `if(event.key==='Enter') window.location='${href}'`
    }
    return props
  }

  const chip = (txt) => span({ class: 'chip' }, txt)

  function headerLine({ sentAt, from, toLinks, subject }) {
    return table({ class: 'pm-info-table' },
      tr(
        td({ class: 'card-label' }, i18n.pmFromLabel || 'From:'),
        td({ class: 'card-value' }, linkAuthor(from))
      ),
      tr(
        td({ class: 'card-label' }, i18n.privateDate || 'Date'),
        td({ class: 'card-value' }, moment(sentAt).format('YYYY/MM/DD HH:mm:ss'))
      ),
      tr(
        td({ class: 'card-label' }, i18n.pmToLabel || 'To:'),
        td({ class: 'card-value' }, ...toLinks.reduce((acc, lnk, i) => i > 0 ? [...acc, br(), lnk] : [lnk], []))
      ),
      tr(
        td({ class: 'card-label' }, i18n.pmSubjectLabel || 'Subject:'),
        td({ class: 'card-value' }, subject || i18n.pmNoSubject || '(no subject)')
      )
    )
  }

  function actions({ key, replyId, subjectRaw, text }) {
    const stop = { onclick: 'event.stopPropagation()' }
    const subjectReply = /^(\s*RE:\s*)/i.test(subjectRaw || '') ? (subjectRaw || '') : `RE: ${subjectRaw || ''}`
    return div({ class: 'pm-actions' },
      form({ method: 'GET', action: '/pm', class: 'pm-action-form', ...stop },
        input({ type: 'hidden', name: 'recipients', value: replyId }),
        input({ type: 'hidden', name: 'subject', value: subjectReply }),
        input({ type: 'hidden', name: 'quote', value: text || '' }),
        button({ type: 'submit', class: 'pm-btn reply-btn' }, i18n.pmReply.toUpperCase())
      ),
      form({ method: 'POST', action: `/inbox/delete/${encodeURIComponent(key)}`, class: 'pm-action-form', ...stop },
        button({ type: 'submit', class: 'pm-btn delete-btn' }, i18n.privateDelete.toUpperCase())
      )
    )
  }

  function canonicalSubject(s) {
    return (s || '').replace(/^\s*(RE:\s*)+/i, '').trim()
  }

  function participantsKey(m) {
    const c = m?.value?.content || {}
    const set = new Set([m?.value?.author, ...(Array.isArray(c.to) ? c.to : [])])
    return Array.from(set).sort().join('|')
  }

  function threadId(m) {
    return canonicalSubject(m?.value?.content?.subject || '') + '||' + participantsKey(m)
  }

  function threadLevel(s) {
    const m = (s || '').match(/RE:/gi)
    return m ? Math.min(m.length, 8) : 0
  }

  function quoted(str) {
    const m = str.match(/"([^"]+)"/)
    return m ? m[1] : ''
  }

  function pickLink(str, kind) {
    if (kind === 'job') {
      const m = str.match(/\/jobs\/([%A-Za-z0-9/+._=-]+\.sha256)/)
      return m ? m[1] : ''
    }
    if (kind === 'project') {
      const m = str.match(/\/projects\/([%A-Za-z0-9/+._=-]+\.sha256)/)
      return m ? m[1] : ''
    }
    if (kind === 'market') {
      const m = str.match(/\/market\/([%A-Za-z0-9/+._=-]+\.sha256)/)
      return m ? m[1] : ''
    }
    return ''
  }

  function clickableLinks(str) {
    const lines = str.split('\n')
    const parts = []
    let quoteBuffer = []
    const flushQuote = () => {
      if (quoteBuffer.length) {
        parts.push(`<div class="pm-quote">${quoteBuffer.join('<br>')}</div>`)
        quoteBuffer = []
      }
    }
    for (const line of lines) {
      if (/^>\s?/.test(line)) {
        quoteBuffer.push(line.replace(/^>\s?/, ''))
      } else {
        flushQuote()
        parts.push(line)
      }
    }
    flushQuote()
    return parts.join('<br>')
      .replace(/(@[a-zA-Z0-9/+._=-]+\.ed25519)/g, (match, id) => `<a class="user-link" href="/author/${encodeURIComponent(id)}">${match}</a>`)
      .replace(/\/jobs\/([%A-Za-z0-9/+._=-]+\.sha256)/g, (match, id) => `<a class="job-link" href="${hrefFor.job(id)}">${match}</a>`)
      .replace(/\/projects\/([%A-Za-z0-9/+._=-]+\.sha256)/g, (match, id) => `<a class="project-link" href="${hrefFor.project(id)}">${match}</a>`)
      .replace(/\/market\/([%A-Za-z0-9/+._=-]+\.sha256)/g, (match, id) => `<a class="market-link" href="${hrefFor.market(id)}">${match}</a>`)
      .replace(/\/calendars\/([%A-Za-z0-9/+._=-]+\.sha256)/g, (match, id) => `<a class="calendar-link" href="/calendars/${encodeURIComponent(id)}">${match}</a>`)
      .replace(/(https?:\/\/[^\s<"]+)/g, (match) => `<a href="${match}" target="_blank" rel="noopener noreferrer">${match}</a>`)
  }

  const threads = {}
  for (const m of messages) {
    const tid = threadId(m)
    if (!threads[tid]) threads[tid] = []
    threads[tid].push(m)
  }

  const inboxSet = new Set()
  for (const arr of Object.values(threads)) {
    const hasInbound = arr.some(isToUser)
    if (hasInbound) for (const m of arr) inboxSet.add(m)
  }

  const data =
    filter === 'sent' ? messages.filter(isSent) :
    filter === 'inbox' ? Array.from(inboxSet) :
    messages

  const inboxCount = Array.from(inboxSet).length
  const sentCount = messages.filter(isSent).length

  const sorted = [...data].sort((a, b) => {
    const ta = threadId(a)
    const tb = threadId(b)
    if (ta < tb) return -1
    if (ta > tb) return 1
    const sa = new Date(a?.value?.content?.sentAt || a.timestamp || 0).getTime()
    const sb = new Date(b?.value?.content?.sentAt || b.timestamp || 0).getTime()
    return sa - sb
  })

  function JobCard({ type, sentAt, from, toLinks, text, key }) {
    const isSub = type === 'JOB_SUBSCRIBED'
    const icon = isSub ? '🟡' : '🟠'
    const titleH = isSub ? (i18n.inboxJobSubscribedTitle || 'New subscription to your job offer') : (i18n.inboxJobUnsubscribedTitle || 'Unsubscription from your job offer')
    const jobTitle = quoted(text) || 'job'
    const jobId = pickLink(text, 'job')
    const href = jobId ? hrefFor.job(jobId) : null
    return div(
      clickableCardProps(href, `job-notification thread-level-0`),
      headerLine({ sentAt, from, toLinks, subject: type }),
      h2({ class: 'pm-title' }, `${icon} ${i18n.pmBotJobs} · ${titleH}`),
      p(
        i18n.pmInhabitantWithId, ' ',
        linkAuthor(from), ' ',
        isSub ? i18n.pmHasSubscribedToYourJobOffer : (i18n.pmHasUnsubscribedFromYourJobOffer || 'has unsubscribed from your job offer'),
        ' ',
        href ? a({ class: 'job-link', href }, `"${jobTitle}"`) : `"${jobTitle}"`
      ),
      actions({ key, replyId: from, subjectRaw: jobTitle, text })
    )
  }

  function ProjectFollowCard({ type, sentAt, from, toLinks, text, key }) {
    const isFollow = type === 'PROJECT_FOLLOWED'
    const icon = isFollow ? '🔔' : '🔕'
    const titleH = isFollow
      ? (i18n.inboxProjectFollowedTitle || 'New follower of your project')
      : (i18n.inboxProjectUnfollowedTitle || 'Unfollowed your project')
    const projectTitle = quoted(text) || 'project'
    const projectId = pickLink(text, 'project')
    const href = projectId ? hrefFor.project(projectId) : null
    return div(
      clickableCardProps(href, `project-${isFollow ? 'follow' : 'unfollow'}-notification thread-level-0`),
      headerLine({ sentAt, from, toLinks, subject: type }),
      h2({ class: 'pm-title' }, `${icon} ${i18n.pmBotProjects} · ${titleH}`),
      p(
        i18n.pmInhabitantWithId, ' ',
        a({ class: 'user-link', href: `/author/${encodeURIComponent(from)}` }, from),
        ' ',
        isFollow ? (i18n.pmHasFollowedYourProject || 'has followed your project') : (i18n.pmHasUnfollowedYourProject || 'has unfollowed your project'),
        ' ',
        href ? a({ class: 'project-link', href }, `"${projectTitle}"`) : `"${projectTitle}"`
      ),
      actions({ key, replyId: from, subjectRaw: projectTitle, text })
    )
  }

  function MarketSoldCard({ sentAt, from, toLinks, subject, text, key }) {
    const itemTitle = quoted(subject) || quoted(text) || 'item'
    const buyerId = (text.match(/OASIS ID:\s*([\w=/+.-]+)/) || [])[1] || from
    const price = (text.match(/for:\s*\$([\d.]+)/) || [])[1] || ''
    const marketId = pickLink(text, 'market')
    const href = marketId ? hrefFor.market(marketId) : null
    return div(
      clickableCardProps(href, 'market-sold-notification thread-level-0'),
      headerLine({ sentAt, from, toLinks, subject }),
      h2({ class: 'pm-title' }, `💰 ${i18n.pmBotMarket} · ${i18n.inboxMarketItemSoldTitle}`),
      p(
        i18n.pmYourItem, ' ',
        href ? a({ class: 'market-link', href }, `"${itemTitle}"`) : `"${itemTitle}"`,
        ' ',
        i18n.pmHasBeenSoldTo, ' ',
        linkAuthor(buyerId),
        price ? ` ${i18n.pmFor} $${price}.` : '.'
      ),
      actions({ key, replyId: buyerId, subjectRaw: itemTitle, text })
    )
  }

  function ProjectPledgeCard({ sentAt, from, toLinks, content, text, key }) {
    const amount = content.meta?.amount ?? (text.match(/pledged\s+([\d.]+)/)?.[1] || '0')
    const projectTitle = content.meta?.projectTitle ?? (text.match(/project\s+"([^"]+)"/)?.[1] || 'project')
    const projectId = content.meta?.projectId ?? pickLink(text, 'project')
    const href = projectId ? hrefFor.project(projectId) : null
    return div(
      clickableCardProps(href, 'project-pledge-notification thread-level-0'),
      headerLine({ sentAt, from, toLinks, subject: 'PROJECT_PLEDGE' }),
      h2({ class: 'pm-title' }, `💚 ${i18n.pmBotProjects} · ${i18n.inboxProjectPledgedTitle}`),
      p(
        i18n.pmInhabitantWithId, ' ',
        linkAuthor(from), ' ',
        i18n.pmHasPledged, ' ',
        chip(`${amount} ECO`), ' ',
        i18n.pmToYourProject, ' ',
        href ? a({ class: 'project-link', href }, `"${projectTitle}"`) : `"${projectTitle}"`
      ),
      actions({ key, replyId: from, subjectRaw: projectTitle, text })
    )
  }

  return template(
    i18n.private,
    section(
      div({ class: 'tags-header' },
        h2(i18n.private),
        p(i18n.privateDescription)
      ),
      div({ class: 'filters' },
        form({ method: 'GET', action: '/inbox' }, [
          button({
            type: 'submit',
            name: 'filter',
            value: 'inbox',
            class: filter === 'inbox' ? 'filter-btn active' : 'filter-btn'
          }, `${i18n.privateInbox} (${inboxCount})`),
          button({
            type: 'submit',
            name: 'filter',
            value: 'sent',
            class: filter === 'sent' ? 'filter-btn active' : 'filter-btn'
          }, `${i18n.privateSent} (${sentCount})`),
          button({
            type: 'submit',
            name: 'filter',
            value: 'create',
            class: 'create-button',
            formaction: '/pm',
            formmethod: 'GET'
          }, i18n.pmCreateButton)
        ])
      ),
      div({ class: 'message-list' },
        (() => {
          function renderMsg(msg) {
            const content = msg.value.content
            const author = msg.value.author
            const subjectRaw = content.subject || ''
            const subjectU = subjectRaw.toUpperCase()
            const text = content.text || ''
            const sentAt = new Date(content.sentAt || msg.timestamp)
            const fromResolved = content.from || author
            const toLinks = Array.isArray(content.to) ? content.to.map(addr => linkAuthor(addr)) : []
            const level = threadLevel(subjectRaw)

            if (subjectU === 'JOB_SUBSCRIBED' || subjectU === 'JOB_UNSUBSCRIBED') {
              return JobCard({ type: subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key })
            }
            if (subjectU === 'PROJECT_FOLLOWED' || subjectU === 'PROJECT_UNFOLLOWED') {
              return ProjectFollowCard({ type: subjectU, sentAt, from: fromResolved, toLinks, text, key: msg.key })
            }
            if (subjectU === 'MARKET_SOLD') {
              return MarketSoldCard({ sentAt, from: fromResolved, toLinks, subject: subjectRaw, text, key: msg.key })
            }
            if (subjectU === 'PROJECT_PLEDGE' || content.meta?.type === 'project-pledge') {
              return ProjectPledgeCard({ sentAt, from: fromResolved, toLinks, content, text, key: msg.key })
            }

            return div(
              { class: 'pm-card normal-pm' },
              headerLine({ sentAt, from: fromResolved, toLinks, subject: subjectRaw }),
              div({ class: 'message-text', innerHTML: sanitizeHtml(clickableLinks(text)) }),
              actions({ key: msg.key, replyId: fromResolved, subjectRaw, text })
            )
          }

          const threadGroups = {}
          const threadOrder = []
          for (const msg of sorted) {
            const tid = threadId(msg)
            if (!threadGroups[tid]) {
              threadGroups[tid] = []
              threadOrder.push(tid)
            }
            threadGroups[tid].push(msg)
          }

          if (!threadOrder.length) return p({ class: 'empty' }, i18n.noPrivateMessages)

          return threadOrder.map(tid => {
            const msgs = threadGroups[tid]
            const original = msgs[0]
            const replies = msgs.slice(1)

            if (!replies.length) {
              return renderMsg(original)
            }

            const replyLabel = `${replies.length} ${replies.length === 1 ? (i18n.pmReply || 'reply') : (i18n.pmReplies || 'replies')}`

            return div({ class: 'pm-thread' },
              renderMsg(original),
              details({ class: 'pm-thread-details' },
                summary({ class: 'pm-thread-toggle' },
                  span({ class: 'pm-thread-icon' }, '▶'),
                  span(replyLabel)
                ),
                div({ class: 'pm-thread-replies' },
                  ...replies.map(renderMsg)
                )
              )
            )
          })
        })()
      )
    )
  )
}

exports.publishCustomView = async () => {
  const action = "/publish/custom";
  const method = "post";

  return template(
    i18n.publishCustom,
    section(
      div({ class: "tags-header" },
        h2(i18n.publishCustom),
        p(i18n.publishCustomDescription)
      ),
      form(
        { action, method },
        textarea(
          {
            autofocus: true,
            required: true,
            name: "text",
            rows: 10,
            class: "textarea-full"
          },
          "{\n",
          '  "type": "feed",\n',
          '  "hello": "world"\n',
          "}"
        ),
        br(),
        br(),
        button({ type: "submit" }, i18n.submit)
      )
    ),
    section(
      div({ class: "tags-header" },
        p(i18n.publishBasicInfo({ href: "/publish" }))
      )
    )
  );
};

exports.threadView = ({ messages }) => {
  const rootMessage = messages[0];
  const rootAuthorName = rootMessage.value.meta.author.name;

  const needsPdfViewer = Array.isArray(messages) && messages.some((m) => {
    const t = String(m?.value?.content?.type || "").toLowerCase();
    return t === "document";
  });

  const tpl = template(
    [`@${rootAuthorName}`],
    div(thread(messages))
  );

  return `${tpl}${
    needsPdfViewer
      ? `<script type="module" src="/js/pdf.min.mjs"></script>
         <script src="/js/pdf-viewer.js"></script>`
      : ""
  }`;
};

exports.publishView = (preview, text, contentWarning) => {
  return template(
    i18n.publish,
    section(
      div({ class: "tags-header" },
        h2(i18n.publishBlog),
        p(i18n.publishLabel({ markdownUrl, linkTarget: "_blank" }))
      )
    ),
    section(
      div({ class: "publish-form" },
        form(
          {
            action: "/publish/preview",
            method: "post",
            enctype: "multipart/form-data",
          },
          [
            label({ for: "contentWarning" }, i18n.blogSubject),
            br(),
            input({
              name: "contentWarning",
              id: "contentWarning",
              type: "text",
              class: "contentWarning",
              value: contentWarning || "",
              placeholder: i18n.contentWarningPlaceholder
            }),
            br(),
            label({ for: "text" }, i18n.blogMessage),
            br(),
            textarea(
              {
                required: true,
                name: "text",
                id: "text",
                rows: "6",
                cols: "50",
                placeholder: i18n.publishWarningPlaceholder,
                class: "publish-textarea"
              },
              text || ""
            ),
            br(),
            label({ for: "blob" }, i18n.blogImage || "Upload media (max-size: 50MB)"),
            br(),
            input({ type: "file", id: "blob", name: "blob" }),
            br(), br(),
            button({ type: "submit" }, i18n.blogPublish)
          ]
        )
      )
    ),
    preview || "",
    section(
      div({ class: "tags-header" },
        p(i18n.publishCustomInfo({ href: "/publish/custom" }))
      )
    )
  );
};

//generate preview
const ensureAt = (id) => {
  const s = String(id || "").trim()
  if (!s) return ""
  return s.startsWith("@") ? s : `@${s.replace(/^@+/, "")}`
}

const stripAt = (id) => String(id || "").trim().replace(/^@+/, "")

const authorHref = (feed) => `/author/${encodeURIComponent(ensureAt(feed))}`

const escapeRegex = (s) => String(s || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")

const escapeHtml = (s) => {
  return String(s || "")
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;")
}

const normalizeMentionLinks = (text) => {
  let t = String(text || "")
  t = t.replace(
    /\[@([^\]]+)\]\s*\(\s*@?([^) \t\r\n]+\.ed25519)\s*\)/g,
    (_m, label, feed) => `[@${String(label || "").replace(/^@+/, "")}](@${String(feed || "").replace(/^@+/, "")})`
  )
  return t
}

const injectResolvedMentions = (text, mentions) => {
  let out = String(text || "")
  const obj = mentions && typeof mentions === "object" ? mentions : {}

  const entries = Object.entries(obj)
    .map(([k, v]) => [String(k || "").trim().replace(/\s+/g, " "), Array.isArray(v) ? v : []])
    .filter(([k, v]) => k && v.length === 1)

  entries.sort((a, b) => b[0].length - a[0].length)

  for (const [token, list] of entries) {
    const m = list[0] || {}
    const feed = ensureAt(m.feed || m.link || m.id || "")
    if (!feed) continue

    const label = String(m.name || token).replace(/^@+/, "")
    const parts = token.split(/\s+/).filter(Boolean).map(escapeRegex)
    if (!parts.length) continue

    const tokenPattern = parts.join("\\s+")
    const re = new RegExp(`(^|\\s)(?!\\[)@${tokenPattern}(?=\\b|$)`, "g")
    out = out.replace(re, (match, prefix) => `${prefix}[@${label}](${feed})`)
  }

  return out
}

const markdownMentionsToHtml = (markdownText) => {
  const escaped = escapeHtml(String(markdownText || ""))
  const withBr = escaped.replace(/\r\n|\r|\n/g, "<br>")

  const unescapeBlob = (b) => b.replace(/&amp;/g, '&')

  const withImages = withBr.replace(
    /!\[([^\]]*)\]\(\s*(&amp;[^)\s]+\.sha256)\s*\)/g,
    (_m, alt, blob) => `<img src="/blob/${encodeURIComponent(unescapeBlob(blob))}" alt="${alt}" class="post-image">`
  )

  const withVideos = withImages.replace(
    /\[video:([^\]]*)\]\(\s*(&amp;[^)\s]+\.sha256)\s*\)/g,
    (_m, _name, blob) => `<video controls class="post-video" src="/blob/${encodeURIComponent(unescapeBlob(blob))}"></video>`
  )

  const withAudios = withVideos.replace(
    /\[audio:([^\]]*)\]\(\s*(&amp;[^)\s]+\.sha256)\s*\)/g,
    (_m, _name, blob) => `<audio controls class="post-audio" src="/blob/${encodeURIComponent(unescapeBlob(blob))}"></audio>`
  )

  const withPdfs = withAudios.replace(
    /\[pdf:([^\]]*)\]\(\s*(&amp;[^)\s]+\.sha256)\s*\)/g,
    (_m, name, blob) => `<a class="post-pdf" href="/blob/${encodeURIComponent(unescapeBlob(blob))}" target="_blank">${name || i18n.pdfFallbackLabel || 'PDF'}</a>`
  )

  const withMentions = withPdfs.replace(
    /\[@([^\]]+)\]\(\s*@?([^) \t\r\n]+\.ed25519)\s*\)/g,
    (_m, label, feed) => {
      const href = authorHref(feed)
      const shown = `@${String(label || "").replace(/^@+/, "")}`
      return `<a class="mention" href="${href}">${escapeHtml(shown)}</a>`
    }
  )

  const withLinks = withMentions.replace(
    /(https?:\/\/[^\s<]+)/g,
    (u) => `<a href="${u}" target="_blank" rel="noopener noreferrer">${u}</a>`
  )

  return withLinks
}

const generatePreview = ({ previewData, contentWarning, action }) => {
  const mentions =
    previewData && previewData.mentions && typeof previewData.mentions === "object"
      ? previewData.mentions
      : {}

  const rawText = String((previewData && previewData.text) || "")
  const normalized = normalizeMentionLinks(rawText)
  const injected = injectResolvedMentions(normalized, mentions)
  const publishText = normalizeMentionLinks(injected)

  const previewHtml = markdownMentionsToHtml(publishText)

  const mentionCards = Object.entries(mentions)
    .map(([_token, matches]) => {
      const list = Array.isArray(matches) ? matches : []
      const first = list.find((x) => x && (x.feed || x.link || x.id)) || null
      if (!first) return null

      const feed = ensureAt(first.feed || first.link || first.id || "")
      if (!feed) return null

      const nameRaw = String(first.name || stripAt(feed) || "")
      const nameText = nameRaw.startsWith("@") ? nameRaw : `@${nameRaw}`

      const rel = first.rel || {}

      const relationshipBadge = rel.me
        ? span({ class: "status you" }, i18n.relationshipYou)
        : rel.blocking
          ? span({ class: "status blocked" }, i18n.relationshipBlocking)
          : rel.following && rel.followsMe
            ? span({ class: "status mutual" }, i18n.relationshipMutuals)
            : rel.following
              ? span({ class: "status supporting" }, i18n.relationshipFollowing)
              : rel.followsMe
                ? span({ class: "status supported-by" }, i18n.relationshipTheyFollow)
                : span({ class: "status" }, i18n.relationshipNone)

      const avatar = first.img || first.image || ""
      const avatarUrl =
        typeof avatar === "string" && avatar.startsWith("&")
          ? `/blob/${encodeURIComponent(avatar)}`
          : (typeof avatar === "string" && avatar ? avatar : "/assets/images/default-avatar.png")

      return div(
        { class: "mention-card" },
        a({ href: authorHref(feed) }, img({ src: avatarUrl, class: "avatar-profile" })),
        br(),
        div(
          { class: "mention-name" },
          span({ class: "label" }, `${i18n.mentionsName}: `),
          a({ href: authorHref(feed) }, nameText)
        ),
        div(
          { class: "mention-relationship" },
          span({ class: "label" }, `${i18n.mentionsRelationship}: `),
          relationshipBadge,
          div(
            { class: "mention-relationship-details" },
            span(
              { class: "mentions-listing" },
              a({ class: "user-link", href: authorHref(feed) }, `@${stripAt(feed)}`)
            )
          )
        )
      )
    })
    .filter(Boolean)

  return div(
    section(
      { class: "post-preview" },
      div(
        { class: "preview-content" },
        h2(i18n.messagePreview),
        contentWarning ? div({ class: "content-warning-preview" }, escapeHtml(contentWarning)) : null,
        div({ class: "preview-rendered", innerHTML: previewHtml })
      )
    ),
    section(
      { class: "mention-suggestions" },
      mentionCards.length ? h2(i18n.mentionsMatching) : null,
      ...mentionCards
    ),
    section(
      form(
        { action, method: "post" },
        input({ type: "hidden", name: "text", value: publishText }),
        input({ type: "hidden", name: "contentWarning", value: contentWarning || "" }),
        input({ type: "hidden", name: "mentions", value: JSON.stringify(mentions) }),
        button({ type: "submit" }, i18n.publish)
      )
    )
  )
}

exports.previewView = ({ previewData, contentWarning }) => {
  const publishAction = "/publish"
  const preview = generatePreview({
    previewData,
    contentWarning,
    action: publishAction,
  })
  return exports.publishView(preview, (previewData && previewData.text) || "", contentWarning)
}

const viewInfoBox = ({ viewTitle = null, viewDescription = null }) => {
  if (!viewTitle && !viewDescription) {
    return null
  }
  return section(
    { class: "viewInfo" },
    viewTitle ? h1(viewTitle) : null,
    viewDescription ? em(viewDescription) : null
  )
}
//generate preview

exports.likesView = async ({ messages, feed, name }) => {
  const authorLink = a(
    { href: `/author/${encodeURIComponent(feed)}` },
    "@" + name
  );

  return template(
    ["@", name],
    viewInfoBox({
      viewTitle: span(authorLink),
      viewDescription: span(i18n.spreadedDescription)
    }),
    messages.map((msg) => post({ msg }))
  );
};

const messageListView = ({
  messages,
  viewTitle = null,
  viewDescription = null,
  viewElements = null,
  aside = null,
}) => {
  const hasHeader = !!viewElements;
  const titleBlock = hasHeader
    ? viewElements
    : div({ class: "tags-header" },
        h2(viewTitle),
        p(viewDescription)
      );
  return template(
    viewTitle,
    section(titleBlock),
    messages.map((msg) => post({ msg, aside }))
  );
};

exports.popularView = ({ messages, prefix }) => {
  const header = div({ class: "tags-header" },
    h2(i18n.popular),
    p(i18n.popularDescription)
  );
  return messageListView({
    messages,
    viewTitle: i18n.popular,
    viewElements: [header, prefix]
  });
};

exports.extendedView = ({ messages }) => {
  const header = div({ class: "tags-header" },
    h2(i18n.extended),
    p(i18n.extendedDescription)
  );
  return messageListView({
    messages,
    viewTitle: i18n.extended,
    viewElements: header
  });
};

exports.latestView = ({ messages }) => {
  const header = div({ class: "tags-header" },
    h2(i18n.latest),
    p(i18n.latestDescription)
  );
  return messageListView({
    messages,
    viewTitle: i18n.latest,
    viewElements: header
  });
};

exports.topicsView = ({ messages, prefix }) => {
  const header = div({ class: "tags-header" },
    h2(i18n.topics),
    p(i18n.topicsDescription)
  );
  return messageListView({
    messages,
    viewTitle: i18n.topics,
    viewElements: [header, prefix]
  });
};

exports.summaryView = ({ messages }) => {
  const header = div({ class: "tags-header" },
    h2(i18n.summaries),
    p(i18n.summariesDescription)
  );
  return messageListView({
    messages,
    viewTitle: i18n.summaries,
    viewElements: header,
    aside: true
  });
};

exports.spreadedView = ({ messages }) => {
  const header = div({ class: "tags-header" },
    h2(i18n.spreaded),
    p(i18n.spreadedDescription)
  );
  return spreadedListView({
    messages,
    viewTitle: i18n.spreaded,
    viewElements: header
  });
};

exports.threadsView = ({ messages }) => {
  const header = div({ class: "tags-header" },
    h2(i18n.threads),
    p(i18n.threadsDescription)
  );
  return messageListView({
    messages,
    viewTitle: i18n.threads,
    viewElements: header,
    aside: true
  });
};

exports.previewSubtopicView = async ({
  previewData,
  messages,
  myFeedId,
  contentWarning,
}) => {
  const publishAction = `/subtopic/${encodeURIComponent(messages[0].key)}`;
  const preview = generatePreview({
    previewData,
    contentWarning,
    action: publishAction,
  });
  return exports.subtopicView(
    { messages, myFeedId },
    preview, 
    previewData.text,
    contentWarning
  );
};

exports.subtopicView = async (
  { messages, myFeedId },
  preview,
  text,
  contentWarning
) => {
  const subtopicForm = `/subtopic/preview/${encodeURIComponent(
    messages[messages.length - 1].key
  )}`;

  let markdownMention;

  const messageElements = await Promise.all(
    messages.reverse().map((message) => {
      debug("%O", message);
      const authorName = message.value.meta.author.name;
      const authorFeedId = message.value.author;
      if (authorFeedId !== myFeedId) {
        if (message.key === messages[0].key) {
          const x = `[@${authorName}](${authorFeedId})\n\n`;
          markdownMention = x;
        }
      }
      return post({ msg: message });
    })
  );

  const authorName = messages[messages.length - 1].value.meta.author.name;

  return template(
    i18n.subtopicTitle({ authorName }),
    div({ class: "thread-container" }, messageElements),
    form(
      { action: subtopicForm, method: "post", enctype: "multipart/form-data" },
      i18n.blogSubject,
      br(),
      label(
        i18n.contentWarningLabel,
        input({
          name: "contentWarning",
          type: "text",
          class: "contentWarning",
          value: contentWarning ? contentWarning : "",
          placeholder: i18n.contentWarningPlaceholder,
        })
      ),
      br(),
      label({ for: "text" }, i18n.blogMessage),
      br(),
      textarea(
        {
          autofocus: true,
          required: true,
          name: "text",
          rows: "6",
          cols: "50",
          placeholder: i18n.publishWarningPlaceholder,
        },
        text ? text : markdownMention
      ),
      br(),
      label(
        { for: "blob" },
        i18n.blogImage || "Upload media (max-size: 50MB)"
      ),
      input({ type: "file", id: "blob", name: "blob" }),
      br(),
      br(),
      button({ type: "submit" }, i18n.blogPublish)
    ),
    preview ? div({ class: "comment-preview" }, preview) : ""
  );
};
