import { URL } from "node:url";
import chalk from "chalk";
import { IsNull } from "typeorm";
import config from "../config/index.js";
import { UserProfiles, Users } from "../models/index.js";
import { toPuny } from "../misc/convert-host.js";
import webFinger from "./webfinger.js";
import { createPerson, updatePerson } from "./activitypub/models/person.js";
import { remoteLogger } from "./logger.js";
import { Cache } from "../misc/cache.js";
import { RecursionLimiter } from "../models/repositories/user-profile.js";
import { promiseEarlyReturn } from "../prelude/promise.js";
const logger = remoteLogger.createSubLogger("resolve-user");
const uriHostCache = new Cache("resolveUserUriHost", 60 * 60 * 24);
const localUsernameCache = new Cache("localUserNameCapitalization", 60 * 60 * 24);
const profileMentionCache = new Cache("resolveProfileMentions", 60 * 60);
export async function resolveUser(username, host, refresh = 'refresh', limiter = new RecursionLimiter()) {
    const usernameLower = username.toLowerCase();
    // Return local user if host part is empty
    if (host == null) {
        logger.info(`return local user: ${usernameLower}`);
        return await Users.findOneBy({
            usernameLower,
            host: IsNull()
        }).then((u)=>{
            if (u == null) {
                throw new Error("user not found");
            } else {
                return u;
            }
        });
    }
    host = toPuny(host);
    // Also return local user if host part is specified but referencing the local instance
    if (config.host === host || config.domain === host) {
        logger.info(`return local user: ${usernameLower}`);
        return await Users.findOneBy({
            usernameLower,
            host: IsNull()
        }).then((u)=>{
            if (u == null) {
                throw new Error("user not found");
            } else {
                return u;
            }
        });
    }
    // Check if remote user is already in the database
    let user = await Users.findOneBy({
        usernameLower,
        host
    });
    const acctLower = `${usernameLower}@${host}`;
    // If not, look up the user on the remote server
    if (user == null) {
        // Run WebFinger
        const fingerRes = await resolveUserWebFinger(acctLower);
        const finalAcct = subjectToAcct(fingerRes.subject);
        const finalAcctLower = finalAcct.toLowerCase();
        const m = finalAcct.match(/^([^@]+)@(.*)/);
        const subjectHost = m ? m[2] : undefined;
        // If subject is different, we're dealing with a split domain setup (that's already been validated by resolveUserWebFinger)
        if (acctLower != finalAcctLower) {
            logger.info('re-resolving split domain redirect user...');
            const m = finalAcct.match(/^([^@]+)@(.*)/);
            if (m) {
                // Re-check if we already have the user in the database post-redirect
                user = await Users.findOneBy({
                    usernameLower: usernameLower,
                    host: subjectHost
                });
                // If yes, return existing user
                if (user != null) {
                    logger.succ(`return existing remote user: ${chalk.magenta(finalAcctLower)}`);
                    return user;
                } else {
                    logger.succ(`return new remote user: ${chalk.magenta(finalAcctLower)}`);
                    return await createPerson(fingerRes.self.href, undefined, subjectHost, limiter);
                }
            }
        }
        // Not a split domain setup, so we can simply create and return the new user
        logger.succ(`return new remote user: ${chalk.magenta(finalAcctLower)}`);
        return await createPerson(fingerRes.self.href, undefined, subjectHost, limiter);
    }
    // If user information is out of date, return it by starting over from WebFinger
    if ((refresh === 'refresh' || refresh === 'refresh-timeout-1500ms') && (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24)) {
        // Prevent multiple attempts to connect to unconnected instances, update before each attempt to prevent subsequent similar attempts
        await Users.update(user.id, {
            lastFetchedAt: new Date()
        });
        logger.info(`try resync: ${acctLower}`);
        const fingerRes = await resolveUserWebFinger(acctLower);
        if (user.uri !== fingerRes.self.href) {
            // if uri mismatch, Fix (user@host <=> AP's Person id(IRemoteUser.uri)) mapping.
            logger.info(`uri missmatch: ${acctLower}`);
            logger.info(`recovery mismatch uri for (username=${username}, host=${host}) from ${user.uri} to ${fingerRes.self.href}`);
            // validate uri
            const uri = new URL(fingerRes.self.href);
            if (uri.hostname !== host) {
                throw new Error("Invalid uri");
            }
            await Users.update({
                usernameLower,
                host: host
            }, {
                uri: fingerRes.self.href
            });
        } else {
            logger.info(`uri is fine: ${acctLower}`);
        }
        const finalAcct = subjectToAcct(fingerRes.subject);
        const finalAcctLower = finalAcct.toLowerCase();
        const m = finalAcct.match(/^([^@]+)@(.*)/);
        const finalHost = m ? m[2] : null;
        // Update user.host if we're dealing with an account that's part of a split domain setup that hasn't been fixed yet
        if (m && user.host != finalHost) {
            logger.info(`updating user host to subject acct host: ${user.host} -> ${finalHost}`);
            await Users.update({
                usernameLower,
                host: user.host
            }, {
                host: finalHost
            });
        }
        if (refresh === 'refresh') {
            await updatePerson(fingerRes.self.href);
            logger.info(`return resynced remote user: ${finalAcctLower}`);
        } else if (refresh === 'refresh-timeout-1500ms') {
            const res = await promiseEarlyReturn(updatePerson(fingerRes.self.href), 1500);
            logger.info(`return possibly resynced remote user: ${finalAcctLower}`);
        }
        return await Users.findOneBy({
            uri: fingerRes.self.href
        }).then((u)=>{
            if (u == null) {
                throw new Error("user not found");
            } else {
                return u;
            }
        });
    } else if (refresh === 'refresh-in-background' && (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24)) {
        // Run the refresh in the background
        // noinspection ES6MissingAwait
        resolveUser(username, host, 'refresh', limiter);
    }
    logger.info(`return existing remote user: ${acctLower}`);
    return user;
}
export async function resolveMentionToUserAndProfile(username, host, objectHost, limiter) {
    return profileMentionCache.fetch(`${username}@${host ?? objectHost}`, async ()=>{
        try {
            const user = await resolveUser(username, host ?? objectHost, 'no-refresh', limiter);
            const profile = await UserProfiles.findOneBy({
                userId: user.id
            });
            const data = {
                username,
                host: host ?? objectHost
            };
            return {
                user,
                profile,
                data
            };
        } catch  {
            return null;
        }
    });
}
export function getMentionFallbackUri(username, host, objectHost) {
    let fallback = `${config.url}/@${username}`;
    if (host !== null && host !== config.domain) fallback += `@${host}`;
    else if (objectHost !== null && objectHost !== config.domain && host !== config.domain) fallback += `@${objectHost}`;
    return fallback;
}
async function getLocalUsernameCached(username) {
    return localUsernameCache.fetch(username.toLowerCase(), ()=>Users.findOneBy({
            usernameLower: username.toLowerCase(),
            host: IsNull()
        }).then((p)=>p ? p.username : null));
}
export async function resolveMentionFromCache(username, host, objectHost, cache) {
    const isLocal = host === null && objectHost === null || host === config.domain;
    if (isLocal) {
        const finalUsername = await getLocalUsernameCached(username);
        if (finalUsername === null) return null;
        username = finalUsername;
    }
    const fallback = getMentionFallbackUri(username, host, objectHost);
    const cached = cache.find((r)=>r.username.toLowerCase() === username.toLowerCase() && r.host === (host ?? objectHost));
    const href = cached?.url ?? cached?.uri;
    if (cached && href != null) return {
        username: cached.username,
        href: href
    };
    if (isLocal) return {
        username: username,
        href: fallback
    };
    return null;
}
export async function getSubjectHostFromUri(uri) {
    try {
        const acct = subjectToAcct((await webFinger(uri)).subject);
        const res = await resolveUserWebFinger(acct.toLowerCase());
        const finalAcct = subjectToAcct(res.subject);
        const m = finalAcct.match(/^([^@]+)@(.*)/);
        if (!m) {
            return null;
        }
        return m[2];
    } catch  {
        return null;
    }
}
export async function getSubjectHostFromUriAndUsernameCached(uri, username) {
    const url = new URL(uri);
    const hostname = url.hostname;
    username = username.substring(1); // remove leading @ from username
    // This resolves invalid mentions with the URL format https://host.tld/@user@otherhost.tld
    const match = url.pathname.match(/^\/@(?<user>[a-zA-Z0-9_]+|$)@(?<host>[a-zA-Z0-9-.]+\.[a-zA-Z0-9-]+)$/);
    if (match && match.groups?.host) {
        return match.groups.host;
    }
    if (hostname === config.hostname) {
        // user is local, return local account domain
        return config.domain;
    }
    const user = await Users.findOneBy({
        usernameLower: username.toLowerCase(),
        host: hostname
    });
    return user ? user.host : await uriHostCache.fetch(uri, async ()=>await getSubjectHostFromUri(uri) ?? await getSubjectHostFromAcctParts(username, hostname) ?? hostname);
}
export async function getSubjectHostFromAcct(acct) {
    try {
        const res = await resolveUserWebFinger(acct.toLowerCase());
        const finalAcct = subjectToAcct(res.subject);
        const m = finalAcct.match(/^([^@]+)@(.*)/);
        if (!m) {
            return null;
        }
        return m[2];
    } catch  {
        return null;
    }
}
export async function getSubjectHostFromRemoteUser(user) {
    return user ? getSubjectHostFromAcct(`${user.username}@${user.host}`) : null;
}
export async function getSubjectHostFromAcctParts(username, host) {
    return username !== null && host !== null ? getSubjectHostFromAcct(`${username}@${host}`) : null;
}
async function resolveUserWebFinger(acctLower, recurse = true) {
    logger.info(`WebFinger for ${chalk.yellow(acctLower)}`);
    const fingerRes = await webFinger(acctLower).catch((e)=>{
        logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${e.statusCode || e.message}`);
        throw new Error(`Failed to WebFinger for ${acctLower}: ${e.statusCode || e.message}`);
    });
    const self = fingerRes.links.find((link)=>link.rel != null && link.rel.toLowerCase() === "self");
    if (!self) {
        logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: self link not found`);
        throw new Error("self link not found");
    }
    if (`${acctToSubject(acctLower)}` !== normalizeSubject(fingerRes.subject)) {
        logger.info(`acct subject mismatch (${acctToSubject(acctLower)} !== ${normalizeSubject(fingerRes.subject)}), possible split domain deployment detected, repeating webfinger`);
        if (!recurse) {
            logger.error('split domain verification failed (recurse limit reached), aborting');
            throw new Error('split domain verification failed (recurse limit reached), aborting');
        }
        const initialAcct = subjectToAcct(fingerRes.subject);
        const initialAcctLower = initialAcct.toLowerCase();
        const splitFingerRes = await resolveUserWebFinger(initialAcctLower, false);
        const finalAcct = subjectToAcct(splitFingerRes.subject);
        const finalAcctLower = finalAcct.toLowerCase();
        if (initialAcct !== finalAcct) {
            logger.error('split domain verification failed (subject mismatch), aborting');
            throw new Error('split domain verification failed (subject mismatch), aborting');
        }
        logger.info(`split domain configuration detected: ${acctLower} -> ${finalAcctLower}`);
        return splitFingerRes;
    }
    return {
        subject: fingerRes.subject,
        self: self
    };
}
function subjectToAcct(subject) {
    if (!subject.startsWith('acct:')) {
        logger.error("Subject isnt a valid acct");
        throw "Subject isnt a valid acct";
    }
    return subject.substring(5);
}
function acctToSubject(acct) {
    return normalizeSubject(`acct:${acct}`);
}
function normalizeSubject(subject) {
    return subject.toLowerCase();
}
