import { URL } from "node:url";
import chalk from "chalk";
import { IsNull } from "typeorm";
import config from "@/config/index.js";
import type { User, IRemoteUser } from "@/models/entities/user.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 { IMentionedRemoteUsers } from "@/models/entities/note.js";
import { UserProfile } from "@/models/entities/user-profile.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<string>("resolveUserUriHost", 60 * 60 * 24);
const localUsernameCache = new Cache<string | null>("localUserNameCapitalization", 60 * 60 * 24);
const profileMentionCache = new Cache<ProfileMention | null>("resolveProfileMentions", 60 * 60);

type ProfileMention = {
	user: User;
	profile: UserProfile | null;
	data: {
		username: string;
		host: string | null;
	};
};

type refreshType = 'refresh' | 'refresh-in-background' | 'refresh-timeout-1500ms' | 'no-refresh';

export async function resolveUser(
	username: string,
	host: string | null,
	refresh: refreshType = 'refresh',
	limiter: RecursionLimiter = new RecursionLimiter()
): Promise<User> {
	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,
	})) as IRemoteUser | null;

	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,
				})) as IRemoteUser | null;

				// If yes, return existing user
				if (user != null) {
					logger.succ(`return existing remote user: ${chalk.magenta(finalAcctLower)}`);
					return user;
				}
				// Otherwise create and return new 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: string, host: string | null, objectHost: string | null, limiter: RecursionLimiter) {
	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: string, host: string | null, objectHost: string | null): string {
	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: string): Promise<string | null> {
	return localUsernameCache.fetch(username.toLowerCase(), () =>
		Users.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })
			.then(p => p ? p.username : null));
}

export async function resolveMentionFromCache(username: string, host: string | null, objectHost: string | null, cache: IMentionedRemoteUsers): Promise<{ username: string, href: string } | null> {
	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: string): Promise<string | null> {
	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: string, username: string): Promise<string | null> {
	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: string): Promise<string | null> {
	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: IRemoteUser | undefined): Promise<string | null> {
	return user ? getSubjectHostFromAcct(`${user.username}@${user.host}`) : null;
}

export async function getSubjectHostFromAcctParts(username?: string | undefined, host?: string | undefined): Promise<string | null> {
	return username !== null && host !== null ? getSubjectHostFromAcct(`${username}@${host}`) : null;
}

async function resolveUserWebFinger(acctLower: string, recurse: boolean = true): Promise<{
	subject: string,
	self: {
		href: string;
		rel?: string;
	}
}> {
	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: string): string {
	if (!subject.startsWith('acct:')) {
		logger.error("Subject isnt a valid acct");
		throw ("Subject isnt a valid acct");
	}
	return subject.substring(5);
}

function acctToSubject(acct: string): string {
	return normalizeSubject(`acct:${acct}`);
}

function normalizeSubject(subject: string): string {
	return subject.toLowerCase();
}
