/**
 * routes/blog.js
 *
 * Public-facing blog routes.
 *
 *   GET /blog                  — article listing page (server-renders listing data
 *                                into the static blog.html template)
 *   GET /blog/:slug            — single article page (server-renders full SEO
 *                                meta + JSON-LD into the static blog-article.html)
 *   GET /blog/api/posts        — JSON list of published posts
 *   GET /blog/api/post/:slug   — JSON single post
 *   GET /sitemap-blog.xml      — dynamic blog sitemap (replaces the static file)
 *
 * The HTML pages are static files in public/views/ with placeholder markers
 * like <!--SLOT_TITLE-->. At request time we read the file, do string
 * replacement, and send. This avoids pulling in a template engine while
 * still giving every article its own server-rendered <title>, meta tags,
 * JSON-LD, and body.
 */

'use strict';

const fs    = require('fs');
const path  = require('path');
const express = require('express');
const BlogPost = require('../models/blogPost');

const router = express.Router();

const PUBLIC_BASE = (process.env.PUBLIC_BASE_URL || 'https://takedowns.vesamuni.com').replace(/\/+$/, '');
const VIEWS_DIR = path.join(__dirname, '..', 'public', 'views');

// Cache the HTML templates (read once, ~50KB each)
const TEMPLATES = {};
function loadTemplate(name) {
  if (!TEMPLATES[name]) {
    TEMPLATES[name] = fs.readFileSync(path.join(VIEWS_DIR, name), 'utf8');
  }
  return TEMPLATES[name];
}

function esc(s) {
  return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({
    '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
  })[c]);
}

function escJson(s) {
  // Escape for safe inclusion in a <script type="application/ld+json"> block.
  // The JSON-LD spec only requires < and </script> be escaped.
  return String(s == null ? '' : s)
    .replace(/</g, '\\u003c')
    .replace(/>/g, '\\u003e')
    .replace(/&/g, '\\u0026')
    .replace(/\u2028/g, '\\u2028')
    .replace(/\u2029/g, '\\u2029');
}

function absoluteUrl(url) {
  const s = String(url || '');
  if (/^https?:\/\//i.test(s)) return s;
  if (s.startsWith('/')) return PUBLIC_BASE + s;
  return PUBLIC_BASE + '/' + s;
}

function isoDate(ts) {
  return new Date(ts).toISOString();
}

function humanDate(ts) {
  const d = new Date(ts);
  return d.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
}

function slugifyHeading(s) {
  return String(s || '')
    .toLowerCase()
    .replace(/<[^>]+>/g, '')
    .replace(/&[^;]+;/g, ' ')
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .slice(0, 80) || 'section';
}

function initials(name) {
  return String(name || 'takedowns Editorial')
    .split(/\s+/)
    .filter(Boolean)
    .slice(0, 2)
    .map(part => part[0].toUpperCase())
    .join('');
}

function enhanceArticleBody(bodyHtml) {
  const headings = [];
  const seen = new Map();
  const body = String(bodyHtml || '').replace(/<h2(?![^>]*\bid=)([^>]*)>([\s\S]*?)<\/h2>/gi, (match, attrs, text) => {
    const plain = text.replace(/<[^>]+>/g, '').trim();
    let id = slugifyHeading(plain);
    const count = seen.get(id) || 0;
    seen.set(id, count + 1);
    if (count) id = `${id}-${count + 1}`;
    headings.push({ id, text: plain });
    return `<h2 id="${esc(id)}"${attrs}>${text}</h2>`;
  });
  return { body, headings };
}

function renderOutlineLinks(headings) {
  if (!headings.length) {
    return '<li><a href="/blog">&larr; Back to blog hub</a></li>';
  }
  return headings.slice(0, 7).map(h => `<li><a href="#${esc(h.id)}">${esc(h.text)}</a></li>`).join('\n            ');
}

function renderArticleVisual(post) {
  return `
        <aside class="article-brief" aria-label="Article summary">
          <div class="article-brief-stat">
            <span>${esc(post.reading_time_min || 5)}</span>
            <small>min read</small>
          </div>
          <div class="article-brief-flow" aria-hidden="true">
            <span>Reference</span>
            <span>Scan</span>
            <span>Notice</span>
            <span>Monitor</span>
          </div>
          <p>${esc(post.excerpt)}</p>
        </aside>`;
}

/**
 * Build the full <script type="application/ld+json"> ... </script>
 * block(s) for an article page. We emit three: BlogPosting, BreadcrumbList,
 * Person (author), and an optional FAQPage if the post has FAQ content.
 *
 * Multiple schemas in separate <script> blocks is the recommended pattern
 * (per Google Search Central) — easier to validate, easier to debug.
 */
const ALTERNATE_LOCALES = ['en_GB', 'en_AU', 'en_CA', 'en_IN', 'en_IE', 'de_DE', 'fr_FR', 'es_ES'];

/**
 * Build the multi-market og:locale:alternate <meta> tag block.
 * Helps social and search engines understand the page is relevant across
 * English-speaking markets (and the major EU locales). Sits next to the
 * primary og:locale tag in the article template.
 */
function buildAlternateLocaleMeta() {
  return ALTERNATE_LOCALES
    .map(loc => `  <meta property="og:locale:alternate" content="${loc}" />`)
    .join('\n');
}
function buildJsonLdBlocks(post, related) {
  const blocks = [];

  // 1. BlogPosting — the primary article schema
  blocks.push(`<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "${escJson(post.title)}",
  "description": "${escJson(post.meta_description)}",
  "image": ["${escJson(absoluteUrl(post.og_image_url))}"],
  "datePublished": "${isoDate(post.published_at)}",
  "dateModified": "${isoDate(post.updated_at)}",
  "author": {
    "@type": "Person",
    "name": "${escJson(post.author.name)}",
    "jobTitle": "${escJson(post.author.role || '')}"
  },
  "publisher": {
    "@type": "Organization",
    "name": "Vesamuni Cybersecurity",
    "url": "${PUBLIC_BASE}/",
    "logo": {
      "@type": "ImageObject",
      "url": "${PUBLIC_BASE}/public/og/logo.png",
      "width": 512,
      "height": 512
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "${escJson(post.canonical_url)}"
  },
  "articleSection": "${escJson(post.category)}",
  "keywords": "${escJson(post.meta_keywords || '')}",
  "wordCount": ${Math.max(1, Math.round((post.body_html || '').replace(/<[^>]+>/g, ' ').split(/\s+/).filter(Boolean).length))},
  "inLanguage": "en"
}
</script>`);

  // 2. BreadcrumbList — Home → Blog → Article
  blocks.push(`<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    { "@type": "ListItem", "position": 1, "name": "Home", "item": "${PUBLIC_BASE}/" },
    { "@type": "ListItem", "position": 2, "name": "Blog", "item": "${PUBLIC_BASE}/blog" },
    { "@type": "ListItem", "position": 3, "name": "${escJson(post.title)}", "item": "${escJson(post.canonical_url)}" }
  ]
}
</script>`);

  // 3. FAQPage — only if the post has FAQ content
  if (Array.isArray(post.faq) && post.faq.length) {
    const mainEntity = post.faq.map((qa, i) => ({
      '@type': 'Question',
      'name': qa.q,
      'acceptedAnswer': {
        '@type': 'Answer',
        'text': qa.a,
      },
    }));
    blocks.push(`<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": ${JSON.stringify(mainEntity)}
}
</script>`);
  }

  return blocks.join('\n');
}

/**
 * Render the article page: read blog-article.html template, replace
 * placeholders, send. Placeholders are HTML-comment-delimited so they're
 * invisible in the source view (and easy to scan for in a code editor).
 */
function renderArticlePage(post, related) {
  let html = loadTemplate('blog-article.html');

  const ogDescription = post.meta_description || post.excerpt;
  const pageUrl = post.canonical_url;
  const keywords = (post.meta_keywords || '').split(',').map(s => s.trim()).filter(Boolean).join(', ');
  const enhanced = enhanceArticleBody(post.body_html);

  const replacements = {
    '<!--SLOT_TITLE-->':           esc(post.title) + ' — Shield',
    '<!--SLOT_META_DESCRIPTION-->': esc(ogDescription),
    '<!--SLOT_META_KEYWORDS-->':    esc(keywords),
    '<!--SLOT_CANONICAL_URL-->':    esc(pageUrl),
    '<!--SLOT_OG_TITLE-->':         esc(post.title),
    '<!--SLOT_OG_DESCRIPTION-->':   esc(ogDescription),
    '<!--SLOT_OG_URL-->':           esc(pageUrl),
    '<!--SLOT_OG_IMAGE-->':         esc(absoluteUrl(post.og_image_url)),
    '<!--SLOT_TWITTER_TITLE-->':    esc(post.title),
    '<!--SLOT_TWITTER_DESCRIPTION-->': esc(ogDescription),
    '<!--SLOT_TWITTER_IMAGE-->':    esc(absoluteUrl(post.og_image_url)),
    '<!--SLOT_HERO_IMAGE-->':       (post.hero_image_url && !/logo-mark(?:-\d+)?\.(?:png|svg)$/i.test(post.hero_image_url)) ? esc(post.hero_image_url) : '',
    '<!--SLOT_HERO_BLOCK-->':       (post.hero_image_url && !/logo-mark(?:-\d+)?\.(?:png|svg)$/i.test(post.hero_image_url)) ? `
  <div class="article-hero" style="background-image: url('${esc(post.hero_image_url)}');" role="img" aria-label="${esc(post.title)}"></div>
  <p class="hero-credit" style="max-width:960px;margin:8px auto 0;padding:0 24px;font-size:12px;color:var(--fg-muted);font-family:var(--font-mono);">
    Feature image from <a href="https://commons.wikimedia.org/" rel="noopener" target="_blank" style="color:var(--accent);text-decoration:none;">Wikimedia Commons</a> under a free license. Credit and source on the linked file page.
  </p>` : '',
    '<!--SLOT_HERO_PRELOAD-->':     (post.hero_image_url && !/logo-mark(?:-\d+)?\.(?:png|svg)$/i.test(post.hero_image_url)) ? '<link rel="preload" as="image" href="' + esc(post.hero_image_url) + '" />' : '',
    '<!--SLOT_CATEGORY-->':         esc(post.category),
    '<!--SLOT_PUBLISHED_DATE-->':   humanDate(post.published_at),
    '<!--SLOT_PUBLISHED_ISO-->':    isoDate(post.published_at),
    '<!--SLOT_UPDATED_ISO-->':      isoDate(post.updated_at),
    '<!--SLOT_READING_TIME-->':     String(post.reading_time_min || 5),
    '<!--SLOT_AUTHOR_NAME-->':      esc(post.author.name),
    '<!--SLOT_AUTHOR_INITIALS-->':  esc(initials(post.author.name)),
    '<!--SLOT_AUTHOR_ROLE-->':      esc(post.author.role || ''),
    '<!--SLOT_AUTHOR_BIO-->':       esc(post.author.bio || 'Practical guidance from the Shield editorial team.'),
    '<!--SLOT_OUTLINE_LINKS-->':    renderOutlineLinks(enhanced.headings),
    '<!--SLOT_ARTICLE_VISUAL-->':   renderArticleVisual(post),
    '<!--SLOT_BODY_HTML-->':        enhanced.body,
    '<!--SLOT_JSONLD-->':           buildJsonLdBlocks(post, related),
    '<!--SLOT_OG_LOCALE_ALTERNATE-->': buildAlternateLocaleMeta(),
    '<!--RELATED_SECTION-->':       renderRelatedSection(related),
  };
  for (const [key, val] of Object.entries(replacements)) {
    // Replace ALL occurrences (some placeholders appear in 2-3 spots)
    html = html.split(key).join(val);
  }
  return html;
}

/**
 * Render the "Related reading" cards below the article body.
 * Returns an empty string if there's no related content, in which case
 * the <!--RELATED_SECTION--> placeholder is left as a no-op.
 */
function renderRelatedSection(related) {
  if (!related || !related.length) return '';
  const cards = related.map(p => `
      <a href="/blog/${esc(p.slug)}" class="blog-card related-card">
        <div class="blog-card-image">
          <img src="${esc(p.hero_image_url)}" alt="${esc(p.title)}" loading="lazy" />
          <span class="blog-card-badge">${esc(p.category)}</span>
        </div>
        <div class="blog-card-content">
          <div class="blog-card-meta">
            <span>${humanDate(p.published_at)}</span>
            <span class="blog-card-dot">&middot;</span>
            <span>${p.reading_time_min} min read</span>
          </div>
          <h3 class="blog-card-title">${esc(p.title)}</h3>
          <p class="blog-card-excerpt">${esc(p.excerpt)}</p>
          <span class="blog-card-cta">Read article &rarr;</span>
        </div>
      </a>`).join('\n');
  return `
  <section class="related-section">
    <div class="container">
      <div class="related-head">
        <span class="section-label">Related Reading</span>
        <h2>Keep going</h2>
      </div>
      <div class="related-grid">${cards}
      </div>
    </div>
  </section>`;
}

/**
 * Render the blog listing page. Reads blog.html, replaces the static
 * cards with a server-rendered list of all published posts, sends.
 *
 * Layout:
 *   - Filter pills (category)
 *   - Featured post (the most-recent post, full-width two-column)
 *   - Card grid (everything else, image-on-top)
 *
 * Supports ?cat=Category for category filtering. The active category
 * persists in the pill UI and the URL.
 */
function renderListingPage(queryCat) {
  let html = loadTemplate('blog.html');
  const allPosts = BlogPost.listPublished(50);

  // Filter by category if requested. Unknown category = show all.
  const catFilter = (queryCat || '').trim();
  const posts = catFilter
    ? allPosts.filter(p => p.category.toLowerCase() === catFilter.toLowerCase())
    : allPosts;

  // Build the cards: featured (first) + grid (rest).
  let cardsHtml = '';
  if (posts.length === 0) {
    cardsHtml = `
      <div class="blog-empty">
        <p>No articles in this category yet — check back soon.</p>
        <a href="/blog" class="blog-empty-back">← See all articles</a>
      </div>`;
  } else {
    cardsHtml += renderFeaturedCard(posts[0]);
    for (let i = 1; i < posts.length; i++) {
      cardsHtml += '\n' + renderCard(posts[i]);
    }
  }

  // Replace the static cards block with server-rendered ones.
  // The blog.html file contains the static card markup between two HTML comments.
  const startMarker = '<!--BLOG_CARDS_START-->';
  const endMarker   = '<!--BLOG_CARDS_END-->';
  if (html.includes(startMarker) && html.includes(endMarker)) {
    const start = html.indexOf(startMarker);
    const end   = html.indexOf(endMarker);
    html = html.slice(0, start + startMarker.length) + '\n' + cardsHtml + '\n      ' + html.slice(end);
  }

  // Inject category filter pills.
  const filterPills = renderCategoryPills(catFilter || null);
  if (html.includes('<!--BLOG_FILTER_PILLS-->')) {
    html = html.replace('<!--BLOG_FILTER_PILLS-->', filterPills);
  }

  html = html.replace('<!--BLOG_TOTAL_COUNT-->', String(allPosts.length));

  // Update the meta description with a dynamic count of articles.
  const totalArticles = allPosts.length;
  const filteredNote = catFilter ? ` Filtered by ${catFilter}.` : '';
  const metaDesc = `Practical guides for NCII response, content creator leak protection, OnlyFans DMCA workflows, and bulk takedown operations. ${totalArticles} in-depth articles for survivors, advocates, agencies, and creators.${filteredNote}`;
  html = html.replace(
    '<!--SLOT_BLOG_META_DESCRIPTION-->',
    esc(metaDesc),
  );

  // Inject og:locale:alternate meta tags for multi-market SEO on the listing.
  const altMeta = buildAlternateLocaleMeta();
  if (html.includes('<!--BLOG_OG_LOCALE_ALTERNATE-->')) {
    html = html.replace('<!--BLOG_OG_LOCALE_ALTERNATE-->', altMeta);
  }

  return html;
}

/**
 * Render the featured-post treatment for the most-recent article.
 * Full-width horizontal layout, image left, content right.
 */
function renderFeaturedCard(p) {
  return `
      <a href="/blog/${esc(p.slug)}" class="blog-featured">
        <div class="blog-featured-image">
          <img src="${esc(p.hero_image_url)}" alt="${esc(p.title)}" loading="eager" />
        </div>
        <div class="blog-featured-content">
          <div class="blog-featured-meta">
            <span class="blog-pill blog-pill-accent">Featured</span>
            <span class="blog-pill">${esc(p.category)}</span>
            <span class="blog-featured-date">${humanDate(p.published_at)} &middot; ${p.reading_time_min} min read</span>
          </div>
          <h2 class="blog-featured-title">${esc(p.title)}</h2>
          <p class="blog-featured-subtitle">${esc(p.subtitle || p.excerpt)}</p>
          <div class="blog-featured-bars" aria-hidden="true">
            <span style="--bar: 92%"></span>
            <span style="--bar: 74%"></span>
            <span style="--bar: 58%"></span>
          </div>
          <span class="blog-featured-cta">Read article &rarr;</span>
        </div>
      </a>`;
}

/**
 * Render a regular blog card. Image-on-top, content below. Includes a
 * category badge on the image and a hover lift.
 */
function renderCard(p) {
  return `
      <a href="/blog/${esc(p.slug)}" class="blog-card">
        <div class="blog-card-image">
          <img src="${esc(p.hero_image_url)}" alt="${esc(p.title)}" loading="lazy" />
          <span class="blog-card-badge">${esc(p.category)}</span>
        </div>
        <div class="blog-card-content">
          <div class="blog-card-meta">
            <span>${humanDate(p.published_at)}</span>
            <span class="blog-card-dot">&middot;</span>
            <span>${p.reading_time_min} min read</span>
          </div>
          <h3 class="blog-card-title">${esc(p.title)}</h3>
          <p class="blog-card-excerpt">${esc(p.excerpt)}</p>
          <span class="blog-card-cta">Read article &rarr;</span>
        </div>
      </a>`;
}

/**
 * Render the category filter pills row. The "All" pill links to /blog
 * with no filter. Each other pill links to ?cat=CategoryName.
 */
function renderCategoryPills(activeCat) {
  const categories = ['All', 'Safety Guide', 'Creators', 'Regulatory', 'Legal', 'Technical', 'Advocacy'];
  const active = activeCat || 'All';
  return categories.map(cat => {
    const isActive = active.toLowerCase() === cat.toLowerCase();
    const href = cat === 'All' ? '/blog' : `/blog?cat=${encodeURIComponent(cat)}`;
    return `<a href="${href}" class="blog-pill blog-pill-filter${isActive ? ' is-active' : ''}">${esc(cat)}</a>`;
  }).join('\n        ');
}

// ----------------- routes -----------------

// /blog — listing page
router.get('/', (req, res) => {
  try {
    res.set('Cache-Control', 'public, max-age=300');
    res.type('html').send(renderListingPage(req.query.cat));
  } catch (e) {
    console.error('[blog] listing error:', e.message);
    res.status(500).send('Blog listing unavailable.');
  }
});

// /blog/api/posts — JSON list
router.get('/api/posts', (req, res) => {
  const limit = Math.min(parseInt(req.query.limit, 10) || 50, 100);
  res.json({ posts: BlogPost.listPublished(limit) });
});

// /blog/api/post/:slug — JSON single post
router.get('/api/post/:slug', (req, res) => {
  const post = BlogPost.getBySlug(req.params.slug);
  if (!post) return res.status(404).json({ error: 'NOT_FOUND' });
  res.json({ post, related: BlogPost.getRelated(post, 3) });
});

// /blog/:slug — single article. Slug must be a kebab-case string (lowercase,
// dashes, no path traversal). We anchor with a regex on the route to
// prevent conflicts with /api/posts / api/post/:slug / sitemap-blog.xml.
router.get('/:slug([a-z0-9][a-z0-9-]{0,120}[a-z0-9])', (req, res) => {
  const post = BlogPost.getBySlug(req.params.slug);
  if (!post) {
    // 404 — but redirect to /blog if it's just a malformed slug
    return res.status(404).sendFile(path.join(VIEWS_DIR, '404.html'), err => {
      if (err) res.status(404).send('Not Found');
    });
  }
  const related = BlogPost.getRelated(post, 3);
  try {
    res.set('Cache-Control', 'public, max-age=600');
    res.type('html').send(renderArticlePage(post, related));
  } catch (e) {
    console.error('[blog] render error:', e.message);
    res.status(500).send('Article unavailable.');
  }
});

// /sitemap-blog.xml — dynamic blog sitemap (replaces the static file).
// Routed here so it picks up new posts without a deploy.
router.get('/sitemap-blog.xml', (req, res) => {
  const posts = BlogPost.listPublished(500);
  const today = new Date().toISOString().slice(0, 10);
  const urls = posts.map(p => `  <url>
    <loc>${esc(p.canonical_url)}</loc>
    <lastmod>${isoDate(p.updated_at).slice(0, 10)}</lastmod>
    <changefreq>monthly</changefreq>
    <priority>0.8</priority>
    <image:image>
      <image:loc>${esc(p.og_image_url)}</image:loc>
      <image:title>${esc(p.title)}</image:title>
    </image:image>
  </url>`).join('\n');
  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
${urls}
</urlset>
`;
  res.set('Content-Type', 'application/xml; charset=utf-8');
  res.set('Cache-Control', 'public, max-age=3600');
  res.send(xml);
});

module.exports = router;
