/**
 * Turns raw Ghost payloads into {@link BlogPost} values that are always safe to
 * render: missing authors, images, excerpts and SEO fields are resolved to
 * sensible fallbacks here so the templates stay branch-free.
 */

import type { BlogPost, GhostConfig, GhostPost, GhostTag, PostSeo } from "./types";
import { SITE_TAG } from "./client";

const FALLBACK_AUTHOR = "LynkedUp Pro Team";
const FALLBACK_EXCERPT =
  "Field notes for contractors from the LynkedUp Pro team.";

/** Strips HTML tags and collapses whitespace — used to derive text from Ghost HTML. */
function toPlainText(html: string): string {
  return html
    .replace(/<[^>]*>/g, " ")
    .replace(/&nbsp;/g, " ")
    .replace(/\s+/g, " ")
    .trim();
}

function truncate(text: string, max: number): string {
  if (text.length <= max) return text;
  const cut = text.slice(0, max);
  const lastSpace = cut.lastIndexOf(" ");
  return `${cut.slice(0, lastSpace > 0 ? lastSpace : max).trimEnd()}…`;
}

/**
 * Upgrades a Ghost-hosted `http://` URL to `https://`.
 *
 * Ghost builds asset URLs from its own configured `url` setting. If that is set
 * to http (as api.lynkedup.dev currently is), every image comes back insecure and
 * browsers block it as mixed content on our HTTPS pages — silently, before the
 * server's 301 to https can help.
 *
 * Only the configured Ghost host is upgraded: an arbitrary external http image in
 * post content may genuinely have no https, and forcing it would break it.
 */
export function secureGhostUrl(url: string | null, ghostHost: string): string | null {
  if (!url || !url.startsWith("http://")) return url;
  try {
    const parsed = new URL(url);
    if (parsed.host !== ghostHost) return url;
    parsed.protocol = "https:";
    return parsed.toString();
  } catch {
    return url;
  }
}

/** Applies {@link secureGhostUrl} to src/srcset/href attributes inside post HTML. */
function secureHtmlUrls(html: string, ghostHost: string): string {
  return html.replace(
    /(\s(?:src|href)=")(http:\/\/[^"]+)(")/gi,
    (match, pre: string, url: string, post: string) => {
      const secure = secureGhostUrl(url, ghostHost);
      return secure ? `${pre}${secure}${post}` : match;
    }
  );
}

/**
 * Rewrites a Ghost image URL to a resized variant. Ghost serves derivatives from
 * `/content/images/size/w<width>/...`; any non-Ghost or already-sized URL is
 * returned untouched.
 */
export function ghostImage(url: string | null, width: number): string | null {
  if (!url) return null;
  if (!url.includes("/content/images/")) return url;
  if (/\/content\/images\/size\/w\d+\//.test(url)) return url;
  return url.replace("/content/images/", `/content/images/size/w${width}/`);
}

/** Builds a `srcset` across Ghost's derivative widths for responsive loading. */
export function ghostSrcset(url: string | null, widths: number[]): string | null {
  if (!url || !url.includes("/content/images/")) return null;
  return widths
    .map((w) => `${ghostImage(url, w)} ${w}w`)
    .join(", ");
}

/** Ghost's estimate, or a ~200wpm fallback derived from the body text. */
function resolveReadingTime(post: GhostPost, plain: string): number {
  if (post.reading_time && post.reading_time > 0) return post.reading_time;
  const words = plain ? plain.split(" ").length : 0;
  return Math.max(1, Math.round(words / 200));
}

function resolveAuthor(post: GhostPost): string {
  const name = post.primary_author?.name?.trim() || post.authors?.[0]?.name?.trim();
  return name || FALLBACK_AUTHOR;
}

/** Public tags, with the routing tag and Ghost's internal `#` tags removed. */
function resolveTags(post: GhostPost): GhostTag[] {
  return (post.tags ?? []).filter(
    (t) => t.slug !== SITE_TAG && t.visibility === "public" && !t.name.startsWith("#")
  );
}

function resolveExcerpt(post: GhostPost, plain: string): string {
  const raw = post.custom_excerpt?.trim() || post.excerpt?.trim();
  if (raw) return truncate(raw.replace(/\s+/g, " "), 200);
  if (plain) return truncate(plain, 200);
  return FALLBACK_EXCERPT;
}

/** Prefers Ghost's SEO fields, falling back to post content. */
function resolveSeo(
  post: GhostPost,
  config: GhostConfig,
  ghostHost: string,
  title: string,
  excerpt: string
): PostSeo {
  const metaTitle = post.meta_title?.trim() || `${title} - LynkedUp Pro Blog`;
  const metaDescription = post.meta_description?.trim() || excerpt;
  const ogImage = secureGhostUrl(ghostImage(post.og_image || post.feature_image, 1200), ghostHost);

  return {
    metaTitle,
    metaDescription,
    ogTitle: post.og_title?.trim() || metaTitle,
    ogDescription: post.og_description?.trim() || metaDescription,
    ogImage,
    twitterTitle: post.twitter_title?.trim() || metaTitle,
    twitterDescription: post.twitter_description?.trim() || metaDescription,
    twitterImage: secureGhostUrl(ghostImage(post.twitter_image, 1200), ghostHost) || ogImage,
    // Ghost's canonical_url wins when an editor has set one (e.g. syndicated
    // posts); otherwise the canonical is this site's own URL for the slug.
    canonicalUrl: post.canonical_url?.trim() || `${config.siteUrl}/blog/${post.slug}/`,
  };
}

const DATE_FORMAT = new Intl.DateTimeFormat("en-US", {
  month: "short",
  day: "numeric",
  year: "numeric",
  timeZone: "UTC",
});

/** The Ghost host (e.g. "api.lynkedup.dev"), for upgrading its http asset URLs. */
function ghostHostFrom(config: GhostConfig): string {
  try {
    return new URL(config.apiUrl).host;
  } catch {
    return "";
  }
}

export function normalizePost(post: GhostPost, config: GhostConfig): BlogPost {
  const ghostHost = ghostHostFrom(config);
  const rawHtml = post.html ?? "";
  const html = secureHtmlUrls(rawHtml, ghostHost);
  const plain = toPlainText(rawHtml);
  const title = post.title?.trim() || "Untitled";
  const excerpt = resolveExcerpt(post, plain);
  const tags = resolveTags(post);
  const publishedAt = post.published_at ? new Date(post.published_at) : null;
  const validDate = publishedAt && !Number.isNaN(publishedAt.getTime()) ? publishedAt : null;

  return {
    slug: post.slug,
    title,
    html,
    excerpt,
    featureImage: secureGhostUrl(post.feature_image, ghostHost),
    featureImageAlt: post.feature_image_alt?.trim() || title,
    author: resolveAuthor(post),
    publishedAt: validDate,
    publishedLabel: validDate ? DATE_FORMAT.format(validDate) : "",
    publishedIso: validDate ? validDate.toISOString() : "",
    readingTime: resolveReadingTime(post, plain),
    tags,
    primaryTagLabel: (tags[0]?.name || "Field notes").toUpperCase(),
    seo: resolveSeo(post, config, ghostHost, title, excerpt),
  };
}

/**
 * Normalises a batch, dropping posts Ghost returned without a slug (they have no
 * addressable URL) and de-duplicating against the hand-authored static posts,
 * which own their routes.
 */
export function normalizePosts(
  posts: GhostPost[],
  config: GhostConfig,
  reservedSlugs: string[] = []
): BlogPost[] {
  const reserved = new Set(reservedSlugs);
  return posts
    .filter((p) => Boolean(p.slug) && !reserved.has(p.slug))
    .map((p) => normalizePost(p, config));
}
