import { Subprocess } from 'bun'
import { Hype } from 'hype'
import { readdirSync, readFileSync } from 'fs'
import { join } from 'path'

const APPS_DIR = join(process.env.DATA_DIR ?? '.', 'apps')

type RunningApp = {
  name: string
  port: number
  proc: Subprocess
}

const runningApps = new Map<string, RunningApp>()

const err = (app: string, ...msg: string[]) => 
  console.error('🐾', `${app}:`, ...msg)

const info = (app: string, ...msg: string[]) => 
  console.log('🐾', `${app}:`, ...msg)

const log = (app: string, ...msg: string[]) => 
  console.log('🐾', `${app}»`, ...msg)

const appNames = () => {
  return readdirSync(APPS_DIR, { withFileTypes: true })
    .filter(e => e.isDirectory())
    .map(e => e.name)
    .sort()
}

let NEXT_PORT = 3001
const getPort = () => NEXT_PORT++

const runApps = () => {
  for (const dir of appNames()) {
    if (!isApp(dir)) continue
    const port = getPort()
    runApp(dir, port)
  }
}

const isApp = (dir: string): boolean => {
  try {
    const file = readFileSync(join(APPS_DIR, dir, 'package.json'), 'utf-8')
    const json = JSON.parse(file)
    return !!json.scripts?.toes
  } catch (e) {
    return false
  }
}

const loadApp = (dir: string) => {
  try {
    const file = readFileSync(join(APPS_DIR, dir, 'package.json'), 'utf-8')
    const json = JSON.parse(file)

    if (json.scripts?.toes) {
      return json
    } else {
      err(dir, 'No `bun toes` script in package.json')
      return {}
    }
  } catch (e) {
    err(dir, 'No package.json')
    return {}
  }
}

const runApp = (dir: string, port: number) => {
  const pkg = loadApp(dir)
  if (!pkg.scripts?.toes) return

  const cwd = join(APPS_DIR, dir)
  const cmd = ['bun', 'run', 'toes']

  info(dir, `Starting on port ${port}...`)

  const proc = Bun.spawn(cmd, {
    cwd,
    env: { ...process.env, PORT: String(port) },
    stdout: 'pipe',
    stderr: 'pipe',
  })

  runningApps.set(dir, { name: dir, port, proc })

  const streamOutput = async (stream: ReadableStream<Uint8Array> | null, isErr: boolean) => {
    if (!stream) return
    const reader = stream.getReader()
    const decoder = new TextDecoder()
    while (true) {
      const { done, value } = await reader.read()
      if (done) break
      const text = decoder.decode(value).trimEnd()
      if (text) {
        //isErr ? err(dir, text) : info(dir, text)
        log(dir, text)
      }
    }
  }

  streamOutput(proc.stdout, false)
  streamOutput(proc.stderr, true)

  // Handle process exit
  proc.exited.then(code => {
    if (code !== 0) 
      err(dir, `Exited with code ${code}`)
     else 
      info(dir, 'Stopped')
    runningApps.delete(dir)
  })
}

const getRunningApps = () => 
  Array.from(runningApps.values()).map(({ name, port }) => ({ name, port }))

const stopApp = (dir: string) => {
  const app = runningApps.get(dir)
  if (app) {
    info(dir, 'Stopping...')
    app.proc.kill()
  }
}

console.log('🐾 Toes!')
runApps()


const app = new Hype()

app.get('/', c => {
  return c.html(
    <>
      <h1>🐾 Running Apps</h1>
      {getRunningApps().map(app => <h2><a href={`http://localhost:${app.port}`}>{app.port}: {app.name}</a></h2>)}
    </>
  )
})

export { getRunningApps, stopApp }
export default app.defaults
