/*
 * 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 * as GuildActionCreators from '@app/actions/GuildActionCreators';
import {Select, type SelectOption} from '@app/components/form/Select';
import {
	type AuditLogActionKind,
	DEFAULT_FOR_STRINGS_KEY,
	LOG_PAGE_SIZE,
} from '@app/components/modals/guild_tabs/GuildAuditLogTab.Constants';
import styles from '@app/components/modals/guild_tabs/GuildAuditLogTab.module.css';
import {getRendererTableForTarget} from '@app/components/modals/guild_tabs/GuildAuditLogTab.Renderers';
import {
	type AuditLogUserOption,
	buildUserOptions,
	formatTimestamp,
	renderEntrySummary,
	renderFallbackChangeDetail,
	renderOptionDetailSentence,
	resolveChannelLabel,
	resolveTargetLabel,
	shouldNotRenderChangeDetail,
	shouldShowFallbackChangeDetail,
} from '@app/components/modals/guild_tabs/GuildAuditLogTab.Utils';
import {EmptySlate} from '@app/components/modals/shared/EmptySlate';
import {Avatar} from '@app/components/uikit/Avatar';
import {Button} from '@app/components/uikit/button/Button';
import FocusRing from '@app/components/uikit/focus_ring/FocusRing';
import {MockAvatar} from '@app/components/uikit/MockAvatar';
import {Spinner} from '@app/components/uikit/Spinner';
import {
	AUDIT_LOG_ACTIONS,
	AUDIT_LOG_TARGET_TYPES,
	type AuditLogTargetType,
	getTranslatedAuditLogActions,
} from '@app/constants/AuditLogConstants';
import {Logger} from '@app/lib/Logger';
import GuildMemberStore from '@app/stores/GuildMemberStore';
import UserStore from '@app/stores/UserStore';
import {
	getActionKind,
	getTargetType,
	looksLikeSnowflake,
	normalizeChanges,
	resolveIdToName,
	safeScalarString,
	shouldSuppressDetailsForAction,
	toChangeShape,
} from '@app/utils/modals/guild_tabs/GuildAuditLogTabUtils';
import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType';
import type {GuildAuditLogEntryResponse} from '@fluxer/schema/src/domains/guild/GuildAuditLogSchemas';
import {Trans, useLingui} from '@lingui/react/macro';
import type {IconWeight} from '@phosphor-icons/react';
import {
	BuildingsIcon,
	CaretDownIcon,
	ClipboardTextIcon,
	FunnelSimpleIcon,
	GearIcon,
	HashIcon,
	LinkIcon,
	MinusIcon,
	PencilSimpleIcon,
	PlugIcon,
	PlusIcon,
	SmileyIcon,
	StampIcon,
	TagIcon,
	UserGearIcon,
	WarningCircleIcon,
} from '@phosphor-icons/react';
import clsx from 'clsx';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import type {ReactElement} from 'react';
import {useCallback, useEffect, useMemo, useState} from 'react';
import {type OptionProps, components as reactSelectComponents, type SingleValueProps} from 'react-select';

type IconComponent = React.ComponentType<{size?: number | string; weight?: IconWeight; className?: string}>;
type ChangeTone = 'add' | 'remove' | 'update';

const logger = new Logger('GuildAuditLogTab');

const actionIconMap: Partial<Record<AuditLogActionType, IconComponent>> = {
	[AuditLogActionType.GUILD_UPDATE]: GearIcon,
	[AuditLogActionType.CHANNEL_CREATE]: HashIcon,
	[AuditLogActionType.CHANNEL_UPDATE]: HashIcon,
	[AuditLogActionType.CHANNEL_DELETE]: HashIcon,
	[AuditLogActionType.CHANNEL_OVERWRITE_CREATE]: HashIcon,
	[AuditLogActionType.CHANNEL_OVERWRITE_UPDATE]: HashIcon,
	[AuditLogActionType.CHANNEL_OVERWRITE_DELETE]: HashIcon,
	[AuditLogActionType.MEMBER_KICK]: UserGearIcon,
	[AuditLogActionType.MEMBER_PRUNE]: UserGearIcon,
	[AuditLogActionType.MEMBER_BAN_ADD]: UserGearIcon,
	[AuditLogActionType.MEMBER_BAN_REMOVE]: UserGearIcon,
	[AuditLogActionType.MEMBER_UPDATE]: UserGearIcon,
	[AuditLogActionType.MEMBER_ROLE_UPDATE]: UserGearIcon,
	[AuditLogActionType.MEMBER_MOVE]: UserGearIcon,
	[AuditLogActionType.MEMBER_DISCONNECT]: UserGearIcon,
	[AuditLogActionType.BOT_ADD]: UserGearIcon,
	[AuditLogActionType.ROLE_CREATE]: TagIcon,
	[AuditLogActionType.ROLE_UPDATE]: TagIcon,
	[AuditLogActionType.ROLE_DELETE]: TagIcon,
	[AuditLogActionType.INVITE_CREATE]: LinkIcon,
	[AuditLogActionType.INVITE_UPDATE]: LinkIcon,
	[AuditLogActionType.INVITE_DELETE]: LinkIcon,
	[AuditLogActionType.WEBHOOK_CREATE]: PlugIcon,
	[AuditLogActionType.WEBHOOK_UPDATE]: PlugIcon,
	[AuditLogActionType.WEBHOOK_DELETE]: PlugIcon,
	[AuditLogActionType.EMOJI_CREATE]: SmileyIcon,
	[AuditLogActionType.EMOJI_UPDATE]: SmileyIcon,
	[AuditLogActionType.EMOJI_DELETE]: SmileyIcon,
	[AuditLogActionType.STICKER_CREATE]: StampIcon,
	[AuditLogActionType.STICKER_UPDATE]: StampIcon,
	[AuditLogActionType.STICKER_DELETE]: StampIcon,
	[AuditLogActionType.MESSAGE_DELETE]: PencilSimpleIcon,
	[AuditLogActionType.MESSAGE_BULK_DELETE]: PencilSimpleIcon,
	[AuditLogActionType.MESSAGE_PIN]: PencilSimpleIcon,
	[AuditLogActionType.MESSAGE_UNPIN]: PencilSimpleIcon,
};

const targetIconMap: Record<AuditLogTargetType, IconComponent> = {
	[AUDIT_LOG_TARGET_TYPES.ALL]: BuildingsIcon,
	[AUDIT_LOG_TARGET_TYPES.GUILD]: GearIcon,
	[AUDIT_LOG_TARGET_TYPES.MEMBER]: UserGearIcon,
	[AUDIT_LOG_TARGET_TYPES.CHANNEL]: HashIcon,
	[AUDIT_LOG_TARGET_TYPES.USER]: UserGearIcon,
	[AUDIT_LOG_TARGET_TYPES.ROLE]: TagIcon,
	[AUDIT_LOG_TARGET_TYPES.INVITE]: LinkIcon,
	[AUDIT_LOG_TARGET_TYPES.WEBHOOK]: PlugIcon,
	[AUDIT_LOG_TARGET_TYPES.EMOJI]: SmileyIcon,
	[AUDIT_LOG_TARGET_TYPES.STICKER]: StampIcon,
	[AUDIT_LOG_TARGET_TYPES.MESSAGE]: PencilSimpleIcon,
};

const getActionIcon = (actionType: AuditLogActionType): IconComponent => {
	const targetType = getTargetType(actionType);
	return targetIconMap[targetType as AuditLogTargetType] ?? actionIconMap[actionType] ?? BuildingsIcon;
};
const getActionOptionIcon = (value: string): IconComponent => {
	if (!value) return FunnelSimpleIcon;

	const action = AUDIT_LOG_ACTIONS.find((item) => item.value.toString() === value);
	if (!action) return FunnelSimpleIcon;

	const actionType = Number(value) as AuditLogActionType;
	const targetType = getTargetType(actionType);
	return targetIconMap[targetType as AuditLogTargetType] ?? getActionIcon(actionType);
};
const USER_FILTER_AVATAR_SIZE = 28;

const getActionSelectIconToneClass = (actionKind: AuditLogActionKind): string => {
	switch (actionKind) {
		case 'create':
			return styles.actionSelectIconCreate;
		case 'update':
			return styles.actionSelectIconUpdate;
		case 'delete':
			return styles.actionSelectIconDelete;
		default:
			return styles.actionSelectIconNeutral;
	}
};

const getChangeTone = (change: {key: string; oldValue: unknown; newValue: unknown}): ChangeTone => {
	if (change.key === '$remove') return 'remove';

	if (typeof change.newValue === 'boolean' && typeof change.oldValue === 'boolean') {
		return change.newValue ? 'add' : 'remove';
	}

	if (change.oldValue != null && change.newValue == null) return 'remove';

	return 'add';
};

const getOptionTone = (value: unknown): ChangeTone => {
	if (typeof value === 'boolean') return value ? 'add' : 'remove';
	return 'add';
};

const getChangeIcon = (tone: ChangeTone): IconComponent => {
	switch (tone) {
		case 'remove':
			return MinusIcon;
		default:
			return PlusIcon;
	}
};

const getChangeBulletToneClass = (tone: ChangeTone): string => {
	if (tone === 'remove') {
		return styles.changeBulletRemove;
	}

	return styles.changeBulletAdd;
};

type UserFilterOption = AuditLogUserOption | SelectOption<string>;
const isAuditLogUserOption = (option: UserFilterOption): option is AuditLogUserOption =>
	'user' in option && option.user != null;

const GuildAuditLogTab: React.FC<{guildId: string}> = observer(({guildId}) => {
	const {t, i18n} = useLingui();
	const [entries, setEntries] = useState<Array<GuildAuditLogEntryResponse>>([]);
	const [isLoading, setIsLoading] = useState(false);
	const [error, setError] = useState<string | null>(null);
	const [hasSuccessfulEmptyLoad, setHasSuccessfulEmptyLoad] = useState(false);
	const [selectedUserId, setSelectedUserId] = useState('');
	const [selectedAction, setSelectedAction] = useState('');
	const [hasMore, setHasMore] = useState(true);
	const [expandedEntryId, setExpandedEntryId] = useState<string | null>(null);

	const members = GuildMemberStore.getMembers(guildId);
	const userOptions = useMemo<Array<UserFilterOption>>(
		() => [{value: '', label: t`All Users`}, ...buildUserOptions(members)],
		[members],
	);

	const actionOptions = useMemo<Array<SelectOption<string>>>(
		() => [
			{value: '', label: t`All Actions`},
			...getTranslatedAuditLogActions(i18n).map((action) => ({
				value: action.value.toString(),
				label: action.label,
			})),
		],
		[],
	);

	const userSelectComponents = useMemo(() => {
		const renderContent = (option: UserFilterOption) => {
			if (!option.value) {
				return (
					<div className={styles.userSelectRowGlobal}>
						<span className={styles.userSelectLabel}>{option.label}</span>
					</div>
				);
			}

			return (
				<div className={styles.userSelectRow}>
					{isAuditLogUserOption(option) && (
						<div className={styles.userSelectAvatarWrapper}>
							<Avatar user={option.user} size={USER_FILTER_AVATAR_SIZE} guildId={guildId} />
						</div>
					)}
					<span className={styles.userSelectLabel}>{option.label}</span>
				</div>
			);
		};

		const OptionComponent = observer((props: OptionProps<UserFilterOption, false>) => (
			<reactSelectComponents.Option {...props}>{renderContent(props.data)}</reactSelectComponents.Option>
		));

		const SingleValueComponent = observer((props: SingleValueProps<UserFilterOption, false>) => (
			<reactSelectComponents.SingleValue {...props}>{renderContent(props.data)}</reactSelectComponents.SingleValue>
		));

		return {Option: OptionComponent, SingleValue: SingleValueComponent};
	}, [guildId]);

	const actionSelectComponents = useMemo(() => {
		const renderContent = (option: SelectOption<string>) => {
			const Icon = getActionOptionIcon(option.value);
			const actionKind = option.value ? getActionKind(Number(option.value) as AuditLogActionType) : null;
			const actionToneClass =
				actionKind != null ? getActionSelectIconToneClass(actionKind) : styles.actionSelectIconNeutral;

			return (
				<div className={styles.actionSelectRow}>
					<span className={clsx(styles.actionSelectIcon, actionToneClass)} aria-hidden>
						<Icon size={18} weight="bold" />
					</span>
					<span className={styles.actionSelectLabel}>{option.label}</span>
				</div>
			);
		};

		const OptionComponent = observer((props: OptionProps<SelectOption<string>, false>) => (
			<reactSelectComponents.Option {...props}>{renderContent(props.data)}</reactSelectComponents.Option>
		));

		const SingleValueComponent = observer((props: SingleValueProps<SelectOption<string>, false>) => (
			<reactSelectComponents.SingleValue {...props}>{renderContent(props.data)}</reactSelectComponents.SingleValue>
		));

		return {Option: OptionComponent, SingleValue: SingleValueComponent};
	}, []);

	const loadLogs = useCallback(
		async ({reset = false, before}: {reset?: boolean; before?: string | null} = {}) => {
			setIsLoading(true);
			setError(null);

			try {
				const actionType = selectedAction ? Number(selectedAction) : undefined;

				const response = await GuildActionCreators.fetchGuildAuditLogs(guildId, {
					limit: LOG_PAGE_SIZE,
					beforeLogId: reset ? undefined : (before ?? undefined),
					userId: selectedUserId || undefined,
					actionType: actionType ?? undefined,
				});

				const fetchedEntries = response.audit_log_entries;
				UserStore.cacheUsers(response.users);

				setEntries((current) => {
					const updatedEntries = reset ? fetchedEntries : [...current, ...fetchedEntries];
					setHasSuccessfulEmptyLoad(reset && updatedEntries.length === 0);
					return updatedEntries;
				});

				setHasMore(fetchedEntries.length === LOG_PAGE_SIZE);

				if (reset) setExpandedEntryId(null);
			} catch (err) {
				logger.error('Failed to load audit logs', err);
				setError(t`Something went wrong while loading the audit log.`);
				setHasSuccessfulEmptyLoad(false);
			} finally {
				setIsLoading(false);
			}
		},
		[guildId, selectedAction, selectedUserId],
	);

	useEffect(() => {
		loadLogs({reset: true});
	}, [loadLogs]);

	useEffect(() => {
		if (entries.length === 0) {
			return;
		}
		const userIds = new Set<string>();
		for (const entry of entries) {
			if (entry.user_id) {
				userIds.add(entry.user_id);
			}
			if (entry.target_id) {
				userIds.add(entry.target_id);
			}
		}
		if (userIds.size > 0) {
			GuildMemberStore.ensureMembersLoaded(guildId, Array.from(userIds)).catch((error) => {
				logger.error('Failed to ensure members', error);
			});
		}
	}, [guildId, entries]);

	const shouldShowErrorState = Boolean(error);
	const errorDescription = error ?? t`Something went wrong while loading the audit log.`;

	const shouldShowEmptyState = !shouldShowErrorState && !isLoading && entries.length === 0 && hasSuccessfulEmptyLoad;

	const handleLoadMore = useCallback(() => {
		if (!hasMore || entries.length === 0) return;
		loadLogs({before: entries[entries.length - 1].id});
	}, [entries, hasMore, loadLogs]);

	const toggleExpanded = (id: string) => setExpandedEntryId((current) => (current === id ? null : id));

	return (
		<div className={styles.container}>
			<div className={styles.headerTop}>
				<h2 className={styles.pageTitle}>{t`Activity Log`}</h2>
				<p className={styles.pageSubtitle}>{t`Track moderator actions across the community.`}</p>
			</div>

			<div className={styles.filterRow}>
				<Select
					value={selectedUserId}
					onChange={(value) => setSelectedUserId(value)}
					options={userOptions}
					placeholder={t`All Users`}
					components={userSelectComponents}
					label={t`Filter by user`}
				/>
				<Select
					value={selectedAction}
					onChange={(value) => setSelectedAction(value)}
					options={actionOptions}
					placeholder={t`All Actions`}
					label={t`Filter by action`}
					components={actionSelectComponents}
				/>
			</div>

			<div className={styles.entries}>
				{isLoading && entries.length === 0 && (
					<div className={styles.spinnerRow}>
						<Spinner size="large" />
					</div>
				)}

				{entries.length > 0 && !shouldShowErrorState && (
					<div className={styles.entryList}>
						{entries.map((entry) => {
							const entryId = entry.id;

							const targetType = getTargetType(entry.action_type as AuditLogActionType);
							const actionKind = getActionKind(entry.action_type as AuditLogActionType);

							const targetClassKey = `target_${targetType}` as keyof typeof styles;
							const actionClassKey = `type_${actionKind}` as keyof typeof styles;

							const entryClasses = clsx(styles.auditLog, styles[targetClassKey], styles[actionClassKey]);

							const ActionIcon = getActionIcon(entry.action_type as AuditLogActionType);

							const actorUser = entry.user_id ? (UserStore.getUser(entry.user_id) ?? null) : null;
							const targetUser = entry.target_id ? (UserStore.getUser(entry.target_id) ?? null) : null;

							const targetLabel = resolveTargetLabel(entry, i18n);
							const channelLabel = resolveChannelLabel(entry, guildId, i18n);

							const summaryNode = renderEntrySummary({
								entry,
								actorUser,
								targetUser,
								targetLabel,
								channelLabel,
								guildId,
								i18n,
							});

							const suppressDetails = shouldSuppressDetailsForAction(entry.action_type as AuditLogActionType);
							const changeShapes = suppressDetails
								? []
								: normalizeChanges(entry.changes)
										.map(toChangeShape)
										.filter((change) => change.key && !shouldNotRenderChangeDetail(targetType, change.key));

							const rendererTable = getRendererTableForTarget(targetType);

							const renderedChangeKeys = new Set(
								changeShapes
									.filter((change) => rendererTable[change.key]?.(change, {entry, guildId, i18n}) != null)
									.map((change) => change.key),
							);
							const optionEntries =
								suppressDetails || !entry.options
									? []
									: Object.entries(entry.options).filter(([key, value]) => {
											if (key === DEFAULT_FOR_STRINGS_KEY) return false;
											if (renderedChangeKeys.has(key)) return false;
											if (
												key !== 'channel_id' &&
												key !== 'message_id' &&
												key !== 'inviter_id' &&
												(key === 'id' || key.endsWith('_id'))
											)
												return false;
											const scalar = safeScalarString(value, i18n);
											if (scalar && looksLikeSnowflake(scalar)) {
												return resolveIdToName(scalar, guildId) != null;
											}
											return true;
										});

							const reasonText =
								typeof entry.reason === 'string' && entry.reason.trim()
									? entry.reason.trim()
									: t`No reason was provided.`;

							const changeRows = changeShapes
								.map((change, changeIndex) => {
									const renderer = rendererTable[change.key];
									const rendered = renderer?.(change, {entry, guildId, i18n});
									if (!rendered && !shouldShowFallbackChangeDetail(change)) {
										return null;
									}
									const tone = getChangeTone(change);
									const ChangeIcon = getChangeIcon(tone);
									const toneClass = getChangeBulletToneClass(tone);

									return (
										<div className={styles.changeItem} key={`${entryId}-${change.key}-${changeIndex}`}>
											<span className={clsx(styles.changeBullet, toneClass)} aria-hidden>
												<ChangeIcon size={12} weight="bold" className={styles.changeBulletIcon} />
											</span>
											<span className={styles.changeText}>
												{rendered ?? renderFallbackChangeDetail(change, guildId, i18n)}
											</span>
										</div>
									);
								})
								.filter((row): row is ReactElement => row !== null);

							const shouldShowReasonPreview = typeof entry.reason === 'string' && entry.reason.trim().length > 0;
							const isExpandable = shouldShowReasonPreview || changeRows.length > 0 || optionEntries.length > 0;
							const isExpandedView = isExpandable && expandedEntryId === entryId;

							const headerClasses = clsx(styles.header, {
								[styles.headerClickable]: isExpandable,
								[styles.headerStatic]: !isExpandable,
								[styles.headerExpanded]: isExpandedView,
								[styles.headerDefault]: !isExpandedView,
							});

							return (
								<div key={entryId} className={entryClasses}>
									{isExpandable ? (
										<FocusRing offset={-2}>
											<button
												type="button"
												onClick={() => toggleExpanded(entryId)}
												className={headerClasses}
												aria-expanded={isExpandedView}
											>
												<span className={styles.icon} aria-hidden>
													<ActionIcon size={20} weight="bold" className={styles.iconGlyph} />
												</span>

												<div className={styles.avatar}>
													{actorUser ? (
														<Avatar user={actorUser} size={32} guildId={guildId} />
													) : (
														<MockAvatar size={32} userTag={entry.user_id ?? 'Unknown user'} />
													)}
												</div>

												<div className={styles.textBlock}>
													<div className={styles.titleRow}>
														<span className={styles.summary}>{summaryNode}</span>
													</div>
													<div className={styles.metaRow}>
														<span className={styles.timestamp}>{formatTimestamp(entry.id)}</span>
													</div>
												</div>

												<CaretDownIcon
													size={20}
													weight="bold"
													className={clsx(styles.chevron, {[styles.chevronExpanded]: isExpandedView})}
												/>
											</button>
										</FocusRing>
									) : (
										<div className={headerClasses}>
											<span className={styles.icon} aria-hidden>
												<ActionIcon size={20} weight="bold" className={styles.iconGlyph} />
											</span>

											<div className={styles.avatar}>
												{actorUser ? (
													<Avatar user={actorUser} size={32} guildId={guildId} />
												) : (
													<MockAvatar size={32} userTag={entry.user_id ?? 'Unknown user'} />
												)}
											</div>

											<div className={styles.textBlock}>
												<div className={styles.titleRow}>
													<span className={styles.summary}>{summaryNode}</span>
												</div>
												<div className={styles.metaRow}>
													<span className={styles.timestamp}>{formatTimestamp(entry.id)}</span>
												</div>
											</div>
										</div>
									)}

									{isExpandedView && (
										<div className={styles.details}>
											{shouldShowReasonPreview && (
												<div className={styles.reasonRow}>
													<span className={styles.reasonLabel}>
														<Trans>Reason</Trans>
													</span>
													<span className={styles.reasonValue}>{reasonText}</span>
												</div>
											)}

											{(changeRows.length > 0 || optionEntries.length > 0) && (
												<div className={styles.changeList}>
													{changeRows}
													{optionEntries.map(([key, value]) => (
														<div className={styles.changeItem} key={key}>
															{(() => {
																const tone = getOptionTone(value);
																const ChangeIcon = getChangeIcon(tone);
																const toneClass = getChangeBulletToneClass(tone);
																return (
																	<span className={clsx(styles.changeBullet, toneClass)} aria-hidden>
																		<ChangeIcon size={12} weight="bold" className={styles.changeBulletIcon} />
																	</span>
																);
															})()}
															<span className={styles.changeText}>
																{renderOptionDetailSentence(
																	key,
																	value,
																	guildId,
																	entry.action_type as AuditLogActionType,
																	i18n,
																)}
															</span>
														</div>
													))}
												</div>
											)}
										</div>
									)}
								</div>
							);
						})}
					</div>
				)}

				{shouldShowEmptyState && (
					<div className={styles.emptyState}>
						<EmptySlate
							Icon={ClipboardTextIcon}
							title={t`No Logs Yet`}
							description={t`When moderators begin moderating, you can moderate the moderation here.`}
						/>
					</div>
				)}

				{!isLoading && shouldShowErrorState && (
					<div className={styles.errorState}>
						<EmptySlate
							Icon={WarningCircleIcon}
							title={t`Unable to Load Activity Logs`}
							description={errorDescription}
						/>
						<div className={styles.statusActions}>
							<Button variant="secondary" onClick={() => loadLogs({reset: true})}>
								{t`Retry`}
							</Button>
						</div>
					</div>
				)}

				{hasMore && entries.length > 0 && !shouldShowErrorState && (
					<div className={styles.loadMore}>
						<Button onClick={handleLoadMore} submitting={isLoading}>
							{t`Load more`}
						</Button>
					</div>
				)}
			</div>
		</div>
	);
});

export default GuildAuditLogTab;
