import net from 'node:net'
import fs from 'node:fs'
import path from 'node:path'
import { EventEmitter } from 'node:events'

export interface IpcMessage {
  type: 'trade' | 'signal' | 'orderbook' | 'engine_state' | 'meta' | 'ping' | 'pong'
  ts: number
  data: unknown
}

const RUNTIME_DIR = process.env.TXOCAP_REPO_ROOT
  ? path.join(process.env.TXOCAP_REPO_ROOT, '.run')
  : path.join(process.cwd(), '.run')

function socketPath(name: string): string {
  fs.mkdirSync(RUNTIME_DIR, { recursive: true })
  return path.join(RUNTIME_DIR, `${name}.sock`)
}

export class IpcServer extends EventEmitter {
  private server?: net.Server
  private clients = new Set<net.Socket>()
  private readonly name: string
  private readonly sockPath: string

  constructor(name: string) {
    super()
    this.name = name
    this.sockPath = socketPath(name)
  }

  start(): void {
    try { fs.unlinkSync(this.sockPath) } catch {}

    this.server = net.createServer(socket => {
      this.clients.add(socket)
      this.emit('client_connect', this.clients.size)

      socket.on('data', buf => {
        const lines = buf.toString().split('\n').filter(Boolean)
        for (const line of lines) {
          try {
            const msg = JSON.parse(line) as IpcMessage
            this.emit('message', msg, socket)
            if (msg.type === 'ping') {
              this.sendTo(socket, { type: 'pong', ts: Date.now(), data: { name: this.name, clients: this.clients.size } })
            }
          } catch {}
        }
      })

      socket.on('close', () => {
        this.clients.delete(socket)
        this.emit('client_disconnect', this.clients.size)
      })

      socket.on('error', () => {
        this.clients.delete(socket)
      })
    })

    this.server.listen(this.sockPath)
    this.server.on('error', (err: Error) => this.emit('error', err))
  }

  broadcast(msg: IpcMessage): void {
    const line = JSON.stringify(msg) + '\n'
    for (const client of this.clients) {
      try { client.write(line) } catch { this.clients.delete(client) }
    }
  }

  sendTo(socket: net.Socket, msg: IpcMessage): void {
    try { socket.write(JSON.stringify(msg) + '\n') } catch {}
  }

  stop(): void {
    for (const client of this.clients) { try { client.destroy() } catch {} }
    this.clients.clear()
    this.server?.close()
    try { fs.unlinkSync(this.sockPath) } catch {}
  }

  get clientCount(): number { return this.clients.size }
  get path(): string { return this.sockPath }
}

export class IpcClient extends EventEmitter {
  private socket?: net.Socket
  private reconnectTimer?: NodeJS.Timeout
  private stopped = false
  private buffer = ''
  private readonly sockPath: string

  constructor(name: string) {
    super()
    this.sockPath = socketPath(name)
  }

  connect(): void {
    this.stopped = false
    if (!fs.existsSync(this.sockPath)) {
      this.scheduleReconnect()
      return
    }

    const socket = net.createConnection(this.sockPath)
    this.socket = socket

    socket.on('connect', () => {
      this.emit('connect')
      this.sendMessage({ type: 'ping', ts: Date.now(), data: null })
    })

    socket.on('data', buf => {
      this.buffer += buf.toString()
      const lines = this.buffer.split('\n')
      this.buffer = lines.pop() ?? ''
      for (const line of lines) {
        if (!line) continue
        try {
          const msg = JSON.parse(line) as IpcMessage
          this.emit('message', msg)
          this.emit(msg.type, msg.data, msg.ts)
        } catch {}
      }
    })

    socket.on('close', () => {
      this.emit('disconnect')
      if (!this.stopped) this.scheduleReconnect()
    })

    socket.on('error', () => {
      if (!this.stopped) this.scheduleReconnect()
    })
  }

  sendMessage(msg: IpcMessage): void {
    try { this.socket?.write(JSON.stringify(msg) + '\n') } catch {}
  }

  disconnect(): void {
    this.stopped = true
    if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = undefined }
    this.socket?.destroy()
  }

  get connected(): boolean { return this.socket?.writable ?? false }

  private scheduleReconnect(): void {
    if (this.stopped) return
    this.reconnectTimer = setTimeout(() => this.connect(), 2000)
  }
}

export function listRunningServices(): string[] {
  try {
    return fs.readdirSync(RUNTIME_DIR).filter(f => f.endsWith('.sock')).map(f => f.replace('.sock', ''))
  } catch { return [] }
}
