import { Blockings, DriveFiles, Followings, FollowRequests, Mutings, NoteFavorites, NoteReactions, Notes, NoteWatchings, RegistryItems, UserNotePinings, UserProfiles, Users } from "../../../../models/index.js";
import { generateVisibilityQuery } from "../../common/generate-visibility-query.js";
import { generateMutedUserQuery } from "../../common/generate-muted-user-query.js";
import { generateBlockedUserQuery } from "../../common/generate-block-query.js";
import AsyncLock from "async-lock";
import { getUser } from "../../common/getters.js";
import { PaginationHelpers } from "./pagination.js";
import { awaitAll } from "../../../../prelude/await-all.js";
import createFollowing from "../../../../services/following/create.js";
import deleteFollowing from "../../../../services/following/delete.js";
import cancelFollowRequest from "../../../../services/following/requests/cancel.js";
import createBlocking from "../../../../services/blocking/create.js";
import deleteBlocking from "../../../../services/blocking/delete.js";
import { genId } from "../../../../misc/gen-id.js";
import { publishUserEvent } from "../../../../services/stream.js";
import { UserConverter } from "../converters/user.js";
import acceptFollowRequest from "../../../../services/following/requests/accept.js";
import { rejectFollowRequest } from "../../../../services/following/reject.js";
import { Brackets, IsNull } from "typeorm";
import { VisibilityConverter } from "../converters/visibility.js";
import { toSingleLast } from "../../../../prelude/array.js";
import { MediaHelpers } from "./media.js";
import { verifyLink } from "../../../../services/fetch-rel-me.js";
import { MastoApiError } from "../middleware/catch-errors.js";
import { resolveUser } from "../../../../remote/resolve-user.js";
import { updatePerson } from "../../../../remote/activitypub/models/person.js";
import { promiseEarlyReturn } from "../../../../prelude/promise.js";
import { updateUserProfileData } from "../../../../services/i/update.js";
export class UserHelpers {
    static async followUser(target, reblogs, notify, ctx) {
        //FIXME: implement reblogs & notify params
        const localUser = ctx.user;
        const following = await Followings.exist({
            where: {
                followerId: localUser.id,
                followeeId: target.id
            }
        });
        const requested = await FollowRequests.exist({
            where: {
                followerId: localUser.id,
                followeeId: target.id
            }
        });
        if (!following && !requested) await createFollowing(localUser, target);
        return this.getUserRelationshipTo(target.id, localUser.id);
    }
    static async unfollowUser(target, ctx) {
        const localUser = ctx.user;
        const following = await Followings.exist({
            where: {
                followerId: localUser.id,
                followeeId: target.id
            }
        });
        const requested = await FollowRequests.exist({
            where: {
                followerId: localUser.id,
                followeeId: target.id
            }
        });
        if (following) await deleteFollowing(localUser, target);
        if (requested) await cancelFollowRequest(target, localUser);
        return this.getUserRelationshipTo(target.id, localUser.id);
    }
    static async blockUser(target, ctx) {
        const localUser = ctx.user;
        const blocked = await Blockings.exist({
            where: {
                blockerId: localUser.id,
                blockeeId: target.id
            }
        });
        if (!blocked) await createBlocking(localUser, target);
        return this.getUserRelationshipTo(target.id, localUser.id);
    }
    static async unblockUser(target, ctx) {
        const localUser = ctx.user;
        const blocked = await Blockings.exist({
            where: {
                blockerId: localUser.id,
                blockeeId: target.id
            }
        });
        if (blocked) await deleteBlocking(localUser, target);
        return this.getUserRelationshipTo(target.id, localUser.id);
    }
    static async muteUser(target, notifications = true, duration = 0, ctx) {
        //FIXME: respect notifications parameter
        const localUser = ctx.user;
        const muted = await Mutings.exist({
            where: {
                muterId: localUser.id,
                muteeId: target.id
            }
        });
        if (!muted) {
            await Mutings.insert({
                id: genId(),
                createdAt: new Date(),
                expiresAt: duration === 0 ? null : new Date(new Date().getTime() + duration * 1000),
                muterId: localUser.id,
                muteeId: target.id
            });
            publishUserEvent(localUser.id, "mute", target);
            NoteWatchings.delete({
                userId: localUser.id,
                noteUserId: target.id
            });
        }
        return this.getUserRelationshipTo(target.id, localUser.id);
    }
    static async unmuteUser(target, ctx) {
        const localUser = ctx.user;
        const muting = await Mutings.findOneBy({
            muterId: localUser.id,
            muteeId: target.id
        });
        if (muting) {
            await Mutings.delete({
                id: muting.id
            });
            publishUserEvent(localUser.id, "unmute", target);
        }
        return this.getUserRelationshipTo(target.id, localUser.id);
    }
    static async acceptFollowRequest(target, ctx) {
        const localUser = ctx.user;
        const pending = await FollowRequests.exist({
            where: {
                followerId: target.id,
                followeeId: localUser.id
            }
        });
        if (pending) await acceptFollowRequest(localUser, target);
        return this.getUserRelationshipTo(target.id, localUser.id);
    }
    static async rejectFollowRequest(target, ctx) {
        const localUser = ctx.user;
        const pending = await FollowRequests.exist({
            where: {
                followerId: target.id,
                followeeId: localUser.id
            }
        });
        if (pending) await rejectFollowRequest(localUser, target);
        return this.getUserRelationshipTo(target.id, localUser.id);
    }
    static async updateCredentials(ctx) {
        const user = ctx.user;
        const files = ctx.request.files;
        const formData = ctx.request.body;
        const updates = {};
        const profileUpdates = {};
        const avatar = toSingleLast(files?.avatar);
        const header = toSingleLast(files?.header);
        if (avatar) {
            const file = await MediaHelpers.uploadMediaBasic(avatar, ctx);
            updates.avatarId = file.id;
            updates.avatarBlurhash = file.blurhash;
            updates.avatarUrl = DriveFiles.getDatabasePrefetchUrl(file, true);
        }
        if (header) {
            const file = await MediaHelpers.uploadMediaBasic(header, ctx);
            updates.bannerId = file.id;
            updates.bannerBlurhash = file.blurhash;
            updates.bannerUrl = DriveFiles.getDatabasePrefetchUrl(file, false);
        }
        if (formData.fields_attributes) {
            profileUpdates.fields = await Promise.all(formData.fields_attributes.map(async (field)=>{
                if (!(field.name.trim() === "" && field.value.trim() === "")) {
                    if (field.name.trim() === "") throw new MastoApiError(400, "Field name can not be empty");
                    if (field.value.trim() === "") throw new MastoApiError(400, "Field value can not be empty");
                }
                const verified = field.value.startsWith("http") ? await promiseEarlyReturn(verifyLink(field.value, user.username), 1500) ?? false : undefined;
                return {
                    ...field,
                    verified
                };
            })).then((p)=>p.filter((field)=>field.name.trim().length > 0 && field.value.length > 0));
        }
        if (formData.display_name) updates.name = formData.display_name;
        if (formData.note) profileUpdates.description = formData.note;
        if (formData.locked) updates.isLocked = formData.locked;
        if (formData.bot) updates.isBot = formData.bot;
        if (formData.discoverable) updates.isExplorable = formData.discoverable;
        await updateUserProfileData(user, null, updates, profileUpdates, false);
        return this.verifyCredentials(ctx);
    }
    static async verifyCredentials(ctx) {
        const user = ctx.user;
        const acct = UserConverter.encode(user, ctx);
        const profile = UserProfiles.findOneByOrFail({
            userId: user.id
        });
        const followRequests = FollowRequests.count({
            where: {
                followeeId: user.id
            }
        });
        const privacy = this.getDefaultNoteVisibility(ctx);
        const fields = profile.then((profile)=>profile.fields.map((field)=>{
                return {
                    name: field.name,
                    value: field.value
                };
            }));
        return acct.then((acct)=>{
            const source = {
                note: profile.then((profile)=>profile.description ?? ''),
                fields: fields,
                privacy: privacy.then((p)=>VisibilityConverter.encode(p)),
                sensitive: profile.then((p)=>p.alwaysMarkNsfw),
                language: profile.then((p)=>p.lang ?? ''),
                follow_requests_count: followRequests
            };
            const result = {
                ...acct,
                source: awaitAll(source)
            };
            return awaitAll(result);
        });
    }
    static async getUserFromAcct(acct) {
        const split = acct.toLowerCase().split('@');
        if (split.length > 2) throw new Error('Invalid acct');
        return split[1] == null ? Users.findOneBy({
            usernameLower: split[0],
            host: split[1] ?? IsNull()
        }).then((p)=>{
            if (p) return p;
            throw new MastoApiError(404);
        }) : resolveUser(split[0], split[1], 'no-refresh').catch(()=>{
            throw new MastoApiError(404);
        });
    }
    static async getUserMutes(maxId, sinceId, minId, limit = 40, ctx) {
        if (limit > 80) limit = 80;
        const user = ctx.user;
        const query = PaginationHelpers.makePaginationQuery(Mutings.createQueryBuilder("muting"), sinceId, maxId, minId);
        query.andWhere("muting.muterId = :userId", {
            userId: user.id
        }).innerJoinAndSelect("muting.mutee", "mutee");
        return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined, ctx).then(async (mutes)=>{
            const users = mutes.map((p)=>p.mutee).filter((p)=>p);
            return await UserConverter.encodeMany(users, ctx).then((res)=>res.map((m)=>{
                    const muting = mutes.find((acc)=>acc.muteeId === m.id);
                    return {
                        ...m,
                        mute_expires_at: muting?.expiresAt?.toISOString() ?? null
                    };
                }));
        });
    }
    static async getUserBlocks(maxId, sinceId, minId, limit = 40, ctx) {
        if (limit > 80) limit = 80;
        const user = ctx.user;
        const query = PaginationHelpers.makePaginationQuery(Blockings.createQueryBuilder("blocking"), sinceId, maxId, minId);
        query.andWhere("blocking.blockerId = :userId", {
            userId: user.id
        }).innerJoinAndSelect("blocking.blockee", "blockee");
        return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined, ctx).then((blocks)=>{
            return blocks.map((p)=>p.blockee).filter((p)=>p);
        });
    }
    static async getUserFollowRequests(maxId, sinceId, minId, limit = 40, ctx) {
        if (limit > 80) limit = 80;
        const user = ctx.user;
        const query = PaginationHelpers.makePaginationQuery(FollowRequests.createQueryBuilder("request"), sinceId, maxId, minId);
        query.andWhere("request.followeeId = :userId", {
            userId: user.id
        }).innerJoinAndSelect("request.follower", "follower");
        return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined, ctx).then((requests)=>{
            return requests.map((p)=>p.follower).filter((p)=>p);
        });
    }
    static async getUserStatuses(user, maxId, sinceId, minId, limit = 20, onlyMedia = false, excludeReplies = false, excludeReblogs = false, pinned = false, tagged, ctx) {
        if (limit > 40) limit = 40;
        const localUser = ctx.user;
        if (tagged !== undefined && tagged.length > 0) {
            //FIXME respect tagged
            return [];
        }
        const query = PaginationHelpers.makePaginationQuery(Notes.createQueryBuilder("note"), sinceId, maxId, minId).andWhere("note.userId = :userId");
        if (pinned) {
            const sq = UserNotePinings.createQueryBuilder("pin").select("pin.noteId").where("pin.userId = :userId");
            query.andWhere(`note.id IN (${sq.getQuery()})`);
        }
        if (excludeReblogs) {
            query.andWhere(new Brackets((qb)=>{
                qb.where('note.renoteId IS NULL').orWhere('note.text IS NOT NULL').orWhere('note.hasPoll = TRUE').orWhere("note.fileIds != '{}'");
            }));
        }
        if (excludeReplies) {
            query.leftJoin("note", "thread", "note.threadId = thread.id").andWhere(new Brackets((qb)=>{
                qb.where("note.replyId IS NULL").orWhere(new Brackets((qb)=>{
                    qb.where('note.mentions = :mentions', {
                        mentions: []
                    }).andWhere('thread.userId = :userId');
                }));
            }));
        }
        query.leftJoinAndSelect("note.renote", "renote");
        generateVisibilityQuery(query, localUser);
        if (localUser) {
            generateMutedUserQuery(query, localUser, user);
            generateBlockedUserQuery(query, localUser);
        }
        if (onlyMedia) query.andWhere("note.fileIds != '{}'");
        query.andWhere("note.visibility != 'hidden'");
        query.andWhere("note.visibility != 'specified'");
        query.setParameters({
            userId: user.id
        });
        return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined, ctx);
    }
    static async getUserBookmarks(maxId, sinceId, minId, limit = 20, ctx) {
        if (limit > 40) limit = 40;
        const localUser = ctx.user;
        const query = PaginationHelpers.makePaginationQuery(NoteFavorites.createQueryBuilder("favorite"), sinceId, maxId, minId).andWhere("favorite.userId = :meId", {
            meId: localUser.id
        }).leftJoinAndSelect("favorite.note", "note");
        generateVisibilityQuery(query, localUser);
        return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined, ctx).then((res)=>res.map((p)=>p.note));
    }
    static async getUserFavorites(maxId, sinceId, minId, limit = 20, ctx) {
        if (limit > 40) limit = 40;
        const localUser = ctx.user;
        const query = PaginationHelpers.makePaginationQuery(NoteReactions.createQueryBuilder("reaction"), sinceId, maxId, minId).andWhere("reaction.userId = :meId", {
            meId: localUser.id
        }).leftJoinAndSelect("reaction.note", "note");
        generateVisibilityQuery(query, localUser);
        return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined, ctx).then((res)=>res.map((p)=>p.note));
    }
    static async getUserRelationships(type, user, maxId, sinceId, minId, limit = 40, ctx) {
        if (limit > 80) limit = 80;
        const localUser = ctx.user;
        const profile = await UserProfiles.findOneByOrFail({
            userId: user.id
        });
        if (profile.ffVisibility === "private") {
            if (!localUser || user.id !== localUser.id) return [];
        } else if (profile.ffVisibility === "followers") {
            if (!localUser) return [];
            if (user.id !== localUser.id) {
                const isFollowed = await Followings.exist({
                    where: {
                        followeeId: user.id,
                        followerId: localUser.id
                    }
                });
                if (!isFollowed) return [];
            }
        }
        const query = PaginationHelpers.makePaginationQuery(Followings.createQueryBuilder("following"), sinceId, maxId, minId);
        if (type === "followers") {
            query.andWhere("following.followeeId = :userId", {
                userId: user.id
            }).innerJoinAndSelect("following.follower", "follower");
        } else {
            query.andWhere("following.followerId = :userId", {
                userId: user.id
            }).innerJoinAndSelect("following.followee", "followee");
        }
        return PaginationHelpers.execQueryLinkPagination(query, limit, minId !== undefined, ctx).then((relations)=>relations.map((p)=>type === "followers" ? p.follower : p.followee).filter((p)=>p));
    }
    static async getUserFollowers(user, maxId, sinceId, minId, limit = 40, ctx) {
        return this.getUserRelationships('followers', user, maxId, sinceId, minId, limit, ctx);
    }
    static async getUserFollowing(user, maxId, sinceId, minId, limit = 40, ctx) {
        return this.getUserRelationships('following', user, maxId, sinceId, minId, limit, ctx);
    }
    static async getUserRelationhipToMany(targetIds, localUserId) {
        return Promise.all(targetIds.map((targetId)=>this.getUserRelationshipTo(targetId, localUserId)));
    }
    static async getUserRelationshipTo(targetId, localUserId) {
        const relation = await Users.getRelation(localUserId, targetId);
        const response = {
            id: targetId,
            following: relation.isFollowing,
            followed_by: relation.isFollowed,
            blocking: relation.isBlocking,
            blocked_by: relation.isBlocked,
            muting: relation.isMuted,
            muting_notifications: relation.isMuted,
            requested: relation.hasPendingFollowRequestFromYou,
            domain_blocking: false,
            showing_reblogs: !relation.isRenoteMuted,
            endorsed: false,
            notifying: false,
            note: '' //FIXME
        };
        return awaitAll(response);
    }
    static async getUserCached(id, ctx) {
        const cache = ctx.cache;
        return cache.locks.acquire(id, async ()=>{
            const cacheHit = cache.users.find((p)=>p.id == id);
            if (cacheHit) return cacheHit;
            return getUser(id).then((p)=>{
                cache.users.push(p);
                return p;
            });
        });
    }
    static async getUserCachedOr404(id, ctx) {
        return this.getUserCached(id, ctx).catch((_)=>{
            throw new MastoApiError(404);
        });
    }
    static async getUserOr404(id) {
        return getUser(id).catch((_)=>{
            throw new MastoApiError(404);
        });
    }
    static async updateUserInBackground(user) {
        if (Users.isLocalUser(user)) return;
        if (user.lastFetchedAt != null && Date.now() - user.lastFetchedAt.getTime() < 1000 * 60 * 60 * 24) return;
        await Users.update(user.id, {
            lastFetchedAt: new Date()
        });
        // noinspection ES6MissingAwait
        updatePerson(user.uri, undefined, undefined, user);
    }
    static getFreshAccountCache() {
        return {
            locks: new AsyncLock(),
            accounts: [],
            users: []
        };
    }
    static async getDefaultNoteVisibility(ctx) {
        const user = ctx.user;
        return RegistryItems.findOneBy({
            domain: IsNull(),
            userId: user.id,
            key: 'defaultNoteVisibility',
            scope: '{client,base}'
        }).then((p)=>p?.value ?? 'public');
    }
}
