import * as fs from 'node:fs'
import { readFile } from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'

import { application } from '@application'
import { loggerService } from '@logger'
import type { FileMetadata } from '@shared/data/types/file/legacyFileMetadata'
import type { FileType } from '@shared/types/file'
import { FILE_TYPE } from '@shared/types/file'
import { MB } from '@shared/utils/constants'
import { audioExts, documentExts, imageExts, textExts, videoExts } from '@shared/utils/file'
import { sanitizeFilename, validateFileName } from '@shared/utils/file/filename'

// Re-export the promoted utilities so existing import sites
// (`@main/utils/file → sanitizeFilename / validateFileName`) keep working.
// SoT lives in `@shared/utils/file/filename` per `utils-file-migration.md`
// Phase 1b.1; callers migrate to the shared path opportunistically.
export { sanitizeFilename, validateFileName } from '@shared/utils/file/filename'
import chardet from 'chardet'
import iconv from 'iconv-lite'
import { v4 as uuidv4 } from 'uuid'

const logger = loggerService.withContext('Utils:File')

// 创建文件类型映射表，提高查找效率
const fileTypeMap = new Map<string, FileType>()

// 初始化映射表
function initFileTypeMap() {
  imageExts.forEach((ext) => fileTypeMap.set(ext, FILE_TYPE.IMAGE))
  videoExts.forEach((ext) => fileTypeMap.set(ext, FILE_TYPE.VIDEO))
  audioExts.forEach((ext) => fileTypeMap.set(ext, FILE_TYPE.AUDIO))
  textExts.forEach((ext) => fileTypeMap.set(ext, FILE_TYPE.TEXT))
  documentExts.forEach((ext) => fileTypeMap.set(ext, FILE_TYPE.DOCUMENT))
}

// 初始化映射表
initFileTypeMap()

/**
 * Resolves a relative path against a base directory and validates it is within bounds.
 * Prevents path traversal attacks (e.g., ../../../etc/passwd).
 *
 * @param baseDir - The base directory
 * @param relativePath - The relative path (may contain '..')
 * @returns The resolved absolute file path
 * @throws Error if resolved path is outside base directory
 */
export function resolveAndValidatePath(baseDir: string, relativePath: string): string {
  const resolvedBase = path.resolve(baseDir)
  const resolvedPath = path.resolve(baseDir, relativePath)
  const separator = resolvedBase.endsWith(path.sep) ? '' : path.sep
  if (!resolvedPath.startsWith(resolvedBase + separator)) {
    throw new Error('Invalid file path: path traversal detected')
  }
  return resolvedPath
}

export function untildify(pathWithTilde: string) {
  if (pathWithTilde.startsWith('~')) {
    const homeDirectory = os.homedir()
    return pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory)
  }
  return pathWithTilde
}

export async function hasWritePermission(dir: string) {
  try {
    logger.info(`Checking write permission for ${dir}`)
    await fs.promises.access(dir, fs.constants.W_OK)
    return true
  } catch (error) {
    return false
  }
}

/**
 * Check if a path is inside another path (proper parent-child relationship)
 * This function correctly handles edge cases that string.startsWith() cannot handle,
 * such as distinguishing between '/root/test' and '/root/test aaa'
 *
 * @param childPath - The path that might be inside the parent path
 * @param parentPath - The path that might contain the child path
 * @returns true if childPath is inside parentPath, false otherwise
 */
export function isPathInside(childPath: string, parentPath: string): boolean {
  try {
    const resolvedChild = path.resolve(childPath)
    const resolvedParent = path.resolve(parentPath)

    // Normalize paths to handle different separators
    const normalizedChild = path.normalize(resolvedChild)
    const normalizedParent = path.normalize(resolvedParent)

    // Check if they are the same path
    if (normalizedChild === normalizedParent) {
      return true
    }

    // Get relative path from parent to child
    const relativePath = path.relative(normalizedParent, normalizedChild)

    // If relative path is empty, they are the same
    // If relative path starts with '..', child is not inside parent
    // If relative path is absolute, child is not inside parent
    return relativePath !== '' && !relativePath.startsWith('..') && !path.isAbsolute(relativePath)
  } catch (error) {
    logger.error('Failed to check path relationship:', error as Error)
    return false
  }
}

export function getFileType(ext: string): FileType {
  ext = ext.toLowerCase()
  return fileTypeMap.get(ext) || FILE_TYPE.OTHER
}

export function getFileDir(filePath: string) {
  return path.dirname(filePath)
}

export function getFileName(filePath: string) {
  return path.basename(filePath)
}

export function getFileExt(filePath: string) {
  return path.extname(filePath)
}

export function getAllFiles(dirPath: string, arrayOfFiles: FileMetadata[] = []): FileMetadata[] {
  const files = fs.readdirSync(dirPath)

  files.forEach((file) => {
    if (file.startsWith('.')) {
      return
    }

    const fullPath = path.join(dirPath, file)
    if (fs.statSync(fullPath).isDirectory()) {
      arrayOfFiles = getAllFiles(fullPath, arrayOfFiles)
    } else {
      const ext = path.extname(file)
      const fileType = getFileType(ext)

      if ([FILE_TYPE.OTHER, FILE_TYPE.IMAGE, FILE_TYPE.VIDEO, FILE_TYPE.AUDIO].some((type) => type === fileType)) {
        return
      }

      const name = path.basename(file)
      const size = fs.statSync(fullPath).size

      const fileItem: FileMetadata = {
        id: uuidv4(),
        name,
        path: fullPath,
        size,
        ext,
        count: 1,
        origin_name: name,
        type: fileType,
        created_at: new Date().toISOString()
      }

      arrayOfFiles.push(fileItem)
    }
  })

  return arrayOfFiles
}

/**
 * Decode raw bytes to text, auto-detecting the encoding (chardet) with a UTF-8
 * fallback. Buffer-based sibling of {@link readTextFileWithAutoEncoding} for
 * callers that already hold the bytes (e.g. via `FileManager.read`) and have no
 * filesystem path.
 */
export function decodeTextWithAutoEncoding(data: Buffer): string {
  const sample = data.length > MB ? data.subarray(0, MB) : data
  const detected = chardet.detect(sample) || 'UTF-8'

  for (const encoding of [detected, 'UTF-8']) {
    try {
      const content = iconv.decode(data, encoding)
      if (!content.includes('�')) return content
      logger.warn(`Auto-detected ${encoding} but content has invalid characters; trying other encodings`)
    } catch (error) {
      logger.error(`Failed to decode buffer with encoding ${encoding}: ${error}`)
    }
  }

  logger.error('Failed to decode buffer with all candidate encodings; falling back to UTF-8')
  return iconv.decode(data, 'UTF-8')
}

/**
 * 读取文件内容并自动检测编码格式进行解码
 * @param filePath - 文件路径
 * @returns 解码后的文件内容
 * @throws 如果路径不存在抛出错误
 */
export async function readTextFileWithAutoEncoding(filePath: string): Promise<string> {
  const encoding = (await chardet.detectFile(filePath, { sampleSize: MB })) || 'UTF-8'
  logger.debug(`File ${filePath} detected encoding: ${encoding}`)

  const encodings = [encoding, 'UTF-8']
  const data = await readFile(filePath)

  for (const encoding of encodings) {
    try {
      const content = iconv.decode(data, encoding)
      if (!content.includes('\uFFFD')) {
        return content
      } else {
        logger.warn(
          `File ${filePath} was auto-detected as ${encoding} encoding, but contains invalid characters. Trying other encodings`
        )
      }
    } catch (error) {
      logger.error(`Failed to decode file ${filePath} with encoding ${encoding}: ${error}`)
    }
  }

  logger.error(`File ${filePath} failed to decode with all possible encodings, trying UTF-8 encoding`)
  return iconv.decode(data, 'UTF-8')
}

export async function writeWithLock(
  filePath: string,
  data: string | NodeJS.ArrayBufferView,
  options: (fs.ObjectEncodingOptions & { mode?: number; flag?: string }) & {
    atomic?: boolean
    tempPath?: string
    lockFilePath?: string
    retries?: number
    retryDelayMs?: number
    lockStaleMs?: number
  } = {}
): Promise<void> {
  const {
    atomic = false,
    tempPath,
    lockFilePath = `${filePath}.lock`,
    retries = 50,
    retryDelayMs = 50,
    lockStaleMs = 30_000,
    ...writeOptions
  } = options

  const finalTempPath = tempPath ?? `${filePath}.tmp`

  for (let attempt = 0; attempt <= retries; attempt += 1) {
    try {
      const handle = await fs.promises.open(lockFilePath, 'wx')
      await handle.close()

      try {
        if (atomic) {
          await fs.promises.writeFile(finalTempPath, data, writeOptions)
          await fs.promises.rename(finalTempPath, filePath)
        } else {
          await fs.promises.writeFile(filePath, data, writeOptions)
        }
      } finally {
        await fs.promises.unlink(lockFilePath).catch(() => undefined)
      }

      return
    } catch (error) {
      const nodeError = error as NodeJS.ErrnoException
      if (nodeError.code !== 'EEXIST' || attempt >= retries) {
        throw error
      }

      if (lockStaleMs > 0) {
        try {
          const stats = await fs.promises.stat(lockFilePath)
          if (Date.now() - stats.mtimeMs > lockStaleMs) {
            await fs.promises.unlink(lockFilePath)
            continue
          }
        } catch {
          // Ignore stale checks if lock file disappears or stat fails
        }
      }

      await new Promise((resolve) => setTimeout(resolve, retryDelayMs))
    }
  }
}

export async function base64Image(file: FileMetadata): Promise<{ mime: string; base64: string; data: string }> {
  const filePath = application.getPath('feature.files.data', `${file.id}${file.ext}`)
  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}`
  }
}

/**
 * 文件名唯一性约束
 * @param baseDir 基础目录
 * @param fileName 文件名
 * @param isFile 是否为文件
 * @returns 唯一的文件名
 */
export function getName(baseDir: string, fileName: string, isFile: boolean): string {
  // 首先清理文件名
  const baseName = sanitizeFilename(fileName)
  let candidate = isFile ? baseName + '.md' : baseName
  let counter = 1

  while (fs.existsSync(path.join(baseDir, candidate))) {
    candidate = isFile ? `${baseName}${counter}.md` : `${baseName}${counter}`
    counter++
  }

  return isFile ? candidate.slice(0, -3) : candidate
}

/**
 * 文件名合法性检查
 * @param fileName 文件名
 * @throws 如果文件名不合法则抛出异常
 * @returns 合法的文件名
 */
export function checkName(fileName: string): string {
  const baseName = path.basename(fileName)
  const validation = validateFileName(baseName)
  if (!validation.valid) {
    // 自动清理非法字符，而不是抛出错误
    const sanitized = sanitizeFilename(baseName)
    logger.warn(`File name contains invalid characters, auto-sanitized: "${baseName}" -> "${sanitized}"`)
    return sanitized
  }
  return baseName
}

/**
 * Check if a directory exists at the given path
 */
export async function directoryExists(dirPath: string): Promise<boolean> {
  try {
    const stats = await fs.promises.stat(dirPath)
    return stats.isDirectory()
  } catch {
    return false
  }
}

/**
 * Check if a path exists (file or directory)
 */
export async function pathExists(targetPath: string): Promise<boolean> {
  try {
    await fs.promises.access(targetPath, fs.constants.R_OK)
    return true
  } catch {
    return false
  }
}

/**
 * Check if a file exists at the given path
 */
export async function fileExists(filePath: string): Promise<boolean> {
  try {
    const stats = await fs.promises.stat(filePath)
    return stats.isFile()
  } catch {
    return false
  }
}
