////
// Sneaker is our tunneling service that allows you to share your local NOSE webapps
// with the public internet. It requires a sneaker server, usually hosted by us.

import nose from "./server"
import { clearState, setState, getState } from "./state"

const SNEAKER_URL = "nose.space"
const SNEAKER_TLS = true
// const SNEAKER_URL = "localhost:3100"
// const SNEAKER_TLS = false

const PREFIX = "sneaker:"

type Connection = {
    subdomain: string
    ws: any
    close: boolean // manual close
}
const conns: Record<string, Connection> = {}

export function initSneakers() {
    const state = getState()
    if (!state) return

    for (const key in state) {
        if (key.startsWith(PREFIX)) {
            const app = key.replace(PREFIX, "")
            connectSneaker(app, state[key])
        }
    }
}

export function sneakerUrl(appOrSubdomain: string): string {
    let conn = conns[appOrSubdomain]
    if (!conn) {
        for (const appName of Object.keys(conns)) {
            if (conns[appName]?.subdomain === appOrSubdomain) {
                conn = conns[appName]
                break
            }
        }
        if (!conn)
            return "none"
    }

    let url = "http" + (SNEAKER_TLS ? "s" : "") + "://"
    url += conn.subdomain + "." + SNEAKER_URL

    return url
}

export function sneakers(): string[] {
    return Object.keys(conns)
}

export async function disconnectSneakers() {
    for (const app in conns) {
        const conn = conns[app]!
        conn.close = true
        conn.ws.close()
    }
}

export async function disconnectSneaker(app: string): Promise<boolean> {
    if (!sneakers().includes(app) || !conns[app])
        return false

    await clearState(`${PREFIX}${app}`)
    conns[app].close = true
    conns[app].ws.close()

    return true
}

// returns the sneaker subdomain if successful
export async function connectSneaker(app: string, subdomain = ""): Promise<string> {
    if (conns[app]) {
        return conns[app].subdomain
    }

    let url = `ws${SNEAKER_TLS ? "s" : ""}://${SNEAKER_URL}/tunnel?app=${app}`
    if (subdomain) url += `&subdomain=${subdomain}`

    const ws = new WebSocket(url)
    ws.binaryType = 'arraybuffer'

    let resolve: (v: string) => void
    let promise = new Promise<string>(res => resolve = res)

    ws.onclose = e => {
        if (!conns[app]?.close)
            setTimeout(() => connectSneaker(app, subdomain), 1000) // simple retry
        delete conns[app]
    }

    ws.onerror = e => console.error("sneaker error", e)

    ws.onmessage = async event => {
        const msg = JSON.parse(event.data.toString())

        if (msg.subdomain) {
            conns[app] = { subdomain: msg.subdomain, ws, close: false }
            await setState(`${PREFIX}${app}`, msg.subdomain)
            subdomain = msg.subdomain
            resolve(msg.subdomain)
            return
        }

        try {
            const req = new Request(`http://${msg.app}.localhost` + msg.path, {
                method: msg.method,
                headers: msg.headers,
                body: msg.body || undefined,
            })

            const res = await nose.fetch(req)

            const headers: Record<string, string> = {}
            res.headers.forEach((v, k) => (headers[k] = v))

            const contentType = res.headers.get('content-type') || ''
            const isBinary = isBinaryContentType(contentType)

            let body: string | ArrayBuffer
            let responseData: any

            if (isBinary) {
                const arrayBuffer = await res.arrayBuffer()
                const base64 = Buffer.from(arrayBuffer).toString('base64')

                responseData = {
                    id: msg.id,
                    status: res.status,
                    headers,
                    body: base64,
                    isBinary: true
                }
            } else {
                body = await res.text()

                responseData = {
                    id: msg.id,
                    status: res.status,
                    headers,
                    body,
                    isBinary: false
                }
            }

            ws.send(JSON.stringify(responseData))
        } catch (err: any) {
            ws.send(JSON.stringify({
                id: msg.id,
                status: 500,
                headers: { "content-type": "text/plain" },
                body: "error: " + err.message,
                isBinary: false
            }))
        }
    }

    return promise
}

function isBinaryContentType(contentType: string): boolean {
    const binaryTypes = [
        'image/', 'audio/', 'video/', 'application/octet-stream',
        'application/pdf', 'application/zip', 'font/'
    ]
    return binaryTypes.some(type => contentType.startsWith(type))
}
