import type { WriteStream } from 'node:fs'
import { appendFileSync, createWriteStream } from 'node:fs'
import { createEnvReader, readProcessEnv } from '../env'

function env() {
  return createEnvReader(readProcessEnv())
}

function formatClockTime(ms: number): string {
  const d = new Date(ms)
  const hh = String(d.getHours()).padStart(2, '0')
  const mm = String(d.getMinutes()).padStart(2, '0')
  const ss = String(d.getSeconds()).padStart(2, '0')
  const mmm = String(d.getMilliseconds()).padStart(3, '0')
  return `${hh}:${mm}:${ss}.${mmm}`
}

function parseEnabled(v: unknown): boolean {
  const s = String(v ?? '')
    .trim()
    .toLowerCase()
  return s === '1' || s === 'true' || s === 'yes' || s === 'on'
}

type TraceOptions = Readonly<{
  tag: string
  message: string
  payload?: unknown
}>

let lastSecond = 0
let linesThisSecond = 0
let ws: WriteStream | null = null
let wsPath: string | null = null
let wsBuffer: string[] = []
let wsBufferedBytes = 0
let wsFlushScheduled = false
let wsDrainPending = false
let wsDroppedLines = 0

function mode(): 'sync' | 'async' {
  const m = env().prefixedStr('TRACE_LOG_MODE').toLowerCase()
  return m === 'sync' ? 'sync' : 'async'
}

function ensureWriteStream(path: string): WriteStream | null {
  const p = String(path ?? '').trim()
  if (!p)
    return null
  if (ws && wsPath === p)
    return ws
  try {
    try {
      ws?.end?.()
    }
    catch {
      // ignore
    }
    ws = createWriteStream(p, { flags: 'a' })
    wsPath = p
    ws.on('error', () => {})
    ;(ws as any).unref?.()
    return ws
  }
  catch {
    ws = null
    wsPath = null
    return null
  }
}

function scheduleFlush(p: string): void {
  if (wsFlushScheduled || wsDrainPending)
    return
  wsFlushScheduled = true
  setImmediate(() => {
    wsFlushScheduled = false
    flushNow(p)
  })
}

function flushNow(p: string): void {
  const stream = ensureWriteStream(p)
  if (!stream)
    return
  if (wsBuffer.length === 0)
    return
  const chunk = wsBuffer.length === 1 ? wsBuffer[0]! : wsBuffer.join('')
  wsBuffer = []
  wsBufferedBytes = 0
  try {
    const ok = stream.write(chunk)
    if (!ok) {
      wsDrainPending = true
      stream.once('drain', () => {
        wsDrainPending = false
        if (wsBuffer.length > 0)
          scheduleFlush(p)
      })
    }
  }
  catch {
    // ignore
  }
}

function enqueueAsync(p: string, line: string): void {
  const maxBytesRaw = Number(
    env().prefixedStr('TRACE_LOG_MAX_BUFFER_BYTES') || '2000000',
  )
  const maxBytes = Number.isFinite(maxBytesRaw)
    ? Math.max(64_000, Math.floor(maxBytesRaw))
    : 2_000_000
  const bytes = Buffer.byteLength(line, 'utf8')
  if (wsBufferedBytes + bytes > maxBytes) {
    wsDroppedLines++
    if (wsDroppedLines % 200 === 1) {
      const dropLine = `[${formatClockTime(Date.now())}] [TRACE] dropped_lines=${wsDroppedLines}\n`
      try {
        wsBuffer.push(dropLine)
        wsBufferedBytes += Buffer.byteLength(dropLine, 'utf8')
      }
      catch {
        // ignore
      }
    }
    scheduleFlush(p)
    return
  }
  wsBuffer.push(line)
  wsBufferedBytes += bytes
  scheduleFlush(p)
}

export function traceLog(opts: TraceOptions): void {
  const e = env()
  if (!parseEnabled(e.prefixedStr('TRACE')))
    return

  const path = e.prefixedStr('TRACE_LOG_PATH') || '/tmp/dimcode-trace.log'
  const maxPerSecRaw = Number(
    e.prefixedStr('TRACE_MAX_LINES_PER_SEC') || '5000',
  )
  const maxPerSec = Number.isFinite(maxPerSecRaw)
    ? Math.max(100, Math.floor(maxPerSecRaw))
    : 5000

  const now = Date.now()
  const sec = Math.floor(now / 1000)
  if (sec !== lastSecond) {
    lastSecond = sec
    linesThisSecond = 0
  }
  if (linesThisSecond >= maxPerSec)
    return
  linesThisSecond++

  const tag = String(opts.tag ?? '').trim() || 'TRACE'
  const msg = String(opts.message ?? '').trim()
  let line = `[${formatClockTime(now)}] [${tag}] ${msg}`
  if (opts.payload !== undefined) {
    try {
      line += ` ${JSON.stringify(opts.payload)}`
    }
    catch {
      // ignore
    }
  }
  line += '\n'

  try {
    if (mode() === 'sync')
      appendFileSync(path, line)
    else enqueueAsync(path, line)
  }
  catch {
    // ignore
  }
}
