import { Blockings, Followings, Mutings, RenoteMutings, UserListJoinings, UserProfiles } from "../../../../models/index.js";
import { apiLogger } from "../../logger.js";
import { MastodonStreamUser } from "./channels/user.js";
import { MastodonStreamDirect } from "./channels/direct.js";
import { MastodonStreamPublic } from "./channels/public.js";
import { MastodonStreamList } from "./channels/list.js";
import { toSingleLast } from "../../../../prelude/array.js";
import { MastodonStreamTag } from "./channels/tag.js";
const logger = apiLogger.createSubLogger("streaming").createSubLogger("mastodon");
const channels = {
    "user": MastodonStreamUser,
    "user:notification": MastodonStreamUser,
    "direct": MastodonStreamDirect,
    "list": MastodonStreamList,
    "public": MastodonStreamPublic,
    "public:media": MastodonStreamPublic,
    "public:local": MastodonStreamPublic,
    "public:local:media": MastodonStreamPublic,
    "public:remote": MastodonStreamPublic,
    "public:remote:media": MastodonStreamPublic,
    "public:allow_local_only": MastodonStreamPublic,
    "public:allow_local_only:media": MastodonStreamPublic,
    "hashtag": MastodonStreamTag,
    "hashtag:local": MastodonStreamTag
};
export class MastodonStreamingConnection {
    user;
    userProfile;
    following = new Set();
    muting = new Set();
    renoteMuting = new Set();
    blocking = new Set();
    hidden = new Set();
    token;
    wsConnection;
    channels = [];
    subscriber;
    constructor(wsConnection, subscriber, user, token, query){
        const channel = toSingleLast(query.stream);
        logger.debug(`New connection on channel: ${channel}`);
        this.wsConnection = wsConnection;
        this.subscriber = subscriber;
        if (user) this.user = user;
        if (token) this.token = token;
        this.onMessage = this.onMessage.bind(this);
        this.onUserEvent = this.onUserEvent.bind(this);
        this.wsConnection.on("message", this.onMessage);
        if (this.user) {
            this.updateFollowing();
            this.updateMuting();
            this.updateRenoteMuting();
            this.updateBlocking();
            this.updateHidden();
            this.updateUserProfile();
            this.subscriber.on(`user:${this.user.id}`, this.onUserEvent);
        }
        if (channel) {
            const list = toSingleLast(query.list);
            const tag = toSingleLast(query.tag);
            this.onMessage({
                type: "utf8",
                utf8Data: JSON.stringify({
                    stream: channel,
                    type: "subscribe",
                    list,
                    tag
                })
            });
        }
    }
    onUserEvent(data) {
        switch(data.type){
            case "follow":
                this.following.add(data.body.id);
                break;
            case "unfollow":
                this.following.delete(data.body.id);
                break;
            case "mute":
                this.muting.add(data.body.id);
                break;
            case "unmute":
                this.muting.delete(data.body.id);
                break;
            case "userHidden":
                this.hidden.add(data.body);
                break;
            case "userUnhidden":
                this.hidden.delete(data.body);
                break;
            // TODO: renote mute events
            // TODO: block events
            case "updateUserProfile":
                this.userProfile = data.body;
                break;
            case "terminate":
                this.closeConnection();
                break;
            default:
                break;
        }
    }
    async onMessage(data) {
        if (data.type !== "utf8") return;
        if (data.utf8Data == null) return;
        let message;
        try {
            message = JSON.parse(data.utf8Data);
        } catch (e) {
            logger.error("Failed to parse json data, ignoring");
            return;
        }
        const { stream, type, list, tag } = message;
        if (!message.stream || !message.type) {
            logger.error("Invalid message received, ignoring");
            return;
        }
        if (list ?? tag) logger.info(`${type}: ${stream} ${list ?? tag}`);
        else logger.info(`${type}: ${stream}`);
        switch(type){
            case "subscribe":
                this.connectChannel(stream, list, tag);
                break;
            case "unsubscribe":
                this.disconnectChannel(stream);
                break;
        }
    }
    send(stream, event, payload) {
        const json = JSON.stringify({
            stream: [
                stream
            ],
            event: event,
            payload: typeof payload === "string" ? payload : JSON.stringify(payload)
        });
        this.wsConnection.send(json);
    }
    connectChannel(channel, list, tag) {
        if (!channels[channel]) {
            logger.info(`Ignoring connection to unknown channel ${channel}`);
            return;
        }
        if (channels[channel].requireCredential) {
            if (this.user == null) {
                logger.info(`Refusing connection to channel ${channel} without authentication, terminating connection`);
                this.closeConnection();
                return;
            } else if (!channels[channel].requiredScopes.every((p)=>this.token?.scopes?.includes(p))) {
                logger.info(`Refusing connection to channel ${channel} without required OAuth scopes, terminating connection`);
                this.closeConnection();
                return;
            }
        }
        if (channels[channel].shouldShare && this.channels.some((c)=>c.chName === channel)) {
            return;
        }
        let ch;
        if (channel === "list") {
            ch = new channels[channel](this, channel, list);
        } else if (channel.startsWith("hashtag")) ch = new channels[channel](this, channel, tag);
        else ch = new channels[channel](this, channel);
        this.channels.push(ch);
        ch.init(null);
    }
    disconnectChannel(channelName) {
        const channel = this.channels.find((c)=>c.chName === channelName);
        if (channel) {
            if (channel.dispose) channel.dispose();
            this.channels = this.channels.filter((c)=>c.chName !== channelName);
        }
    }
    async updateFollowing() {
        const followings = await Followings.find({
            where: {
                followerId: this.user.id
            },
            select: [
                "followeeId"
            ]
        });
        this.following = new Set(followings.map((x)=>x.followeeId));
    }
    async updateMuting() {
        const mutings = await Mutings.find({
            where: {
                muterId: this.user.id
            },
            select: [
                "muteeId"
            ]
        });
        this.muting = new Set(mutings.map((x)=>x.muteeId));
    }
    async updateRenoteMuting() {
        const renoteMutings = await RenoteMutings.find({
            where: {
                muterId: this.user.id
            },
            select: [
                "muteeId"
            ]
        });
        this.renoteMuting = new Set(renoteMutings.map((x)=>x.muteeId));
    }
    async updateBlocking() {
        const blockings = await Blockings.find({
            where: {
                blockeeId: this.user.id
            },
            select: [
                "blockerId"
            ]
        });
        this.blocking = new Set(blockings.map((x)=>x.blockerId));
    }
    async updateHidden() {
        const hidden = await UserListJoinings.find({
            where: {
                userList: {
                    userId: this.user.id,
                    hideFromHomeTl: true
                }
            },
            select: [
                "userId"
            ]
        });
        this.hidden = new Set(hidden.map((x)=>x.userId));
    }
    async updateUserProfile() {
        this.userProfile = await UserProfiles.findOneBy({
            userId: this.user.id
        });
    }
    closeConnection() {
        this.wsConnection.close();
        this.dispose();
    }
    dispose() {
        for (const c of this.channels.filter((c)=>c.dispose)){
            if (c.dispose) c.dispose();
        }
    }
}
