/**
 * Shared helpers used across multiple `app/[...slug]/page.tsx`-derived page
 * components (samples, questions, blog, writers, services). Extracted here
 * purely to keep the route file itself thin — every comment below documents
 * a real, specific CMS/Joomla behavior and is preserved verbatim from the
 * original monolithic `page.tsx`.
 */

export const siteName = process.env.NEXT_PUBLIC_SITE_NAME || "Global Assignment Help";

// "Blog" and "Writers" are real category/contact listings, backed by the
// read-only bridge rather than a single article (see AGENTS.md "Category
// listing bridge") — handled separately from the generic article flow below.
export const LISTING_ROUTES = new Set(["blog", "writers", "free-samples", "reviews", "questions", "answers"]);
export const LISTING_PAGE_SIZE = 12;

const DEFAULT_WRITER_IMAGE = "/images/defaultWritter.png";
export { DEFAULT_WRITER_IMAGE };

/**
 * Deterministic (not random-per-request) display number for the one figure
 * this database has no real column for anywhere — per-writer "Completed
 * Orders" (the live site's own equivalent reuses the writer's unrelated
 * `postcode` column, which we deliberately don't replicate since it isn't
 * numeric or meaningful). Seeded off the writer's own real database id so
 * it's stable across reloads/pagination rather than reshuffling every
 * render, but it is NOT a real counter — explicitly flagged here so a
 * future reader doesn't mistake it for one.
 */
export function displayOnlyCount(seed: number, min: number, max: number): number {
  const x = Math.sin(seed) * 10000;
  const frac = x - Math.floor(x);
  return Math.floor(min + frac * (max - min));
}

/**
 * The bridge's `excerpt()` helper (`api-bridge/category-listing.php`)
 * strips tags but doesn't decode entities, so rich-text content saved with
 * literal `&amp;`/`&#39;`/etc. in the CMS comes back still encoded. Fine
 * when dropped into `dangerouslySetInnerHTML`, but rendered as plain React
 * text it gets escaped a second time (`&amp;` shows up on the page
 * literally). Decodes the handful of entities actually seen in this data
 * rather than pulling in a full HTML-entity library for it.
 */
export function decodeHtmlEntities(value: string): string {
  return value
    .replace(/&amp;/g, "&")
    .replace(/&#39;/g, "'")
    .replace(/&quot;/g, '"')
    .replace(/&nbsp;/g, " ")
    .replace(/&lt;/g, "<")
    .replace(/&gt;/g, ">");
}

export function escapeHtml(value: string): string {
  return value
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;");
}

/**
 * Fills the real "Samples listing sidebar" module's own placeholder tokens
 * with live values. The surrounding markup — every heading, label, list item
 * and the block order — is whatever that module currently stores in Joomla,
 * so editing it in Joomla Admin changes this page without any code change.
 *
 * The one part that still can't resolve from real data is the seasonal offer
 * image/code (`Sidebar_image` / `offercode`): those are substituted on the
 * live site from a central `crm_offers` table that doesn't exist on this
 * server (already documented in AGENTS.md), so they fall back to the same
 * static EXTRA25 asset the Services sidebar uses.
 */
export function fillSamplesSidebarTokens(
  html: string,
  values: {
    writersCount: number | null;
    categoryTitle: string;
    rating: string;
    reviewsCount: string;
    orderNowUrl: string;
    writersHtml: string;
  }
): string {
  return html
    .replace(/image_pathSidebar_image/g, "/images/service-offer-strip.png")
    .replace(/offercode/g, "EXTRA25")
    .replace(/<ul>\s*dynamic_writer_section\s*<\/ul>/g, `<ul>${values.writersHtml}</ul>`)
    .replace(/dynamic_writer_section/g, values.writersHtml)
    .replace(/experts_count/g, String(values.writersCount ?? ""))
    .replace(/category_sample_details/g, escapeHtml(values.categoryTitle))
    .replace(/dynamic_rating/g, escapeHtml(values.rating))
    .replace(/dynamic_review/g, escapeHtml(values.reviewsCount))
    .replace(/order_now_url/g, escapeHtml(values.orderNowUrl))
    .replace(/root_url/g, "/")
    .replace(/image_path/g, "/");
}

/** Page numbers to render around the current page, with `null` standing in for an ellipsis gap. */
export function paginationRange(page: number, totalPages: number): (number | null)[] {
  const pages = new Set([1, totalPages, page - 1, page, page + 1]);
  const sorted = [...pages].filter((p) => p >= 1 && p <= totalPages).sort((a, b) => a - b);

  const withGaps: (number | null)[] = [];
  sorted.forEach((p, i) => {
    if (i > 0 && p - sorted[i - 1] > 1) withGaps.push(null);
    withGaps.push(p);
  });
  return withGaps;
}

/**
 * Writer photos are legacy Joomla upload paths (`images/GAH/data/img/...`)
 * and some no longer exist on disk (same class of issue as the broken
 * `page_upload` images seen in article content) — checked with a HEAD
 * request rather than assumed, falling back to a generic placeholder
 * instead of a broken-image icon.
 */
export async function resolveImageUrl(url: string | null): Promise<string> {
  if (!url) return DEFAULT_WRITER_IMAGE;
  try {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 4000);
    const res = await fetch(url, { method: "HEAD", signal: controller.signal, next: { revalidate: 3600 } });
    clearTimeout(timeoutId);
    return res.ok ? url : DEFAULT_WRITER_IMAGE;
  } catch {
    return DEFAULT_WRITER_IMAGE;
  }
}

// Confirmed live on the real site's own "Hire Now" buttons (writer profile
// pages) — the customer panel's order form accepts a `writer` query param to
// pre-select who's being hired, so this isn't a guessed URL shape.
export function hireWriterUrl(writerName: string): string {
  const cpanelUrl = process.env.NEXT_PUBLIC_CPANEL_URL || "https://customerpanel.globalassignmenthelp.com";
  return `${cpanelUrl}/Order?writer=${encodeURIComponent(writerName)}`;
}

/**
 * Real reading-time estimate (word count / 200wpm, the standard estimate
 * most blog platforms use) — computed from the article's actual body, not
 * a guessed/fixed number.
 */
export function estimateReadMinutes(plainText: string): number {
  const words = plainText.trim().split(/\s+/).filter(Boolean).length;
  return Math.max(1, Math.round(words / 200));
}

/**
 * The live site never shows a sample's full body to a logged-out visitor:
 * `sample.php` echoes a short visible lead (everything up to the first
 * `</p>` past a small byte offset near the start) then clips the rest
 * behind a fixed-height `overflow:hidden` box. Splitting at the first
 * `</p>` in the real article HTML reproduces that same visible slice (the
 * opening heading + its first paragraph) without inventing a teaser.
 */
export function splitSampleContent(html: string): { visible: string; hidden: string } {
  const idx = html.indexOf("</p>");
  if (idx === -1) return { visible: html, hidden: "" };
  const cut = idx + 4;
  return { visible: html.slice(0, cut), hidden: html.slice(cut) };
}

/**
 * The live "Table of Content" box on a sample page is built by walking the
 * article's own `<h2>` headings (`sample.php`: `$xpath->query("/html/body/h2")`)
 * — real section titles from the CMS content, not invented copy. Returns
 * the heading list plus the HTML with a matching `id` stamped onto each
 * `<h2>` so the TOC's links can scroll to them, same as the live site's
 * `scrollTodiv1()`.
 */
export function extractSampleToc(html: string): { items: { id: string; label: string }[]; html: string } {
  const items: { id: string; label: string }[] = [];
  let i = 0;
  const idHtml = html.replace(/<h2([^>]*)>(.*?)<\/h2>/gi, (full, attrs, inner) => {
    const label = inner.replace(/<[^>]+>/g, "").trim();
    if (!label) return full;
    const id = `toc-h2-${i++}`;
    items.push({ id, label });
    return `<h2${attrs} id="${id}">${inner}</h2>`;
  });
  return { items, html: idHtml };
}

/**
 * Sample-landing articles (`/case-study-examples`, ...) store two legacy
 * page-builder blocks at the top of their own CMS content — a promo strip
 * (`.sample-strip`) and an empty writer carousel (`.writer-section`) meant
 * to be filled by the live site's own client-side jQuery/AJAX after page
 * load, which this server-rendered app doesn't run. `SampleLandingPage`
 * rebuilds both as real React components instead (the promo CTA, and a
 * real `getWriters()`-backed carousel), so this strips the two dead
 * `<section>` blocks from the raw HTML and keeps the rest — the genuine
 * long-form SEO article body underneath — unchanged.
 */
export function stripSampleLandingChrome(html: string): string {
  return html
    .replace(/<section[^>]*class="[^"]*\bsample-strip-section\b[^"]*"[\s\S]*?<\/section>/gi, "")
    .replace(/<section[^>]*>\s*<div[^>]*>\s*<div[^>]*class="[^"]*\bsample-strip\b[^"]*"[\s\S]*?<\/div>\s*<\/div>\s*<\/section>/i, "")
    .replace(/<section[^>]*class="[^"]*\bwriter-section\b[^"]*"[\s\S]*?<\/section>/i, "");
}

/**
 * The live breadcrumb module shows the real menu path to a Services page,
 * not just "Home > {title}" — e.g. `/solve-my-assignment` is
 * `Home > Academic Writing Service > Solve My Assignment`, because that's
 * where it actually sits in the real "Assignment Services" mega-menu tree
 * (as a child of the "Academic Writing Service" subject group). Walks the
 * same real nav data the header's mega menu already uses to find that
 * parent, instead of guessing or hardcoding one.
 */
export function findServiceBreadcrumbParent(
  mainNav: { label: string; categories?: { items: { href: string; label: string; children?: { href: string }[] }[] }[] }[],
  articleHref: string
): { label: string; href: string } | null {
  const servicesItem = mainNav.find((item) => item.label === "Services");
  if (!servicesItem?.categories) return null;

  for (const category of servicesItem.categories) {
    for (const groupItem of category.items) {
      if (groupItem.children?.some((child) => child.href === articleHref)) {
        return { label: groupItem.label, href: groupItem.href };
      }
    }
  }
  return null;
}
