import { In, Not } from "typeorm";
import Ajv from "ajv";
import { User } from "../entities/user.js";
import config from "../../config/index.js";
import { awaitAll } from "../../prelude/await-all.js";
import { populateEmojis } from "../../misc/populate-emojis.js";
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from "../../const.js";
import { Cache } from "../../misc/cache.js";
import { db } from "../../db/postgre.js";
import { isActor, getApId } from "../../remote/activitypub/type.js";
import DbResolver from "../../remote/activitypub/db-resolver.js";
import Resolver from "../../remote/activitypub/resolver.js";
import { createPerson } from "../../remote/activitypub/models/person.js";
import { AnnouncementReads, Announcements, Blockings, ChannelFollowings, DriveFiles, Followings, FollowRequests, Instances, MessagingMessages, Mutings, RenoteMutings, Notes, NoteUnreads, Notifications, Pages, Plans, UserGroupJoinings, UserNotePinings, UserPlans, UserProfiles, UserSecurityKeys } from "../index.js";
import AsyncLock from "async-lock";
const userInstanceCache = new Cache("userInstance", 60 * 60 * 3);
function isMissingRelationError(err) {
    const error = err;
    return error.code === "42P01" || error.driverError?.code === "42P01";
}
const ajv = new Ajv();
const localUsernameSchema = {
    type: "string",
    pattern: /^\w{1,20}$/.toString().slice(1, -1)
};
const passwordSchema = {
    type: "string",
    minLength: 1
};
const nameSchema = {
    type: "string",
    minLength: 1,
    maxLength: 50
};
const descriptionSchema = {
    type: "string",
    minLength: 1,
    maxLength: 2048
};
const locationSchema = {
    type: "string",
    minLength: 1,
    maxLength: 50
};
const birthdaySchema = {
    type: "string",
    pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1)
};
/**
 * Returns true if the user is local.
 *
 * @param user The user to check.
 * @returns True if the user is local.
 */ function isLocalUser(user) {
    return user.host == null;
}
/**
 * Returns true if the user is remote.
 *
 * @param user The user to check.
 * @returns True if the user is remote.
 */ function isRemoteUser(user) {
    return !isLocalUser(user);
}
export const UserRepository = db.getRepository(User).extend({
    localUsernameSchema,
    passwordSchema,
    nameSchema,
    descriptionSchema,
    locationSchema,
    birthdaySchema,
    //#region Validators
    validateLocalUsername: ajv.compile(localUsernameSchema),
    validatePassword: ajv.compile(passwordSchema),
    validateName: ajv.compile(nameSchema),
    validateDescription: ajv.compile(descriptionSchema),
    validateLocation: ajv.compile(locationSchema),
    validateBirthday: ajv.compile(birthdaySchema),
    //#endregion
    async getRelation (me, target) {
        return awaitAll({
            id: target,
            isFollowing: Followings.count({
                where: {
                    followerId: me,
                    followeeId: target
                },
                take: 1
            }).then((n)=>n > 0),
            isFollowed: Followings.count({
                where: {
                    followerId: target,
                    followeeId: me
                },
                take: 1
            }).then((n)=>n > 0),
            hasPendingFollowRequestFromYou: FollowRequests.count({
                where: {
                    followerId: me,
                    followeeId: target
                },
                take: 1
            }).then((n)=>n > 0),
            hasPendingFollowRequestToYou: FollowRequests.count({
                where: {
                    followerId: target,
                    followeeId: me
                },
                take: 1
            }).then((n)=>n > 0),
            isBlocking: Blockings.count({
                where: {
                    blockerId: me,
                    blockeeId: target
                },
                take: 1
            }).then((n)=>n > 0),
            isBlocked: Blockings.count({
                where: {
                    blockerId: target,
                    blockeeId: me
                },
                take: 1
            }).then((n)=>n > 0),
            isMuted: Mutings.count({
                where: {
                    muterId: me,
                    muteeId: target
                },
                take: 1
            }).then((n)=>n > 0),
            isRenoteMuted: RenoteMutings.count({
                where: {
                    muterId: me,
                    muteeId: target
                },
                take: 1
            }).then((n)=>n > 0)
        });
    },
    async getHasUnreadMessagingMessage (userId) {
        const mute = await Mutings.findBy({
            muterId: userId
        });
        const joinings = await UserGroupJoinings.findBy({
            userId: userId
        });
        const groupQs = Promise.all(joinings.map((j)=>MessagingMessages.createQueryBuilder("message").where("message.groupId = :groupId", {
                groupId: j.userGroupId
            }).andWhere("message.userId != :userId", {
                userId: userId
            }).andWhere("NOT (:userId = ANY(message.reads))", {
                userId: userId
            }).andWhere("message.createdAt > :joinedAt", {
                joinedAt: j.createdAt
            }) // 自分が加入する前の会話については、未読扱いしない
            .getOne().then((x)=>x != null)));
        const [withUser, withGroups] = await Promise.all([
            MessagingMessages.count({
                where: {
                    recipientId: userId,
                    isRead: false,
                    ...mute.length > 0 ? {
                        userId: Not(In(mute.map((x)=>x.muteeId)))
                    } : {}
                },
                take: 1
            }).then((count)=>count > 0),
            groupQs
        ]);
        return withUser || withGroups.some((x)=>x);
    },
    async getHasUnreadAnnouncement (userId) {
        const reads = await AnnouncementReads.findBy({
            userId: userId
        });
        const count = await Announcements.countBy(reads.length > 0 ? {
            id: Not(In(reads.map((read)=>read.announcementId)))
        } : {});
        return count > 0;
    },
    async userFromURI (uri) {
        try {
            const dbResolver = new DbResolver();
            let local = await dbResolver.getUserFromApId(uri);
            if (local) {
                return local;
            }
            // fetching Object once from remote
            const resolver = new Resolver();
            const object = await resolver.resolve(uri);
            // /@user If a URI other than the id is specified,
            // the URI is determined here
            if (uri !== object.id) {
                local = await dbResolver.getUserFromApId(object.id);
                if (local != null) return local;
            }
            return isActor(object) ? await createPerson(getApId(object)) : null;
        } catch  {
            return null;
        }
    },
    async getHasUnreadAntenna (userId) {
        // try {
        // 	const myAntennas = (await getAntennas()).filter(
        // 		(a) => a.userId === userId,
        // 	);
        // 	const unread =
        // 		myAntennas.length > 0
        // 			? await AntennaNotes.findOneBy({
        // 					antennaId: In(myAntennas.map((x) => x.id)),
        // 					read: false,
        // 			  })
        // 			: null;
        // 	return unread != null;
        // } catch (e) {
        // 	return false;
        // }
        return false; // TODO
    },
    async getHasUnreadChannel (userId) {
        const channels = await ChannelFollowings.findBy({
            followerId: userId
        });
        const unread = channels.length > 0 ? await NoteUnreads.findOneBy({
            userId: userId,
            noteChannelId: In(channels.map((x)=>x.followeeId))
        }) : null;
        return unread != null;
    },
    async getHasUnreadNotification (userId) {
        const mute = await Mutings.findBy({
            muterId: userId
        });
        const mutedUserIds = mute.map((m)=>m.muteeId);
        const count = await Notifications.count({
            where: {
                notifieeId: userId,
                ...mutedUserIds.length > 0 ? {
                    notifierId: Not(In(mutedUserIds))
                } : {},
                isRead: false
            },
            take: 1
        });
        return count > 0;
    },
    async getHasPendingReceivedFollowRequest (userId) {
        const count = await FollowRequests.countBy({
            followeeId: userId
        });
        return count > 0;
    },
    getOnlineStatus (user) {
        if (user.hideOnlineStatus) return "unknown";
        if (user.lastActiveDate == null) return "unknown";
        const elapsed = Date.now() - user.lastActiveDate.getTime();
        return elapsed < USER_ONLINE_THRESHOLD ? "online" : elapsed < USER_ACTIVE_THRESHOLD ? "active" : "offline";
    },
    async getAvatarUrl (user) {
        if (user.avatar) {
            return DriveFiles.getPublicUrl(user.avatar, true) || this.getIdenticonUrl(user.id);
        } else if (user.avatarId) {
            if (user.avatarUrl) return DriveFiles.getFinalUrl(user.avatarUrl);
            const avatar = await DriveFiles.findOneByOrFail({
                id: user.avatarId
            });
            return DriveFiles.getPublicUrl(avatar, true) || this.getIdenticonUrl(user.id);
        } else {
            return this.getIdenticonUrl(user.id);
        }
    },
    getAvatarUrlSync (user) {
        if (user.avatarId && user.avatarUrl) {
            return DriveFiles.getFinalUrl(user.avatarUrl);
        } else if (user.avatar) {
            return DriveFiles.getPublicUrl(user.avatar, true) || this.getIdenticonUrl(user.id);
        } else {
            return this.getIdenticonUrl(user.id);
        }
    },
    getIdenticonUrl (userId) {
        return `${config.url}/identicon/${userId}`;
    },
    getFreshPackedUserCache () {
        return {
            locks: new AsyncLock(),
            results: []
        };
    },
    async getRandomFollower (targetId) {
        return await this.createQueryBuilder("u").select(`u.id`).leftJoinAndSelect("following", "f", `f."followerId" = u.id`).where(`f."followeeId" = :id`, {
            id: targetId
        }).getOne();
    },
    async packCached (src, cache, me, options) {
        const id = typeof src === "object" ? src.id : src;
        return cache.locks.acquire(id, async ()=>{
            const result = cache.results.find((p)=>p.id === id);
            if (result) return result;
            return this.pack(src, me, options).then((result)=>{
                cache.results.push(result);
                return result;
            });
        });
    },
    async pack (src, me, options) {
        const opts = Object.assign({
            detail: false,
            includeSecrets: false,
            isPrivateMode: false
        }, options);
        let user;
        if (typeof src === "object") {
            user = src;
        } else {
            user = await this.findOneOrFail({
                where: {
                    id: src
                }
            });
        }
        const meId = me ? me.id : null;
        const isMe = meId === user.id;
        const relation = meId && !isMe && opts.detail ? await this.getRelation(meId, user.id) : null;
        const pins = opts.detail ? await UserNotePinings.createQueryBuilder("pin").where("pin.userId = :userId", {
            userId: user.id
        }).innerJoinAndSelect("pin.note", "note").orderBy("pin.id", "DESC").getMany() : [];
        const profile = opts.detail ? await UserProfiles.findOneByOrFail({
            userId: user.id
        }) : null;
        const followingCount = profile == null ? null : profile.ffVisibility === "public" || isMe ? user.followingCount : profile.ffVisibility === "followers" && relation && relation.isFollowing ? user.followingCount : null;
        const followersCount = profile == null ? null : profile.ffVisibility === "public" || isMe ? user.followersCount : profile.ffVisibility === "followers" && relation && relation.isFollowing ? user.followersCount : null;
        const falsy = opts.detail ? false : undefined;
        if (opts.isPrivateMode) {
            const packed = {
                id: user.id,
                username: user.username,
                host: user.host,
                ...opts.detail ? {
                    twoFactorEnabled: profile.twoFactorEnabled,
                    usePasswordLessLogin: profile.usePasswordLessLogin,
                    securityKeys: profile.twoFactorEnabled ? UserSecurityKeys.countBy({
                        userId: user.id
                    }).then((result)=>result >= 1) : false
                } : {}
            };
            return await awaitAll(packed);
        }
        const packed = {
            id: user.id,
            name: user.name,
            username: user.username,
            host: user.host,
            avatarUrl: this.getAvatarUrlSync(user),
            avatarBlurhash: user.avatarId ? user.avatarBlurhash ?? user.avatar?.blurhash ?? null : null,
            avatarColor: null,
            isAdmin: user.isAdmin || falsy,
            isModerator: user.isModerator || falsy,
            isVerified: user.isVerified || falsy,
            minorBadges: user.minorBadges ?? [],
            plans: UserPlans.find({
                where: {
                    userId: user.id
                },
                relations: [
                    "plan"
                ],
                order: {
                    createdAt: "ASC"
                }
            }).then((joins)=>Plans.packMany(joins.map((join)=>join.plan).filter((plan)=>plan != null))).catch((err)=>{
                if (isMissingRelationError(err)) return [];
                throw err;
            }),
            isBot: user.isBot || falsy,
            isLocked: user.isLocked,
            isCat: user.isCat || falsy,
            speakAsCat: user.speakAsCat || falsy,
            instance: user.host ? userInstanceCache.fetch(user.host, ()=>Instances.findOneBy({
                    host: user.host
                }), (v)=>v != null).then((instance)=>instance ? {
                    name: instance.name,
                    softwareName: instance.softwareName,
                    softwareVersion: instance.softwareVersion,
                    iconUrl: instance.iconUrl,
                    faviconUrl: instance.faviconUrl,
                    themeColor: instance.themeColor
                } : undefined) : undefined,
            emojis: populateEmojis(user.emojis, user.host),
            onlineStatus: this.getOnlineStatus(user),
            driveCapacityOverrideMb: user.driveCapacityOverrideMb,
            canBite: user.canBite,
            ...opts.detail ? {
                url: profile.url,
                uri: user.uri,
                movedToUri: user.movedToUri ? await this.userFromURI(user.movedToUri) : null,
                alsoKnownAs: user.alsoKnownAs,
                createdAt: user.createdAt.toISOString(),
                updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
                lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
                bannerUrl: user.bannerId ? DriveFiles.getFinalUrlMaybe(user.bannerUrl) ?? (user.banner ? DriveFiles.getPublicUrl(user.banner, false) : null) : null,
                bannerBlurhash: user.bannerId ? user.bannerBlurhash ?? user.banner?.blurhash ?? null : null,
                bannerColor: null,
                isSilenced: user.isSilenced || falsy,
                isSuspended: user.isSuspended || falsy,
                description: profile.description,
                location: profile.location,
                birthday: profile.birthday,
                lang: profile.lang,
                fields: profile.fields,
                followersCount: followersCount || 0,
                followingCount: followingCount || 0,
                notesCount: user.notesCount,
                pinnedNoteIds: pins.map((pin)=>pin.noteId),
                pinnedNotes: Notes.packMany(pins.map((pin)=>pin.note), me, {
                    detail: true
                }),
                pinnedPageId: profile.pinnedPageId,
                pinnedPage: profile.pinnedPageId ? Pages.pack(profile.pinnedPageId, me) : null,
                publicReactions: profile.publicReactions,
                allowCalls: profile.allowCalls,
                symbolFileId: profile.symbolFileId,
                ffVisibility: profile.ffVisibility,
                twoFactorEnabled: profile.twoFactorEnabled,
                usePasswordLessLogin: profile.usePasswordLessLogin,
                securityKeys: profile.twoFactorEnabled ? UserSecurityKeys.countBy({
                    userId: user.id
                }).then((result)=>result >= 1) : false,
                pronouns: profile.pronouns
            } : {},
            ...opts.detail && isMe ? {
                avatarId: user.avatarId,
                bannerId: user.bannerId,
                injectFeaturedNote: profile.injectFeaturedNote,
                receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
                alwaysMarkNsfw: profile.alwaysMarkNsfw,
                carefulBot: profile.carefulBot,
                autoAcceptFollowed: profile.autoAcceptFollowed,
                noCrawle: profile.noCrawle,
                preventAiLearning: profile.preventAiLearning,
                isExplorable: user.isExplorable,
                isDeleted: user.isDeleted,
                hideOnlineStatus: user.hideOnlineStatus,
                hasUnreadSpecifiedNotes: NoteUnreads.count({
                    where: {
                        userId: user.id,
                        isSpecified: true
                    },
                    take: 1
                }).then((count)=>count > 0),
                hasUnreadMentions: NoteUnreads.count({
                    where: {
                        userId: user.id,
                        isMentioned: true
                    },
                    take: 1
                }).then((count)=>count > 0),
                hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id),
                hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
                hasUnreadChannel: this.getHasUnreadChannel(user.id),
                hasUnreadMessagingMessage: this.getHasUnreadMessagingMessage(user.id),
                hasUnreadNotification: this.getHasUnreadNotification(user.id),
                hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
                integrations: profile.integrations,
                mutedWords: profile.mutedWords,
                mutedInstances: profile.mutedInstances,
                mutingNotificationTypes: profile.mutingNotificationTypes,
                emailNotificationTypes: profile.emailNotificationTypes
            } : {},
            ...opts.includeSecrets ? {
                email: profile.email,
                emailVerified: profile.emailVerified,
                securityKeysList: profile.twoFactorEnabled ? UserSecurityKeys.find({
                    where: {
                        userId: user.id
                    },
                    select: {
                        id: true,
                        name: true,
                        lastUsed: true
                    }
                }) : []
            } : {},
            ...relation ? {
                isFollowing: relation.isFollowing,
                isFollowed: relation.isFollowed,
                hasPendingFollowRequestFromYou: relation.hasPendingFollowRequestFromYou,
                hasPendingFollowRequestToYou: relation.hasPendingFollowRequestToYou,
                isBlocking: relation.isBlocking,
                isBlocked: relation.isBlocked,
                isMuted: relation.isMuted,
                isRenoteMuted: relation.isRenoteMuted
            } : {}
        };
        return await awaitAll(packed);
    },
    packMany (users, me, options, cache) {
        return Promise.all(users.map((u)=>this.packCached(u, cache ?? this.getFreshPackedUserCache(), me, options)));
    },
    isLocalUser,
    isRemoteUser
});
