import type { Server } from 'node:http'
import { appendFile, mkdir, unlink, writeFile } from 'node:fs/promises'
import { createServer } from 'node:http'
import path from 'node:path'

const HOST = '127.0.0.1'
const START_PORT = 13442
const HEADER_NAME = 'x-dimcode-session-id'

export type DebugServerInfo = Readonly<{
  port: number
  baseUrl: string
  logFile: string
  cwd: string
}>

interface DebugServerState {
  server: Server
  port: number
  baseUrl: string
  cwd: string
  logFile: string
  logFileRef: { current: string }
}

let active: DebugServerState | null = null

export function resolveDebugLogFile(cwd: string): string {
  return path.resolve(cwd, '.dimcode', 'debug', 'latest.ndjson')
}

async function ensureLogDir(filePath: string): Promise<void> {
  const dir = path.dirname(filePath)
  await mkdir(dir, { recursive: true })
}

async function appendDebugLine(filePath: string, line: string): Promise<void> {
  await ensureLogDir(filePath)
  await appendFile(filePath, `${line}\n`, 'utf8')
}

function isDebugRequest(req: any): boolean {
  const method = String(req?.method ?? '').toUpperCase()
  if (method !== 'POST')
    return false
  const url = String(req?.url ?? '')
  return url === '/debug' || url.startsWith('/debug?')
}

function createDebugServer(logFileRef: { current: string }): Server {
  return createServer((req, res) => {
    if (!isDebugRequest(req)) {
      res.statusCode = 404
      res.end()
      return
    }

    const sessionId = String(req.headers?.[HEADER_NAME] ?? '').trim()
    if (!sessionId) {
      res.statusCode = 400
      res.end()
      return
    }

    let body = ''
    req.on('data', (chunk) => {
      body += chunk
    })
    req.on('error', () => {
      res.statusCode = 400
      res.end()
    })
    req.on('end', async () => {
      try {
        const payload = JSON.stringify({
          receivedAt: new Date().toISOString(),
          sessionId,
          body,
        })
        const target = logFileRef.current
        if (!target) {
          res.statusCode = 500
          res.end()
          return
        }
        await appendDebugLine(target, payload)
        res.statusCode = 204
        res.end()
      }
      catch {
        res.statusCode = 500
        res.end()
      }
    })
  })
}

async function listenOnPort(server: Server, port: number): Promise<void> {
  await new Promise<void>((resolve, reject) => {
    function onListening() {
      server.off('error', onError)
      resolve()
    }
    function onError(err: any) {
      server.off('listening', onListening)
      reject(err)
    }
    server.once('error', onError)
    server.once('listening', onListening)
    server.listen(port, HOST)
  })
}

async function bindAvailablePort(logFileRef: {
  current: string
}): Promise<{ server: Server, port: number }> {
  let port = START_PORT
  while (port < START_PORT + 2000) {
    const server = createDebugServer(logFileRef)
    try {
      await listenOnPort(server, port)
      return { server, port }
    }
    catch (err: any) {
      if (err?.code === 'EADDRINUSE') {
        try {
          server.close()
        }
        catch {}
        port += 1
        continue
      }
      try {
        server.close()
      }
      catch {}
      throw err
    }
  }
  throw new Error('No available debug server port found')
}

export async function ensureDebugServer(
  opts: Readonly<{ cwd: string }>,
): Promise<DebugServerInfo> {
  const cwd = String(opts.cwd ?? '').trim() || '.'
  if (active && active.server.listening) {
    if (active.cwd !== cwd) {
      active.cwd = cwd
      active.logFile = resolveDebugLogFile(cwd)
      active.logFileRef.current = active.logFile
    }
    return {
      port: active.port,
      baseUrl: active.baseUrl,
      logFile: active.logFile,
      cwd: active.cwd,
    }
  }

  const logFile = resolveDebugLogFile(cwd)
  const logFileRef = { current: logFile }
  const { server, port } = await bindAvailablePort(logFileRef)
  const baseUrl = `http://${HOST}:${port}/debug`
  active = { server, port, baseUrl, cwd, logFile, logFileRef }
  return { port, baseUrl, logFile, cwd }
}

export function getActiveDebugServer(): DebugServerInfo | null {
  if (!active || !active.server.listening)
    return null
  return {
    port: active.port,
    baseUrl: active.baseUrl,
    logFile: active.logFile,
    cwd: active.cwd,
  }
}

export async function stopDebugServer(): Promise<void> {
  if (!active)
    return
  const server = active.server
  active = null
  await new Promise<void>((resolve) => {
    try {
      server.close(() => resolve())
    }
    catch {
      resolve()
    }
  })
}

export async function resetDebugLogFile(cwd: string): Promise<string> {
  const filePath = resolveDebugLogFile(cwd)
  await ensureLogDir(filePath)
  await writeFile(filePath, '', 'utf8')
  return filePath
}

export async function deleteDebugLogFile(cwd: string): Promise<void> {
  const filePath = resolveDebugLogFile(cwd)
  try {
    await unlink(filePath)
  }
  catch {}
}
