/**
 * Policies (Privacy Policy, Terms of Use, Cookie Notice) served by the central
 * Young Engineers hub rather than by each franchisee's own CMS page.
 *
 * These documents are written once for the whole platform and are legally the
 * same text for every licensee, so they live in one place and every site reads
 * the published version from there. The hub also carries a real version label
 * ("Version 3"), which the franchisee `pages/*` endpoint never returned - it
 * answered `version: null`, so consent records had nothing meaningful to pin
 * the agreement to.
 *
 * Read-only and unauthenticated: no hub token is involved, and none may be
 * committed to this repository.
 */

const HUB_API_URL = (
  process.env.NEXT_PUBLIC_HUB_API_URL || "https://backend.hub.youngengineers.org"
).replace(/\/+$/, "");

const HUB_APP_SLUG = process.env.NEXT_PUBLIC_HUB_APP_SLUG || "licensee-website";

/** Kinds as the hub names them - "tou" is Terms of Use. */
export type PolicyKind = "privacy" | "tou" | "cookies";

export interface HubPolicy {
  kind: PolicyKind;
  title: string;
  /** Rich text (HTML), preamble removed. The policy page normalises it. */
  body: string;
  /** e.g. "18 June 2026", lifted out of the preamble for the hero date pill. */
  effectiveDate: string;
  /** Numeric form, for the consent record. */
  version: number;
  /** What the hub actually calls this version, e.g. "Version 3". */
  versionLabel: string;
  publishedAt: string;
  pageUrl: string;
}

interface HubPolicyRow {
  kind?: string;
  title?: string;
  body?: string;
  version?: string;
  status?: string;
  published_at?: string;
  page_url?: string;
}

/**
 * `policy_consents.version` is an unsigned integer, but the hub labels versions
 * as free text ("Version 3", "2026-09-04-v1"). Take the last number in the
 * label so "Version 3" records as 3; anything unparseable records as 1 rather
 * than failing the consent write.
 */
function toVersionNumber(label?: string): number {
  const numbers = String(label ?? "").match(/\d+/g);
  if (!numbers) return 1;
  return Number(numbers[numbers.length - 1]) || 1;
}

/**
 * A preamble this long is not a preamble - the document probably has no
 * numbered sections and we would be deleting real copy. Leave it alone.
 */
const PREAMBLE_MAX_LENGTH = 800;

/**
 * Every hub body opens with the same three lines before the policy proper:
 *
 *   <h1>Privacy Policy</h1>
 *   <p>Young Engineers class website</p>
 *   <p><strong>Last updated:</strong>… ·<strong>Effective date:</strong>…</p>
 *
 * All three are already on the page - the title in the hero heading, the date
 * in the hero's date pill - so printing them again at the top of the copy just
 * repeats the header in body text.
 *
 * The document proper starts at its first numbered heading ("1. About these
 * terms"), which the editor writes as <h2>-<h6>. So everything before that
 * first sub-heading is preamble. Bounded by length, and skipped entirely when
 * there is no sub-heading, so a differently shaped document is never truncated.
 */
function splitPreamble(body: string): { preamble: string; content: string } {
  const match = body.match(/<h[2-6][\s>]/i);

  if (!match || match.index === undefined) return { preamble: "", content: body };
  if (match.index > PREAMBLE_MAX_LENGTH) return { preamble: "", content: body };

  return { preamble: body.slice(0, match.index), content: body.slice(match.index) };
}

/**
 * The date the policy takes effect, read out of the preamble so the hero can
 * show it. Falls back to "Last updated" when the document states only that,
 * and to "" when it states neither - the pill hides itself.
 */
function extractEffectiveDate(preamble: string): string {
  const text = preamble
    .replace(/<[^>]+>/g, " ")
    .replace(/&nbsp;/gi, " ")
    .replace(/\s+/g, " ");

  // Stop at the separator between the two dates, so "Last updated" does not
  // swallow "· Effective date: …" as part of its own value.
  const effective = text.match(/effective date\s*:?\s*([^·|]+)/i);
  if (effective?.[1]?.trim()) return effective[1].trim();

  const updated = text.match(/last updated\s*:?\s*([^·|]+)/i);
  return updated?.[1]?.trim() ?? "";
}

/**
 * The published policy of this kind, or null when the hub cannot be reached or
 * has nothing published - callers fall back to their own default copy rather
 * than showing an empty page.
 */
export async function fetchHubPolicy(kind: PolicyKind): Promise<HubPolicy | null> {
  const url = `${HUB_API_URL}/api/v1/policies?app=${encodeURIComponent(
    HUB_APP_SLUG,
  )}&type=${encodeURIComponent(kind)}`;

  try {
    const response = await fetch(url, {
      headers: { Accept: "application/json" },
      // Policies change a few times a year; this only spares the hub a request
      // per page view, while still picking up a new publish within minutes.
      next: { revalidate: 300 },
    });

    if (!response.ok) return null;

    const payload = (await response.json()) as { data?: HubPolicyRow[] };
    const rows = Array.isArray(payload?.data) ? payload.data : [];

    // The hub returns only published rows today, but filter anyway so a draft
    // can never reach a visitor if that ever changes.
    const row = rows.find(
      (r) => (r?.status ?? "published") === "published" && (r?.body ?? "").trim() !== "",
    );

    if (!row) return null;

    const { preamble, content } = splitPreamble(row.body ?? "");

    return {
      kind,
      title: (row.title ?? "").trim(),
      body: content,
      effectiveDate: extractEffectiveDate(preamble),
      version: toVersionNumber(row.version),
      versionLabel: (row.version ?? "").trim(),
      publishedAt: row.published_at ?? "",
      pageUrl: row.page_url ?? "",
    };
  } catch {
    // Network/parse failure - the caller shows its own fallback copy.
    return null;
  }
}
