import { IsNull, Not } from "typeorm";
import { Followings, Users } from "../../models/index.js";
import { deliver } from "../../queue/index.js";
import { skippedInstances } from "../../misc/skipped-instances.js";
import { apLogger } from "./logger.js";
const isFollowers = (recipe)=>recipe.type === "Followers";
const isDirect = (recipe)=>recipe.type === "Direct";
//#endregion
export default class DeliverManager {
    actor;
    activity;
    recipes = [];
    /**
	 * Constructor
	 * @param actor Actor
	 * @param activity Activity to deliver
	 */ constructor(actor, activity){
        this.actor = actor;
        this.activity = activity;
    }
    /**
	 * Add recipe for followers deliver
	 */ addFollowersRecipe() {
        const deliver = {
            type: "Followers"
        };
        this.addRecipe(deliver);
    }
    /**
	 * Add recipe for direct deliver
	 * @param to To
	 */ addDirectRecipe(to) {
        const recipe = {
            type: "Direct",
            to
        };
        this.addRecipe(recipe);
    }
    /**
	 * Add recipe
	 * @param recipe Recipe
	 */ addRecipe(recipe) {
        this.recipes.push(recipe);
    }
    /**
	 * Execute delivers
	 */ async execute() {
        if (!Users.isLocalUser(this.actor)) return;
        const inboxes = new Set();
        /*
		build inbox list

		Process follower recipes first to avoid duplication when processing
		direct recipes later.
		*/ if (this.recipes.some((r)=>isFollowers(r))) {
            // followers deliver
            // TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
            // ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう？
            const followers = await Followings.find({
                where: {
                    followeeId: this.actor.id,
                    followerHost: Not(IsNull())
                },
                select: {
                    followerSharedInbox: true,
                    followerInbox: true
                }
            });
            for (const following of followers){
                const inbox = following.followerSharedInbox || following.followerInbox;
                inboxes.add(inbox);
            }
        }
        this.recipes.filter((recipe)=>// followers recipes have already been processed
            isDirect(recipe) && // check that shared inbox has not been added yet
            !(recipe.to.sharedInbox && inboxes.has(recipe.to.sharedInbox)) && // check that they actually have an inbox
            recipe.to.inbox != null).forEach((recipe)=>inboxes.add(recipe.to.inbox));
        const instancesToSkip = await skippedInstances(// get (unique) list of hosts
        Array.from(new Set(Array.from(inboxes).map((inbox)=>{
            try {
                return new URL(inbox).host;
            } catch (e) {
                apLogger.error(`Invalid inbox URL: ${inbox}`);
                return null;
            }
        }).filter((host)=>host != null))));
        // deliver
        for (const inbox of inboxes){
            // skip instances as indicated
            try {
                const host = new URL(inbox).host;
                if (instancesToSkip.includes(host)) continue;
            } catch (e) {
                // skip invalid URLs
                apLogger.error(`Invalid inbox URL: ${inbox}`);
                continue;
            }
            deliver(this.actor, this.activity, inbox);
        }
    }
}
//#region Utilities
/**
 * Deliver activity to followers
 * @param activity Activity
 * @param from Followee
 */ export async function deliverToFollowers(actor, activity) {
    const manager = new DeliverManager(actor, activity);
    manager.addFollowersRecipe();
    await manager.execute();
}
/**
 * Deliver activity to user
 * @param activity Activity
 * @param to Target user
 */ export async function deliverToUser(actor, activity, to) {
    const manager = new DeliverManager(actor, activity);
    manager.addDirectRecipe(to);
    await manager.execute();
} //#endregion
