import { Buffer } from 'node:buffer';
import {
	AllowedMentionsTypes,
	BaseThemeType,
	ComponentType,
	MessageFlags,
	MessageReferenceType,
} from 'discord-api-types/v10';
import { z } from 'zod';
import { snowflakePredicate } from '../Assertions.js';
import { embedPredicate } from './embed/Assertions.js';
import { pollPredicate } from './poll/Assertions.js';

const fileKeyRegex = /^files\[(?<placeholder>\d+?)]$/;

export const rawFilePredicate = z.object({
	data: z.union([z.instanceof(Buffer), z.instanceof(Uint8Array), z.string()]),
	name: z.string().min(1),
	contentType: z.string().optional(),
	key: z.string().regex(fileKeyRegex).optional(),
});

export const attachmentPredicate = z.object({
	// As a string it only makes sense for edits when we do have an attachment snowflake
	id: z.union([snowflakePredicate, z.number()]),
	description: z.string().max(1_024).optional(),
	duration_secs: z
		.number()
		.max(2 ** 31 - 1)
		.optional(),
	filename: z.string().max(1_024).optional(),
	title: z.string().max(1_024).optional(),
	waveform: z.string().max(400).optional(),
});

export const allowedMentionPredicate = z
	.object({
		parse: z.enum(AllowedMentionsTypes).array().optional(),
		roles: z.string().array().max(100).optional(),
		users: z.string().array().max(100).optional(),
		replied_user: z.boolean().optional(),
	})
	.refine(
		(data) =>
			!(
				(data.parse?.includes(AllowedMentionsTypes.User) && data.users?.length) ||
				(data.parse?.includes(AllowedMentionsTypes.Role) && data.roles?.length)
			),
		{
			error:
				'Cannot specify both parse: ["users"] and non-empty users array, or parse: ["roles"] and non-empty roles array. These are mutually exclusive',
		},
	);

export const messageReferencePredicate = z.object({
	channel_id: z.string().optional(),
	fail_if_not_exists: z.boolean().optional(),
	guild_id: z.string().optional(),
	message_id: z.string(),
	type: z.enum(MessageReferenceType).optional(),
});

const baseMessagePredicate = z.object({
	nonce: z.union([z.string().max(25), z.number()]).optional(),
	tts: z.boolean().optional(),
	allowed_mentions: allowedMentionPredicate.optional(),
	message_reference: messageReferencePredicate.optional(),
	attachments: attachmentPredicate.array().max(10).optional(),
	enforce_nonce: z.boolean().optional(),
});

const basicActionRowPredicate = z.object({
	type: z.literal(ComponentType.ActionRow),
	components: z
		.object({
			type: z.literal([
				ComponentType.Button,
				ComponentType.ChannelSelect,
				ComponentType.MentionableSelect,
				ComponentType.RoleSelect,
				ComponentType.StringSelect,
				ComponentType.UserSelect,
			]),
		})
		.array(),
});

export const sharedClientThemePredicate = z.object({
	colors: z
		.array(z.string().regex(/^[\da-f]{6}$/i))
		.min(1)
		.max(5),
	gradient_angle: z.int().min(0).max(360),
	base_mix: z.int().min(0).max(100),
	base_theme: z.enum(BaseThemeType).nullish(),
});

const messageNoComponentsV2Predicate = baseMessagePredicate
	.extend({
		content: z.string().max(2_000).optional(),
		embeds: embedPredicate.array().max(10).optional(),
		sticker_ids: z.array(z.string()).max(3).optional(),
		poll: pollPredicate.optional(),
		shared_client_theme: sharedClientThemePredicate.optional(),
		components: basicActionRowPredicate.array().max(5).optional(),
		flags: z
			.int()
			.optional()
			.refine((flags) => !flags || (flags & MessageFlags.IsComponentsV2) === 0, {
				error: 'Cannot set content, embeds, stickers, poll, or shared client theme with IsComponentsV2 flag set',
			}),
	})
	.refine(
		(data) =>
			data.content !== undefined ||
			(data.embeds !== undefined && data.embeds.length > 0) ||
			data.poll !== undefined ||
			(data.attachments !== undefined && data.attachments.length > 0) ||
			(data.components !== undefined && data.components.length > 0) ||
			(data.sticker_ids !== undefined && data.sticker_ids.length > 0) ||
			data.shared_client_theme !== undefined,
		{
			error: 'Messages must have content, embeds, a poll, attachments, components, stickers, or a shared client theme',
		},
	);

const allTopLevelComponentsPredicate = z
	.union([
		basicActionRowPredicate,
		z.object({
			type: z.literal([
				// Components v2
				ComponentType.Container,
				ComponentType.File,
				ComponentType.MediaGallery,
				ComponentType.Section,
				ComponentType.Separator,
				ComponentType.TextDisplay,
				ComponentType.Thumbnail,
			]),
		}),
	])
	.array()
	.min(1)
	.max(10);

const messageComponentsV2Predicate = baseMessagePredicate.extend({
	components: allTopLevelComponentsPredicate,
	flags: z.int().refine((flags) => (flags & MessageFlags.IsComponentsV2) === MessageFlags.IsComponentsV2, {
		error: 'Must set IsComponentsV2 flag to use Components V2',
	}),
	// These fields cannot be set
	content: z.string().length(0).nullish(),
	embeds: z.array(z.never()).nullish(),
	sticker_ids: z.array(z.never()).nullish(),
	poll: z.null().optional(),
	shared_client_theme: z.null().optional(),
});

export const messagePredicate = z.union([messageNoComponentsV2Predicate, messageComponentsV2Predicate]);

// This validator does not assert file.key <-> attachment.id coherence. This is fine, because the builders
// should effectively guarantee that.
export const fileBodyMessagePredicate = z.object({
	body: messagePredicate,
	// No min length to support message edits
	files: rawFilePredicate.array().max(10),
});
