////
// Hosting for your NOSE webapps!

import type { Child } from "hono/jsx"
import { type Context, Hono } from "hono"
import { join } from "path"
import { readdirSync, watch } from "fs"
import { sendAll } from "./websocket"
import { expectDir } from "./utils"
import { NOSE_DIR } from "./config"
import { isFile, isDir, mtimeEpoch } from "./utils"
import { importUrl } from "./shared/utils"

export type Handler = (r: Context) => string | Child | Response | Promise<Response>
export type App = Hono | Handler

export function initWebapps() {
    startWatcher()
}

export async function serveApp(c: Context, subdomain: string): Promise<Response> {
    const app = await findApp(subdomain)
    const path = appDir(subdomain)

    if (!path) return c.text(`App not found: ${subdomain}`, 404)

    const staticPath = join(path, "pub", c.req.path === "/" ? "/index.html" : c.req.path)

    if (isFile(staticPath))
        return serveStatic(staticPath)

    if (!app) return c.text(`App not found: ${subdomain}`, 404)

    if (app instanceof Hono)
        return app.fetch(c.req.raw)
    else
        return toResponse(await app(c))
}

export function apps(): string[] {
    const apps: string[] = []

    for (const entry of readdirSync(NOSE_DIR))
        if (isApp(entry))
            apps.push(entry)

    return apps.sort()
}

function isApp(name: string): boolean {
    return isFile(join(NOSE_DIR, name, "index.ts"))
        || isFile(join(NOSE_DIR, name, "index.tsx"))
        || isDir(join(NOSE_DIR, name, "pub"))
}

export function appDir(name: string): string | undefined {
    if (isApp(name))
        return join(NOSE_DIR, name)
}

async function findApp(name: string): Promise<App | undefined> {
    const paths = [
        join(name, "index.ts"),
        join(name, "index.tsx")
    ]

    for (const path of paths) {
        const app = await loadApp(join(NOSE_DIR, path))
        if (app) return app
    }
}

async function loadApp(path: string): Promise<App | undefined> {
    if (!await Bun.file(path).exists()) return

    const mod = await importUrl(path, await mtimeEpoch(path))
    if (mod?.default)
        return mod.default as App
}

export async function toResponse(source: string | Child | Response): Promise<Response> {
    if (source instanceof Response)
        return source
    else if (typeof source === "string")
        return new Response(source)
    else
        return new Response(await source?.toString(), {
            headers: {
                "Content-Type": "text/html; charset=utf-8"
            }
        })
}

function serveStatic(path: string): Response {
    const file = Bun.file(path)
    return new Response(file, {
        headers: {
            "Content-Type": file.type
        }
    })
}

let wwwWatcher
function startWatcher() {
    if (!expectDir(NOSE_DIR)) return

    wwwWatcher = watch(NOSE_DIR, { recursive: true }, async (event, filename) => {
        if (!filename) return

        if (/^.+\/index\.tsx?$/.test(filename))
            sendAll({ type: "apps", data: apps() })
    })
}