import type { WSContext, WSMessageReceive } from 'hono/ws'
import { getAppBySubdomain } from '$apps'
import { serveStatic } from '$static'

export const perf = { timing: false }

const upstreams = new WeakMap<WSContext, WebSocket>()
const pendingMessages = new WeakMap<WSContext, (string | ArrayBuffer | Uint8Array)[]>()

export function extractSubdomain(host: string): string | null {
  // Strip port
  const hostname = host.replace(/:\d+$/, '')

  // *.localhost -> take prefix (e.g. clock.localhost -> clock)
  if (hostname.endsWith('.localhost')) {
    const sub = hostname.slice(0, -'.localhost'.length)
    return sub || null
  }

  // *.X.local -> take first segment if 3+ parts (e.g. clock.toes.local -> clock)
  if (hostname.endsWith('.local')) {
    const parts = hostname.split('.')
    if (parts.length >= 3) {
      return parts[0]!
    }
  }

  return null
}

export async function proxySubdomain(subdomain: string, req: Request): Promise<Response> {
  const app = getAppBySubdomain(subdomain)

  if (!app || app.state !== 'running') {
    return new Response(`App "${subdomain}" not found or not running`, { status: 502 })
  }

  // Static apps: serve from pub/ directory
  if (app.static) {
    return serveStatic(app.name, req)
  }

  if (!app.port) {
    return new Response(`App "${subdomain}" not found or not running`, { status: 502 })
  }

  const url = new URL(req.url)
  const target = `http://localhost:${app.port}${url.pathname}${url.search}`
  const hasBody = ['POST', 'PUT', 'PATCH'].includes(req.method)
  const body = hasBody ? req.body : undefined

  const headers = new Headers(req.headers)
  headers.set('host', `localhost:${app.port}`)
  if (!headers.has('x-app-url')) {
    headers.set('x-app-url', app.tunnelUrl ?? `${url.protocol}//${subdomain}.${url.hostname}`)
  }
  headers.delete('connection')
  headers.delete('keep-alive')
  headers.delete('transfer-encoding')

  try {
    const shouldTime = perf.timing
    const start = shouldTime ? performance.now() : 0
    const res = await fetch(target, {
      method: req.method,
      headers,
      body,
      redirect: 'manual',
    })
    if (shouldTime) {
      const ms = (performance.now() - start).toFixed(1)
      console.log(`[perf] ${req.method} ${subdomain}${url.pathname} → ${res.status} in ${ms}ms`)
    }
    return res
  } catch (e) {
    console.error(`Proxy error for ${subdomain}:`, e)
    return new Response(`App "${subdomain}" is not responding`, { status: 502 })
  }
}

export function wsProxyEvents(subdomain: string, req: Request) {
  const app = getAppBySubdomain(subdomain)

  if (!app || app.state !== 'running' || !app.port) return null

  const url = new URL(req.url)
  const path = url.pathname + url.search
  const protocolHeader = req.headers.get('sec-websocket-protocol')
  const protocols = protocolHeader ? protocolHeader.split(',').map(p => p.trim()) : []

  // Collect headers to forward to the upstream app
  const forwardHeaders: Record<string, string> = {}
  for (const name of ['cookie', 'authorization', 'x-app-url']) {
    const value = req.headers.get(name)
    if (value) forwardHeaders[name] = value
  }

  if (!forwardHeaders['x-app-url']) {
    forwardHeaders['x-app-url'] = app.tunnelUrl ?? `${url.protocol}//${subdomain}.${url.hostname}`
  }

  const port = app.port

  return {
    onOpen(_evt: Event, ws: WSContext) {
      const upstream = new WebSocket(`ws://localhost:${port}${path}`, {
        headers: { ...forwardHeaders, host: `localhost:${port}` },
        protocols,
      })

      upstream.binaryType = 'arraybuffer'
      upstreams.set(ws, upstream)
      pendingMessages.set(ws, [])

      const timeout = setTimeout(() => {
        if (upstream.readyState !== WebSocket.OPEN) {
          upstream.close()
          ws.close()
        }
      }, 10_000)

      upstream.addEventListener('open', () => {
        clearTimeout(timeout)
        const buffered = pendingMessages.get(ws)
        if (buffered) {
          for (const msg of buffered) upstream.send(msg)
          pendingMessages.delete(ws)
        }
      })

      upstream.addEventListener('message', e => {
        ws.send(e.data as string | ArrayBuffer)
      })

      upstream.addEventListener('close', () => {
        clearTimeout(timeout)
        pendingMessages.delete(ws)
        upstreams.delete(ws)
        ws.close()
      })

      upstream.addEventListener('error', () => {
        clearTimeout(timeout)
        pendingMessages.delete(ws)
        upstreams.delete(ws)
        ws.close()
      })
    },

    onMessage(evt: MessageEvent<WSMessageReceive>, ws: WSContext) {
      const upstream = upstreams.get(ws)
      if (!upstream) return
      if (upstream.readyState !== WebSocket.OPEN) {
        const msg = typeof evt.data === 'string' ? evt.data : evt.data as ArrayBuffer
        pendingMessages.get(ws)?.push(msg)
        return
      }
      upstream.send(typeof evt.data === 'string' ? evt.data : evt.data as ArrayBuffer)
    },

    onClose(_evt: CloseEvent, ws: WSContext) {
      const upstream = upstreams.get(ws)
      if (upstream) {
        upstream.close()
        upstreams.delete(ws)
      }
      pendingMessages.delete(ws)
    },
  }
}
