/* global BufferEncoding */
import crypto from 'node:crypto'
import fs from 'node:fs/promises'
import path from 'node:path'
import stream from 'node:stream'

/**
 * 生成指定长度的随机字符串
 * @param size 字符串长度
 * @returns 随机生成的字符串
 */
function randomString(size: number) {
  return crypto.randomBytes(size).toString('base64url').slice(0, size)
}

/**
 * StreamSplitter 配置选项
 */
type Options = {
  chunkSize: number // 每个块的大小
  directory: string // 存储块的目录
}

/**
 * 回调函数类型
 */
type Callback = (error: Error | null) => void

/**
 * StreamSplitter 类，用于将流数据分割成指定大小的块
 */
export class StreamSplitter extends stream.Writable {
  directory: Options['directory'] // 存储块的目录
  currentChunkPath: string | null // 当前块的路径
  currentChunkSize: number // 当前块的大小
  fileHandle: fs.FileHandle | null // 当前块的文件句柄
  filenameTemplate: string // 文件名模板
  chunkSize: Options['chunkSize'] // 每个块的大小
  part: number // 当前块的编号

  /**
   * 构造函数
   * @param chunkSize 每个块的大小
   * @param directory 存储块的目录
   * @param options 可选的流写入选项
   */
  constructor({ chunkSize, directory }: Options, options?: stream.WritableOptions) {
    super(options)
    this.chunkSize = chunkSize
    this.currentChunkPath = null
    this.currentChunkSize = 0
    this.fileHandle = null
    this.directory = directory
    this.filenameTemplate = randomString(10)
    this.part = 0

    this.on('error', this._handleError.bind(this))
  }

  /**
   * 写入数据到当前块
   * @param chunk 数据块
   * @param _ 编码方式（未使用）
   * @param callback 回调函数
   */
  async _write(chunk: Buffer, _: BufferEncoding, callback: Callback) {
    try {
      // 如果当前没有文件句柄，则创建一个新的块
      if (this.fileHandle === null) {
        await this._newChunk()
      }

      let overflow = this.currentChunkSize + chunk.length - this.chunkSize

      // 如果写入的数据会导致当前块超过指定大小，则进行分割
      while (overflow > 0) {
        // 只写入不超过指定大小的部分
        await this._writeChunk(chunk.subarray(0, chunk.length - overflow))
        await this._finishChunk()

        // 剩余的数据写入新的块
        await this._newChunk()
        chunk = chunk.subarray(chunk.length - overflow, chunk.length)
        overflow = this.currentChunkSize + chunk.length - this.chunkSize
      }

      // 如果数据块小于指定大小，则直接写入
      await this._writeChunk(chunk)
      callback(null)
    } catch (error: any) {
      callback(error)
    }
  }

  /**
   * 完成写入操作
   * @param callback 回调函数
   */
  async _final(callback: Callback) {
    if (this.fileHandle === null) {
      callback(null)
      return
    }

    try {
      await this._finishChunk()
      callback(null)
    } catch (error: any) {
      callback(error)
    }
  }

  /**
   * 写入数据块到文件
   * @param chunk 数据块
   */
  async _writeChunk(chunk: Buffer): Promise<void> {
    await fs.appendFile(this.fileHandle as fs.FileHandle, chunk)
    this.currentChunkSize += chunk.length
  }

  /**
   * 处理错误
   */
  async _handleError() {
    await this.emitEvent('chunkError', this.currentChunkPath)
    // 如果发生错误，停止写入操作，防止数据丢失
    if (this.fileHandle === null) { return }
    await this.fileHandle.close()
    this.currentChunkPath = null
    this.fileHandle = null
  }

  /**
   * 完成当前块的写入
   */
  async _finishChunk(): Promise<void> {
    if (this.fileHandle === null) {
      return 
    }

    await this.fileHandle.close()

    await this.emitEvent('chunkFinished', {
      path: this.currentChunkPath,
      size: this.currentChunkSize,
    })

    this.currentChunkPath = null
    this.fileHandle = null
    this.currentChunkSize = 0
    this.part += 1
  }

  /**
   * 触发事件
   * @param name 事件名称
   * @param payload 事件负载
   */
  async emitEvent<T>(name: string, payload: T) {
    const listeners = this.listeners(name)
    for (const listener of listeners) {
      await listener(payload)
    }
  }

  /**
   * 创建新的块
   */
  async _newChunk(): Promise<void> {
    const currentChunkPath = path.join(
      this.directory,
      `${this.filenameTemplate}-${this.part}`
    )
    await this.emitEvent('beforeChunkStarted', currentChunkPath)
    this.currentChunkPath = currentChunkPath

    const fileHandle = await fs.open(this.currentChunkPath, 'w')
    await this.emitEvent('chunkStarted', this.currentChunkPath)
    this.currentChunkSize = 0
    this.fileHandle = fileHandle
  }
}