import { defineCollection } from 'astro:content';
import type { SchemaContext } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';

function isRootRelativeUrl(url: string) {
  return url.startsWith('/') && !url.startsWith('//');
}

const linkUrl = z.string().refine(
  (url) => {
    if (isRootRelativeUrl(url)) return true;
    try {
      const parsed = new URL(url);
      return ['https:', 'mailto:'].includes(parsed.protocol);
    } catch {
      return false;
    }
  },
  { message: 'URL must be an absolute https/mailto URL or a root-relative path.' }
);

const mediaUrl = z.string().refine(
  (url) => {
    if (isRootRelativeUrl(url)) return true;
    try {
      return new URL(url).protocol === 'https:';
    } catch {
      return false;
    }
  },
  { message: 'Media URL must be an absolute https URL or a root-relative path.' }
);

function isIframeUrl(url: string) {
  if (isRootRelativeUrl(url)) return true;
  try {
    return new URL(url).protocol === 'https:';
  } catch {
    return false;
  }
}

export const TAGS = [
  'ai',
  'systems',
  'graphics',
  'simulation',
  'embedded',
  'web',
  'tools',
  'games',
] as const;

const linkSchema = z.object({
  label: z.string(),
  url: linkUrl,
  download: z.boolean().optional(),
});

const thumbnailSchema = ({ image }: SchemaContext) =>
  z.object({
    src: image(),
    alt: z.string().min(1, 'Thumbnail alt text must not be empty.'),
  });

// Thumbnail override used by the project card to swap just the image when the
// card wants a different crop from the article. The alt text is never
// overridden, so it stays a single source of truth on the top-level thumbnail.
const thumbnailOverrideSchema = ({ image }: SchemaContext) =>
  z.object({
    src: image(),
  });

const mediaSchema = ({ image }: SchemaContext) =>
  z
    .discriminatedUnion('type', [
      z.object({
        type: z.enum(['image', 'diagram']),
        src: image(),
        alt: z.string().optional(),
        decorative: z.boolean().optional(),
        caption: z.string().optional(),
        transcript: z.string().optional(),
      }),
      z
        .object({
          type: z.literal('video'),
          poster: image().optional(),
          mp4: mediaUrl.optional(),
          webm: mediaUrl.optional(),
          alt: z.string().optional(),
          decorative: z.boolean().optional(),
          caption: z.string().optional(),
          transcript: z.string().optional(),
        })
        .refine((item) => Boolean(item.mp4) || Boolean(item.webm), {
          message: 'Video media needs at least one mp4 or webm source.',
        }),
    ])
    .refine((item) => item.decorative || (Boolean(item.alt) && Boolean(item.caption)), {
      message: 'Meaningful media needs both alt text and a caption.',
    });

// A single collection where each entry can carry an `article` facet (a written
// page under /articles/<slug>), a `project` facet (a card in the /projects/
// index), or both. Fields shared by both facets live at the top level and are
// written once; the few where the project card deliberately differs from the
// article are overridden inside `project`. The markdown body is the article's
// content.
const articleFacet = ({ image }: SchemaContext) =>
  z.object({
    updated: z.coerce.date().optional(),
    draft: z.boolean().default(false),
    iframeThumbnail: z.boolean().default(false),
    featuredOrder: z.number().optional(),
    tags: z.array(z.enum(TAGS)).default([]),
    stack: z.array(z.string()).optional(),
    scale: z.string().min(1).optional(),
    outcome: z.string().min(1).optional(),
    media: z.array(mediaSchema({ image })).default([]),
  });

const projectFacet = ({ image }: SchemaContext) =>
  z.object({
    selected: z.boolean().default(false),
    // Card chips. When omitted, the card falls back to the article's `stack`.
    technologies: z.array(z.string()).optional(),
    // Card overrides. When omitted, the card uses the top-level value.
    title: z.string().optional(),
    thumbnail: thumbnailOverrideSchema({ image }).optional(),
  });

const work = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/work' }),
  schema: ({ image }) =>
    z
      .object({
        // Shared identity (single source of truth).
        title: z.string(),
        description: z.string().max(160),
        thumbnail: thumbnailSchema({ image }),
        period: z.string().min(1).optional(),
        date: z.coerce.date(),
        links: z.array(linkSchema).default([]),

        article: articleFacet({ image }).optional(),
        project: projectFacet({ image }).optional(),
      })
      .refine((entry) => Boolean(entry.article) || Boolean(entry.project), {
        message: 'An entry needs at least an `article` or a `project` facet.',
      })
      .refine(
        (entry) =>
          !entry.article?.iframeThumbnail ||
          entry.links.some(
            (link) =>
              !link.download &&
              link.label.trim().toLowerCase() === 'demo' &&
              isIframeUrl(link.url)
          ),
        {
          path: ['article', 'iframeThumbnail'],
          message:
            'iframeThumbnail requires a non-download Demo link with an https or root-relative URL.',
        }
      ),
});

export const collections = { work };
