import { defineCollection, reference, z } from "astro:content";

// Constants

export const DEFAULT_LANG = "en";
export const ANONYMOUS_USER_ID = "anonymous";

// Transformers

function parseRegex<R extends { [key: string]: string }>(regex: RegExp) {
  return (url: string, ctx: z.RefinementCtx) => {
    const match = regex.exec(url);
    if (!match?.groups) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: `"${ctx.path}" did not match regex`,
      });
      return z.NEVER;
    }
    return match.groups as R;
  };
}

// Schema definitions

/** Record of website links for a user.
 *
 * For each entry, you can enter a URL for the value, or a pre-parsed object
 * containing a link and a username (except for `website`).
 */
const websiteLinks = z.object({
  website: z.object({ link: z.string().url() }).or(
    z
      .string()
      .url()
      .transform((link) => {
        link;
      }),
  ),
  eka: z.object({ link: z.string().url(), username: z.string() }).or(
    z.string().transform((link, ctx) => {
      const { username } = parseRegex<{ username: string }>(
        /^(?:https?:\/\/)?(?:www\.)?aryion\.com\/g4\/user\/(?<username>[^\/]+)\/?$/,
      )(link, ctx);
      return { link, username };
    }),
  ),
  furaffinity: z.object({ link: z.string().url(), username: z.string() }).or(
    z.string().transform((link, ctx) => {
      const { username } = parseRegex<{ username: string }>(
        /^(?:https?:\/\/)?(?:www\.)?furaffinity\.net\/user\/(?<username>[^\/]+)\/?$/,
      )(link, ctx);
      return { link, username };
    }),
  ),
  weasyl: z.object({ link: z.string().url(), username: z.string() }).or(
    z.string().transform((link, ctx) => {
      const { username } = parseRegex<{ username: string }>(
        /^(?:https?:\/\/)?(?:www\.)?weasyl\.com\/\~(?<username>[^\/]+)\/?$/,
      )(link, ctx);
      return { link, username };
    }),
  ),
  inkbunny: z.object({ link: z.string().url(), username: z.string() }).or(
    z.string().transform((link, ctx) => {
      const { username } = parseRegex<{ username: string }>(
        /^(?:https?:\/\/)?(?:www\.)?inkbunny\.net\/(?<username>[^\/]+)\/?$/,
      )(link, ctx);
      return { link, username };
    }),
  ),
  sofurry: z.object({ link: z.string().url(), username: z.string() }).or(
    z.string().transform((link, ctx) => {
      const { username } = parseRegex<{ username: string }>(/^(?:https?:\/\/)?(?<username>[^\.]+).sofurry.com\/?$/)(
        link,
        ctx,
      );
      return { link, username };
    }),
  ),
  mastodon: z
    .object({ link: z.string().url(), username: z.string().regex(/^[^@]+@[^@]+$/) })
    .or(
      z.string().transform((link, ctx) => {
        const { username, instance } = parseRegex<{ username: string; instance: string }>(
          /^(?:https?:\/\/)(?<instance>(?:[a-zA-Z0-9_-]+\.)+[a-z]+)\/(?:@|users\/)(?<username>[a-zA-Z][a-zA-Z0-9_-]+)\/?$/,
        )(link, ctx);
        return { link, username: `${username}@${instance}` };
      }),
    )
    .transform(({ link, username }) => {
      const i = username.indexOf("@");
      return { link, username, handle: username.slice(0, i), instance: username.slice(i + 1) };
    }),
  twitter: z.object({ link: z.string().url(), username: z.string() }).or(
    z.string().transform((link, ctx) => {
      const { username } = parseRegex<{ username: string }>(
        /^(?:https?:\/\/)?(?:www\.)?(?:twitter\.com|x\.com)\/@?(?<username>[^\/]+)\/?$/,
      )(link, ctx);
      return { link, username };
    }),
  ),
  bluesky: z.object({ link: z.string().url(), username: z.string() }).or(
    z.string().transform((link, ctx) => {
      const { username } = parseRegex<{ username: string }>(
        /^(?:https?:\/\/)?bsky\.app\/profile\/(?<username>[^\/]+)\/?$/,
      )(link, ctx);
      return { link, username };
    }),
  ),
  itaku: z.object({ link: z.string().url(), username: z.string() }).or(
    z.string().transform((link, ctx) => {
      const { username } = parseRegex<{ username: string }>(
        /^(?:https?:\/\/)?(?:www\.)?itaku\.ee\/profile\/(?<username>[^\/]+)\/?$/,
      )(link, ctx);
      return { link, username };
    }),
  ),
});
/** Available languages. See https://r12a.github.io/app-subtags/ */
const lang = z.enum(["en", "tok"]).default(DEFAULT_LANG);
/** Platforms for a game. */
const platform = z.enum(["web", "windows", "linux", "macos", "android", "ios"]);
const userList = z
  .array(reference("users"))
  .refine((value) => value.length > 0, `user list cannot be empty`)
  .or(reference("users").transform((user) => [user]));
/** A record of the format `{"Character name": "user-id"}`.
 *
 * An empty character name `""` indicates that all characters are copyrighted
 * by a certain user.
 */
const copyrightedCharacters = z
  .record(z.string(), reference("users"))
  .refine(
    (value) => !("" in value) || Object.keys(value).length == 1,
    `"copyrightedCharacters" cannot mix empty catch-all key with other keys`,
  )
  .default({});
/** A record of the format `{ en: string, tok?: string, ... }`. */
const langRecord = z.object({ [DEFAULT_LANG]: z.string() }).and(z.record(lang, z.string()));
/** Common attributes for published content (stories + games). */
const publishedContent = z.object({
  // Required parameters
  title: z.string(),
  authors: userList,
  contentWarning: z.string().trim(),
  // Required parameters, but optional for drafts (isDraft == true)
  pubDate: z
    .date()
    .transform((date: Date) => new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 0, 0, 0))
    .optional(),
  description: z.string().trim(),
  tags: z.array(z.string()),
  // Optional parameters
  isDraft: z.boolean().default(false),
  relatedStories: z.array(reference("stories")).default([]),
  relatedGames: z.array(reference("games")).default([]),
  lang: lang,
  copyrightedCharacters: copyrightedCharacters,
  series: reference("series").optional(),
  posts: z
    .object({
      eka: z.string().transform((link, ctx) => {
        const { postId } = parseRegex<{ postId: string }>(
          /^(?:https?:\/\/)(?:www\.)?aryion\.com\/g4\/view\/(?<postId>[1-9]\d*)\/?$/,
        )(link, ctx);
        return { link, postId };
      }),
      furaffinity: z.string().transform((link, ctx) => {
        const { postId } = parseRegex<{ postId: string }>(
          /^(?:https?:\/\/)(?:www\.)?furaffinity\.net\/view\/(?<postId>[1-9]\d*)\/?$/,
        )(link, ctx);
        return { link, postId };
      }),
      weasyl: z.string().transform((link, ctx) => {
        const { user, postId } = parseRegex<{ user: string; postId: string }>(
          /^(?:https?:\/\/)(?:www\.)?weasyl\.com\/~(?<user>[a-zA-Z][a-zA-Z0-9_-]+)\/submissions\/(?<postId>[1-9]\d*(?:\/[a-zA-Z0-9_-]+)?)\/?$/,
        )(link, ctx);
        return { link, user, postId };
      }),
      inkbunny: z.string().transform((link, ctx) => {
        const { postId } = parseRegex<{ postId: string }>(
          /^(?:https?:\/\/)(?:www\.)?inkbunny\.net\/s\/(?<postId>[1-9]\d*)\/?$/,
        )(link, ctx);
        return { link, postId };
      }),
      sofurry: z.string().transform((link, ctx) => {
        const { postId } = parseRegex<{ postId: string }>(
          /^(?:https?:\/\/)www\.sofurry\.com\/view\/(?<postId>[1-9]\d*)\/?$/,
        )(link, ctx);
        return { link, postId };
      }),
      twitter: z.string().transform((link, ctx) => {
        const { user, postId } = parseRegex<{ user: string; postId: string }>(
          /^(?:https?:\/\/)(?:www\.)?(?:twitter\.com|x\.com)\/(?<user>[a-zA-Z0-9_-]+)\/status\/(?<postId>[1-9]\d*)\/?$/,
        )(link, ctx);
        return { link, user, postId };
      }),
      bluesky: z.string().transform((link, ctx) => {
        const { user, postId } = parseRegex<{ user: string; postId: string }>(
          /^(?:https?:\/\/)bsky\.app\/profile\/(?<user>(?:[a-zA-Z0-9_-]+\.)+[a-z]+)\/post\/(?<postId>[a-z0-9]+)\/?$/,
        )(link, ctx);
        return { link, user, postId };
      }),
      mastodon: z.string().transform((link, ctx) => {
        const { instance, user, postId } = parseRegex<{ instance: string; user: string; postId: string }>(
          /^(?:https?:\/\/)(?<instance>(?:[a-zA-Z0-9_-]+\.)+[a-z]+)\/@(?<user>[a-zA-Z][a-zA-Z0-9_-]+)\/(?<postId>[1-9]\d*)\/?$/,
        )(link, ctx);
        return { link, instance, user, postId };
      }),
    })
    .partial()
    .default({}),
});

// Types

export type Lang = z.output<typeof lang>;
export type Website = keyof z.input<typeof websiteLinks>;
export type GamePlatform = z.infer<typeof platform>;
export type CopyrightedCharacters = z.infer<typeof copyrightedCharacters>;
export type PublishedContent = z.infer<typeof publishedContent>;

// Content collections

const storiesCollection = defineCollection({
  type: "content",
  schema: ({ image }) =>
    z
      .object({
        // Required parameters, but optional for drafts (isDraft == true)
        wordCount: z.number().int().optional(),
        thumbnail: image().optional(),
        // Optional parameters
        shortTitle: z.string().optional(),
        commissioner: userList.optional(),
        requester: userList.optional(),
        summary: z.string().trim().optional(),
        thumbnailWidth: z.number().int().optional(),
        thumbnailHeight: z.number().int().optional(),
        prev: reference("stories").nullish(),
        next: reference("stories").nullish(),
      })
      .and(publishedContent)
      .refine(({ isDraft, description }) => isDraft || description, `Missing "description" for published story`)
      .refine(
        ({ isDraft, contentWarning }) => isDraft || contentWarning,
        `Missing "contentWarning" for published story`,
      )
      .refine(({ isDraft, wordCount }) => isDraft || wordCount, `Missing "wordCount" for published story`)
      .refine(({ isDraft, pubDate }) => isDraft || pubDate, `Missing "pubDate" for published story`)
      .refine(({ isDraft, thumbnail }) => isDraft || thumbnail, `Missing "thumbnail" for published story`)
      .refine(({ isDraft, tags }) => isDraft || tags.length, `Missing "tags" for published story`),
});

const gamesCollection = defineCollection({
  type: "content",
  schema: ({ image }) =>
    z
      .object({
        // Required parameters, but optional for drafts (isDraft == true)
        platforms: z.array(platform).default([]),
        thumbnail: image().optional(),
        // Optional parameters
        thumbnailWidth: z.number().int().optional(),
        thumbnailHeight: z.number().int().optional(),
        prev: reference("games").nullish(),
        next: reference("games").nullish(),
      })
      .and(publishedContent)
      .refine(({ isDraft, description }) => isDraft || description, `Missing "description" for published game`)
      .refine(({ isDraft, contentWarning }) => isDraft || contentWarning, `Missing "contentWarning" for published game`)
      .refine(({ isDraft, platforms }) => isDraft || platforms.length, `Missing "platforms" for published game`)
      .refine(({ isDraft, pubDate }) => isDraft || pubDate, `Missing "pubDate" for published game`)
      .refine(({ isDraft, thumbnail }) => isDraft || thumbnail, `Missing "thumbnail" for published game`)
      .refine(({ isDraft, tags }) => isDraft || tags.length, `Missing "tags" for published game`),
});

// Data collections

const usersCollection = defineCollection({
  type: "data",
  schema: ({ image }) =>
    z
      .object({
        // Required parameters
        name: langRecord.or(z.string()),
        links: websiteLinks.partial(),
        // Optional parameters
        preferredLink: websiteLinks.keyof().nullish(),
        avatar: image().optional(),
      })
      .refine(
        ({ links, preferredLink }) => !preferredLink || preferredLink in links,
        ({ preferredLink }) => ({
          message: `"${preferredLink}" not defined in "links"`,
          path: ["preferredLink"],
        }),
      ),
});

const seriesCollection = defineCollection({
  type: "data",
  schema: z.object({
    // Required parameters
    name: z.string(),
    url: z.string().regex(/^(\/[a-z0-9_-]+)+\/?$/, `"url" must be a local URL`),
  }),
});

const tagCategoriesCollection = defineCollection({
  type: "data",
  schema: z.object({
    // Required parameters
    name: z.string(),
    index: z.number().int(),
    tags: z
      .array(
        z.object({
          name: langRecord.or(z.string()),
          description: z.string().trim().optional(),
          related: z.array(z.string()).default([]),
        }),
      )
      .refine((tags) => tags.length, `"tags" cannot be empty`),
  }),
});

export const collections = {
  stories: storiesCollection,
  games: gamesCollection,
  users: usersCollection,
  series: seriesCollection,
  "tag-categories": tagCategoriesCollection,
};