/*
 * SPDX-FileCopyrightText: syuilo and misskey-project
 * SPDX-License-Identifier: AGPL-3.0-only
 */

import { Injectable } from '@nestjs/common';
import { MiAbuseUserReport, MiNote, MiUser, MiWebhook } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { Packed } from '@/misc/json-schema.js';
import { type WebhookEventTypes } from '@/models/Webhook.js';
import { UserWebhookService } from '@/core/UserWebhookService.js';
import { QueueService } from '@/core/QueueService.js';

const oneDayMillis = 24 * 60 * 60 * 1000;

type AbuseUserReportDto = Omit<MiAbuseUserReport, 'targetUser' | 'reporter' | 'assignee'> & {
	targetUser: Packed<'UserLite'> | null,
	reporter: Packed<'UserLite'> | null,
	assignee: Packed<'UserLite'> | null,
};

function generateAbuseReport(override?: Partial<MiAbuseUserReport>): AbuseUserReportDto {
	const result: MiAbuseUserReport = {
		id: 'dummy-abuse-report1',
		targetUserId: 'dummy-target-user',
		targetUser: null,
		reporterId: 'dummy-reporter-user',
		reporter: null,
		assigneeId: null,
		assignee: null,
		resolved: false,
		forwarded: false,
		comment: 'This is a dummy report for testing purposes.',
		targetUserHost: null,
		reporterHost: null,
		resolvedAs: null,
		moderationNote: 'foo',
		...override,
	};

	return {
		...result,
		targetUser: result.targetUser ? toPackedUserLite(result.targetUser) : null,
		reporter: result.reporter ? toPackedUserLite(result.reporter) : null,
		assignee: result.assignee ? toPackedUserLite(result.assignee) : null,
	};
}

function generateDummyUser(override?: Partial<MiUser>): MiUser {
	return {
		id: 'dummy-user-1',
		updatedAt: new Date(Date.now() - oneDayMillis * 7),
		lastFetchedAt: new Date(Date.now() - oneDayMillis * 5),
		lastActiveDate: new Date(Date.now() - oneDayMillis * 3),
		hideOnlineStatus: false,
		username: 'dummy1',
		usernameLower: 'dummy1',
		name: 'DummyUser1',
		followersCount: 10,
		followingCount: 5,
		movedToUri: null,
		movedAt: null,
		alsoKnownAs: null,
		notesCount: 30,
		avatarId: null,
		avatar: null,
		bannerId: null,
		banner: null,
		avatarUrl: null,
		bannerUrl: null,
		avatarBlurhash: null,
		bannerBlurhash: null,
		avatarDecorations: [],
		tags: [],
		isSuspended: false,
		isLocked: false,
		isBot: false,
		isCat: true,
		isRoot: false,
		isExplorable: true,
		isHibernated: false,
		isDeleted: false,
		emojis: [],
		score: 0,
		host: null,
		inbox: null,
		sharedInbox: null,
		featured: null,
		uri: null,
		followersUri: null,
		token: null,
		...override,
	};
}

function generateDummyNote(override?: Partial<MiNote>): MiNote {
	return {
		id: 'dummy-note-1',
		replyId: null,
		reply: null,
		renoteId: null,
		renote: null,
		threadId: null,
		text: 'This is a dummy note for testing purposes.',
		name: null,
		cw: null,
		userId: 'dummy-user-1',
		user: null,
		localOnly: true,
		reactionAcceptance: 'likeOnly',
		renoteCount: 10,
		repliesCount: 5,
		clippedCount: 0,
		reactions: {},
		visibility: 'public',
		uri: null,
		url: null,
		fileIds: [],
		attachedFileTypes: [],
		visibleUserIds: [],
		mentions: [],
		mentionedRemoteUsers: '[]',
		reactionAndUserPairCache: [],
		emojis: [],
		tags: [],
		hasPoll: false,
		channelId: null,
		channel: null,
		userHost: null,
		replyUserId: null,
		replyUserHost: null,
		renoteUserId: null,
		renoteUserHost: null,
		renoteChannelId: null,
		...override,
	};
}

function toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Packed<'Note'> {
	return {
		id: note.id,
		createdAt: new Date().toISOString(),
		deletedAt: null,
		text: note.text,
		cw: note.cw,
		userId: note.userId,
		user: toPackedUserLite(note.user ?? generateDummyUser()),
		replyId: note.replyId,
		renoteId: note.renoteId,
		isHidden: false,
		visibility: note.visibility,
		mentions: note.mentions,
		visibleUserIds: note.visibleUserIds,
		fileIds: note.fileIds,
		files: [],
		tags: note.tags,
		poll: null,
		emojis: note.emojis,
		channelId: note.channelId,
		channel: note.channel,
		localOnly: note.localOnly,
		reactionAcceptance: note.reactionAcceptance,
		reactionEmojis: {},
		reactions: {},
		reactionCount: 0,
		renoteCount: note.renoteCount,
		repliesCount: note.repliesCount,
		uri: note.uri ?? undefined,
		url: note.url ?? undefined,
		reactionAndUserPairCache: note.reactionAndUserPairCache,
		...(detail ? {
			clippedCount: note.clippedCount,
			reply: note.reply ? toPackedNote(note.reply, false) : null,
			renote: note.renote ? toPackedNote(note.renote, true) : null,
			myReaction: null,
		} : {}),
		...override,
	};
}

function toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Packed<'UserLite'> {
	return {
		id: user.id,
		name: user.name,
		username: user.username,
		host: user.host,
		avatarUrl: user.avatarUrl,
		avatarBlurhash: user.avatarBlurhash,
		avatarDecorations: user.avatarDecorations.map(it => ({
			id: it.id,
			angle: it.angle,
			flipH: it.flipH,
			url: 'https://example.com/dummy-image001.png',
			offsetX: it.offsetX,
			offsetY: it.offsetY,
		})),
		isBot: user.isBot,
		isCat: user.isCat,
		emojis: user.emojis,
		onlineStatus: 'active',
		badgeRoles: [],
		...override,
	};
}

function toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Packed<'UserDetailedNotMe'> {
	return {
		...toPackedUserLite(user),
		url: null,
		uri: null,
		movedTo: null,
		alsoKnownAs: [],
		createdAt: new Date().toISOString(),
		updatedAt: user.updatedAt?.toISOString() ?? null,
		lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null,
		bannerUrl: user.bannerUrl,
		bannerBlurhash: user.bannerBlurhash,
		isLocked: user.isLocked,
		isSilenced: false,
		isSuspended: user.isSuspended,
		description: null,
		location: null,
		birthday: null,
		lang: null,
		fields: [],
		verifiedLinks: [],
		followersCount: user.followersCount,
		followingCount: user.followingCount,
		notesCount: user.notesCount,
		pinnedNoteIds: [],
		pinnedNotes: [],
		pinnedPageId: null,
		pinnedPage: null,
		publicReactions: true,
		followersVisibility: 'public',
		followingVisibility: 'public',
		twoFactorEnabled: false,
		usePasswordLessLogin: false,
		securityKeys: false,
		roles: [],
		memo: null,
		moderationNote: undefined,
		isFollowing: false,
		isFollowed: false,
		hasPendingFollowRequestFromYou: false,
		hasPendingFollowRequestToYou: false,
		isBlocking: false,
		isBlocked: false,
		isMuted: false,
		isRenoteMuted: false,
		notify: 'none',
		withReplies: true,
		...override,
	};
}

const dummyUser1 = generateDummyUser();
const dummyUser2 = generateDummyUser({
	id: 'dummy-user-2',
	updatedAt: new Date(Date.now() - oneDayMillis * 30),
	lastFetchedAt: new Date(Date.now() - oneDayMillis),
	lastActiveDate: new Date(Date.now() - oneDayMillis),
	username: 'dummy2',
	usernameLower: 'dummy2',
	name: 'DummyUser2',
	followersCount: 40,
	followingCount: 50,
	notesCount: 900,
});
const dummyUser3 = generateDummyUser({
	id: 'dummy-user-3',
	updatedAt: new Date(Date.now() - oneDayMillis * 15),
	lastFetchedAt: new Date(Date.now() - oneDayMillis * 2),
	lastActiveDate: new Date(Date.now() - oneDayMillis * 2),
	username: 'dummy3',
	usernameLower: 'dummy3',
	name: 'DummyUser3',
	followersCount: 60,
	followingCount: 70,
	notesCount: 15900,
});

@Injectable()
export class WebhookTestService {
	public static NoSuchWebhookError = class extends Error {
	};

	constructor(
		private userWebhookService: UserWebhookService,
		private systemWebhookService: SystemWebhookService,
		private queueService: QueueService,
	) {
	}

	/**
	 * UserWebhookのテスト送信を行う.
	 * 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない.
	 *
	 * また、この関数経由で送信されるWebhookは以下の設定を無視する.
	 * - Webhookそのものの有効・無効設定(active)
	 * - 送信対象イベント(on)に関する設定
	 */
	@bindThis
	public async testUserWebhook(
		params: {
			webhookId: MiWebhook['id'],
			type: WebhookEventTypes,
			override?: Partial<Omit<MiWebhook, 'id'>>,
		},
		sender: MiUser | null,
	) {
		const webhooks = await this.userWebhookService.fetchWebhooks({ ids: [params.webhookId] })
			.then(it => it.filter(it => it.userId === sender?.id));
		if (webhooks.length === 0) {
			throw new WebhookTestService.NoSuchWebhookError();
		}

		const webhook = webhooks[0];
		const send = (contents: unknown) => {
			const merged = {
				...webhook,
				...params.override,
			};

			// テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
			// また、Jobの試行回数も1回だけ.
			this.queueService.userWebhookDeliver(merged, params.type, contents, { attempts: 1 });
		};

		const dummyNote1 = generateDummyNote({
			userId: dummyUser1.id,
			user: dummyUser1,
		});
		const dummyReply1 = generateDummyNote({
			id: 'dummy-reply-1',
			replyId: dummyNote1.id,
			reply: dummyNote1,
			userId: dummyUser1.id,
			user: dummyUser1,
		});
		const dummyRenote1 = generateDummyNote({
			id: 'dummy-renote-1',
			renoteId: dummyNote1.id,
			renote: dummyNote1,
			userId: dummyUser2.id,
			user: dummyUser2,
			text: null,
		});
		const dummyMention1 = generateDummyNote({
			id: 'dummy-mention-1',
			userId: dummyUser1.id,
			user: dummyUser1,
			text: `@${dummyUser2.username} This is a mention to you.`,
			mentions: [dummyUser2.id],
		});

		switch (params.type) {
			case 'note': {
				send(toPackedNote(dummyNote1));
				break;
			}
			case 'reply': {
				send(toPackedNote(dummyReply1));
				break;
			}
			case 'renote': {
				send(toPackedNote(dummyRenote1));
				break;
			}
			case 'mention': {
				send(toPackedNote(dummyMention1));
				break;
			}
			case 'follow': {
				send(toPackedUserDetailedNotMe(dummyUser1));
				break;
			}
			case 'followed': {
				send(toPackedUserLite(dummyUser2));
				break;
			}
			case 'unfollow': {
				send(toPackedUserDetailedNotMe(dummyUser3));
				break;
			}
		}
	}

	/**
	 * SystemWebhookのテスト送信を行う.
	 * 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない.
	 *
	 * また、この関数経由で送信されるWebhookは以下の設定を無視する.
	 * - Webhookそのものの有効・無効設定(isActive)
	 * - 送信対象イベント(on)に関する設定
	 */
	@bindThis
	public async testSystemWebhook(
		params: {
			webhookId: MiSystemWebhook['id'],
			type: SystemWebhookEventType,
			override?: Partial<Omit<MiSystemWebhook, 'id'>>,
		},
	) {
		const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ ids: [params.webhookId] });
		if (webhooks.length === 0) {
			throw new WebhookTestService.NoSuchWebhookError();
		}

		const webhook = webhooks[0];
		const send = (contents: unknown) => {
			const merged = {
				...webhook,
				...params.override,
			};

			// テスト目的なのでSystemWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
			// また、Jobの試行回数も1回だけ.
			this.queueService.systemWebhookDeliver(merged, params.type, contents, { attempts: 1 });
		};

		switch (params.type) {
			case 'abuseReport': {
				send(generateAbuseReport({
					targetUserId: dummyUser1.id,
					targetUser: dummyUser1,
					reporterId: dummyUser2.id,
					reporter: dummyUser2,
				}));
				break;
			}
			case 'abuseReportResolved': {
				send(generateAbuseReport({
					targetUserId: dummyUser1.id,
					targetUser: dummyUser1,
					reporterId: dummyUser2.id,
					reporter: dummyUser2,
					assigneeId: dummyUser3.id,
					assignee: dummyUser3,
					resolved: true,
				}));
				break;
			}
			case 'userCreated': {
				send(toPackedUserLite(dummyUser1));
				break;
			}
		}
	}
}