import { application } from '@application'
import { loggerService } from '@logger'
import { isWin } from '@main/core/platform'
import { checkName, getFileType as getFileTypeByExt, getName, readTextFileWithAutoEncoding } from '@main/utils/file'
import { t } from '@main/utils/language'
import type { FileMetadata } from '@shared/data/types/file/legacyFileMetadata'
import type { FileType } from '@shared/types/file'
import { FILE_TYPE } from '@shared/types/file'
import { KB, MB } from '@shared/utils/constants'
import { parseDataUrl } from '@shared/utils/dataUrl'
import { documentExts, imageExts } from '@shared/utils/file'
import chardet from 'chardet'
import * as crypto from 'crypto'
import type { OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
import { dialog, net, shell } from 'electron'
import * as fs from 'fs'
import { writeFileSync } from 'fs'
import { readFile } from 'fs/promises'
import { isBinaryFile } from 'isbinaryfile'
import officeParser from 'officeparser'
import * as path from 'path'
import { PDFDocument } from 'pdf-lib'
import { v4 as uuidv4 } from 'uuid'
import WordExtractor from 'word-extractor'

const logger = loggerService.withContext('FileStorage')

function resolveHomeRelativeFilePath(filePath: string): string {
  if (!filePath.startsWith('~/') && !filePath.startsWith('~\\')) return filePath
  return path.join(application.getPath('sys.home'), filePath.slice(2))
}

class FileStorage {
  // TODO(v2): Lazy getter is a workaround, not a fix.
  //
  // The real problem is that `FileStorage` is exported as a top-level
  // singleton at the bottom of this file
  // (`export const fileStorage = new FileStorage()`). That singleton is
  // instantiated during the static import graph of `src/main/index.ts`
  // (via both `ipc.ts` and the `ApiGatewayService → ApiGateway → routes
  // → KnowledgeService` chain), BEFORE `application.bootstrap()` runs
  // and builds the path registry. The previous shape used field
  // initializers (`private storageDir = application.getPath(...)`),
  // which threw "PATHS not initialized" at module-load time.
  //
  // Lazy getters defer the path lookup until first *access*, by which
  // point bootstrap has finished — but the class itself is still being
  // constructed too early. We've merely moved the path lookup out of
  // construction; we have NOT solved the architectural issue.
  //
  // The proper v2 fix is to migrate `FileStorage` into the lifecycle
  // system: extend `BaseService`, add `@Injectable`, register in
  // `serviceRegistry.ts`, and have callers resolve it via
  // `application.get('FileStorage')` instead of importing the singleton.
  // Once that's done, the DI container will instantiate it inside
  // `application.bootstrap()` after the path registry is built, and
  // these getters can become plain field initializers (or move into
  // `onInit`). Until then, keep them as getters — do NOT "simplify"
  // them back to fields.
  private get storageDir(): string {
    return application.getPath('feature.files.data')
  }

  private get tempDir(): string {
    return application.getPath('app.temp')
  }

  // @TraceProperty({ spanName: 'getFileHash', tag: 'FileStorage' })
  private getFileHash = async (filePath: string): Promise<string> => {
    return new Promise((resolve, reject) => {
      const hash = crypto.createHash('md5')
      const stream = fs.createReadStream(filePath)
      stream.on('data', (data) => hash.update(data))
      stream.on('end', () => resolve(hash.digest('hex')))
      stream.on('error', reject)
    })
  }

  private findDuplicateFile = async (filePath: string): Promise<FileMetadata | null> => {
    const stats = fs.statSync(filePath)
    logger.debug(`stats: ${stats}, filePath: ${filePath}`)
    const fileSize = stats.size

    const files = await fs.promises.readdir(this.storageDir)
    for (const file of files) {
      const storedFilePath = path.join(this.storageDir, file)
      const storedStats = fs.statSync(storedFilePath)

      if (storedStats.size === fileSize) {
        const [originalHash, storedHash] = await Promise.all([
          this.getFileHash(filePath),
          this.getFileHash(storedFilePath)
        ])

        if (originalHash === storedHash) {
          const ext = path.extname(file)
          const id = path.basename(file, ext)
          const type = await this.getFileType(filePath)

          return {
            id,
            origin_name: file,
            name: file + ext,
            path: storedFilePath,
            created_at: storedStats.birthtime.toISOString(),
            size: storedStats.size,
            ext,
            type,
            count: 2
          }
        }
      }
    }

    return null
  }

  public getFileType = async (filePath: string): Promise<FileType> => {
    const ext = path.extname(filePath)
    const fileType = getFileTypeByExt(ext)

    return fileType === FILE_TYPE.OTHER && (await this._isTextFile(filePath)) ? FILE_TYPE.TEXT : fileType
  }

  public selectFile = async (
    _: Electron.IpcMainInvokeEvent,
    options?: OpenDialogOptions
  ): Promise<FileMetadata[] | null> => {
    const defaultOptions: OpenDialogOptions = {
      properties: ['openFile']
    }

    const dialogOptions = { ...defaultOptions, ...options }

    const result = await dialog.showOpenDialog(dialogOptions)

    if (result.canceled || result.filePaths.length === 0) {
      return null
    }

    const fileMetadataPromises = result.filePaths.map(async (filePath) => {
      const stats = fs.statSync(filePath)
      const ext = path.extname(filePath)
      const fileType = await this.getFileType(filePath)

      return {
        id: uuidv4(),
        origin_name: path.basename(filePath),
        name: path.basename(filePath),
        path: filePath,
        created_at: stats.birthtime.toISOString(),
        size: stats.size,
        ext: ext,
        type: fileType,
        count: 1
      }
    })

    return Promise.all(fileMetadataPromises)
  }

  private async compressImage(sourcePath: string, destPath: string): Promise<void> {
    try {
      const stats = fs.statSync(sourcePath)
      const fileSizeInMB = stats.size / MB

      // 如果图片大于1MB才进行压缩
      if (fileSizeInMB > 1) {
        try {
          await fs.promises.copyFile(sourcePath, destPath)
          logger.debug(`Image compressed successfully: ${sourcePath}`)
        } catch (jimpError) {
          logger.error('Image compression failed:', jimpError as Error)
          await fs.promises.copyFile(sourcePath, destPath)
        }
      } else {
        // 小图片直接复制
        await fs.promises.copyFile(sourcePath, destPath)
      }
    } catch (error) {
      logger.error('Image handling failed:', error as Error)
      // 错误情况下直接复制原文件
      await fs.promises.copyFile(sourcePath, destPath)
    }
  }

  public uploadFile = async (_: Electron.IpcMainInvokeEvent, file: FileMetadata): Promise<FileMetadata> => {
    const filePath = file.path
    const duplicateFile = await this.findDuplicateFile(filePath)

    if (duplicateFile) {
      return duplicateFile
    }

    const uuid = uuidv4()
    const origin_name = path.basename(file.path)
    const ext = path.extname(origin_name).toLowerCase()
    const destPath = path.join(this.storageDir, uuid + ext)

    logger.info(`[FileStorage] Uploading file: ${filePath}`)

    // 根据文件类型选择处理方式
    if (imageExts.includes(ext)) {
      await this.compressImage(filePath, destPath)
    } else {
      await fs.promises.copyFile(filePath, destPath)
    }

    const stats = await fs.promises.stat(destPath)
    const fileType = await this.getFileType(destPath)

    const fileMetadata: FileMetadata = {
      id: uuid,
      origin_name,
      name: uuid + ext,
      path: destPath,
      created_at: stats.birthtime.toISOString(),
      size: stats.size,
      ext: ext,
      type: fileType,
      count: 1
    }

    logger.debug(`File uploaded: ${fileMetadata}`)

    return fileMetadata
  }

  public getFile = async (_: Electron.IpcMainInvokeEvent, filePath: string): Promise<FileMetadata | null> => {
    if (!fs.existsSync(filePath)) {
      return null
    }

    const stats = fs.statSync(filePath)
    const fileType = await this.getFileType(filePath)

    return {
      id: uuidv4(),
      origin_name: path.basename(filePath),
      name: path.basename(filePath),
      path: filePath,
      created_at: stats.birthtime.toISOString(),
      size: stats.size,
      ext: path.extname(filePath),
      type: fileType,
      count: 1
    }
  }

  // @TraceProperty({ spanName: 'deleteFile', tag: 'FileStorage' })
  public deleteFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<void> => {
    if (!fs.existsSync(path.join(this.storageDir, id))) {
      return
    }
    await fs.promises.unlink(path.join(this.storageDir, id))
  }

  public deleteDir = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<void> => {
    if (!fs.existsSync(path.join(this.storageDir, id))) {
      return
    }
    await fs.promises.rm(path.join(this.storageDir, id), { recursive: true })
  }

  public deleteExternalFile = async (_: Electron.IpcMainInvokeEvent, filePath: string): Promise<void> => {
    try {
      if (!fs.existsSync(filePath)) {
        return
      }

      await shell.trashItem(filePath)
      logger.debug(`External file moved to trash successfully: ${filePath}`)
    } catch (error) {
      logger.error('Failed to delete external file:', error as Error)
      throw error
    }
  }

  public deleteExternalDir = async (_: Electron.IpcMainInvokeEvent, dirPath: string): Promise<void> => {
    try {
      if (!fs.existsSync(dirPath)) {
        return
      }

      await shell.trashItem(dirPath)
      logger.debug(`External directory moved to trash successfully: ${dirPath}`)
    } catch (error) {
      logger.error('Failed to delete external directory:', error as Error)
      throw error
    }
  }

  public moveFile = async (_: Electron.IpcMainInvokeEvent, filePath: string, newPath: string): Promise<void> => {
    try {
      if (!fs.existsSync(filePath)) {
        throw new Error(`Source file does not exist: ${filePath}`)
      }

      // 确保目标目录存在
      const destDir = path.dirname(newPath)
      if (!fs.existsSync(destDir)) {
        await fs.promises.mkdir(destDir, { recursive: true })
      }

      // 移动文件
      await fs.promises.rename(filePath, newPath)
      logger.debug(`File moved successfully: ${filePath} to ${newPath}`)
    } catch (error) {
      logger.error('Move file failed:', error as Error)
      throw error
    }
  }

  public moveDir = async (_: Electron.IpcMainInvokeEvent, dirPath: string, newDirPath: string): Promise<void> => {
    try {
      if (!fs.existsSync(dirPath)) {
        throw new Error(`Source directory does not exist: ${dirPath}`)
      }

      // 确保目标父目录存在
      const parentDir = path.dirname(newDirPath)
      if (!fs.existsSync(parentDir)) {
        await fs.promises.mkdir(parentDir, { recursive: true })
      }

      // 移动目录
      await fs.promises.rename(dirPath, newDirPath)
      logger.debug(`Directory moved successfully: ${dirPath} to ${newDirPath}`)
    } catch (error) {
      logger.error('Move directory failed:', error as Error)
      throw error
    }
  }

  public renameFile = async (_: Electron.IpcMainInvokeEvent, filePath: string, newName: string): Promise<void> => {
    try {
      if (!fs.existsSync(filePath)) {
        throw new Error(`Source file does not exist: ${filePath}`)
      }

      const dirPath = path.dirname(filePath)
      const newFilePath = path.join(dirPath, newName + '.md')

      // 如果目标文件已存在，抛出错误
      if (fs.existsSync(newFilePath)) {
        throw new Error(`Target file already exists: ${newFilePath}`)
      }

      // 重命名文件
      await fs.promises.rename(filePath, newFilePath)
      logger.debug(`File renamed successfully: ${filePath} to ${newFilePath}`)
    } catch (error) {
      logger.error('Rename file failed:', error as Error)
      throw error
    }
  }

  public renameDir = async (_: Electron.IpcMainInvokeEvent, dirPath: string, newName: string): Promise<void> => {
    try {
      if (!fs.existsSync(dirPath)) {
        throw new Error(`Source directory does not exist: ${dirPath}`)
      }

      const parentDir = path.dirname(dirPath)
      const newDirPath = path.join(parentDir, newName)

      // 如果目标目录已存在，抛出错误
      if (fs.existsSync(newDirPath)) {
        throw new Error(`Target directory already exists: ${newDirPath}`)
      }

      // 重命名目录
      await fs.promises.rename(dirPath, newDirPath)
      logger.debug(`Directory renamed successfully: ${dirPath} to ${newDirPath}`)
    } catch (error) {
      logger.error('Rename directory failed:', error as Error)
      throw error
    }
  }

  /**
   * Core file reading logic that handles both documents and text files.
   *
   * @private
   * @param filePath - Full path to the file
   * @param detectEncoding - Whether to auto-detect text file encoding
   * @returns Promise resolving to the extracted text content
   * @throws Error if file reading fails
   */
  private async readFileCore(filePath: string, detectEncoding: boolean = false): Promise<string> {
    const fileExtension = path.extname(filePath)

    if (documentExts.includes(fileExtension)) {
      try {
        if (fileExtension === '.doc') {
          const extractor = new WordExtractor()
          const extracted = await extractor.extract(filePath)
          return extracted.getBody()
        }

        const data = await officeParser.parseOfficeAsync(filePath, {
          tempFilesLocation: this.tempDir
        })
        return data
      } catch (error) {
        logger.error('Failed to read document file:', error as Error)
        throw error
      }
    }

    try {
      if (detectEncoding) {
        return readTextFileWithAutoEncoding(filePath)
      } else {
        return fs.readFileSync(filePath, 'utf-8')
      }
    } catch (error) {
      logger.error('Failed to read text file:', error as Error)
      throw new Error(`Failed to read file: ${filePath}.`)
    }
  }

  /**
   * Reads and extracts content from a stored file.
   *
   * Supports multiple file formats including:
   * - Complex documents: .pdf, .doc, .docx, .pptx, .xlsx, .odt, .odp, .ods
   * - Text files: .txt, .md, .json, .csv, etc.
   * - Code files: .js, .ts, .py, .java, etc.
   *
   * For document formats, extracts text content using specialized parsers:
   * - .doc files: Uses word-extractor library
   * - Other Office formats: Uses officeparser library
   *
   * For text files, can optionally detect encoding automatically.
   *
   * @param _ - Electron IPC invoke event (unused)
   * @param id - File identifier with extension (e.g., "uuid.docx")
   * @param detectEncoding - Whether to auto-detect text file encoding (default: false)
   * @returns Promise resolving to the extracted text content of the file
   * @throws Error if file reading fails or file is not found
   *
   * @example
   * // Read a DOCX file
   * const content = await readFile(event, "document.docx");
   *
   * @example
   * // Read a text file with encoding detection
   * const content = await readFile(event, "text.txt", true);
   *
   * @example
   * // Read a PDF file
   * const content = await readFile(event, "manual.pdf");
   */
  public readFile = async (
    _: Electron.IpcMainInvokeEvent,
    id: string,
    detectEncoding: boolean = false
  ): Promise<string> => {
    const filePath = path.join(this.storageDir, id)
    return this.readFileCore(filePath, detectEncoding)
  }

  /**
   * Reads and extracts content from an external file path.
   *
   * Similar to readFile, but operates on external file paths instead of stored files.
   * Supports the same file formats including complex documents and text files.
   *
   * @param _ - Electron IPC invoke event (unused)
   * @param filePath - Absolute path to the external file
   * @param detectEncoding - Whether to auto-detect text file encoding (default: false)
   * @returns Promise resolving to the extracted text content of the file
   * @throws Error if file does not exist or reading fails
   *
   * @example
   * // Read an external DOCX file
   * const content = await readExternalFile(event, "/path/to/document.docx");
   *
   * @example
   * // Read an external text file with encoding detection
   * const content = await readExternalFile(event, "/path/to/text.txt", true);
   */
  public readExternalFile = async (
    _: Electron.IpcMainInvokeEvent,
    filePath: string,
    detectEncoding: boolean = false
  ): Promise<string> => {
    if (!fs.existsSync(filePath)) {
      throw new Error(`File does not exist: ${filePath}`)
    }

    return this.readFileCore(filePath, detectEncoding)
  }

  public createTempFile = async (_: Electron.IpcMainInvokeEvent, fileName: string): Promise<string> => {
    // `fileName` is renderer-supplied; basename it so a value like `../../evil` can't escape tempDir.
    return path.join(this.tempDir, `temp_file_${uuidv4()}_${path.basename(fileName)}`)
  }

  public writeFile = async (
    _: Electron.IpcMainInvokeEvent,
    filePath: string,
    data: Uint8Array | string
  ): Promise<void> => {
    await fs.promises.writeFile(filePath, data)
  }

  public fileNameGuard = async (
    _: Electron.IpcMainInvokeEvent,
    dirPath: string,
    fileName: string,
    isFile: boolean
  ): Promise<{ safeName: string; exists: boolean }> => {
    const safeName = checkName(fileName)
    const finalName = getName(dirPath, safeName, isFile)
    const fullPath = path.join(dirPath, finalName + (isFile ? '.md' : ''))
    const exists = fs.existsSync(fullPath)

    logger.debug(`File name guard: ${fileName} -> ${finalName}, exists: ${exists}`)
    return { safeName: finalName, exists }
  }

  public mkdir = async (_: Electron.IpcMainInvokeEvent, dirPath: string): Promise<string> => {
    try {
      logger.debug(`Attempting to create directory: ${dirPath}`)
      await fs.promises.mkdir(dirPath, { recursive: true })
      return dirPath
    } catch (error) {
      logger.error('Failed to create directory:', error as Error)
      throw new Error(`Failed to create directory: ${dirPath}. Error: ${(error as Error).message}`)
    }
  }

  public base64Image = async (
    _: Electron.IpcMainInvokeEvent,
    id: string
  ): Promise<{ mime: string; base64: string; data: string }> => {
    const filePath = path.join(this.storageDir, id)
    const data = await fs.promises.readFile(filePath)
    const base64 = data.toString('base64')
    const rawExt = path.extname(filePath).slice(1)
    const ext = rawExt === 'jpg' ? 'jpeg' : rawExt
    const mime = ext ? `image/${ext}` : 'image/png'
    return {
      mime,
      base64,
      data: `data:${mime};base64,${base64}`
    }
  }

  public saveBase64Image = async (_: Electron.IpcMainInvokeEvent, base64Data: string): Promise<FileMetadata> => {
    try {
      if (!base64Data) {
        throw new Error('Base64 data is required')
      }

      const parseResult = parseDataUrl(base64Data)
      const base64String = parseResult?.data ?? base64Data
      const ext = parseResult?.mediaType ? this.getExtensionFromMimeType(parseResult.mediaType) : '.png'

      const buffer = Buffer.from(base64String, 'base64')
      const uuid = uuidv4()
      const destPath = path.join(this.storageDir, uuid + ext)

      logger.debug('Saving base64 image:', {
        storageDir: this.storageDir,
        destPath,
        bufferSize: buffer.length
      })

      // 确保目录存在
      if (!fs.existsSync(this.storageDir)) {
        fs.mkdirSync(this.storageDir, { recursive: true })
      }

      await fs.promises.writeFile(destPath, buffer)

      return {
        id: uuid,
        origin_name: uuid + ext,
        name: uuid + ext,
        path: destPath,
        created_at: new Date().toISOString(),
        size: buffer.length,
        ext: ext.slice(1),
        type: getFileTypeByExt(ext),
        count: 1
      }
    } catch (error) {
      logger.error('Failed to save base64 image:', error as Error)
      throw error
    }
  }

  public savePastedImage = async (
    _: Electron.IpcMainInvokeEvent,
    imageData: Uint8Array | Buffer,
    extension?: string
  ): Promise<FileMetadata> => {
    try {
      const uuid = uuidv4()
      const ext = extension || '.png'
      const destPath = path.join(this.storageDir, uuid + ext)

      logger.debug('Saving pasted image:', {
        storageDir: this.storageDir,
        destPath,
        bufferSize: imageData.length
      })

      // 确保目录存在
      if (!fs.existsSync(this.storageDir)) {
        fs.mkdirSync(this.storageDir, { recursive: true })
      }

      // 确保 imageData 是 Buffer
      const buffer = Buffer.isBuffer(imageData) ? imageData : Buffer.from(imageData)

      // 如果图片大于1MB，进行压缩处理
      if (buffer.length > MB) {
        await this.compressImageBuffer(buffer, destPath, ext)
      } else {
        await fs.promises.writeFile(destPath, buffer)
      }

      const stats = await fs.promises.stat(destPath)

      return {
        id: uuid,
        origin_name: `pasted_image_${uuid}${ext}`,
        name: uuid + ext,
        path: destPath,
        created_at: new Date().toISOString(),
        size: stats.size,
        ext: ext.slice(1),
        type: getFileTypeByExt(ext),
        count: 1
      }
    } catch (error) {
      logger.error('Failed to save pasted image:', error as Error)
      throw error
    }
  }

  private async compressImageBuffer(imageBuffer: Buffer, destPath: string, ext: string): Promise<void> {
    try {
      // 创建临时文件
      const tempPath = path.join(this.tempDir, `temp_${uuidv4()}${ext}`)
      await fs.promises.writeFile(tempPath, imageBuffer)

      // 使用现有的压缩方法
      await this.compressImage(tempPath, destPath)

      // 清理临时文件
      try {
        await fs.promises.unlink(tempPath)
      } catch (error) {
        logger.warn('Failed to cleanup temp file:', error as Error)
      }
    } catch (error) {
      logger.error('Image buffer compression failed, saving original:', error as Error)
      // 压缩失败时保存原始文件
      await fs.promises.writeFile(destPath, imageBuffer)
    }
  }

  public base64File = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<{ data: string; mime: string }> => {
    const filePath = path.join(this.storageDir, id)
    const buffer = await fs.promises.readFile(filePath)
    const base64 = buffer.toString('base64')
    const mime = `application/${path.extname(filePath).slice(1)}`
    return { data: base64, mime }
  }

  public pdfPageCount = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<number> => {
    const filePath = path.join(this.storageDir, id)
    const buffer = await fs.promises.readFile(filePath)

    const pdfDoc = await PDFDocument.load(buffer)
    return pdfDoc.getPageCount()
  }

  public binaryImage = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<{ data: Buffer; mime: string }> => {
    const filePath = path.join(this.storageDir, id)
    const data = await fs.promises.readFile(filePath)
    const mime = `image/${path.extname(filePath).slice(1)}`
    return { data, mime }
  }

  public clear = async (): Promise<void> => {
    await fs.promises.rm(this.storageDir, { recursive: true })
    await fs.promises.mkdir(this.storageDir, { recursive: true })
  }

  public clearTemp = async (): Promise<void> => {
    await fs.promises.rm(this.tempDir, { recursive: true })
    await fs.promises.mkdir(this.tempDir, { recursive: true })
  }

  public open = async (
    _: Electron.IpcMainInvokeEvent,
    options: OpenDialogOptions
  ): Promise<{ fileName: string; filePath: string; content?: Buffer; size: number } | null> => {
    try {
      const result: OpenDialogReturnValue = await dialog.showOpenDialog({
        title: t('dialog.open_file'),
        properties: ['openFile'],
        filters: [{ name: t('dialog.all_files'), extensions: ['*'] }],
        ...options
      })

      if (!result.canceled && result.filePaths.length > 0) {
        const filePath = result.filePaths[0]
        const fileName = filePath.split('/').pop() || ''
        const stats = await fs.promises.stat(filePath)

        // If the file is less than 2GB, read the content
        if (stats.size < 2 * 1024 * 1024 * 1024) {
          const content = await readFile(filePath)
          return { fileName, filePath, content, size: stats.size }
        }

        // For large files, only return file information, do not read content
        return { fileName, filePath, size: stats.size }
      }

      return null
    } catch (err) {
      logger.error('[IPC - Error] An error occurred opening the file:', err as Error)
      return null
    }
  }

  public openPath = async (_: Electron.IpcMainInvokeEvent, path: string): Promise<void> => {
    const resolved = await shell.openPath(resolveHomeRelativeFilePath(path))
    if (resolved !== '') {
      throw new Error(resolved)
    }
  }

  /**
   * 通过相对路径打开文件，跨设备时使用
   * @param _
   * @param file
   */
  public openFileWithRelativePath = async (_: Electron.IpcMainInvokeEvent, file: FileMetadata): Promise<void> => {
    const filePath = path.join(this.storageDir, file.name)
    if (fs.existsSync(filePath)) {
      shell.openPath(filePath).catch((err) => logger.error('[IPC - Error] Failed to open file:', err))
    } else {
      logger.warn(`[IPC - Warning] File does not exist: ${filePath}`)
    }
  }

  public validateNotesDirectory = async (_: Electron.IpcMainInvokeEvent, dirPath: string): Promise<boolean> => {
    try {
      if (!dirPath || typeof dirPath !== 'string') {
        return false
      }

      // Normalize path
      const normalizedPath = path.resolve(dirPath)

      // Check if directory exists
      if (!fs.existsSync(normalizedPath)) {
        return false
      }

      // Check if it's actually a directory
      const stats = fs.statSync(normalizedPath)
      if (!stats.isDirectory()) {
        return false
      }

      // Get app paths to prevent selection of restricted directories
      const appDataPath = path.resolve(application.getPath('sys.appdata'))
      const filesDir = path.resolve(application.getPath('feature.files.data'))
      const currentNotesDir = path.resolve(application.getPath('feature.notes.data'))

      // Prevent selecting app data directories
      if (
        normalizedPath.startsWith(filesDir) ||
        normalizedPath.startsWith(appDataPath) ||
        normalizedPath === currentNotesDir
      ) {
        logger.warn(`Invalid directory selection: ${normalizedPath} (app data directory)`)
        return false
      }

      // Prevent selecting system root directories
      const isSystemRoot = isWin
        ? /^[a-zA-Z]:[\\/]?$/.test(normalizedPath)
        : normalizedPath === '/' ||
          normalizedPath === '/usr' ||
          normalizedPath === '/etc' ||
          normalizedPath === '/System'

      if (isSystemRoot) {
        logger.warn(`Invalid directory selection: ${normalizedPath} (system root directory)`)
        return false
      }

      // Check write permissions
      try {
        fs.accessSync(normalizedPath, fs.constants.W_OK)
      } catch (error) {
        logger.warn(`Directory not writable: ${normalizedPath}`)
        return false
      }

      return true
    } catch (error) {
      logger.error('Failed to validate notes directory:', error as Error)
      return false
    }
  }

  public save = async (
    _: Electron.IpcMainInvokeEvent,
    fileName: string,
    content: string,
    options?: SaveDialogOptions
  ): Promise<string | null> => {
    try {
      const result: SaveDialogReturnValue = await dialog.showSaveDialog({
        title: t('dialog.save_file'),
        defaultPath: fileName,
        ...options
      })

      if (result.canceled || !result.filePath) {
        return null
      }

      writeFileSync(result.filePath, content, { encoding: 'utf-8' })

      return result.filePath
    } catch (err: any) {
      logger.error('[IPC - Error] An error occurred saving the file:', err as Error)
      return Promise.reject('An error occurred saving the file: ' + err?.message)
    }
  }

  public saveImage = async (_: Electron.IpcMainInvokeEvent, name: string, data: string): Promise<boolean> => {
    try {
      const filePath = dialog.showSaveDialogSync({
        defaultPath: `${name}.png`,
        filters: [{ name: t('dialog.png_image'), extensions: ['png'] }]
      })

      if (filePath) {
        const parseResult = parseDataUrl(data)
        fs.writeFileSync(filePath, parseResult?.data ?? data, 'base64')
        return true
      }
    } catch (error) {
      logger.error('[IPC - Error] An error occurred saving the image:', error as Error)
    }
    return false
  }

  public selectFolder = async (_: Electron.IpcMainInvokeEvent, options: OpenDialogOptions): Promise<string | null> => {
    try {
      const result: OpenDialogReturnValue = await dialog.showOpenDialog({
        title: t('dialog.select_folder'),
        properties: ['openDirectory'],
        ...options
      })

      if (!result.canceled && result.filePaths.length > 0) {
        return result.filePaths[0]
      }

      return null
    } catch (err) {
      logger.error('[IPC - Error] An error occurred selecting the folder:', err as Error)
      return null
    }
  }

  public downloadFile = async (
    _: Electron.IpcMainInvokeEvent,
    url: string,
    isUseContentType?: boolean
  ): Promise<FileMetadata> => {
    try {
      const response = await net.fetch(url)
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }

      // 尝试从Content-Disposition获取文件名
      const contentDisposition = response.headers.get('Content-Disposition')
      let filename = 'download'

      if (contentDisposition) {
        const filenameMatch = contentDisposition.match(/filename="?(.+)"?/i)
        if (filenameMatch) {
          filename = filenameMatch[1]
        }
      }

      // 如果URL中有文件名，使用URL中的文件名
      const urlFilename = url.split('/').pop()?.split('?')[0]
      if (urlFilename && urlFilename.includes('.')) {
        filename = urlFilename
      }

      // 如果文件名没有后缀，根据Content-Type添加后缀
      if (isUseContentType || !filename.includes('.')) {
        const contentType = response.headers.get('Content-Type')
        const ext = this.getExtensionFromMimeType(contentType)
        filename += ext
      }

      const uuid = uuidv4()
      const ext = path.extname(filename)
      const destPath = path.join(this.storageDir, uuid + ext)

      // 将响应内容写入文件
      const buffer = Buffer.from(await response.arrayBuffer())
      await fs.promises.writeFile(destPath, buffer)

      const stats = await fs.promises.stat(destPath)
      const fileType = await this.getFileType(destPath)

      return {
        id: uuid,
        origin_name: filename,
        name: uuid + ext,
        path: destPath,
        created_at: stats.birthtime.toISOString(),
        size: stats.size,
        ext: ext,
        type: fileType,
        count: 1
      }
    } catch (error) {
      logger.error('Download file error:', error as Error)
      throw error
    }
  }

  private getExtensionFromMimeType(mimeType: string | null): string {
    if (!mimeType) return '.bin'

    const mimeToExtension: { [key: string]: string } = {
      'image/jpeg': '.jpg',
      'image/png': '.png',
      'image/gif': '.gif',
      'image/webp': '.webp',
      'image/bmp': '.bmp',
      'application/pdf': '.pdf',
      'text/plain': '.txt',
      'application/msword': '.doc',
      'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
      'application/zip': '.zip',
      'application/x-zip-compressed': '.zip',
      'application/octet-stream': '.bin'
    }

    return mimeToExtension[mimeType] || '.bin'
  }

  // @TraceProperty({ spanName: 'copyFile', tag: 'FileStorage' })
  public copyFile = async (_: Electron.IpcMainInvokeEvent, id: string, destPath: string): Promise<void> => {
    try {
      const sourcePath = path.join(this.storageDir, id)

      // 确保目标目录存在
      const destDir = path.dirname(destPath)
      if (!fs.existsSync(destDir)) {
        await fs.promises.mkdir(destDir, { recursive: true })
      }

      // 复制文件
      await fs.promises.copyFile(sourcePath, destPath)
      logger.debug(`File copied successfully: ${sourcePath} to ${destPath}`)
    } catch (error) {
      logger.error('Copy file failed:', error as Error)
      throw error
    }
  }

  public writeFileWithId = async (_: Electron.IpcMainInvokeEvent, id: string, content: string): Promise<void> => {
    try {
      const filePath = path.join(this.storageDir, id)
      logger.debug(`Writing file: ${filePath}`)

      // 确保目录存在
      if (!fs.existsSync(this.storageDir)) {
        logger.debug(`Creating storage directory: ${this.storageDir}`)
        fs.mkdirSync(this.storageDir, { recursive: true })
      }

      await fs.promises.writeFile(filePath, content, 'utf8')
      logger.debug(`File written successfully: ${filePath}`)
    } catch (error) {
      logger.error('Failed to write file:', error as Error)
      throw error
    }
  }

  public getFilePathById(file: FileMetadata): string {
    return path.join(this.storageDir, file.id + file.ext)
  }

  public isTextFile = async (_: Electron.IpcMainInvokeEvent, filePath: string): Promise<boolean> => {
    return this._isTextFile(filePath)
  }

  private _isTextFile = async (filePath: string): Promise<boolean> => {
    try {
      const isBinary = await isBinaryFile(filePath)
      if (isBinary) {
        return false
      }

      const length = 8 * KB
      const fileHandle = await fs.promises.open(filePath, 'r')
      const buffer = Buffer.alloc(length)
      const { bytesRead } = await fileHandle.read(buffer, 0, length, 0)
      await fileHandle.close()

      const sampleBuffer = buffer.subarray(0, bytesRead)
      const matches = chardet.analyse(sampleBuffer)

      // 如果检测到的编码置信度较高，认为是文本文件
      if (matches.length > 0 && matches[0].confidence > 0.8) {
        return true
      }

      return false
    } catch (error) {
      logger.error('Failed to check if file is text:', error as Error)
      return false
    }
  }

  public isDirectory = async (_: Electron.IpcMainInvokeEvent, filePath: string): Promise<boolean> => {
    try {
      const stat = await fs.promises.stat(filePath)
      return stat.isDirectory()
    } catch {
      return false
    }
  }

  public showInFolder = async (_: Electron.IpcMainInvokeEvent, path: string): Promise<void> => {
    const resolvedPath = resolveHomeRelativeFilePath(path)
    if (!fs.existsSync(resolvedPath)) {
      const msg = `File or folder does not exist: ${resolvedPath}`
      logger.error(msg)
      throw new Error(msg)
    }
    try {
      shell.showItemInFolder(resolvedPath)
    } catch (error) {
      logger.error('Failed to show item in folder:', error as Error)
    }
  }

  /**
   * Batch upload markdown files from native File objects
   * This handles all I/O operations in the Main process to avoid blocking Renderer
   */
  public batchUploadMarkdownFiles = async (
    _: Electron.IpcMainInvokeEvent,
    filePaths: string[],
    targetPath: string
  ): Promise<{
    fileCount: number
    folderCount: number
    skippedFiles: number
    failedFiles: number
  }> => {
    try {
      logger.info('Starting batch upload', { fileCount: filePaths.length, targetPath })

      const basePath = path.resolve(targetPath)
      const MARKDOWN_EXTS = ['.md', '.markdown']

      // Filter markdown files
      const markdownFiles = filePaths.filter((filePath) => {
        const ext = path.extname(filePath).toLowerCase()
        return MARKDOWN_EXTS.includes(ext)
      })

      const skippedFiles = filePaths.length - markdownFiles.length

      if (markdownFiles.length === 0) {
        return { fileCount: 0, folderCount: 0, skippedFiles, failedFiles: 0 }
      }

      // Collect unique folders needed
      const foldersSet = new Set<string>()
      const fileOperations: Array<{ sourcePath: string; targetPath: string }> = []
      let failedFiles = 0

      for (const filePath of markdownFiles) {
        try {
          // Get relative path if file is from a directory upload
          const fileName = path.basename(filePath)
          const relativePath = path.dirname(filePath)

          // Determine target directory structure
          let targetDir = basePath
          const folderParts: string[] = []

          // Extract folder structure from file path for nested uploads
          // This is a simplified version - in real scenario we'd need the original directory structure
          if (relativePath && relativePath !== '.') {
            const parts = relativePath.split(path.sep)
            // Get the last few parts that represent the folder structure within upload
            const relevantParts = parts.slice(Math.max(0, parts.length - 3))
            folderParts.push(...relevantParts)
          }

          // Build target directory path
          for (const part of folderParts) {
            targetDir = path.join(targetDir, part)
            foldersSet.add(targetDir)
          }

          // Determine final file name
          const nameWithoutExt = fileName.endsWith('.md')
            ? fileName.slice(0, -3)
            : fileName.endsWith('.markdown')
              ? fileName.slice(0, -9)
              : fileName

          const { safeName } = await this.fileNameGuard(_, targetDir, nameWithoutExt, true)
          const finalPath = path.join(targetDir, safeName + '.md')

          fileOperations.push({ sourcePath: filePath, targetPath: finalPath })
        } catch (error) {
          failedFiles += 1
          logger.error('Failed to prepare file operation:', error as Error, { filePath })
        }
      }

      // Create folders in order (shallow to deep)
      const sortedFolders = Array.from(foldersSet).sort((a, b) => a.length - b.length)
      for (const folder of sortedFolders) {
        try {
          if (!fs.existsSync(folder)) {
            await fs.promises.mkdir(folder, { recursive: true })
          }
        } catch (error) {
          logger.debug('Folder already exists or creation failed', { folder, error: (error as Error).message })
        }
      }

      // Process files in batches
      const BATCH_SIZE = 10 // Higher batch size since we're in Main process
      let successCount = 0

      for (let i = 0; i < fileOperations.length; i += BATCH_SIZE) {
        const batch = fileOperations.slice(i, i + BATCH_SIZE)

        const results = await Promise.allSettled(
          batch.map(async (op) => {
            // Read from source and write to target in Main process
            const content = await fs.promises.readFile(op.sourcePath, 'utf-8')
            await fs.promises.writeFile(op.targetPath, content, 'utf-8')
            return true
          })
        )

        results.forEach((result, index) => {
          if (result.status === 'fulfilled') {
            successCount++
          } else {
            failedFiles += 1
            logger.error('Failed to upload file:', result.reason, {
              file: batch[index].sourcePath
            })
          }
        })
      }

      logger.info('Batch upload completed', {
        successCount,
        folderCount: foldersSet.size,
        skippedFiles,
        failedFiles
      })

      return {
        fileCount: successCount,
        folderCount: foldersSet.size,
        skippedFiles,
        failedFiles
      }
    } catch (error) {
      logger.error('Batch upload failed:', error as Error)
      throw error
    }
  }
}

export const fileStorage = new FileStorage()
