import { In, IsNull } from "typeorm";
import { DriveFiles, Emojis, UserEmojis, UserGroups, UserProfiles, Users } from "../models/index.js";
import { Cache } from "./cache.js";
import { isSelfHost, toPunyNullable } from "./convert-host.js";
import { decodeReaction } from "./reaction-lib.js";
import config from "../config/index.js";
import { query } from "../prelude/url.js";
import { redisClient } from "../db/redis.js";
import { resolveUser } from "../remote/resolve-user.js";
const cache = new Cache("populateEmojis", 60 * 60 * 12);
const userEmojiCache = new Cache("populateUserEmojis", 60 * 60 * 12);
function normalizeHost(src, noteUserHost) {
    // クエリに使うホスト
    let host = src === "." ? null // .はローカルホスト (ここがマッチするのはリアクションのみ)
     : src === undefined ? noteUserHost // ノートなどでホスト省略表記の場合はローカルホスト (ここがリアクションにマッチすることはない)
     : isSelfHost(src) ? null // 自ホスト指定
     : src || noteUserHost; // 指定されたホスト || ノートなどの所有者のホスト (こっちがリアクションにマッチすることはない)
    host = toPunyNullable(host);
    return host;
}
function parseEmojiStr(emojiName, noteUserHost) {
    // emojiName may be of the form `emoji@host`, turn it into a suitable form
    const match = emojiName.split("@");
    const name = match[0];
    const host = toPunyNullable(normalizeHost(match[1], noteUserHost));
    return {
        name,
        host
    };
}
function proxiedUrl(url, host) {
    if (host == null) return url;
    return `${config.url}/proxy/${encodeURIComponent(new URL(url).pathname)}?${query({
        url
    })}`;
}
function proxiedGlyphUrl(url, host) {
    if (host == null) return url;
    return `${config.url}/proxy/${encodeURIComponent(new URL(url).pathname)}?${query({
        url,
        glyph: "1"
    })}`;
}
function parseUserIconEmoji(emojiName) {
    const match = emojiName.match(/^@([^@:\s]+)(?:@([^@:\s]+))?$/);
    if (!match) return null;
    return {
        username: match[1],
        host: toPunyNullable(normalizeHost(match[2], null))
    };
}
function parseGroupSymbolEmoji(emojiName) {
    const match = emojiName.match(/^@@([a-zA-Z0-9_]{1,64})$/);
    if (!match) return null;
    return {
        username: match[1].toLowerCase()
    };
}
function parseGroupEmoji(emojiName) {
    const match = emojiName.match(/^([a-z0-9_]{1,64})@@([a-zA-Z0-9_]{1,64})$/);
    if (!match) return null;
    return {
        name: match[1],
        username: match[2].toLowerCase()
    };
}
function parseUserEmoji(emojiName) {
    const parts = emojiName.split("@");
    if (parts.length !== 2 && parts.length !== 3) return null;
    if (!parts[0] || !parts[1]) return null;
    return {
        name: parts[0],
        username: parts[1],
        host: toPunyNullable(normalizeHost(parts[2], null))
    };
}
async function findUserByAcct(username, host) {
    const user = await Users.findOneBy({
        usernameLower: username.toLowerCase(),
        host: host ?? IsNull()
    });
    if (user) return user;
    if (host == null) return null;
    return resolveUser(username, host).catch(()=>null);
}
async function populateUserIconEmoji(emojiName) {
    const parsed = parseUserIconEmoji(emojiName);
    if (!parsed) return null;
    const user = await findUserByAcct(parsed.username, parsed.host);
    if (!user) return null;
    const profile = await UserProfiles.findOneBy({
        userId: user.id
    });
    if (profile?.symbolFileId) {
        const symbol = await DriveFiles.findOneBy({
            id: profile.symbolFileId
        });
        if (symbol) {
            const symbolUrl = symbol.webpublicUrl ?? symbol.url;
            return {
                name: emojiName,
                url: proxiedUrl(symbolUrl, user.host),
                glyph: true,
                glyphUrl: proxiedGlyphUrl(symbol.url, user.host),
                width: null,
                height: null
            };
        }
    }
    const avatarUrl = user.avatarUrl ?? await Users.getAvatarUrl(user);
    return {
        name: emojiName,
        url: proxiedUrl(avatarUrl, user.host),
        glyph: false,
        glyphUrl: null,
        width: null,
        height: null
    };
}
async function populateUserEmoji(emojiName) {
    const parsed = parseUserEmoji(emojiName);
    if (!parsed) return null;
    const user = await findUserByAcct(parsed.username, parsed.host);
    if (!user) return null;
    const userEmoji = await UserEmojis.findOneBy({
        name: parsed.name,
        userId: user.id
    });
    if (!userEmoji) return null;
    const emojiUrl = userEmoji.publicUrl || userEmoji.originalUrl;
    return {
        name: emojiName,
        url: proxiedUrl(emojiUrl, user.host),
        glyph: userEmoji.glyph,
        glyphUrl: userEmoji.glyph ? proxiedGlyphUrl(userEmoji.originalUrl, user.host) : null,
        width: userEmoji.width,
        height: userEmoji.height
    };
}
async function populateGroupSymbolEmoji(emojiName) {
    const parsed = parseGroupSymbolEmoji(emojiName);
    if (!parsed) return null;
    const group = await UserGroups.findOneBy({
        username: parsed.username
    });
    if (!group?.symbolFileId && !group?.iconFileId) return null;
    const file = await DriveFiles.findOneBy({
        id: group.symbolFileId ?? group.iconFileId
    });
    if (!file) return null;
    const symbolUrl = file.webpublicUrl ?? file.url;
    return {
        name: emojiName,
        url: proxiedUrl(symbolUrl, null),
        glyph: true,
        glyphUrl: proxiedGlyphUrl(file.url, null),
        width: null,
        height: null
    };
}
async function populateGroupEmoji(emojiName) {
    const parsed = parseGroupEmoji(emojiName);
    if (!parsed) return null;
    const group = await UserGroups.findOneBy({
        username: parsed.username
    });
    if (!group) return null;
    const groupEmoji = await UserEmojis.findOneBy({
        name: parsed.name,
        userGroupId: group.id
    });
    if (!groupEmoji) return null;
    const emojiUrl = groupEmoji.publicUrl || groupEmoji.originalUrl;
    return {
        name: emojiName,
        url: proxiedUrl(emojiUrl, null),
        glyph: groupEmoji.glyph,
        glyphUrl: groupEmoji.glyph ? proxiedGlyphUrl(groupEmoji.originalUrl, null) : null,
        width: groupEmoji.width,
        height: groupEmoji.height
    };
}
/**
 * 添付用絵文字情報を解決する
 * @param emojiName ノートやユーザープロフィールに添付された、またはリアクションのカスタム絵文字名 (:は含めない, リアクションでローカルホストの場合は@.を付ける (これはdecodeReactionで可能))
 * @param noteUserHost ノートやユーザープロフィールの所有者のホスト
 * @returns 絵文字情報, nullは未マッチを意味する
 */ export async function populateEmoji(emojiName, noteUserHost) {
    const { name, host } = parseEmojiStr(emojiName, noteUserHost);
    if (name == null) return null;
    const queryOrNull = async ()=>await Emojis.findOneBy({
            name,
            host: host ?? IsNull()
        }) || null;
    const cacheKey = `${name} ${host}`;
    let emoji = await cache.fetch(cacheKey, queryOrNull);
    if (emoji && !(emoji.width && emoji.height)) {
        emoji = await queryOrNull();
        await cache.set(cacheKey, emoji);
    }
    if (emoji == null) return null;
    const isLocal = emoji.host == null;
    const emojiUrl = emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl してるのは後方互換性のため
    const url = proxiedUrl(emojiUrl, isLocal ? null : emoji.host);
    return {
        name: emojiName,
        url,
        glyph: emoji.glyph,
        glyphUrl: emoji.glyph ? proxiedGlyphUrl(emoji.originalUrl, isLocal ? null : emoji.host) : null,
        width: emoji.width,
        height: emoji.height
    };
}
export async function populateEmojiOrUserEmoji(emojiName, noteUserHost) {
    const emoji = await populateEmoji(emojiName, noteUserHost);
    if (emoji) return emoji;
    const userEmoji = await userEmojiCache.fetchMaybe(`user ${emojiName}`, async ()=>await populateUserIconEmoji(emojiName) ?? await populateUserEmoji(emojiName) ?? await populateGroupSymbolEmoji(emojiName) ?? await populateGroupEmoji(emojiName) ?? undefined, false, (cached)=>cached != null);
    return userEmoji ?? null;
}
export async function clearUserEmojiCache(name, username, host) {
    const keys = new Set([
        `user ${name}@${username}`
    ]);
    if (host) {
        keys.add(`user ${name}@${username}@${host}`);
    } else {
        keys.add(`user ${name}@${username}@${config.host}`);
    }
    await userEmojiCache.delete(...keys);
}
export async function clearGroupEmojiCache(name, groupUsername) {
    if (!groupUsername) return;
    await userEmojiCache.delete(`user ${name}@@${groupUsername}`, `user @@${groupUsername}`);
}
/**
 * 複数の添付用絵文字情報を解決する (キャシュ付き, 存在しないものは結果から除外される)
 */ export async function populateEmojis(emojiNames, noteUserHost) {
    const emojis = await Promise.all(emojiNames.map((x)=>populateEmojiOrUserEmoji(x, noteUserHost)));
    return emojis.filter((x)=>x != null);
}
export function aggregateNoteEmojis(notes) {
    let emojis = [];
    for (const note of notes){
        emojis = emojis.concat(note.emojis.map((e)=>parseEmojiStr(e, note.userHost)));
        if (note.renote) {
            emojis = emojis.concat(note.renote.emojis.map((e)=>parseEmojiStr(e, note.renote.userHost)));
            if (note.renote.user) {
                emojis = emojis.concat(note.renote.user.emojis.map((e)=>parseEmojiStr(e, note.renote.userHost)));
            }
        }
        const customReactions = Object.keys(note.reactions).map((x)=>decodeReaction(x)).filter((x)=>x.name != null);
        emojis = emojis.concat(customReactions);
        if (note.user) {
            emojis = emojis.concat(note.user.emojis.map((e)=>parseEmojiStr(e, note.userHost)));
        }
    }
    return emojis.filter((x)=>x.name != null);
}
/**
 * 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します
 */ export async function prefetchEmojis(emojis) {
    const notCachedEmojis = emojis.filter(async (emoji)=>!await cache.get(`${emoji.name} ${emoji.host}`));
    const emojisQuery = [];
    const hosts = new Set(notCachedEmojis.map((e)=>e.host));
    for (const host of hosts){
        emojisQuery.push({
            name: In(notCachedEmojis.filter((e)=>e.host === host).map((e)=>e.name)),
            host: host ?? IsNull()
        });
    }
    const _emojis = emojisQuery.length > 0 ? await Emojis.find({
        where: emojisQuery,
        select: [
            "name",
            "host",
            "originalUrl",
            "publicUrl"
        ]
    }) : [];
    const trans = redisClient.multi();
    for (const emoji of _emojis){
        cache.set(`${emoji.name} ${emoji.host}`, emoji, trans);
    }
    await trans.exec();
}
