import { Hono } from "hono"
import { serveStatic, upgradeWebSocket, websocket } from "hono/bun"
import { prettyJSON } from "hono/pretty-json"
import color from "kleur"

import type { Message } from "./shared/types"
import { NOSE_ICON, NOSE_BIN, NOSE_WWW } from "./config"
import { transpile, isFile, tilde } from "./utils"
import { apps, serveApp, publishDNS } from "./webapp"
import { commands } from "./commands"
import { send, addWebsocket, removeWebsocket, closeWebsockets } from "./websocket"

import { Layout } from "./components/layout"
import { Terminal } from "./components/terminal"
import { dispatchMessage } from "./dispatch"

//
// Hono setup
//

const app = new Hono()

app.use("*", prettyJSON())

app.use('/img/*', serveStatic({ root: './public' }))
app.use('/vendor/*', serveStatic({ root: './public' }))
app.use('/css/*', serveStatic({ root: './src' }))

app.use("*", async (c, next) => {
    const start = Date.now()
    await next()
    const end = Date.now()
    const fn = c.res.status < 300 ? color.green : c.res.status < 500 ? color.yellow : color.red
    console.log(fn(`${c.res.status} ${c.req.method} ${c.req.url} (${end - start}ms)`))
})

app.on("GET", ["/js/:path{.+}", "/shared/:path{.+}"], async c => {
    const path = "./src/" + c.req.path.replace("..", ".")

    // path must end in .js
    if (!path.endsWith(".js")) return c.text("File not found", 404)

    const ts = path.replace(".js", ".ts")
    if (isFile(ts)) {
        return new Response(await transpile(ts), { headers: { "Content-Type": "text/javascript" } })
    } else if (isFile(path)) {
        return new Response(Bun.file(path), { headers: { "Content-Type": "text/javascript" } })
    } else {
        return c.text("File not found", 404)
    }
})

//
// app routes
//

app.use("*", async (c, next) => {
    const url = new URL(c.req.url)
    const localhost = url.hostname.endsWith("localhost")
    const domains = url.hostname.split(".")
    let subdomain = ""

    if (domains.length > (localhost ? 1 : 2))
        subdomain = domains[0]!

    if (subdomain) {
        const app = serveApp(c, subdomain)
        return await app
    }

    return next()
})

app.get("/apps", c => {
    const url = new URL(c.req.url)
    const domain = url.hostname
    let port = url.port
    port = port && port !== "80" ? `:${port}` : ""

    return c.html(<>
        <h1>apps</h1>
        <ul>{apps().map(app => <li><a href={`http://${app}.${domain}${port}`}>{app}</a></li>)}</ul>
    </>)
})

app.get("/", c => c.html(<Layout><Terminal /></Layout>))

//
// websocket
//

app.get("/ws", upgradeWebSocket(async c => {
    return {
        async onOpen(_e, ws) {
            addWebsocket(ws)
            send(ws, { type: "commands", data: await commands() })
        },
        async onMessage(event, ws) {
            let data: Message | undefined

            try {
                data = JSON.parse(event.data.toString())
            } catch (e) {
                console.error("JSON parsing error", e)
                send(ws, { type: "error", data: "json parsing error" })
                return
            }

            if (!data) return

            await dispatchMessage(ws, data)
        },
        onClose: (event, ws) => removeWebsocket(ws)
    }
}))

//
// hot reload mode cleanup
//

if (process.env.BUN_HOT) {
    // @ts-ignore
    globalThis.__hot_reload_cleanup?.()

    // @ts-ignore
    globalThis.__hot_reload_cleanup = () => {
        closeWebsockets()
    }

    for (const sig of ["SIGINT", "SIGTERM"] as const) {
        process.on(sig, () => {
            // @ts-ignore
            globalThis.__hot_reload_cleanup?.()
            process.exit(0)
        })
    }
} else {
    publishDNS()
}


//
// server start
//

console.log(color.cyan(NOSE_ICON))
console.log(color.blue("NOSE_BIN:"), color.yellow(tilde(NOSE_BIN)))
console.log(color.blue("NOSE_WWW:"), color.yellow(tilde(NOSE_WWW)))

export default {
    port: process.env.PORT || 3000,
    hostname: "0.0.0.0",
    fetch: app.fetch,
    idleTimeout: 0,
    websocket
}