import { Buffer } from 'buffer'
import { type Subscription } from 'react-native-ble-plx'

import { BleClient } from '../core/BleClient'
import { type APP_COMMAND_TYPES, BLE_UUIDS, FRAME_CONSTANTS } from '../protocol/Constants'
import { ProtocolManager } from '../protocol/ProtocolManager'
import { type ProtocolFrame } from '../protocol/types'

export class BleProtocolService {
  private static instance: BleProtocolService
  private client = BleClient.getInstance()
  private listeners: Map<number, Set<(data: ArrayBuffer, deviceId: string) => void>> = new Map()
  private subscription: Subscription | null = null

  // deviceId_type -> { total: number, frames: ArrayBuffer[] }
  private fragments: Map<string, { total: number; frames: (ArrayBuffer | null)[] }> = new Map()

  private constructor() {}

  public static getInstance(): BleProtocolService {
    if (!BleProtocolService.instance) {
      BleProtocolService.instance = new BleProtocolService()
    }
    return BleProtocolService.instance
  }

  public addListener(type: number, callback: (data: ArrayBuffer, deviceId: string) => void) {
    if (!this.listeners.has(type)) {
      this.listeners.set(type, new Set())
    }
    this.listeners.get(type)!.add(callback)
  }

  public removeListener(type: number, callback: (data: ArrayBuffer, deviceId: string) => void) {
    if (this.listeners.has(type)) {
      this.listeners.get(type)!.delete(callback)
    }
  }

  private emit(type: number, data: ArrayBuffer, deviceId: string) {
    if (this.listeners.has(type)) {
      this.listeners.get(type)!.forEach((cb) => cb(data, deviceId))
    }
  }

  public async initialize(deviceId: string) {
    // Clean up previous subscription
    this.disconnect()

    // Clear fragments for this device
    this.clearFragments(deviceId)

    try {
      this.subscription = await this.client.monitor(
        deviceId,
        BLE_UUIDS.SERVICE,
        BLE_UUIDS.READ_CHARACTERISTIC,
        (error, value) => {
          if (error) {
            // Check for known native crash error and ignore/log as debug
            if (
              error.errorCode === 0 &&
              error.message.includes('Unknown error') &&
              error.reason?.includes('PromiseImpl.reject')
            ) {
              console.debug('Ignored native monitor error', error)
              return
            }
            console.warn('Monitor error', error)
            return
          }
          if (value) {
            const buffer = Buffer.from(value, 'base64')
            const hexString =
              buffer
                .toString('hex')
                .match(/.{1,2}/g)
                ?.join(' ')
                .toUpperCase() || ''

            console.log(`[BleProtocol] Received ${buffer.byteLength} bytes:`, hexString)
            this.handleRawData(deviceId, buffer)
          }
        },
      )
    } catch (error) {
      console.error('Failed to initialize protocol service:', error)
      throw error
    }
  }

  public disconnect() {
    if (this.subscription) {
      try {
        this.subscription.remove()
      } catch (e) {
        console.warn('Failed to remove subscription', e)
      }
      this.subscription = null
    }
  }

  private handleRawData(deviceId: string, data: Buffer) {
    const frame = ProtocolManager.parseFrame(data.buffer)
    if (!frame) return

    if (frame.subpageTotal > 0) {
      this.handleFragment(deviceId, frame)
    } else {
      this.emit(frame.type, frame.data, deviceId)
    }
  }

  private handleFragment(deviceId: string, frame: ProtocolFrame) {
    const key = `${deviceId}_${frame.type}`

    if (!this.fragments.has(key)) {
      this.fragments.set(key, {
        total: frame.subpageTotal,
        frames: new Array(frame.subpageTotal).fill(null),
      })
    }

    const session = this.fragments.get(key)!
    // Basic validation
    if (frame.curPage >= session.total) return

    session.frames[frame.curPage] = frame.data

    // Check if complete
    if (session.frames.every((f) => f !== null)) {
      const combinedLength = session.frames.reduce((acc, val) => acc + (val ? val.byteLength : 0), 0)
      const combined = new Uint8Array(combinedLength)
      let offset = 0

      // Reassemble from High to Low pages
      for (let i = session.total - 1; i >= 0; i--) {
        const part = session.frames[i]
        if (part) {
          combined.set(new Uint8Array(part), offset)
          offset += part.byteLength
        }
      }

      this.fragments.delete(key)
      this.emit(frame.type, combined.buffer as ArrayBuffer, deviceId)
    }
  }

  private clearFragments(deviceId: string) {
    for (const key of this.fragments.keys()) {
      if (key.startsWith(deviceId)) {
        this.fragments.delete(key)
      }
    }
  }

  public async send(
    deviceId: string,
    type: APP_COMMAND_TYPES,
    data: object | ArrayBuffer | Uint8Array,
    onProgress?: (progress: number) => void,
  ): Promise<void> {
    let payload: Uint8Array
    if (data instanceof ArrayBuffer) {
      console.debug('[BleProtocolService] Sending ArrayBuffer')
      payload = new Uint8Array(data)
    } else if (data instanceof Uint8Array) {
      console.debug('[BleProtocolService] Sending Uint8Array')
      payload = data
    } else {
      console.debug('[BleProtocolService] Sending JSON payload')
      const jsonStr = JSON.stringify(data)
      payload = new Uint8Array(Buffer.from(jsonStr))
    }

    const device = this.client.getConnectedDevice()
    console.log('send-------------device', device)

    const mtu = device?.mtu || 23
    // MTU - 3 bytes (ATT overhead) - Protocol Header - Protocol Footer
    const maxPayloadSize = mtu - 3 - FRAME_CONSTANTS.HEADER_SIZE - FRAME_CONSTANTS.FOOTER_SIZE
    // Ensure reasonable bounds (at least 1 byte, max constrained by protocol constant)
    const safeMaxDataSize = Math.max(1, Math.min(maxPayloadSize, FRAME_CONSTANTS.MAX_DATA_SIZE))

    console.debug(`[BleProtocolService] Sending with MTU=${mtu}, maxDataSize=${safeMaxDataSize}`)
    const rawPayloadHex = payload.reduce((acc, val) => acc + val.toString(16).padStart(2, '0') + ' ', '')
    const formattedRawPayloadHex =
      rawPayloadHex.substring(0, 512 * 2) + '\n......\n' + rawPayloadHex.substring(rawPayloadHex.length - 512 * 2)
    console.debug(
      `[BleProtocolService] Sending payload size=${payload.byteLength}, raw payload hex=\n${formattedRawPayloadHex}`,
    )
    const frames = ProtocolManager.createFrame(type, payload, FRAME_CONSTANTS.HEAD_APP_TO_DEVICE, safeMaxDataSize)
    const total = frames.length
    console.debug(`Sending ${total} frames`)
    for (let i = 0; i < total; i++) {
      const frame = frames[i]
      if (i < 3) {
        const rawFrame = Array.from(frame)
          .map((b) => b.toString(16).padStart(2, '0'))
          .join(' ')
        console.debug(`raw ${i + 1} frame \n ${rawFrame}`)
      }
      // console.debug(`Writing frame ${i + 1}/${total}, length = ${frame.length}`)
      const base64 = Buffer.from(frame).toString('base64')
      const result = await this.client.write(deviceId, BLE_UUIDS.SERVICE, BLE_UUIDS.WRITE_CHARACTERISTIC, base64, false)
      await new Promise((resolve) => setTimeout(resolve, FRAME_CONSTANTS.FRAME_INTERVAL))
      if (onProgress) {
        onProgress((i + 1) / total)
      }
      // console.debug("Wrote frame", result);
    }
  }
}
