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

import { Inject, Injectable } from '@nestjs/common';
import { Not, IsNull, Brackets, DataSource, EntityManager } from 'typeorm';
import { bindThis } from '@/decorators.js';
import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js';
import type { MiUser } from '@/models/User.js';
import type {
	AntennasRepository,
	ClipNotesRepository,
	ClipsRepository,
	FollowingsRepository,
	FollowRequestsRepository,
	UserListMembershipsRepository,
	UserListsRepository,
	WebhooksRepository,
} from '@/models/_.js';
import { QueueService } from '@/core/QueueService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { LoggerService } from '@/core/LoggerService.js';

@Injectable()
export class UserSuspendService {
	public logger: Logger;

	constructor(
		@Inject(DI.db)
		private db: DataSource,

		@Inject(DI.followingsRepository)
		private followingsRepository: FollowingsRepository,

		@Inject(DI.followRequestsRepository)
		private followRequestsRepository: FollowRequestsRepository,

		@Inject(DI.antennasRepository)
		private antennasRepository: AntennasRepository,

		@Inject(DI.webhooksRepository)
		private webhooksRepository: WebhooksRepository,

		@Inject(DI.userListsRepository)
		private userListsRepository: UserListsRepository,

		@Inject(DI.clipsRepository)
		private clipsRepository: ClipsRepository,

		@Inject(DI.clipNotesRepository)
		private clipNotesRepository: ClipNotesRepository,

		@Inject(DI.userListMembershipsRepository)
		private userListMembershipsRepository: UserListMembershipsRepository,

		private queueService: QueueService,
		private globalEventService: GlobalEventService,
		private apRendererService: ApRendererService,
		private userEntityService: UserEntityService,
		private loggerService: LoggerService,
	) {
		this.logger = this.loggerService.getLogger('account:suspend');
	}

	@bindThis
	public async doPostSuspend(user: { id: MiUser['id']; host: MiUser['host'] }): Promise<void> {
		this.logger.warn(`doPostSuspend: ${user.id} (host: ${user.host})`);

		this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true });

		const promises: Promise<unknown>[] = [];

		let cursor = '';
		while (true) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition, no-constant-condition
			const clipNotes = await this.clipNotesRepository.createQueryBuilder('c')
				.select('c.id')
				.innerJoin('c.note', 'n')
				.where('n.userId = :userId', { userId: user.id })
				.andWhere('c.id > :cursor', { cursor })
				.orderBy('c.id', 'ASC')
				.limit(500)
				.getRawMany<{ id: string }>();

			if (clipNotes.length === 0) break;

			cursor = clipNotes.at(-1)?.id ?? '';

			promises.push(this.clipNotesRepository.createQueryBuilder()
				.delete()
				.where('id IN (:...ids)', { ids: clipNotes.map((clipNote) => clipNote.id) })
				.execute());
		}

		await Promise.allSettled([
			this.followRequestsRepository.delete({ followeeId: user.id }),
			this.followRequestsRepository.delete({ followerId: user.id }),

			this.antennasRepository.delete({ userId: user.id }),
			this.webhooksRepository.delete({ userId: user.id }),
			this.userListsRepository.delete({ userId: user.id }),
			this.clipsRepository.delete({ userId: user.id }),

			...promises,
			this.userListMembershipsRepository.delete({ userId: user.id }),
		]);

		if (this.userEntityService.isLocalUser(user)) {
			// 知り得る全SharedInboxにDelete配信
			const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user));

			this.logger.info(`Delivering delete activity to all shared inboxes of ${user.id}`);
			await this.db.transaction(async (transactionalEntityManager: EntityManager) => {
				const inboxesCte = transactionalEntityManager.createQueryBuilder()
					.select('distinct coalesce(following.followerSharedInbox, following.followeeSharedInbox)', 'inbox')
					.from(this.followingsRepository.metadata.targetName, 'following')
					.where(new Brackets((qb) => qb.where({ followerHost: Not(IsNull()) }).orWhere({ followeeHost: Not(IsNull()) })))
					.andWhere(new Brackets((qb) => qb.where({ followerSharedInbox: Not(IsNull()) }).orWhere({ followeeSharedInbox: Not(IsNull()) })))
					.orderBy('inbox');

				let offset = 0;
				let cursor = '';
				while (true) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition, no-constant-condition
					const inboxes = await transactionalEntityManager.createQueryBuilder().addCommonTableExpression(inboxesCte, 'inboxes')
						.select('inbox').from('inboxes', 'inboxes').where('inbox > :cursor', { cursor }).limit(500).getRawMany<{ inbox: string }>()
						.then((rows) => rows.map((row) => row.inbox));

					if (inboxes.length === 0) break;

					this.logger.info(`Delivering delete activity to ${offset} - ${offset + inboxes.length} shared inboxes of ${user.id}`);
					for (const inbox of inboxes) {
						try {
							this.queueService.deliver(user, content, inbox, true);
						} catch (err) {
							this.logger.error(`Failed to deliver delete activity to ${inbox}: ${err}`, { error: err, inbox });
						}
					}

					offset += inboxes.length;
					cursor = inboxes[inboxes.length - 1];
				}
			});

			this.logger.info(`Scheduled delete activity delivery to all shared inboxes of ${user.id}`);
		}
	}

	@bindThis
	public async doPostUnsuspend(user: MiUser): Promise<void> {
		this.logger.warn(`doPostUnsuspend: ${user.id}`);

		this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false });

		if (this.userEntityService.isLocalUser(user)) {
			// 知り得る全SharedInboxにUndo Delete配信
			const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user));

			this.logger.info(`Delivering undo delete activity to all shared inboxes of ${user.id}`);
			await this.db.transaction(async (transactionalEntityManager: EntityManager) => {
				const inboxesCte = transactionalEntityManager.createQueryBuilder()
					.select('distinct coalesce(following.followerSharedInbox, following.followeeSharedInbox)', 'inbox')
					.from(this.followingsRepository.metadata.targetName, 'following')
					.where(new Brackets((qb) => qb.where({ followerHost: Not(IsNull()) }).orWhere({ followeeHost: Not(IsNull()) })))
					.andWhere(new Brackets((qb) => qb.where({ followerSharedInbox: Not(IsNull()) }).orWhere({ followeeSharedInbox: Not(IsNull()) })))
					.orderBy('inbox');

				let offset = 0;
				let cursor = '';
				while (true) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition, no-constant-condition
					const inboxes = await transactionalEntityManager.createQueryBuilder().addCommonTableExpression(inboxesCte, 'inboxes')
						.select('inbox').from('inboxes', 'inboxes').where('inbox > :cursor', { cursor }).limit(500).getRawMany<{ inbox: string }>()
						.then((rows) => rows.map((row) => row.inbox));

					if (inboxes.length === 0) break;

					this.logger.info(`Delivering undo delete activity to ${offset} - ${offset + inboxes.length} shared inboxes of ${user.id}`);
					for (const inbox of inboxes) {
						try {
							this.queueService.deliver(user, content, inbox, true);
						} catch (err) {
							this.logger.error(`Failed to deliver undo delete activity to ${inbox}: ${err}`, { error: err, inbox });
						}
					}

					offset += inboxes.length;
					cursor = inboxes[inboxes.length - 1];
				}
			});

			this.logger.info(`Scheduled undo delete activity delivery to all shared inboxes of ${user.id}`);
		}
	}
}
