import type { CollectionEntry } from 'astro:content';
import { getCollection } from 'astro:content';
import { getImage } from 'astro:assets';
import type { ImageMetadata } from 'astro';

// Theme background colors, the single source of truth for the
// <meta name="theme-color"> tags and the FOUC-prevention scripts (injected in
// Base.astro and Header.astro). Keep --color-bg in styles/tokens.css in sync;
// CSS cannot import these values.
export const THEME_BG = { light: '#fbfaf7', dark: '#201f1d' };

export const site = {
  brand: 'schmelczer.dev',
  name: 'Andras Schmelczer',
  title: 'Andras Schmelczer',
  description:
    'Notebook of someone who keeps reaching for the same two moves: let the hard constraint pick the data structure, then keep the API small enough to defend.',
  url: 'https://schmelczer.dev',
  email: 'andras@schmelczer.dev',
  git: 'https://git.schmelczer.dev/andras',
  linkedin: 'https://www.linkedin.com/in/andras-schmelczer',
  cv: '/media/downloads/cv-andras-schmelczer.pdf',
};

// Single source of truth for primary navigation. The Header consumes every
// entry where `footerOnly` is falsy AND `href !== '/'` (Home is implicit via
// the site title). The Footer renders every entry regardless. Items marked
// `footerOnly: true` appear only in the Footer.
export interface NavItem {
  href: string;
  label: string;
  footerOnly?: boolean;
}

export const navItems: readonly NavItem[] = [
  { href: '/', label: 'Home' },
  { href: '/articles/', label: 'Articles' },
  { href: '/projects/', label: 'Projects' },
  { href: '/about/', label: 'About' },
  { href: '/tags/', label: 'Tags', footerOnly: true },
  { href: '/rss.xml', label: 'RSS', footerOnly: true },
];

export function formatDate(date: Date) {
  return new Intl.DateTimeFormat('en', {
    year: 'numeric',
    month: 'short',
    day: 'numeric',
  }).format(date);
}

export function formatDateShort(date: Date) {
  return new Intl.DateTimeFormat('en', {
    month: 'short',
    day: 'numeric',
  }).format(date);
}

export function yearOf(date: Date) {
  return new Intl.DateTimeFormat('en', { year: 'numeric' }).format(date);
}

export function isExternal(url: string) {
  return /^https?:\/\//.test(url);
}

// Directory-style paths get a trailing slash; the root and file-style paths
// (e.g. /rss.xml) pass through unchanged. Shared by canonical URL generation
// and the header's current-page matching so the two can never disagree.
export function normalizeTrailingSlash(path: string) {
  return path === '/' || path.endsWith('/') || /\.[^/]+$/.test(path) ? path : `${path}/`;
}

export function entrySlug(entry: { id: string }) {
  return entry.id.replace(/\.mdx?$/, '').replace(/\/index$/, '');
}

export function articlePath(entry: { id: string } | string) {
  const slug = typeof entry === 'string' ? entry : entrySlug(entry);
  return `/articles/${slug}/`;
}

export function tagSlug(tag: string) {
  return tag
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-|-$/g, '');
}

export function tagPath(tag: string) {
  return `/tags/${tagSlug(tag)}/`;
}

export function getAllTags(articles: CollectionEntry<'work'>[]) {
  return [
    ...new Set(articles.flatMap((article) => article.data.article?.tags ?? [])),
  ].sort((a, b) => a.localeCompare(b));
}

// Memoized article loader. Build steps call `getArticles()` from many pages
// (index, articles, RSS, sitemap, tag pages, article layout). Caching the
// promise means `getCollection('work')` runs once per build. An entry is an
// article when it carries an `article` facet and isn't a draft.
let articlesPromise: Promise<CollectionEntry<'work'>[]> | undefined;

export function getArticles(): Promise<CollectionEntry<'work'>[]> {
  if (!articlesPromise) {
    articlesPromise = getCollection('work').then((entries) =>
      entries
        .filter((entry) => entry.data.article && !entry.data.article.draft)
        .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
    );
  }
  return articlesPromise;
}

// Entries shown in the projects index: those carrying a `project` facet,
// newest first.
export async function getProjects(): Promise<CollectionEntry<'work'>[]> {
  return (await getCollection('work'))
    .filter((entry) => entry.data.project)
    .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
}

export interface ProjectCard {
  title: string;
  description: string;
  thumbnail: { src: ImageMetadata; alt: string };
  technologies: string[];
  selected: boolean;
}

// Resolves a project card's presentation. Shared identity lives at the top
// level; the `project` facet overrides only the fields where the card
// deliberately differs from the article. Technologies fall back to the
// article's stack.
export function projectCard(entry: CollectionEntry<'work'>): ProjectCard {
  const { data } = entry;
  const project = data.project;
  return {
    title: project?.title ?? data.title,
    description: data.description,
    thumbnail: {
      src: project?.thumbnail?.src ?? data.thumbnail.src,
      alt: data.thumbnail.alt,
    },
    technologies: project?.technologies ?? data.article?.stack ?? [],
    selected: project?.selected ?? false,
  };
}

type ArticleData = NonNullable<CollectionEntry<'work'>['data']['article']>;
export type WorkMedia = ArticleData['media'][number];
export type HeaderVideo = Extract<WorkMedia, { type: 'video' }>;

// An entry has a "header video" when one of its article media items is a video
// whose poster is the same image as the top-level thumbnail. That video can
// stand in for the static banner: the header shows the poster, then plays
// inline on click. Entries without such a video keep the plain image header.
export function getHeaderVideo(entry: CollectionEntry<'work'>): HeaderVideo | undefined {
  const thumbnailSrc = entry.data.thumbnail.src.src;
  return (entry.data.article?.media ?? []).find(
    (item): item is HeaderVideo =>
      item.type === 'video' && item.poster?.src === thumbnailSrc
  );
}

export function adjacentArticles(
  articles: CollectionEntry<'work'>[],
  current: CollectionEntry<'work'>
) {
  const index = articles.findIndex((article) => article.id === current.id);
  if (index === -1) return { previous: undefined, next: undefined };
  return {
    previous: index < articles.length - 1 ? articles[index + 1] : undefined,
    next: index > 0 ? articles[index - 1] : undefined,
  };
}

export function getRelatedArticles(
  articles: CollectionEntry<'work'>[],
  current: CollectionEntry<'work'>,
  limit = 3
) {
  const currentTags = new Set(current.data.article?.tags ?? []);
  return articles
    .filter((article) => article.id !== current.id)
    .map((article) => ({
      article,
      overlap: (article.data.article?.tags ?? []).filter((tag) => currentTags.has(tag))
        .length,
    }))
    .filter(({ overlap }) => overlap > 0)
    .sort((a, b) => b.overlap - a.overlap)
    .slice(0, limit)
    .map(({ article }) => article);
}

export function absoluteUrl(path: string) {
  return new URL(path, site.url).toString();
}

// Canonical Person JSON-LD. Used by the home page and About page; both share
// `@id` so search engines treat them as the same entity. Pass `extra` to
// add or override fields (e.g. `jobTitle`, richer `description`).
export function buildPersonJsonLd(extra?: Record<string, unknown>) {
  return {
    '@context': 'https://schema.org',
    '@type': 'Person',
    '@id': absoluteUrl('/about/#person'),
    name: site.name,
    url: site.url,
    email: `mailto:${site.email}`,
    sameAs: [site.git, site.linkedin],
    description: site.description,
    ...extra,
  };
}

// Responsive image config shared by entry listings. Centralized here so a
// change to one breakpoint set is a single edit, not two component changes.
export const ARTICLE_THUMBNAIL = {
  widths: [160, 240, 320, 480, 640],
  sizes: '(max-width: 700px) clamp(64px, 22vw, 80px), (max-width: 960px) 7rem, 8rem',
};

export const PROJECT_THUMBNAIL = {
  widths: [320, 480, 640, 800, 960, 1200, 1280],
  sizes:
    '(max-width: 700px) calc((100vw - 40px - 0.75rem) / 2), (max-width: 960px) calc((100vw - 64px - 1rem) / 2), calc((min(100vw - 64px, 72rem) - 2rem) / 3)',
};

// Wraps `getImage` with the standard OG dimensions (1200x630 JPEG). Used by
// Base.astro for the default OG image and by Post.astro for per-post
// thumbnails. Keeps OG output consistent across the site.
export function optimizeOgImage(src: ImageMetadata) {
  return getImage({
    src,
    width: 1200,
    height: 630,
    format: 'jpg',
  });
}

interface BreadcrumbCrumb {
  name: string;
  href: string;
}

interface BreadcrumbInput {
  articles?: boolean;
  projects?: boolean;
  tagsIndex?: boolean;
  tag?: string;
  post?: CollectionEntry<'work'>;
}

// Builds the breadcrumb trail shared by JSON-LD (BreadcrumbList) and the
// visible Breadcrumbs component. Home is always first. Flags append crumbs
// in a fixed order: Articles → Tags → tag → Post (or Projects). A `tag`
// implies both Articles and Tags so callers don't have to set every flag.
export function buildBreadcrumbTrail({
  articles,
  projects,
  tagsIndex,
  tag,
  post,
}: BreadcrumbInput): BreadcrumbCrumb[] {
  const trail: BreadcrumbCrumb[] = [{ name: 'Home', href: '/' }];
  if (articles || post || tagsIndex || tag) {
    trail.push({ name: 'Articles', href: '/articles/' });
  }
  if (tagsIndex || tag) {
    trail.push({ name: 'Tags', href: '/tags/' });
  }
  if (tag) {
    trail.push({ name: `#${tag}`, href: tagPath(tag) });
  }
  if (post) {
    trail.push({ name: post.data.title, href: articlePath(post) });
  }
  if (projects) {
    trail.push({ name: 'Projects', href: '/projects/' });
  }
  return trail;
}

// Adapts a trail for the visible Breadcrumbs component, which renders the
// current page (the last crumb) as plain text rather than a link.
export function visibleBreadcrumbs(trail: BreadcrumbCrumb[]) {
  return trail.map((crumb, index) => ({
    label: crumb.name,
    href: index === trail.length - 1 ? undefined : crumb.href,
  }));
}

// Builds the schema.org BreadcrumbList JSON-LD object for a given trail.
// Shared by every page that emits breadcrumb structured data.
export function buildBreadcrumbJsonLd(trail: BreadcrumbCrumb[]) {
  return {
    '@context': 'https://schema.org',
    '@type': 'BreadcrumbList',
    itemListElement: trail.map((crumb, index) => ({
      '@type': 'ListItem',
      position: index + 1,
      name: crumb.name,
      item: absoluteUrl(crumb.href),
    })),
  };
}
