import type { BrowserWindow } from 'electron'
import { connect, type Socket } from 'node:net'

const DEFAULT_MASK = 0x9e3779b9
const MAX_FRAME_SIZE = 16 * 1024 * 1024
const UINT32_MASK = 0xffffffffn
const DEFAULT_MASK_BIGINT = 0x9e3779b9n

export const PIPE_PATH = '\\\\.\\pipe\\www.sv-elec.com'
export const PIPE_EVENT_CHANNEL = 'pipe-event'

type PipeLogLevel = 'info' | 'success' | 'warning' | 'error'
type PipeLogCategory = 'connection' | 'auth' | 'message'
type PipeLogDirection = 'system' | 'recv' | 'send'

type PipeEvent = {
  timestamp: string
  level: PipeLogLevel
  category: PipeLogCategory
  direction: PipeLogDirection
  message: string
  detail?: string
  msgId?: number
  msgName?: string
  payloadLength?: number
  payloadHex?: string
  payloadText?: string
  connected: boolean
  authenticated: boolean
}

type PipeEventPayload = Omit<PipeEvent, 'timestamp' | 'connected' | 'authenticated'>

const MESSAGE_NAMES: Record<number, string> = {
  0x0001: 'AUTH_CHALLENGE',
  0x0002: 'AUTH_RESPONSE',
  0x0003: 'AUTH_SUCCESS',
  0x0004: 'AUTH_FAILED',
  0x0005: 'AUTH_TIMEOUT',
  0x0006: 'HEARTBEAT',
  0x1001: 'CONNECT',
  0x1002: 'DISCONNECT',
  0x1003: 'STATUS_REQUEST',
  0x1004: 'STATUS_RESPONSE',
  0x2001: 'TEXT_DATA',
  0x2002: 'BINARY_DATA',
  0x2003: 'FILE_DATA',
  0x3001: 'CUSTOM_1',
  0x3002: 'CUSTOM_2',
  0x3003: 'CUSTOM_3'
}

export class PipeClient {
  private socket?: Socket
  private buffer = Buffer.alloc(0)
  private authenticated = false
  private connecting = false
  private closingReason: string | null = null
  private disposed = false

  constructor(private readonly window: BrowserWindow) {}

  connect(): void {
    if (this.disposed) return

    if (this.isConnected()) {
      this.emit({
        level: 'info',
        category: 'connection',
        direction: 'system',
        message: 'Pipe is already connected'
      })
      return
    }

    if (this.connecting) {
      this.emit({
        level: 'info',
        category: 'connection',
        direction: 'system',
        message: 'Pipe connection is in progress'
      })
      return
    }

    this.buffer = Buffer.alloc(0)
    this.authenticated = false
    this.connecting = true
    this.closingReason = null

    this.emit({
      level: 'info',
      category: 'connection',
      direction: 'system',
      message: 'Connecting to pipe server'
    })

    const socket = connect(PIPE_PATH)
    this.socket = socket

    socket.on('connect', () => {
      this.connecting = false
      this.emit({
        level: 'success',
        category: 'connection',
        direction: 'system',
        message: 'Pipe connected, waiting for auth handshake'
      })
    })

    socket.on('data', (chunk) => {
      this.handleChunk(chunk)
    })

    socket.on('error', (error) => {
      this.connecting = false
      this.emit({
        level: 'error',
        category: 'connection',
        direction: 'system',
        message: 'Pipe connection error',
        detail: formatSafeConnectionError(error)
      })
    })

    socket.on('close', (hadError) => {
      const reason = this.closingReason || (hadError ? 'Pipe closed after error' : 'Pipe closed')
      this.socket = undefined
      this.buffer = Buffer.alloc(0)
      this.connecting = false
      this.authenticated = false
      this.closingReason = null

      if (!this.disposed) {
        this.emit({
          level: hadError ? 'warning' : 'info',
          category: 'connection',
          direction: 'system',
          message: reason
        })
      }
    })
  }

  disconnect(reason = 'Pipe disconnected by client'): void {
    if (!this.socket) {
      this.connecting = false
      this.authenticated = false
      this.buffer = Buffer.alloc(0)
      this.emit({
        level: 'info',
        category: 'connection',
        direction: 'system',
        message: 'No active pipe connection'
      })
      return
    }

    this.closingReason = reason
    this.socket.end()
  }

  destroy(): void {
    this.disposed = true
    this.connecting = false
    this.authenticated = false
    this.buffer = Buffer.alloc(0)
    this.closingReason = null

    if (this.socket) {
      this.socket.removeAllListeners()
      this.socket.destroy()
      this.socket = undefined
    }
  }

  private isConnected(): boolean {
    return !!this.socket && !this.socket.destroyed
  }

  private handleChunk(chunk: Buffer): void {
    this.buffer = Buffer.concat([this.buffer, chunk])

    while (this.buffer.length >= 6) {
      const msgId = this.buffer.readUInt16BE(0)
      const payloadLength = this.buffer.readUInt32BE(2)

      if (payloadLength > MAX_FRAME_SIZE) {
        this.emit({
          level: 'error',
          category: 'message',
          direction: 'recv',
          message: 'Frame length is too large, disconnecting',
          detail: `msgId=0x${msgId.toString(16).toUpperCase().padStart(4, '0')}, length=${payloadLength}`
        })
        this.disconnect('Frame length exceeded limit')
        return
      }

      const frameLength = 6 + payloadLength
      if (this.buffer.length < frameLength) {
        return
      }

      const payload = Buffer.from(this.buffer.subarray(6, frameLength))
      this.buffer = this.buffer.subarray(frameLength)
      this.handleFrame(msgId, payload)
    }
  }

  private handleFrame(msgId: number, payload: Buffer): void {
    if (!this.authenticated && ![0x0001, 0x0003, 0x0004, 0x0005, 0x0006].includes(msgId)) {
      this.emit({
        level: 'warning',
        category: 'auth',
        direction: 'recv',
        message: `Received ${getMessageName(msgId)} before auth completed`,
        msgId,
        msgName: getMessageName(msgId),
        payloadLength: payload.length,
        payloadHex: toHexPreview(payload)
      })
      this.disconnect('Unexpected frame received before auth')
      return
    }

    switch (msgId) {
      case 0x0001:
        this.handleAuthChallenge(payload)
        return
      case 0x0003:
        this.authenticated = true
        this.emit({
          level: 'success',
          category: 'auth',
          direction: 'recv',
          message: 'Auth success',
          msgId,
          msgName: getMessageName(msgId),
          payloadLength: payload.length
        })
        return
      case 0x0004:
        this.emit({
          level: 'error',
          category: 'auth',
          direction: 'recv',
          message: 'Auth failed',
          msgId,
          msgName: getMessageName(msgId),
          payloadLength: payload.length
        })
        this.disconnect('Auth failed')
        return
      case 0x0005:
        this.emit({
          level: 'warning',
          category: 'auth',
          direction: 'recv',
          message: 'Auth timeout',
          msgId,
          msgName: getMessageName(msgId),
          payloadLength: payload.length
        })
        this.disconnect('Auth timeout')
        return
      default:
        this.emit(this.describeMessage(msgId, payload))
    }
  }

  private handleAuthChallenge(payload: Buffer): void {
    this.emit({
      level: 'info',
      category: 'auth',
      direction: 'recv',
      message: 'Received auth challenge',
      detail:
        payload.length === 16 ? payload.toString('hex').toUpperCase() : 'Invalid challenge length',
      msgId: 0x0001,
      msgName: getMessageName(0x0001),
      payloadLength: payload.length,
      payloadHex: toHexPreview(payload)
    })

    if (payload.length !== 16) {
      this.emit({
        level: 'error',
        category: 'auth',
        direction: 'system',
        message: 'Challenge length must be 16 bytes'
      })
      this.disconnect('Invalid auth challenge length')
      return
    }

    const response = simpleCryptoEncrypt(payload, payload)

    this.emit({
      level: 'info',
      category: 'auth',
      direction: 'send',
      message: 'Sending auth response',
      detail: response.toString('hex').toUpperCase(),
      msgId: 0x0002,
      msgName: getMessageName(0x0002),
      payloadLength: response.length,
      payloadHex: toHexPreview(response)
    })

    this.writeFrame(0x0002, response)
  }

  private writeFrame(msgId: number, payload = Buffer.alloc(0)): void {
    if (!this.socket || this.socket.destroyed) {
      this.emit({
        level: 'warning',
        category: 'connection',
        direction: 'system',
        message: 'Cannot write frame, pipe is not connected'
      })
      return
    }

    const frame = Buffer.allocUnsafe(6 + payload.length)
    frame.writeUInt16BE(msgId, 0)
    frame.writeUInt32BE(payload.length, 2)
    payload.copy(frame, 6)

    this.emit({
      level: 'info',
      category: msgId === 0x0002 ? 'auth' : 'message',
      direction: 'send',
      message: `Queue frame ${getMessageName(msgId)}`,
      detail: frame.toString('hex').toUpperCase(),
      msgId,
      msgName: getMessageName(msgId),
      payloadLength: payload.length,
      payloadHex: toHexPreview(payload)
    })

    this.socket.write(frame, (error) => {
      if (error) {
        this.emit({
          level: 'error',
          category: msgId === 0x0002 ? 'auth' : 'message',
          direction: 'send',
          message: `Write failed for ${getMessageName(msgId)}`,
          detail: formatSafeConnectionError(error),
          msgId,
          msgName: getMessageName(msgId),
          payloadLength: payload.length,
          payloadHex: toHexPreview(payload)
        })
        return
      }

      this.emit({
        level: 'success',
        category: msgId === 0x0002 ? 'auth' : 'message',
        direction: 'send',
        message: `Frame sent ${getMessageName(msgId)}`,
        msgId,
        msgName: getMessageName(msgId),
        payloadLength: payload.length,
        payloadHex: toHexPreview(payload)
      })
    })
  }

  private describeMessage(msgId: number, payload: Buffer): PipeEventPayload {
    const msgName = getMessageName(msgId)
    const hex = toHexPreview(payload)
    const base: PipeEventPayload = {
      level: 'info',
      category: 'message',
      direction: 'recv',
      message: `Received ${msgName}`,
      msgId,
      msgName,
      payloadLength: payload.length,
      payloadHex: hex
    }

    switch (msgId) {
      case 0x0006:
        return {
          ...base,
          message: 'Received heartbeat'
        }
      case 0x1004: {
        const text = tryDecodeUtf8(payload)
        return {
          ...base,
          message: 'Received status response',
          detail: text || hex,
          payloadText: text
        }
      }
      case 0x2001: {
        const text = payload.toString('utf8')
        return {
          ...base,
          message: 'Received text message',
          detail: text,
          payloadText: text
        }
      }
      case 0x2002:
        return {
          ...base,
          message: `Received binary message (${payload.length} bytes)`,
          detail: hex
        }
      case 0x2003:
        return describeFileMessage(base, payload)
      default: {
        const text = tryDecodeUtf8(payload)
        return {
          ...base,
          message: `Received ${msgName} (0x${msgId.toString(16).toUpperCase().padStart(4, '0')})`,
          detail: text || hex,
          payloadText: text
        }
      }
    }
  }

  private emit(event: PipeEventPayload): void {
    if (this.window.isDestroyed()) return

    this.window.webContents.send(PIPE_EVENT_CHANNEL, {
      ...event,
      timestamp: new Date().toISOString(),
      connected: this.isConnected(),
      authenticated: this.authenticated
    } satisfies PipeEvent)
  }
}

/** Avoid leaking named-pipe path / host into UI logs (Node error messages often include full path). */
function redactPipeAddress(message: string): string {
  return message
    .replace(/\\\\\.\\pipe\\[^\s"'<>]+/gi, '[pipe]')
    .replace(/www\.sv-elec\.com/gi, '[endpoint]')
    .replace(/\s+/g, ' ')
    .trim()
}

function formatSafeConnectionError(error: Error): string | undefined {
  const code = (error as NodeJS.ErrnoException).code
  const msg = redactPipeAddress(error.message || '')
  if (code && msg) return `${code}: ${msg}`
  if (msg) return msg
  if (code) return code
  return undefined
}

function getMessageName(msgId: number): string {
  return MESSAGE_NAMES[msgId] || `UNKNOWN_0x${msgId.toString(16).toUpperCase().padStart(4, '0')}`
}

function toHexPreview(payload: Buffer, limit = 64): string | undefined {
  if (!payload.length) return undefined
  const preview = payload.subarray(0, limit).toString('hex').toUpperCase()
  return payload.length > limit ? `${preview}...` : preview
}

function tryDecodeUtf8(payload: Buffer): string | undefined {
  if (!payload.length) return undefined
  const text = payload.toString('utf8')
  return /^[\u0009\u000A\u000D\u0020-\u007E\u4E00-\u9FFF]*$/u.test(text) ? text : undefined
}

function describeFileMessage(base: PipeEventPayload, payload: Buffer): PipeEventPayload {
  if (payload.length < 2) {
    return {
      ...base,
      level: 'warning',
      message: 'Received malformed file message',
      detail: base.payloadHex
    }
  }

  const fileNameLength = payload.readUInt16LE(0)
  const fileNameEnd = 2 + fileNameLength

  if (payload.length < fileNameEnd) {
    return {
      ...base,
      level: 'warning',
      message: 'Received invalid file header length',
      detail: base.payloadHex
    }
  }

  const fileName = payload.subarray(2, fileNameEnd).toString('utf8')
  const fileDataLength = payload.length - fileNameEnd

  return {
    ...base,
    message: `Received file message: ${fileName}`,
    detail: `file=${fileName}\nsize=${fileDataLength} bytes`,
    payloadText: fileName
  }
}

function rotateLeft32(value: bigint, shift: number): bigint {
  const shiftBigInt = BigInt(shift)
  return ((value << shiftBigInt) | (value >> BigInt(32 - shift))) & UINT32_MASK
}

function rotateRight32(value: bigint, shift: number): bigint {
  const shiftBigInt = BigInt(shift)
  return ((value >> shiftBigInt) | (value << BigInt(32 - shift))) & UINT32_MASK
}

function seedToKey32(seed: number, mask = DEFAULT_MASK): number {
  const maskValue = mask === DEFAULT_MASK ? DEFAULT_MASK_BIGINT : BigInt(mask >>> 0)
  let value = BigInt(seed >>> 0)

  if (value === 0n) {
    return 0
  }

  // Matches Qt SimpleCrypto::seedToKey / seedToKey16 (8 rounds, bit28 => <<4^mask, else ~seed)
  for (let index = 0; index < 8; index += 1) {
    if ((value & 0x80000000n) !== 0n) {
      value = ((value << 1n) ^ maskValue) & UINT32_MASK
    } else if ((value & 0x40000000n) !== 0n) {
      value = ((value << 2n) ^ maskValue) & UINT32_MASK
    } else if ((value & 0x20000000n) !== 0n) {
      value = ((value << 3n) ^ maskValue) & UINT32_MASK
    } else if ((value & 0x10000000n) !== 0n) {
      value = ((value << 4n) ^ maskValue) & UINT32_MASK
    } else {
      value = ~value & UINT32_MASK
    }
  }

  const result =
    ((value << 3n) ^ (maskValue >> 2n) ^ ((value >> 2n) ^ ((maskValue << 3n) & UINT32_MASK))) &
    UINT32_MASK

  return Number(result)
}

function simpleCryptoEncrypt(
  data: Buffer,
  seed16: Buffer,
  mask = DEFAULT_MASK
): Buffer<ArrayBuffer> {
  const keys = [0, 1, 2, 3].map((index) => seedToKey32(seed16.readUInt32BE(index * 4), mask))
  const result = Buffer.from(data)
  const fullBlocks = Math.floor(result.length / 4)

  for (let index = 0; index < fullBlocks; index += 1) {
    let block = BigInt(result.readUInt32BE(index * 4))
    const key = BigInt(keys[index % 4] >>> 0)

    block = (block ^ key) & UINT32_MASK
    block = (rotateLeft32(block, 3) ^ rotateRight32(key, 5)) & UINT32_MASK
    block = (rotateRight32(block, 2) ^ rotateLeft32(key, 7)) & UINT32_MASK
    block = (block ^ key) & UINT32_MASK

    result.writeUInt32BE(Number(block), index * 4)
  }

  const tail = result.length % 4
  if (tail > 0) {
    const start = fullBlocks * 4
    let block = 0n

    for (let index = 0; index < tail; index += 1) {
      block |= BigInt(result[start + index]) << BigInt(24 - index * 8)
    }

    block = (block ^ BigInt(keys[0] >>> 0)) & UINT32_MASK

    for (let index = 0; index < tail; index += 1) {
      result[start + index] = Number((block >> BigInt(24 - index * 8)) & 0xffn)
    }
  }

  return result
}
