/**
 * Renders Ghost posts into the site's existing markup. Every class name used
 * here already exists in src/styles.css — this module reproduces the structure
 * of the hand-authored posts rather than introducing any new design.
 */

import type { BlogPost } from "./types";
import { ghostImage, ghostSrcset } from "./normalize";

/** Escapes text destined for an HTML text node or a double-quoted attribute. */
export function escapeHtml(value: string): string {
  return value
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

/**
 * Defence-in-depth cleanup of Ghost's HTML. Ghost content is first-party
 * (authored by the team in our own CMS), so this strips the script vectors a
 * compromised or careless editor could introduce rather than attempting to be a
 * full sanitiser for untrusted input.
 */
export function sanitizeGhostHtml(html: string): string {
  return html
    .replace(/<script\b[\s\S]*?<\/script\s*>/gi, "")
    .replace(/<style\b[\s\S]*?<\/style\s*>/gi, "")
    .replace(/\son[a-z]+\s*=\s*"[^"]*"/gi, "")
    .replace(/\son[a-z]+\s*=\s*'[^']*'/gi, "")
    .replace(/\son[a-z]+\s*=\s*[^\s>]+/gi, "")
    .replace(/(<a\b[^>]*\bhref\s*=\s*")\s*javascript:[^"]*(")/gi, "$1#$2");
}

/**
 * Tags each top-level `<h2>` with `data-rail` so the existing section-rail
 * script (src/blog.js) builds its index from Ghost content the same way it does
 * for the hand-authored posts.
 */
function addRailAnchors(html: string): string {
  let index = 1;
  return html.replace(/<h2\b([^>]*)>([\s\S]*?)<\/h2>/gi, (match, attrs: string, inner: string) => {
    if (/\bdata-rail=/i.test(attrs)) return match;
    index += 1;
    const label = inner.replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim();
    const short = label.length > 18 ? `${label.slice(0, 18).trimEnd()}…` : label;
    const rail = `${String(index).padStart(2, "0")} &middot; ${escapeHtml(short.toUpperCase())}`;
    return `<h2${attrs} data-rail="${rail}">${inner}</h2>`;
  });
}

/** Adds native lazy-loading and async decoding to images inside Ghost content. */
function optimizeContentImages(html: string): string {
  return html.replace(/<img\b([^>]*)>/gi, (match, attrs: string) => {
    let next = attrs;
    if (!/\bloading=/i.test(next)) next += ' loading="lazy"';
    if (!/\bdecoding=/i.test(next)) next += ' decoding="async"';
    return `<img${next}>`;
  });
}

export function renderPostContent(post: BlogPost): string {
  return optimizeContentImages(addRailAnchors(sanitizeGhostHtml(post.html)));
}

/** The card markup for the listing grid — mirrors blog/index.html exactly. */
export function renderPostCard(post: BlogPost): string {
  const caption = [post.primaryTagLabel, post.publishedLabel.toUpperCase(), `${post.readingTime} MIN READ`]
    .filter(Boolean)
    .join(" &middot; ");

  const src = ghostImage(post.featureImage, 900);
  const srcset = ghostSrcset(post.featureImage, [600, 900, 1200]);

  // Posts without a feature image keep the card's media block (and so its
  // aspect ratio and grid rhythm) by falling back to the site OG image.
  const media = src
    ? `<img class="feat-thumb" src="${escapeHtml(src)}"${
        srcset ? ` srcset="${escapeHtml(srcset)}" sizes="(max-width: 900px) 100vw, 900px"` : ""
      } alt="${escapeHtml(post.featureImageAlt)}" loading="lazy" decoding="async" width="900" height="510">`
    : `<img class="feat-thumb" src="../og.jpg" alt="${escapeHtml(post.title)}" loading="lazy" decoding="async" width="900" height="510">`;

  return `        <a class="feat blog-card reveal" href="./${escapeHtml(post.slug)}/" aria-label="Read: ${escapeHtml(post.title)}">
          <div class="feat-media">
            ${media}
            <span class="cap">${caption}</span>
          </div>
          <div class="feat-body">
            <h3>${escapeHtml(post.title)}</h3>
            <p>${escapeHtml(post.excerpt)}</p>
            <span class="feat-link">Read the field note <span aria-hidden="true">&#9656;</span></span>
          </div>
        </a>`;
}

/**
 * Shown in place of the card grid when Ghost returns no posts. The listing no
 * longer has hand-authored cards to fall back on, so without this the grid would
 * render as a blank gap.
 */
export function renderEmptyState(): string {
  return `        <div class="blog-empty reveal">
          <span class="tag">No field notes published yet</span>
        </div>`;
}

function renderMetaTag(attr: "name" | "property", key: string, value: string | null): string {
  if (!value) return "";
  return `<meta ${attr}="${key}" content="${escapeHtml(value)}">`;
}

/** Article structured data (requirement 6). */
function renderArticleSchema(post: BlogPost): string {
  const schema: Record<string, unknown> = {
    "@context": "https://schema.org",
    "@type": "Article",
    headline: post.title,
    description: post.seo.metaDescription,
    author: { "@type": "Person", name: post.author },
    publisher: { "@type": "Organization", name: "LynkedUp Pro" },
    mainEntityOfPage: { "@type": "WebPage", "@id": post.seo.canonicalUrl },
    url: post.seo.canonicalUrl,
  };
  if (post.seo.ogImage) schema.image = post.seo.ogImage;
  if (post.publishedIso) schema.datePublished = post.publishedIso;
  if (post.tags.length) schema.keywords = post.tags.map((t) => t.name).join(", ");

  // </script> inside JSON would close the tag early.
  return JSON.stringify(schema, null, 2).replace(/</g, "\\u003c");
}

function renderHead(post: BlogPost): string {
  const { seo } = post;
  return [
    `<title>${escapeHtml(seo.metaTitle)}</title>`,
    renderMetaTag("name", "description", seo.metaDescription),
    `<link rel="canonical" href="${escapeHtml(seo.canonicalUrl)}">`,
    renderMetaTag("property", "og:title", seo.ogTitle),
    renderMetaTag("property", "og:description", seo.ogDescription),
    renderMetaTag("property", "og:type", "article"),
    renderMetaTag("property", "og:url", seo.canonicalUrl),
    renderMetaTag("property", "og:image", seo.ogImage ?? "../../og.jpg"),
    post.publishedIso
      ? renderMetaTag("property", "article:published_time", post.publishedIso)
      : "",
    renderMetaTag("name", "twitter:card", "summary_large_image"),
    renderMetaTag("name", "twitter:title", seo.twitterTitle),
    renderMetaTag("name", "twitter:description", seo.twitterDescription),
    renderMetaTag("name", "twitter:image", seo.twitterImage ?? "../../og.jpg"),
    renderMetaTag("name", "theme-color", "#0B0D10"),
    renderMetaTag("name", "author", post.author),
    `<script type="application/ld+json">\n${renderArticleSchema(post)}\n</script>`,
  ]
    .filter(Boolean)
    .join("\n");
}

function renderCover(post: BlogPost): string {
  if (!post.featureImage) return "";
  const src = ghostImage(post.featureImage, 1600);
  const srcset = ghostSrcset(post.featureImage, [800, 1200, 1600, 2000]);
  return `<figure class="post-cover reveal">
        <img src="${escapeHtml(src!)}"${
          srcset ? ` srcset="${escapeHtml(srcset)}" sizes="(max-width: 1100px) 100vw, 1100px"` : ""
        } alt="${escapeHtml(post.featureImageAlt)}" loading="eager" fetchpriority="high" width="1600" height="762">
      </figure>`;
}

function renderPostMeta(post: BlogPost): string {
  const parts = [`<span>${escapeHtml(post.author)}</span>`];
  if (post.publishedLabel) {
    parts.push(`<span><time datetime="${escapeHtml(post.publishedIso)}">${escapeHtml(post.publishedLabel)}</time></span>`);
  }
  parts.push(`<span>${post.readingTime} min read</span>`);
  return parts.join("<i></i>");
}

/** Tag chips, reusing the existing `.tag` pill style. */
function renderTags(post: BlogPost): string {
  if (!post.tags.length) return "";
  return `<div class="post-tags">${post.tags
    .map((t) => `<span class="tag">${escapeHtml(t.name)}</span>`)
    .join("")}</div>`;
}

function renderFootNav(post: BlogPost, next: BlogPost | null): string {
  const links = [`<a href="../">&larr; All field notes</a>`];
  if (next) {
    links.push(
      `<a href="../${escapeHtml(next.slug)}/">Next: ${escapeHtml(next.title)} &rarr;</a>`
    );
  }
  return links.join("\n      ");
}

/**
 * Fills the shared post shell (blog/_post.template.html) for one post.
 * @param next The following post, linked from the footer nav; null for the last.
 */
export function renderPostPage(template: string, post: BlogPost, next: BlogPost | null): string {
  const replacements: Record<string, string> = {
    HEAD: renderHead(post),
    RAIL_LABEL: escapeHtml(post.primaryTagLabel),
    TAG_LABEL: escapeHtml(post.tags[0]?.name || "Field notes"),
    TITLE: escapeHtml(post.title),
    POST_META: renderPostMeta(post),
    COVER: renderCover(post),
    CONTENT: renderPostContent(post),
    TAGS: renderTags(post),
    FOOT_NAV: renderFootNav(post, next),
  };

  return template.replace(/\{\{(\w+)\}\}/g, (match, key: string) =>
    key in replacements ? replacements[key] : match
  );
}
