/// <reference types="bun-types" />

import fs from 'node:fs'
import path from 'node:path'
import { env } from './lib/env'

const isProduction = env.NODE_ENV === 'production'

// ─── Route Classification ──────────────────────────────
const API_PREFIXES = ['/api/', '/webhook/', '/ws/', '/health', '/docs']

function isApiRoute(pathname: string): boolean {
  return API_PREFIXES.some((p) => pathname.startsWith(p)) || pathname === '/health'
}

// ─── Vite Dev Server (dev only) ────────────────────────
let vite: Awaited<ReturnType<typeof import('./vite').createVite>> | null = null
if (!isProduction) {
  const { createVite } = await import('./vite')
  vite = await createVite()
}

// ─── Frontend Serving ──────────────────────────────────
async function serveFrontend(request: Request): Promise<Response> {
  const url = new URL(request.url)
  const pathname = url.pathname

  if (!isProduction && vite) {
    // === DEVELOPMENT: Vite Middleware Mode ===

    // SPA route → serve index.html via Vite transform
    if (
      pathname === '/' ||
      (!pathname.includes('.') &&
        !pathname.startsWith('/@') &&
        !pathname.startsWith('/__open-stack-frame-in-editor'))
    ) {
      const htmlPath = path.resolve('index.html')
      let htmlContent = fs.readFileSync(htmlPath, 'utf-8')
      htmlContent = await vite.transformIndexHtml(pathname, htmlContent)

      // Dedupe: Vite 8 middlewareMode injects react-refresh preamble twice
      const preamble =
        '<script type="module">import { injectIntoGlobalHook } from "/@react-refresh";\ninjectIntoGlobalHook(window);\nwindow.$RefreshReg$ = () => {};\nwindow.$RefreshSig$ = () => (type) => type;</script>'
      const firstIdx = htmlContent.indexOf(preamble)
      if (firstIdx !== -1) {
        const secondIdx = htmlContent.indexOf(preamble, firstIdx + preamble.length)
        if (secondIdx !== -1) {
          htmlContent = htmlContent.slice(0, secondIdx) + htmlContent.slice(secondIdx + preamble.length)
        }
      }

      return new Response(htmlContent, {
        headers: { 'Content-Type': 'text/html' },
      })
    }

    // Asset/module requests → proxy ke Vite middleware
    // Bridge: Bun Request → Node.js IncomingMessage/ServerResponse
    return new Promise<Response>((resolve) => {
      const req = new Proxy(request, {
        get(target, prop) {
          if (prop === 'url') return pathname + url.search
          if (prop === 'method') return request.method
          if (prop === 'headers') return Object.fromEntries(request.headers as any)
          return (target as any)[prop]
        },
      }) as any

      const chunks: (Buffer | Uint8Array)[] = []
      const res = {
        statusCode: 200,
        headers: {} as Record<string, string>,
        setHeader(name: string, value: string) { this.headers[name.toLowerCase()] = value; return this },
        getHeader(name: string) { return this.headers[name.toLowerCase()] },
        removeHeader(name: string) { delete this.headers[name.toLowerCase()] },
        writeHead(code: number, reasonOrHeaders?: string | Record<string, string>, maybeHeaders?: Record<string, string>) {
          this.statusCode = code
          const hdrs = typeof reasonOrHeaders === 'object' ? reasonOrHeaders : maybeHeaders
          if (hdrs) for (const [k, v] of Object.entries(hdrs)) this.headers[k.toLowerCase()] = String(v)
          return this
        },
        write(chunk: any) {
          if (chunk) chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk)
          return true
        },
        end(data?: any) {
          if (data) {
            if (typeof data === 'string') chunks.push(Buffer.from(data))
            else if (data instanceof Uint8Array || Buffer.isBuffer(data)) chunks.push(data)
          }
          resolve(new Response(
            chunks.length > 0 ? Buffer.concat(chunks) : null,
            { status: this.statusCode, headers: this.headers },
          ))
        },
        once() { return this },
        on() { return this },
        emit() { return this },
        removeListener() { return this },
      } as any

      vite.middlewares(req, res, (err: any) => {
        if (err) {
          resolve(new Response(err.stack || err.toString(), { status: 500 }))
          return
        }
        resolve(new Response('Not Found', { status: 404 }))
      })
    })
  }

  // === PRODUCTION: Static Files + SPA Fallback ===
  const filePath = path.join('dist', pathname === '/' ? 'index.html' : pathname)

  if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
    const ext = path.extname(filePath)
    const contentType: Record<string, string> = {
      '.js': 'application/javascript', '.css': 'text/css',
      '.html': 'text/html; charset=utf-8', '.json': 'application/json',
      '.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon',
    }
    const isHashed = pathname.startsWith('/assets/')
    return new Response(Bun.file(filePath), {
      headers: {
        'Content-Type': contentType[ext] ?? 'application/octet-stream',
        'Cache-Control': isHashed ? 'public, max-age=31536000, immutable' : 'public, max-age=3600',
      },
    })
  }

  // SPA fallback — semua route yang tidak match file → index.html
  const indexHtml = path.join('dist', 'index.html')
  if (fs.existsSync(indexHtml)) {
    return new Response(Bun.file(indexHtml), {
      headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' },
    })
  }

  return new Response('Not Found', { status: 404 })
}

// ─── Elysia App ────────────────────────────────────────
import { createApp } from './app'

const app = createApp()

  // Frontend intercept — onRequest jalan SEBELUM route matching
  .onRequest(async ({ request }) => {
    const pathname = new URL(request.url).pathname

    // Dev inspector: open file di editor
    if (!isProduction && pathname === '/__open-in-editor' && request.method === 'POST') {
      const { relativePath, lineNumber, columnNumber } = (await request.json()) as {
        relativePath: string; lineNumber: string; columnNumber: string
      }
      const file = `${process.cwd()}/${relativePath}`
      const editor = env.REACT_EDITOR
      const loc = `${file}:${lineNumber}:${columnNumber}`
      // zed & subl: editor file:line:col — code & cursor: editor --goto file:line:col
      const noGotoEditors = ['subl', 'zed']
      const args = noGotoEditors.includes(editor) ? [loc] : ['--goto', loc]
      const editorPath = Bun.which(editor)
      if (editorPath) Bun.spawn([editor, ...args], { stdio: ['ignore', 'ignore', 'ignore'] })
      return new Response('ok')
    }

    // Non-API route → serve frontend
    if (!isApiRoute(pathname)) {
      return serveFrontend(request)
    }
    // undefined → lanjut ke Elysia route matching
  })

  .listen(env.PORT)

console.log(`Server running at http://localhost:${app.server!.port}`)
