#!/usr/bin/env bun

// This script runs a separate OpenCode server to be used as a remote
// workspace, simulating a remote environment but all local to make
// debugger easier
//
// *Important*: make sure you add the debug workspace plugin first.
// In `.opencode/opencode.jsonc` in the root of this project add:
//
// "plugin": ["../packages/opencode/src/control-plane/dev/debug-workspace-plugin.ts"]
//
// Afterwards, run `./packages/opencode/script/run-workspace-server`

import { stat } from "node:fs/promises"
import { setTimeout as sleep } from "node:timers/promises"

const DEV_DATA_FILE = "/tmp/opencode-workspace-dev-data.json"
const RESTART_POLL_INTERVAL = 250

async function readData() {
  return await Bun.file(DEV_DATA_FILE).json()
}

async function readDataMtime() {
  return await stat(DEV_DATA_FILE)
    .then((info) => info.mtimeMs)
    .catch((error) => {
      if (typeof error === "object" && error && "code" in error && error.code === "ENOENT") {
        return undefined
      }

      throw error
    })
}

async function readSnapshot() {
  while (true) {
    try {
      const before = await readDataMtime()
      if (before === undefined) {
        await sleep(RESTART_POLL_INTERVAL)
        continue
      }

      const data = await readData()
      const after = await readDataMtime()

      if (before === after) {
        return { data, mtime: after }
      }
    } catch (error) {
      if (typeof error === "object" && error && "code" in error && error.code === "ENOENT") {
        await sleep(RESTART_POLL_INTERVAL)
        continue
      }

      throw error
    }
  }
}

function startDevServer(data: any) {
  const env = Object.fromEntries(Object.entries(data.env ?? {}).filter(([, value]) => value !== undefined))

  return Bun.spawn(["bun", "run", "dev", "serve", "--port", String(data.port), "--print-logs"], {
    env: {
      ...process.env,
      ...env,
      XDG_DATA_HOME: "/tmp/data",
    },
    stdin: "inherit",
    stdout: "inherit",
    stderr: "inherit",
  })
}

async function waitForRestartSignal(mtime: number, signal: AbortSignal) {
  while (!signal.aborted) {
    await sleep(RESTART_POLL_INTERVAL)
    if (signal.aborted) return false
    if ((await readDataMtime()) !== mtime) return true
  }

  return false
}

while (true) {
  const { data, mtime } = await readSnapshot()
  const proc = startDevServer(data)
  const restartAbort = new AbortController()

  const result = await Promise.race([
    proc.exited.then((code) => ({ type: "exit" as const, code })),
    waitForRestartSignal(mtime, restartAbort.signal).then((restart) => ({ type: "restart" as const, restart })),
  ])

  restartAbort.abort()

  if (result.type === "restart" && result.restart) {
    proc.kill()
    await proc.exited
    continue
  }

  process.exit(result.code)
}
