// SPDX-License-Identifier: AGPL-3.0-or-later

import {UnknownGuildError} from '@fluxer/errors/src/domains/guild/UnknownGuildError';
import {UnknownApplicationError} from '@fluxer/errors/src/domains/oauth/UnknownApplicationError';
import {UnknownUserError} from '@fluxer/errors/src/domains/user/UnknownUserError';
import type {
	ApplicationAdminResponse,
	ListGuildApplicationsRequest,
	ListGuildApplicationsResponse,
	ListUserApplicationsRequest,
	ListUserApplicationsResponse,
	LookupApplicationRequest,
	LookupApplicationResponse,
	TransferApplicationOwnershipRequest,
} from '@fluxer/schema/src/domains/admin/AdminApplicationSchemas';
import type {ApiContext} from '../../ApiContext';
import {createApplicationID, createGuildID, createUserID, type UserID} from '../../BrandedTypes';
import type {IGuildRepositoryAggregate} from '../../guild/repositories/IGuildRepositoryAggregate';
import type {Application} from '../../models/Application';
import type {IApplicationRepository} from '../../oauth/repositories/IApplicationRepository';
import type {AdminAuditService} from './AdminAuditService';

interface AdminApplicationServiceDeps {
	apiContext: ApiContext;
	applicationRepository: IApplicationRepository;
	auditService: AdminAuditService;
	guildRepository: IGuildRepositoryAggregate;
}

interface UserDisplay {
	username: string | null;
	global_name: string | null;
	discriminator: string | null;
}

export class AdminApplicationService {
	constructor(private readonly deps: AdminApplicationServiceDeps) {}

	async lookupApplication(data: LookupApplicationRequest): Promise<LookupApplicationResponse> {
		const {applicationRepository} = this.deps;
		const applicationId = createApplicationID(data.application_id);
		const application = await applicationRepository.getApplication(applicationId);
		if (!application) {
			return {application: null};
		}
		const userDisplays = await this.loadUserDisplays([application.ownerUserId, application.botUserId]);
		return {
			application: this.toResponse(application, userDisplays),
		};
	}

	async listUserApplications(data: ListUserApplicationsRequest): Promise<ListUserApplicationsResponse> {
		const {applicationRepository} = this.deps;
		const {users: userRepository} = this.deps.apiContext.services;
		const ownerUserId = createUserID(data.user_id);
		const owner = await userRepository.findUnique(ownerUserId);
		if (!owner) {
			throw new UnknownUserError();
		}
		const applications = await applicationRepository.listApplicationsByOwner(ownerUserId);
		const ownerDisplay: UserDisplay = {
			username: owner.username,
			global_name: owner.globalName ?? null,
			discriminator: String(owner.discriminator).padStart(4, '0'),
		};
		const botUserIds = applications.map((app) => app.botUserId).filter((id): id is UserID => id !== null);
		const botDisplays = await this.loadUserDisplays(botUserIds);
		const displayMap = new Map<string, UserDisplay>();
		displayMap.set(ownerUserId.toString(), ownerDisplay);
		for (const [id, display] of botDisplays.entries()) {
			displayMap.set(id, display);
		}
		return {
			applications: applications.map((app) => this.toResponse(app, displayMap)),
		};
	}

	async listGuildApplications(data: ListGuildApplicationsRequest): Promise<ListGuildApplicationsResponse> {
		const {applicationRepository, guildRepository} = this.deps;
		const guildId = createGuildID(data.guild_id);
		const guild = await guildRepository.findUnique(guildId);
		if (!guild) {
			throw new UnknownGuildError();
		}
		const members = await guildRepository.listMembers(guildId);
		const applications: Array<Application> = [];
		for (const member of members) {
			const application = await applicationRepository.getApplication(
				createApplicationID(BigInt(member.userId.toString())),
			);
			if (!application?.botUserId || application.botUserId !== member.userId) continue;
			applications.push(application);
		}
		const userDisplays = await this.loadUserDisplays(
			applications.flatMap((application) => [application.ownerUserId, application.botUserId]),
		);
		return {
			applications: applications.map((application) => this.toResponse(application, userDisplays)),
		};
	}

	async transferApplicationOwnership(
		data: TransferApplicationOwnershipRequest,
		adminUserId: UserID,
		auditLogReason: string | null,
	) {
		const {applicationRepository, auditService} = this.deps;
		const {users: userRepository} = this.deps.apiContext.services;
		const applicationId = createApplicationID(data.application_id);
		const application = await applicationRepository.getApplication(applicationId);
		if (!application) {
			throw new UnknownApplicationError();
		}
		const newOwnerId = createUserID(data.new_owner_id);
		const newOwner = await userRepository.findUnique(newOwnerId);
		if (!newOwner) {
			throw new UnknownUserError();
		}
		const oldOwnerId = application.ownerUserId;
		const oldRow = application.toRow();
		const updatedApplication = await applicationRepository.upsertApplication(
			{...oldRow, owner_user_id: newOwnerId},
			oldRow,
		);
		await auditService.createAuditLog({
			adminUserId,
			targetType: 'application',
			targetId: BigInt(applicationId),
			action: 'transfer_ownership',
			auditLogReason,
			metadata: new Map([
				['old_owner_id', oldOwnerId.toString()],
				['new_owner_id', newOwnerId.toString()],
			]),
		});
		const userDisplays = await this.loadUserDisplays([updatedApplication.ownerUserId, updatedApplication.botUserId]);
		return {
			application: this.toResponse(updatedApplication, userDisplays),
		};
	}

	private async loadUserDisplays(userIds: ReadonlyArray<UserID | null>): Promise<Map<string, UserDisplay>> {
		const {users: userRepository} = this.deps.apiContext.services;
		const uniqueIds = new Set<string>();
		const nonNullIds: Array<UserID> = [];
		for (const userId of userIds) {
			if (userId === null) continue;
			const key = userId.toString();
			if (uniqueIds.has(key)) continue;
			uniqueIds.add(key);
			nonNullIds.push(userId);
		}
		const users = await Promise.all(nonNullIds.map((id) => userRepository.findUnique(id)));
		const map = new Map<string, UserDisplay>();
		for (let i = 0; i < nonNullIds.length; i += 1) {
			const id = nonNullIds[i];
			if (!id) continue;
			const user = users[i];
			if (!user) continue;
			map.set(id.toString(), {
				username: user.username,
				global_name: user.globalName ?? null,
				discriminator: String(user.discriminator).padStart(4, '0'),
			});
		}
		return map;
	}

	private toResponse(application: Application, userDisplays: Map<string, UserDisplay>): ApplicationAdminResponse {
		const emptyDisplay: UserDisplay = {username: null, global_name: null, discriminator: null};
		const ownerDisplay = userDisplays.get(application.ownerUserId.toString()) ?? emptyDisplay;
		const botDisplay = application.botUserId
			? (userDisplays.get(application.botUserId.toString()) ?? emptyDisplay)
			: emptyDisplay;
		return {
			id: application.applicationId.toString(),
			name: application.name,
			owner_user_id: application.ownerUserId.toString(),
			owner_username: ownerDisplay.username,
			owner_global_name: ownerDisplay.global_name,
			owner_discriminator: ownerDisplay.discriminator,
			bot_user_id: application.botUserId?.toString() ?? null,
			bot_username: botDisplay.username,
			bot_global_name: botDisplay.global_name,
			bot_discriminator: botDisplay.discriminator,
			bot_is_public: application.botIsPublic,
			bot_require_code_grant: application.botRequireCodeGrant,
			oauth2_redirect_uris: Array.from(application.oauth2RedirectUris),
			has_client_secret: application.clientSecretHash !== null,
			has_bot_token: application.botTokenHash !== null,
			bot_token_preview: application.botTokenPreview,
			bot_token_created_at: application.botTokenCreatedAt?.toISOString() ?? null,
			client_secret_created_at: application.clientSecretCreatedAt?.toISOString() ?? null,
			version: application.version,
		};
	}
}
