/*
 * 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 type {I18n} from '@lingui/core';
import {msg} from '@lingui/core/macro';
import * as ModalActionCreators from '~/actions/ModalActionCreators';
import {modal} from '~/actions/ModalActionCreators';
import * as ReadStateActionCreators from '~/actions/ReadStateActionCreators';
import {APIErrorCodes, type JumpTypes, MAX_MESSAGES_PER_CHANNEL, MessageFlags} from '~/Constants';
import {FeatureTemporarilyDisabledModal} from '~/components/alerts/FeatureTemporarilyDisabledModal';
import {MessageDeleteFailedModal} from '~/components/alerts/MessageDeleteFailedModal';
import {MessageDeleteTooQuickModal} from '~/components/alerts/MessageDeleteTooQuickModal';
import {ConfirmModal} from '~/components/modals/ConfirmModal';
import {Endpoints} from '~/Endpoints';
import type {JumpOptions} from '~/lib/ChannelMessages';
import http, {HttpError} from '~/lib/HttpClient';
import {Logger} from '~/lib/Logger';
import MessageQueue from '~/lib/MessageQueue';
import type {
	AllowedMentions,
	Message,
	MessageRecord,
	MessageReference,
	MessageStickerItem,
} from '~/records/MessageRecord';
import AuthenticationStore from '~/stores/AuthenticationStore';
import ChannelStore from '~/stores/ChannelStore';
import DeveloperOptionsStore from '~/stores/DeveloperOptionsStore';
import GuildMemberStore from '~/stores/GuildMemberStore';
import MessageEditMobileStore from '~/stores/MessageEditMobileStore';
import MessageEditStore from '~/stores/MessageEditStore';
import MessageReferenceStore from '~/stores/MessageReferenceStore';
import MessageReplyStore from '~/stores/MessageReplyStore';
import MessageStore from '~/stores/MessageStore';
import ReadStateStore from '~/stores/ReadStateStore';
import * as SnowflakeUtils from '~/utils/SnowflakeUtils';

const logger = new Logger('MessageActionCreators');

const pendingDeletePromises = new Map<string, Promise<void>>();
const pendingFetchPromises = new Map<string, Promise<Array<Message>>>();

function makeFetchKey(
	channelId: string,
	before: string | null,
	after: string | null,
	limit: number,
	jump?: JumpOptions,
): string {
	return JSON.stringify({
		channelId,
		before,
		after,
		limit,
		jump: jump
			? {
					present: !!jump.present,
					messageId: jump.messageId ?? null,
					offset: jump.offset ?? 0,
					flash: !!jump.flash,
					returnMessageId: jump.returnMessageId ?? null,
					jumpType: jump.jumpType ?? null,
				}
			: null,
	});
}

async function requestMissingGuildMembers(channelId: string, messages: Array<Message>): Promise<void> {
	const channel = ChannelStore.getChannel(channelId);
	if (!channel?.guildId) {
		return;
	}

	const guildId = channel.guildId;
	const currentUserId = AuthenticationStore.currentUserId;

	const authorIds = messages
		.filter((msg) => !msg.webhook_id && msg.author.id !== currentUserId)
		.map((msg) => msg.author.id);

	if (authorIds.length === 0) {
		return;
	}

	await GuildMemberStore.ensureMembersLoaded(guildId, authorIds);
}

interface SendMessageParams {
	content: string;
	nonce: string;
	hasAttachments?: boolean;
	allowedMentions?: AllowedMentions;
	messageReference?: MessageReference;
	flags?: number;
	favoriteMemeId?: string;
	stickers?: Array<MessageStickerItem>;
	tts?: boolean;
}

export const jumpToPresent = (channelId: string, limit = MAX_MESSAGES_PER_CHANNEL): void => {
	logger.debug(`Jumping to present in channel ${channelId}`);
	ReadStateActionCreators.clearStickyUnread(channelId);

	const jump: JumpOptions = {
		present: true,
	};

	if (MessageStore.hasPresent(channelId)) {
		MessageStore.handleLoadMessagesSuccessCached({channelId, jump, limit});
	} else {
		fetchMessages(channelId, null, null, limit, jump);
	}
};

export const jumpToMessage = (
	channelId: string,
	messageId: string,
	flash = true,
	offset?: number,
	returnTargetId?: string,
	jumpType?: JumpTypes,
): void => {
	logger.debug(`Jumping to message ${messageId} in channel ${channelId}`);

	fetchMessages(channelId, null, null, MAX_MESSAGES_PER_CHANNEL, {
		messageId,
		flash,
		offset,
		returnMessageId: returnTargetId,
		jumpType,
	});
};

const tryFetchMessagesCached = (
	channelId: string,
	before: string | null,
	after: string | null,
	limit: number,
	jump?: JumpOptions,
): boolean => {
	const messages = MessageStore.getMessages(channelId);

	if (jump?.messageId && messages.has(jump.messageId, true)) {
		MessageStore.handleLoadMessagesSuccessCached({channelId, jump, limit});
		return true;
	} else if (before && messages.hasBeforeCached(before)) {
		MessageStore.handleLoadMessagesSuccessCached({channelId, before, limit});
		return true;
	} else if (after && messages.hasAfterCached(after)) {
		MessageStore.handleLoadMessagesSuccessCached({channelId, after, limit});
		return true;
	}

	return false;
};

export const fetchMessages = async (
	channelId: string,
	before: string | null,
	after: string | null,
	limit: number,
	jump?: JumpOptions,
): Promise<Array<Message>> => {
	const key = makeFetchKey(channelId, before, after, limit, jump);
	const inFlight = pendingFetchPromises.get(key);
	if (inFlight) {
		logger.debug(`Using in-flight fetchMessages for channel ${channelId} (deduped)`);
		return inFlight;
	}

	if (tryFetchMessagesCached(channelId, before, after, limit, jump)) {
		return [];
	}

	const promise = (async () => {
		if (DeveloperOptionsStore.slowMessageLoad) {
			logger.debug('Slow message load enabled, delaying by 3 seconds');
			await new Promise((resolve) => setTimeout(resolve, 3000));
		}

		MessageStore.handleLoadMessages({channelId, jump});

		try {
			const timeStart = Date.now();
			logger.debug(`Fetching messages for channel ${channelId}`);

			const around = jump?.messageId;
			const response = await http.get<Array<Message>>({
				url: Endpoints.CHANNEL_MESSAGES(channelId),
				query: {before, after, limit, around: around ?? null},
				retries: 2,
			});
			const messages = response.body ?? [];

			const isBefore = before != null;
			const isAfter = after != null;
			const isReplacement = before == null && after == null;

			const halfLimit = Math.floor(limit / 2);
			let hasMoreBefore = around != null || (messages.length === limit && (isBefore || isReplacement));
			let hasMoreAfter = around != null || (isAfter && messages.length === limit);

			if (around) {
				const knownLatestMessageId =
					ReadStateStore.lastMessageId(channelId) ?? ChannelStore.getChannel(channelId)?.lastMessageId ?? null;
				const newestFetchedMessageId = messages[0]?.id ?? null;
				const targetIndex = messages.findIndex((msg: Message) => msg.id === around);
				const pageFilled = messages.length === limit;

				if (targetIndex === -1) {
					logger.warn(`Target message ${around} not found in response!`);
				} else {
					const messagesNewerThanTarget = targetIndex;
					const messagesOlderThanTarget = messages.length - targetIndex - 1;
					const isAtKnownLatest = newestFetchedMessageId != null && newestFetchedMessageId === knownLatestMessageId;

					hasMoreBefore = pageFilled || messagesOlderThanTarget >= halfLimit;
					hasMoreAfter = pageFilled || (messagesNewerThanTarget >= halfLimit && !isAtKnownLatest);

					logger.debug(
						`Jump to message ${around}: targetIndex=${targetIndex}, messagesNewer=${messagesNewerThanTarget}, messagesOlder=${messagesOlderThanTarget}, pageFilled=${pageFilled}, hasMoreBefore=${hasMoreBefore}, hasMoreAfter=${hasMoreAfter}, limit=${limit}, knownLatestMessageId=${knownLatestMessageId}, newestFetched=${newestFetchedMessageId}`,
					);
				}
			}

			logger.info(`Fetched ${messages.length} messages for channel ${channelId}, took ${Date.now() - timeStart}ms`);

			MessageStore.handleLoadMessagesSuccess({
				channelId,
				messages,
				isBefore,
				isAfter,
				hasMoreBefore,
				hasMoreAfter,
				cached: false,
				jump,
			});
			ReadStateStore.handleLoadMessages({
				channelId,
				isAfter,
				messages,
			});
			MessageReferenceStore.handleMessagesFetchSuccess(channelId, messages);

			void requestMissingGuildMembers(channelId, messages);

			return messages;
		} catch (error) {
			logger.error(`Failed to fetch messages for channel ${channelId}:`, error);
			MessageStore.handleLoadMessagesFailure({channelId});
			return [];
		}
	})();

	pendingFetchPromises.set(key, promise);
	promise.finally(() => pendingFetchPromises.delete(key));
	return promise;
};

export const send = async (channelId: string, params: SendMessageParams): Promise<Message> => {
	const promise = new Promise<Message>((resolve, reject) => {
		logger.debug(`Enqueueing message for channel ${channelId}`);

		MessageQueue.enqueue(
			{
				type: 'send',
				channelId,
				nonce: params.nonce,
				content: params.content,
				hasAttachments: params.hasAttachments,
				allowedMentions: params.allowedMentions,
				messageReference: params.messageReference,
				flags: params.flags,
				favoriteMemeId: params.favoriteMemeId,
				stickers: params.stickers,
				tts: params.tts,
			},
			(result, error) => {
				if (result?.body) {
					logger.debug(`Message sent successfully in channel ${channelId}`);
					resolve(result.body);
				} else {
					const reason = error ?? new Error('Message send failed');
					logger.error(`Message send failed in channel ${channelId}`, reason);
					reject(reason);
				}
			},
		);
	});

	promise.catch((error) => {
		logger.error(`Unhandled message send rejection in channel ${channelId}`, error);
	});

	return promise;
};

export const edit = async (
	channelId: string,
	messageId: string,
	content?: string,
	flags?: number,
): Promise<Message> => {
	const promise = new Promise<Message>((resolve, reject) => {
		logger.debug(`Enqueueing edit for message ${messageId} in channel ${channelId}`);

		MessageQueue.enqueue(
			{
				type: 'edit',
				channelId,
				messageId,
				content,
				flags,
			},
			(result, error) => {
				if (result?.body) {
					logger.debug(`Message edited successfully: ${messageId} in channel ${channelId}`);
					resolve(result.body);
				} else {
					const reason = error ?? new Error('Message edit failed');
					logger.error(`Message edit failed: ${messageId} in channel ${channelId}`, reason);
					reject(reason);
				}
			},
		);
	});

	promise.catch((error) => {
		logger.error(`Unhandled message edit rejection for ${messageId} in channel ${channelId}`, error);
	});

	return promise;
};

export const remove = async (channelId: string, messageId: string): Promise<void> => {
	const pendingPromise = pendingDeletePromises.get(messageId);
	if (pendingPromise) {
		logger.debug(`Using in-flight delete request for message ${messageId}`);
		return pendingPromise;
	}

	const deletePromise = (async () => {
		try {
			logger.debug(`Deleting message ${messageId} in channel ${channelId}`);
			await http.delete({url: Endpoints.CHANNEL_MESSAGE(channelId, messageId)});
			logger.debug(`Successfully deleted message ${messageId} in channel ${channelId}`);
		} catch (error) {
			logger.error(`Failed to delete message ${messageId} in channel ${channelId}:`, error);

			if (error instanceof HttpError) {
				const {status, body} = error;
				const errorCode =
					typeof body === 'object' && body != null && 'code' in body ? (body as {code?: string}).code : undefined;

				if (status === 429) {
					ModalActionCreators.push(modal(() => <MessageDeleteTooQuickModal />));
				} else if (status === 403 && errorCode === APIErrorCodes.FEATURE_TEMPORARILY_DISABLED) {
					ModalActionCreators.push(modal(() => <FeatureTemporarilyDisabledModal />));
				} else if (status === 404) {
					logger.debug(`Message ${messageId} was already deleted (404 response)`);
				} else {
					ModalActionCreators.push(modal(() => <MessageDeleteFailedModal />));
				}
			} else {
				ModalActionCreators.push(modal(() => <MessageDeleteFailedModal />));
			}

			throw error;
		} finally {
			pendingDeletePromises.delete(messageId);
		}
	})();

	pendingDeletePromises.set(messageId, deletePromise);
	return deletePromise;
};

interface ShowDeleteConfirmationOptions {
	message: MessageRecord;
	onDelete?: () => void;
}

export const showDeleteConfirmation = (i18n: I18n, {message, onDelete}: ShowDeleteConfirmationOptions): void => {
	ModalActionCreators.push(
		modal(() => (
			<ConfirmModal
				title={i18n._(msg`Delete Message`)}
				description={i18n._(msg`This will create a rift in the space-time continuum and cannot be undone.`)}
				message={message}
				primaryText={i18n._(msg`Delete`)}
				onPrimary={() => {
					remove(message.channelId, message.id);
					onDelete?.();
				}}
			/>
		)),
	);
};

export const deleteLocal = (channelId: string, messageId: string): void => {
	logger.debug(`Deleting message ${messageId} locally in channel ${channelId}`);
	MessageStore.handleMessageDelete({id: messageId, channelId});
};

export const revealMessage = (channelId: string, messageId: string | null): void => {
	logger.debug(`Revealing message ${messageId} in channel ${channelId}`);
	MessageStore.handleMessageReveal({channelId, messageId});
};

export const startReply = (channelId: string, messageId: string, mentioning: boolean): void => {
	logger.debug(`Starting reply to message ${messageId} in channel ${channelId}, mentioning=${mentioning}`);
	MessageReplyStore.startReply(channelId, messageId, mentioning);
};

export const stopReply = (channelId: string): void => {
	logger.debug(`Stopping reply in channel ${channelId}`);
	MessageReplyStore.stopReply(channelId);
};

export const setReplyMentioning = (channelId: string, mentioning: boolean): void => {
	logger.debug(`Setting reply mentioning in channel ${channelId}: ${mentioning}`);
	MessageReplyStore.setMentioning(channelId, mentioning);
};

export const startEdit = (channelId: string, messageId: string, initialContent: string): void => {
	logger.debug(`Starting edit for message ${messageId} in channel ${channelId}`);
	MessageEditStore.startEditing(channelId, messageId, initialContent);
};

export const stopEdit = (channelId: string): void => {
	logger.debug(`Stopping edit in channel ${channelId}`);
	MessageEditStore.stopEditing(channelId);
};

export const startEditMobile = (channelId: string, messageId: string): void => {
	logger.debug(`Starting mobile edit for message ${messageId} in channel ${channelId}`);
	MessageEditMobileStore.startEditingMobile(channelId, messageId);
};

export const stopEditMobile = (channelId: string): void => {
	logger.debug(`Stopping mobile edit in channel ${channelId}`);
	MessageEditMobileStore.stopEditingMobile(channelId);
};

export const createOptimistic = (channelId: string, message: Message): void => {
	logger.debug(`Creating optimistic message in channel ${channelId}`);
	MessageStore.handleIncomingMessage({channelId, message});
};

export const deleteOptimistic = (channelId: string, messageId: string): void => {
	logger.debug(`Deleting optimistic message ${messageId} in channel ${channelId}`);
	MessageStore.handleMessageDelete({channelId, id: messageId});
};

export const sendError = (channelId: string, nonce: string): void => {
	logger.debug(`Message send error for nonce ${nonce} in channel ${channelId}`);
	MessageStore.handleSendFailed({channelId, nonce});
};

export const editOptimistic = (
	channelId: string,
	messageId: string,
	content: string,
): {originalContent: string; originalEditedTimestamp: string | null} | null => {
	logger.debug(`Applying optimistic edit for message ${messageId} in channel ${channelId}`);
	return MessageStore.handleOptimisticEdit({channelId, messageId, content});
};

export const editRollback = (
	channelId: string,
	messageId: string,
	originalContent: string,
	originalEditedTimestamp: string | null,
): void => {
	logger.debug(`Rolling back edit for message ${messageId} in channel ${channelId}`);
	MessageStore.handleEditRollback({channelId, messageId, originalContent, originalEditedTimestamp});
};

export const forward = async (
	channelIds: Array<string>,
	messageReference: {message_id: string; channel_id: string; guild_id?: string | null},
	optionalMessage?: string,
): Promise<void> => {
	logger.debug(`Forwarding message ${messageReference.message_id} to ${channelIds.length} channels`);

	try {
		for (const channelId of channelIds) {
			const nonce = SnowflakeUtils.fromTimestamp(Date.now());
			await send(channelId, {
				content: '',
				nonce,
				messageReference: {
					message_id: messageReference.message_id,
					channel_id: messageReference.channel_id,
					guild_id: messageReference.guild_id || undefined,
					type: 1,
				},
				flags: 1,
			});

			if (optionalMessage) {
				const commentNonce = SnowflakeUtils.fromTimestamp(Date.now() + 1);
				await send(channelId, {
					content: optionalMessage,
					nonce: commentNonce,
				});
			}
		}
		logger.debug('Successfully forwarded message to all channels');
	} catch (error) {
		logger.error('Failed to forward message:', error);
		throw error;
	}
};

export const toggleSuppressEmbeds = async (
	channelId: string,
	messageId: string,
	currentFlags: number,
): Promise<void> => {
	try {
		const isSuppressed = (currentFlags & MessageFlags.SUPPRESS_EMBEDS) === MessageFlags.SUPPRESS_EMBEDS;
		const newFlags = isSuppressed
			? currentFlags & ~MessageFlags.SUPPRESS_EMBEDS
			: currentFlags | MessageFlags.SUPPRESS_EMBEDS;

		logger.debug(`${isSuppressed ? 'Unsuppressing' : 'Suppressing'} embeds for message ${messageId}`);

		await http.patch<Message>({
			url: Endpoints.CHANNEL_MESSAGE(channelId, messageId),
			body: {flags: newFlags},
		});

		logger.debug(`Successfully ${isSuppressed ? 'unsuppressed' : 'suppressed'} embeds for message ${messageId}`);
	} catch (error) {
		logger.error('Failed to toggle suppress embeds:', error);
		throw error;
	}
};

export const deleteAttachment = async (channelId: string, messageId: string, attachmentId: string): Promise<void> => {
	try {
		logger.debug(`Deleting attachment ${attachmentId} from message ${messageId}`);

		await http.delete({
			url: Endpoints.CHANNEL_MESSAGE_ATTACHMENT(channelId, messageId, attachmentId),
		});

		logger.debug(`Successfully deleted attachment ${attachmentId} from message ${messageId}`);
	} catch (error) {
		logger.error('Failed to delete attachment:', error);
		throw error;
	}
};
