import Core from "evite/src/core"
import config from "config"
import { Bridge } from "linebridge/dist/client"
import { Session } from "models"

export default class ApiCore extends Core {
    constructor(props) {
        super(props)

        this.namespaces = Object()

        this.onExpiredExceptionEvent = false
        this.excludedExpiredExceptionURL = ["/regenerate_session_token"]

        this.ctx.registerPublicMethod("api", this)
    }

    async customRequest(
        namepace = undefined,
        payload = {
            method: "GET",
        },
        ...args
    ) {
        if (typeof namepace === "undefined") {
            throw new Error("Namespace must be defined")
        }

        if (typeof this.namespaces[namepace] === "undefined") {
            throw new Error("Namespace not found")
        }

        if (typeof payload === "string") {
            payload = {
                url: payload,
            }
        }

        if (typeof payload.headers !== "object") {
            payload.headers = {}
        }

        const sessionToken = await Session.token

        if (sessionToken) {
            payload.headers["Authorization"] = `Bearer ${sessionToken}`

        } else {
            console.warn("Making a request with no session token")
        }

        return await this.namespaces[namepace].httpInterface(payload, ...args)
    }

    request = (namespace = "main", method, endpoint, ...args) => {
        if (!this.namespaces[namespace]) {
            throw new Error(`Namespace ${namespace} not found`)
        }

        if (!this.namespaces[namespace].endpoints[method]) {
            throw new Error(`Method ${method} not found`)
        }

        if (!this.namespaces[namespace].endpoints[method][endpoint]) {
            throw new Error(`Endpoint ${endpoint} not found`)
        }

        return this.namespaces[namespace].endpoints[method][endpoint](...args)
    }

    withEndpoints = (namespace = "main") => {
        if (!this.namespaces[namespace]) {
            throw new Error(`Namespace ${namespace} not found`)
        }

        return this.namespaces[namespace].endpoints
    }

    handleBeforeRequest = async (request) => {
        if (this.onExpiredExceptionEvent) {
            if (this.excludedExpiredExceptionURL.includes(request.url)) return

            await new Promise((resolve) => {
                app.eventBus.once("session.regenerated", () => {
                    console.log(`Session has been regenerated, retrying request`)
                    resolve()
                })
            })
        }
    }

    handleRegenerationEvent = async (refreshToken, makeRequest) => {
        window.app.eventBus.emit("session.expiredExceptionEvent", refreshToken)

        this.onExpiredExceptionEvent = true

        const expiredToken = await Session.token

        // exclude regeneration endpoint

        // send request to regenerate token
        const response = await this.request("main", "post", "regenerateSessionToken", {
            expiredToken: expiredToken,
            refreshToken,
        }).catch((error) => {
            console.error(`Failed to regenerate token: ${error.message}`)
            return false
        })

        if (!response) {
            return window.app.eventBus.emit("session.invalid", "Failed to regenerate token")
        }

        // set new token
        Session.token = response.token

        //this.namespaces["main"].internalAbortController.abort()

        this.onExpiredExceptionEvent = false

        // emit event
        window.app.eventBus.emit("session.regenerated")
    }

    connectBridge = (key, params) => {
        this.namespaces[key] = this.createBridge(params)
    }

    createBridge(params = {}) {
        const getSessionContext = async () => {
            const obj = {}
            const token = await Session.token

            if (token) {
                // append token to context
                obj.headers = {
                    Authorization: `Bearer ${token ?? null}`,
                }
            }

            return obj
        }

        const handleResponse = async (data, makeRequest) => {
            // handle 401 responses
            if (data instanceof Error) {
                if (data.response.status === 401) {
                    // check if the server issue a refresh token on data
                    if (data.response.data.refreshToken) {
                        // handle regeneration event
                        await this.handleRegenerationEvent(data.response.data.refreshToken, makeRequest)
                        return await makeRequest()
                    } else {
                        return window.app.eventBus.emit("session.invalid", "Session expired, but the server did not issue a refresh token")
                    }
                }
                if (data.response.status === 403) {
                    return window.app.eventBus.emit("session.invalid", "Session not valid or not existent")
                }
            }
        }

        if (typeof params !== "object") {
            throw new Error("Params must be an object")
        }

        const bridgeOptions = {
            wsOptions: {
                autoConnect: false,
            },
            onBeforeRequest: this.handleBeforeRequest,
            onRequest: getSessionContext,
            onResponse: handleResponse,
            ...params,
            origin: params.httpAddress ?? config.remotes.mainApi,
        }

        const bridge = new Bridge(bridgeOptions)

        // handle main ws events
        const mainWSSocket = bridge.wsInterface.sockets["main"]

        mainWSSocket.on("authenticated", () => {
            console.debug("[WS] Authenticated")
        })

        mainWSSocket.on("authenticateFailed", (error) => {
            console.error("[WS] Authenticate Failed", error)
        })

        mainWSSocket.on("connect", () => {
            this.ctx.eventBus.emit(`api.ws.main.connect`)
        })

        mainWSSocket.on("disconnect", (...context) => {
            this.ctx.eventBus.emit(`api.ws.main.disconnect`, ...context)
        })

        mainWSSocket.on("connect_error", (...context) => {
            this.ctx.eventBus.emit(`api.ws.main.connect_error`, ...context)
        })

        // generate functions
        bridge.listenEvent = this.generateMainWSEventListener(bridge.wsInterface)
        bridge.unlistenEvent = this.generateMainWSEventUnlistener(bridge.wsInterface)

        // return bridge
        return bridge
    }

    generateMainWSEventListener(obj) {
        return (to, fn) => {
            if (typeof to === "undefined") {
                console.error("handleWSListener: to must be defined")
                return false
            }
            if (typeof fn !== "function") {
                console.error("handleWSListener: fn must be function")
                return false
            }

            let ns = "main"
            let event = null

            if (typeof to === "string") {
                event = to
            } else if (typeof to === "object") {
                ns = to.ns
                event = to.event
            }

            return obj.sockets[ns].on(event, async (...context) => {
                return await fn(...context)
            })
        }
    }

    generateMainWSEventUnlistener(obj) {
        return (to, fn) => {
            if (typeof to === "undefined") {
                console.error("handleWSListener: to must be defined")
                return false
            }
            if (typeof fn !== "function") {
                console.error("handleWSListener: fn must be function")
                return false
            }

            let ns = "main"
            let event = null

            if (typeof to === "string") {
                event = to
            } else if (typeof to === "object") {
                ns = to.ns
                event = to.event
            }

            return obj.sockets[ns].removeListener(event, fn)
        }
    }
}