/*
 * Copyright (C) 2026 Fluxer Contributors
 *
 * This file is part of Fluxer.
 *
 * Fluxer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Fluxer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
 */

import {
	DEFAULT_GUILD_FOLDER_ICON,
	FriendSourceFlags,
	FriendSourceFlagsDescriptions,
	GroupDmAddPermissionFlags,
	GroupDmAddPermissionFlagsDescriptions,
	GuildFolderFlags,
	GuildFolderFlagsDescriptions,
	GuildFolderIcons,
	IncomingCallFlags,
	IncomingCallFlagsDescriptions,
	PublicUserFlags,
	PublicUserFlagsDescriptions,
} from '@fluxer/constants/src/UserConstants';
import {ConnectionResponse} from '@fluxer/schema/src/domains/connection/ConnectionSchemas';
import {GuildMemberResponse} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
import {MessageResponseSchema} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
import {LocaleSchema} from '@fluxer/schema/src/primitives/LocaleSchema';
import {
	createBitflagInt32Type,
	createNamedStringLiteralUnion,
	createStringType,
	HexString32Type,
	Int32Type,
	SignedInt32Type,
	SnowflakeStringType,
	withFieldDescription,
	withOpenApiType,
} from '@fluxer/schema/src/primitives/SchemaPrimitives';
import {
	RelationshipTypesSchema,
	RenderSpoilersSchema,
	StickerAnimationOptionsSchema,
	TimeFormatTypesSchema,
	UserAuthenticatorTypesSchema,
	UserNotificationSettingsSchema,
	UserPremiumTypesSchema,
} from '@fluxer/schema/src/primitives/UserSettingsValidators';
import {z} from 'zod';

export const UserPartialResponse = z.object({
	id: SnowflakeStringType.describe('The unique identifier (snowflake) for this user'),
	username: z.string().describe('The username of the user, not unique across the platform'),
	discriminator: z.string().describe('The four-digit discriminator tag of the user'),
	global_name: z.string().nullable().describe('The display name of the user, if set'),
	avatar: z.string().nullable().describe('The hash of the user avatar image'),
	avatar_color: Int32Type.nullable().describe('The dominant avatar color of the user as an integer'),
	bot: z.boolean().optional().describe('Whether the user is a bot account'),
	system: z.boolean().optional().describe('Whether the user is an official system user'),
	flags: createBitflagInt32Type(
		PublicUserFlags,
		PublicUserFlagsDescriptions,
		'The public flags on the user account',
		'PublicUserFlags',
	),
});

export type UserPartialResponse = z.infer<typeof UserPartialResponse>;

export const UserPrivateResponse = UserPartialResponse.extend({
	is_staff: z.boolean().describe('Whether the user has staff permissions'),
	acls: z.array(z.string()).max(100).describe('Access control list entries for the user'),
	traits: z.array(z.string()).max(100).describe('Special traits assigned to the user account'),
	email: z.string().nullable().describe('The email address associated with the account'),
	email_bounced: z
		.boolean()
		.optional()
		.describe('Whether the current email address is marked as bounced by the mail provider'),
	phone: z.string().nullable().describe('The phone number associated with the account'),
	bio: z.string().nullable().describe('The user biography text'),
	pronouns: z.string().nullable().describe('The preferred pronouns of the user'),
	accent_color: Int32Type.nullable().describe('The user-selected accent color as an integer'),
	banner: z.string().nullable().describe('The hash of the user profile banner image'),
	banner_color: Int32Type.nullable().describe('The default banner color if no custom banner is set'),
	mfa_enabled: z.boolean().describe('Whether multi-factor authentication is enabled'),
	authenticator_types: z
		.array(UserAuthenticatorTypesSchema)
		.max(10)
		.optional()
		.describe('The types of authenticators configured for MFA'),
	verified: z.boolean().describe('Whether the email address has been verified'),
	premium_type: withFieldDescription(UserPremiumTypesSchema, 'The type of premium subscription').nullable(),
	premium_since: z.string().nullable().describe('ISO8601 timestamp of when premium was first activated'),
	premium_until: z.string().nullable().describe('ISO8601 timestamp of when the current premium period ends'),
	premium_will_cancel: z.boolean().describe('Whether premium is set to cancel at the end of the billing period'),
	premium_billing_cycle: z.string().nullable().describe('The billing cycle for the premium subscription'),
	premium_lifetime_sequence: Int32Type.nullable().describe('The sequence number for lifetime premium subscribers'),
	premium_badge_hidden: z.boolean().describe('Whether the premium badge is hidden on the profile'),
	premium_badge_masked: z.boolean().describe('Whether the premium badge shows a masked appearance'),
	premium_badge_timestamp_hidden: z.boolean().describe('Whether the premium start timestamp is hidden'),
	premium_badge_sequence_hidden: z.boolean().describe('Whether the lifetime sequence number is hidden'),
	premium_purchase_disabled: z.boolean().describe('Whether premium purchases are disabled for this account'),
	premium_enabled_override: z.boolean().describe('Whether premium features are enabled via override'),
	password_last_changed_at: z.string().nullable().describe('ISO8601 timestamp of the last password change'),
	required_actions: z
		.array(z.string())
		.max(20)
		.nullable()
		.describe('Actions the user must complete before full access'),
	nsfw_allowed: z.boolean().describe('Whether the user is allowed to view NSFW content'),
	has_dismissed_premium_onboarding: z.boolean().describe('Whether the user has dismissed the premium onboarding flow'),
	has_ever_purchased: z.boolean().describe('Whether the user has ever made a purchase'),
	has_unread_gift_inventory: z.boolean().describe('Whether there are unread items in the gift inventory'),
	unread_gift_inventory_count: Int32Type.describe('The number of unread gift inventory items'),
	used_mobile_client: z.boolean().describe('Whether the user has ever used the mobile client'),
	pending_bulk_message_deletion: z
		.object({
			scheduled_at: z.string().describe('ISO8601 timestamp of when the deletion was scheduled'),
			channel_count: Int32Type.describe('The number of channels with messages to delete'),
			message_count: Int32Type.describe('The total number of messages to delete'),
		})
		.nullable()
		.describe('Information about a pending bulk message deletion request'),
});

export type UserPrivateResponse = z.infer<typeof UserPrivateResponse>;

export const EmailChangeStartResponse = z.object({
	ticket: z.string().describe('Ticket returned for email change actions'),
	require_original: z.boolean().describe('Whether verification of the original email is required'),
	original_email: z.string().nullable().describe('The original email address on record'),
	original_proof: z
		.string()
		.nullable()
		.describe('Proof token generated when original email verification is not required'),
	original_code_expires_at: z
		.string()
		.nullable()
		.describe('ISO8601 timestamp when the original verification code expires'),
	resend_available_at: z
		.string()
		.nullable()
		.describe('ISO8601 timestamp when the original verification code can be resent'),
});
export type EmailChangeStartResponse = z.infer<typeof EmailChangeStartResponse>;

export const EmailChangeVerifyOriginalResponse = z.object({
	original_proof: z.string().describe('Proof token issued after verifying the original email'),
});
export type EmailChangeVerifyOriginalResponse = z.infer<typeof EmailChangeVerifyOriginalResponse>;

export const EmailChangeRequestNewResponse = z.object({
	ticket: z.string().describe('Ticket associated with the email change attempt'),
	new_email: z.string().describe('The new email address the user wants to verify'),
	new_code_expires_at: z.string().describe('ISO8601 timestamp when the new email code expires'),
	resend_available_at: z.string().nullable().describe('ISO8601 timestamp when the new email code can be resent'),
});
export type EmailChangeRequestNewResponse = z.infer<typeof EmailChangeRequestNewResponse>;

export const PasswordChangeStartResponse = z.object({
	ticket: z.string().describe('Ticket for password change actions'),
	code_expires_at: z.string().describe('ISO8601 timestamp when the verification code expires'),
	resend_available_at: z.string().nullable().describe('ISO8601 timestamp when the code can be resent'),
});
export type PasswordChangeStartResponse = z.infer<typeof PasswordChangeStartResponse>;

export const PasswordChangeVerifyResponse = z.object({
	verification_proof: z.string().describe('Proof token issued after verifying the email code'),
});
export type PasswordChangeVerifyResponse = z.infer<typeof PasswordChangeVerifyResponse>;

export interface UserProfileResponse {
	bio: string | null;
	pronouns: string | null;
	banner: string | null;
	banner_color?: number | null;
	accent_color: number | null;
}

export const CustomStatusResponse = z.object({
	text: z.string().nullish().describe('The custom status message text'),
	expires_at: z.iso.datetime().nullish().describe('ISO8601 timestamp of when the custom status expires'),
	emoji_id: SnowflakeStringType.nullish().describe('The ID of the custom emoji used in the status'),
	emoji_name: z.string().nullish().describe('The name of the emoji used in the status'),
	emoji_animated: z.boolean().describe('Whether the status emoji is animated'),
});

export type CustomStatusResponse = z.infer<typeof CustomStatusResponse>;

const GuildFolderIconSchema = withOpenApiType(
	createNamedStringLiteralUnion(
		[
			[GuildFolderIcons.FOLDER, 'FOLDER', 'Classic folder icon'],
			[GuildFolderIcons.STAR, 'STAR', 'Star icon'],
			[GuildFolderIcons.HEART, 'HEART', 'Heart icon'],
			[GuildFolderIcons.BOOKMARK, 'BOOKMARK', 'Bookmark icon'],
			[GuildFolderIcons.GAME_CONTROLLER, 'GAME_CONTROLLER', 'Game controller icon'],
			[GuildFolderIcons.SHIELD, 'SHIELD', 'Shield icon'],
			[GuildFolderIcons.MUSIC_NOTE, 'MUSIC_NOTE', 'Music note icon'],
		] as const,
		'Guild folder icon',
	),
	'GuildFolderIconType',
);

export const UserSettingsResponse = z.object({
	status: z.string().describe('The current online status of the user'),
	status_resets_at: z.iso.datetime().nullish().describe('ISO8601 timestamp of when the status will reset'),
	status_resets_to: z.string().nullish().describe('The status to reset to after the scheduled reset'),
	theme: z.string().describe('The UI theme preference'),
	locale: LocaleSchema,
	restricted_guilds: z.array(SnowflakeStringType).max(200).describe('Guild IDs where direct messages are restricted'),
	bot_restricted_guilds: z
		.array(SnowflakeStringType)
		.max(200)
		.describe('Guild IDs where bot direct messages are restricted'),
	default_guilds_restricted: z.boolean().describe('Whether new guilds have DM restrictions by default'),
	bot_default_guilds_restricted: z.boolean().describe('Whether new guilds have bot DM restrictions by default'),
	inline_attachment_media: z.boolean().describe('Whether to display attachments inline in chat'),
	inline_embed_media: z.boolean().describe('Whether to display embed media inline in chat'),
	gif_auto_play: z.boolean().describe('Whether GIFs auto-play in chat'),
	render_embeds: z.boolean().describe('Whether to render message embeds'),
	render_reactions: z.boolean().describe('Whether to display reactions on messages'),
	animate_emoji: z.boolean().describe('Whether to animate custom emoji'),
	animate_stickers: withFieldDescription(StickerAnimationOptionsSchema, 'Sticker animation preference setting'),
	render_spoilers: withFieldDescription(RenderSpoilersSchema, 'Spoiler rendering preference setting'),
	message_display_compact: z.boolean().describe('Whether to use compact message display mode'),
	friend_source_flags: createBitflagInt32Type(
		FriendSourceFlags,
		FriendSourceFlagsDescriptions,
		'Bitfield for friend request source permissions',
		'FriendSourceFlags',
	),
	incoming_call_flags: createBitflagInt32Type(
		IncomingCallFlags,
		IncomingCallFlagsDescriptions,
		'Bitfield for incoming call notification settings',
		'IncomingCallFlags',
	),
	group_dm_add_permission_flags: createBitflagInt32Type(
		GroupDmAddPermissionFlags,
		GroupDmAddPermissionFlagsDescriptions,
		'Bitfield for group DM add permissions',
		'GroupDmAddPermissionFlags',
	),
	guild_folders: z
		.array(
			z.object({
				id: SignedInt32Type.nullish().describe('The unique identifier for the folder (-1 for uncategorized)'),
				name: z.string().nullish().describe('The display name of the folder'),
				color: Int32Type.nullish().describe('The color of the folder as an integer'),
				flags: createBitflagInt32Type(
					GuildFolderFlags,
					GuildFolderFlagsDescriptions,
					'Bitfield for guild folder display behaviour',
					'GuildFolderFlags',
				)
					.default(0)
					.describe('Bitfield for guild folder display behaviour'),
				icon: GuildFolderIconSchema.default(DEFAULT_GUILD_FOLDER_ICON).describe('Selected icon for the guild folder'),
				guild_ids: z.array(SnowflakeStringType).max(200).describe('The IDs of guilds contained in this folder'),
			}),
		)
		.max(200)
		.describe('The folder structure for organizing guilds in the sidebar'),
	custom_status: CustomStatusResponse.nullable().describe('The custom status set by the user'),
	afk_timeout: Int32Type.describe('The idle timeout in seconds before going AFK'),
	time_format: withFieldDescription(TimeFormatTypesSchema, 'The preferred time format setting'),
	developer_mode: z.boolean().describe('Whether developer mode is enabled'),
	trusted_domains: z.array(z.string()).max(1000).describe('List of trusted external link domains'),
	default_hide_muted_channels: z.boolean().describe('Whether muted channels are hidden by default in new guilds'),
});

export type UserSettingsResponse = z.infer<typeof UserSettingsResponse>;

const UserGuildMuteConfig = z
	.object({
		end_time: z.string().nullable().describe('ISO8601 timestamp of when the mute expires'),
		selected_time_window: Int32Type.describe('The selected mute duration in seconds'),
	})
	.nullable();

const UserGuildChannelOverride = z.object({
	collapsed: z.boolean().describe('Whether the channel category is collapsed in the sidebar'),
	message_notifications: withFieldDescription(
		UserNotificationSettingsSchema,
		'The notification level override for this channel',
	),
	muted: z.boolean().describe('Whether notifications are muted for this channel'),
	mute_config: UserGuildMuteConfig.describe('The mute configuration for this channel'),
});

export const UserGuildSettingsResponse = z.object({
	guild_id: SnowflakeStringType.nullable().describe('The ID of the guild these settings apply to'),
	message_notifications: withFieldDescription(
		UserNotificationSettingsSchema,
		'The default notification level for the guild',
	),
	muted: z.boolean().describe('Whether the guild is muted'),
	mute_config: UserGuildMuteConfig.describe('The mute configuration for the guild'),
	mobile_push: z.boolean().describe('Whether mobile push notifications are enabled'),
	suppress_everyone: z.boolean().describe('Whether @everyone mentions are suppressed'),
	suppress_roles: z.boolean().describe('Whether role mentions are suppressed'),
	hide_muted_channels: z.boolean().describe('Whether muted channels are hidden in the sidebar'),
	channel_overrides: z
		.record(SnowflakeStringType, UserGuildChannelOverride)
		.nullable()
		.describe('Per-channel notification overrides'),
	version: Int32Type.describe('The version number of these settings for sync'),
});

export type UserGuildSettingsResponse = z.infer<typeof UserGuildSettingsResponse>;

export const RelationshipResponse = z.object({
	id: SnowflakeStringType.describe('The unique identifier for the relationship'),
	type: withFieldDescription(RelationshipTypesSchema, 'The type of relationship (friend, blocked, pending, etc.)'),
	user: z.lazy(() => UserPartialResponse).describe('The user involved in this relationship'),
	since: z.iso.datetime().optional().describe('ISO8601 timestamp of when the relationship was established'),
	nickname: z.string().nullable().describe('A custom nickname set for the related user'),
});

export type RelationshipResponse = z.infer<typeof RelationshipResponse>;

export const RelationshipListResponse = z
	.array(RelationshipResponse)
	.max(10000)
	.describe('A list of user relationships');
export type RelationshipListResponse = z.infer<typeof RelationshipListResponse>;

export type RequiredAction =
	| 'REQUIRE_VERIFIED_EMAIL'
	| 'REQUIRE_REVERIFIED_EMAIL'
	| 'REQUIRE_VERIFIED_PHONE'
	| 'REQUIRE_REVERIFIED_PHONE'
	| 'REQUIRE_VERIFIED_EMAIL_OR_VERIFIED_PHONE'
	| 'REQUIRE_REVERIFIED_EMAIL_OR_VERIFIED_PHONE'
	| 'REQUIRE_VERIFIED_EMAIL_OR_REVERIFIED_PHONE'
	| 'REQUIRE_REVERIFIED_EMAIL_OR_REVERIFIED_PHONE';

export interface BackupCode {
	readonly code: string;
	readonly consumed: boolean;
}

export interface PendingBulkMessageDeletion {
	readonly scheduled_at: string;
	readonly channel_count: number;
	readonly message_count: number;
}

export interface UserProfile {
	readonly bio: string | null;
	readonly banner: string | null;
	readonly banner_color?: number | null;
	readonly pronouns: string | null;
	readonly accent_color: number | null;
}

export interface UserPartial {
	readonly id: string;
	readonly username: string;
	readonly discriminator: string;
	readonly global_name: string | null;
	readonly avatar: string | null;
	readonly avatar_color: number | null;
	readonly bot?: boolean;
	readonly system?: boolean;
	readonly flags: number;
}

export interface UserPrivate extends UserPartial, UserProfile {
	readonly is_staff: boolean;
	readonly email: string | null;
	readonly email_bounced?: boolean;
	readonly mfa_enabled: boolean;
	readonly phone: string | null;
	readonly authenticator_types: ReadonlyArray<number>;
	readonly verified: boolean;
	readonly premium_type: number | null;
	readonly premium_since: string | null;
	readonly premium_until: string | null;
	readonly premium_will_cancel: boolean;
	readonly premium_billing_cycle: string | null;
	readonly premium_lifetime_sequence: number | null;
	readonly premium_badge_hidden: boolean;
	readonly premium_badge_masked: boolean;
	readonly premium_badge_timestamp_hidden: boolean;
	readonly premium_badge_sequence_hidden: boolean;
	readonly premium_purchase_disabled: boolean;
	readonly premium_enabled_override: boolean;
	readonly password_last_changed_at: string | null;
	readonly required_actions: ReadonlyArray<RequiredAction> | null;
	readonly nsfw_allowed: boolean;
	readonly pending_bulk_message_deletion: PendingBulkMessageDeletion | null;
	readonly has_dismissed_premium_onboarding: boolean;
	readonly has_ever_purchased: boolean;
	readonly has_unread_gift_inventory: boolean;
	readonly unread_gift_inventory_count: number;
	readonly used_mobile_client: boolean;
	readonly traits: ReadonlyArray<string>;
}

export type User = UserPartial & Partial<UserPrivate>;

export const SavedMessageStatusSchema = withOpenApiType(
	createNamedStringLiteralUnion(
		[
			['available', 'Available', 'The saved message is available and can be retrieved'],
			['missing_permissions', 'Missing Permissions', 'The user no longer has permission to view the message'],
		],
		'Availability status of a saved message',
	),
	'SavedMessageStatus',
);
export type SavedMessageStatus = z.infer<typeof SavedMessageStatusSchema>;

export const SavedMessageEntryResponse = z.object({
	id: SnowflakeStringType.describe('Unique identifier for the saved message entry'),
	channel_id: SnowflakeStringType.describe('ID of the channel containing the message'),
	message_id: SnowflakeStringType.describe('ID of the saved message'),
	status: SavedMessageStatusSchema.describe('Availability status of the saved message'),
	message: MessageResponseSchema.nullable().describe('The message content if available'),
});

export type SavedMessageEntryResponse = z.infer<typeof SavedMessageEntryResponse>;

export const SavedMessageEntryListResponse = z.array(SavedMessageEntryResponse);
export type SavedMessageEntryListResponse = z.infer<typeof SavedMessageEntryListResponse>;

export const EmailTokenResponse = z.object({
	email_token: createStringType(1, 256).describe('The email change token to use for updating email'),
});
export type EmailTokenResponse = z.infer<typeof EmailTokenResponse>;

export const UserTagCheckResponse = z.object({
	taken: z.boolean().describe('Whether the username/discriminator combination is already taken'),
});
export type UserTagCheckResponse = z.infer<typeof UserTagCheckResponse>;

export const UserProfileDataResponse = z.object({
	bio: z.string().nullable().describe('User biography text'),
	pronouns: z.string().nullable().describe('User pronouns'),
	banner: z.string().nullable().describe('Hash of the profile banner image'),
	banner_color: Int32Type.nullable().optional().describe('Default banner color if no custom banner'),
	accent_color: Int32Type.nullable().describe('User-selected accent color'),
});
export type UserProfileDataResponse = z.infer<typeof UserProfileDataResponse>;

export const GuildMemberProfileDataResponse = z
	.object({
		bio: z.string().nullable().describe('Guild-specific biography text'),
		pronouns: z.string().nullable().describe('Guild-specific pronouns'),
		banner: z.string().nullable().describe('Hash of the guild-specific banner image'),
		accent_color: Int32Type.nullable().describe('Guild-specific accent color'),
	})
	.nullable()
	.optional();
export type GuildMemberProfileDataResponse = z.infer<typeof GuildMemberProfileDataResponse>;

export const MutualGuildResponse = z.object({
	id: SnowflakeStringType.describe('The ID of the mutual guild'),
	nick: z.string().nullable().describe('The nickname of the target user in this guild'),
});
export type MutualGuildResponse = z.infer<typeof MutualGuildResponse>;

export const UserProfileFullResponse = z.object({
	user: UserPartialResponse.describe('The user object'),
	user_profile: UserProfileDataResponse.describe('The user profile data'),
	guild_member: z
		.lazy(() => GuildMemberResponse)
		.optional()
		.describe('The guild member data if guild_id was provided'),
	guild_member_profile: GuildMemberProfileDataResponse.describe('Guild-specific profile data'),
	premium_type: withFieldDescription(UserPremiumTypesSchema, 'The type of premium subscription').optional(),
	premium_since: z.string().optional().describe('ISO8601 timestamp of when premium was activated'),
	premium_lifetime_sequence: Int32Type.optional().describe('Sequence number for lifetime premium'),
	mutual_friends: z.array(UserPartialResponse).optional().describe('Array of mutual friends'),
	mutual_guilds: z.array(MutualGuildResponse).optional().describe('Array of mutual guilds'),
	connected_accounts: z.array(ConnectionResponse).optional().describe('Array of verified external connections'),
});
export type UserProfileFullResponse = z.infer<typeof UserProfileFullResponse>;

export const UserNotesRecordResponse = z
	.record(SnowflakeStringType, z.string())
	.describe('A map of user IDs to note text');
export type UserNotesRecordResponse = z.infer<typeof UserNotesRecordResponse>;

export const UserNoteResponse = z.object({
	note: z.string().describe('The note text for this user'),
});
export type UserNoteResponse = z.infer<typeof UserNoteResponse>;

export const PushSubscribeResponse = z.object({
	subscription_id: HexString32Type.describe('The unique identifier for the push subscription'),
});
export type PushSubscribeResponse = z.infer<typeof PushSubscribeResponse>;

export const PushSubscriptionItemResponse = z.object({
	subscription_id: HexString32Type.describe('The unique identifier for the push subscription'),
	user_agent: z.string().nullable().describe('The user agent that registered this subscription'),
});
export type PushSubscriptionItemResponse = z.infer<typeof PushSubscriptionItemResponse>;

export const PushSubscriptionsListResponse = z.object({
	subscriptions: z.array(PushSubscriptionItemResponse).describe('Array of push subscriptions'),
});
export type PushSubscriptionsListResponse = z.infer<typeof PushSubscriptionsListResponse>;

export const PreloadMessagesResponse = z
	.record(SnowflakeStringType, MessageResponseSchema.nullable())
	.describe('A map of channel IDs to the latest message in each channel');
export type PreloadMessagesResponse = z.infer<typeof PreloadMessagesResponse>;
