import { access, open, readFile } from 'node:fs/promises'
import { createHash } from 'node:crypto'
import { createReadStream } from 'node:fs'
import { isAbsolute, resolve } from 'node:path'

export const REQUIRED_FREECAD_NAMING_LIBRARIES = ['FreeCADBase', 'FreeCADApp', 'Part', 'QtCore', 'Python']
export const REQUIRED_FREECAD_NAMING_LINK_DEPENDENCIES = ['QtConcurrent', 'QtNetwork', 'QtXml', 'QtBundledPcre2', 'QtBundledZLIB', 'yaml-cpp', 'ICUCommon', 'ICUI18N', 'ICUData', 'XercesC', 'BoostProgramOptions', 'BoostRegex', 'BoostThread', 'BoostDateTime', 'BoostAtomic', 'PythonMpdecimal', 'PythonExpat', 'PythonHaclSha2', 'PythonZlib', 'PythonBzip2', 'PythonSqlite3']
export const REQUIRED_FREECAD_NAMING_CALLBACKS = ['freecadNamingAbiVersion', 'freecadNamingCapabilitiesJson', 'freecadNamingEvidenceJson']
export const REQUIRED_FREECAD_NAMING_DEFINITIONS = ['__linux__=1', 'QT_NO_KEYWORDS', 'HAVE_CONFIG_H', 'PYCXX_6_2_COMPATIBILITY']

export const exists = async (path) => access(path).then(() => true).catch(() => false)

export const resolveFrom = (base, path) => isAbsolute(path) ? path : resolve(base, path)

export const sha256File = async (path) => new Promise((resolveHash, reject) => {
  const hash = createHash('sha256')
  const stream = createReadStream(path)
  stream.on('error', reject)
  stream.on('data', (chunk) => hash.update(chunk))
  stream.on('end', () => resolveHash(hash.digest('hex')))
})

const readAt = async (handle, length, position) => {
  const buffer = Buffer.alloc(length)
  const { bytesRead } = await handle.read(buffer, 0, length, position)
  return buffer.subarray(0, bytesRead)
}

const isWasmObject = (bytes) => bytes.length >= 8
  && bytes.subarray(0, 4).equals(Buffer.from([0x00, 0x61, 0x73, 0x6d]))
  && bytes.subarray(4, 8).equals(Buffer.from([0x01, 0x00, 0x00, 0x00]))
const isElfObject = (bytes) => bytes.length >= 4 && bytes.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46]))
const isLlvmBitcode = (bytes) => bytes.length >= 4
  && (bytes.subarray(0, 4).equals(Buffer.from([0x42, 0x43, 0xc0, 0xde]))
    || bytes.subarray(0, 4).equals(Buffer.from([0xde, 0xc0, 0x17, 0x0b])))

export async function inspectWasmStaticArchive(path) {
  const handle = await open(path, 'r')
  try {
    const stat = await handle.stat()
    if (stat.size < 68) throw new Error('archive is empty or truncated')
    const magic = await readAt(handle, 8, 0)
    if (magic.toString('ascii') !== '!<arch>\n') throw new Error('file is not a regular static ar archive')
    let offset = 8
    let memberCount = 0
    let wasmObjectCount = 0
    let llvmBitcodeCount = 0
    let hostObjectCount = 0
    let unsupportedMemberCount = 0
    while (offset + 60 <= stat.size) {
      const header = await readAt(handle, 60, offset)
      if (header.length !== 60 || header.subarray(58, 60).toString('ascii') !== '`\n') throw new Error(`invalid ar member header at byte ${offset}`)
      const rawName = header.subarray(0, 16).toString('ascii').trim()
      const sizeText = header.subarray(48, 58).toString('ascii').trim()
      if (!/^\d+$/.test(sizeText)) throw new Error(`invalid ar member size at byte ${offset}`)
      const memberSize = Number(sizeText)
      const dataOffset = offset + 60
      if (!Number.isSafeInteger(memberSize) || memberSize < 0 || dataOffset + memberSize > stat.size) throw new Error(`ar member exceeds archive size at byte ${offset}`)
      const special = rawName === '/' || rawName === '//' || rawName === '/SYM64/'
      if (!special) {
        memberCount += 1
        const extendedName = rawName.startsWith('#1/') ? Number(rawName.slice(3)) : 0
        if (!Number.isSafeInteger(extendedName) || extendedName < 0 || extendedName > memberSize) throw new Error(`invalid BSD ar filename at byte ${offset}`)
        const prefix = await readAt(handle, Math.min(8, memberSize - extendedName), dataOffset + extendedName)
        if (isWasmObject(prefix)) wasmObjectCount += 1
        else if (isLlvmBitcode(prefix)) llvmBitcodeCount += 1
        else if (isElfObject(prefix)) hostObjectCount += 1
        else unsupportedMemberCount += 1
      }
      offset = dataOffset + memberSize + (memberSize % 2)
    }
    if (memberCount === 0) throw new Error('archive has no object members')
    if (hostObjectCount > 0) throw new Error(`archive contains ${hostObjectCount} host ELF object member(s)`)
    if (unsupportedMemberCount > 0) throw new Error(`archive contains ${unsupportedMemberCount} unsupported member(s)`)
    if (wasmObjectCount + llvmBitcodeCount === 0) throw new Error('archive has no wasm or LLVM bitcode object members')
    return { target: 'wasm32-emscripten', bytes: stat.size, memberCount, wasmObjectCount, llvmBitcodeCount }
  } finally {
    await handle.close()
  }
}

export async function loadSdkPlan(projectRoot) {
  const path = resolve(projectRoot, 'config/freecad-naming-sdk-plan.json')
  return JSON.parse(await readFile(path, 'utf8'))
}

export async function inspectPlannedLibrary(projectRoot, library) {
  const path = resolveFrom(projectRoot, library.path)
  if (!await exists(path)) return { name: library.name, path: library.path, status: 'missing' }
  const archive = await inspectWasmStaticArchive(path)
  const sha256 = await sha256File(path)
  if (library.expectedSha256 && sha256 !== library.expectedSha256) throw new Error(`${library.name} hash mismatch: expected ${library.expectedSha256}, got ${sha256}`)
  return { name: library.name, path: library.path, status: 'verified', sha256, ...archive }
}

export async function inspectIncludeDirectory(projectRoot, entry) {
  const path = resolveFrom(projectRoot, entry.path)
  const missingHeaders = []
  for (const header of entry.requiredHeaders ?? []) if (!await exists(resolve(path, header))) missingHeaders.push(header)
  return { name: entry.name, path: entry.path, status: missingHeaders.length === 0 ? 'verified' : 'missing', missingHeaders }
}

export async function inspectPlannedRuntimeAsset(projectRoot, asset) {
  const root = resolveFrom(projectRoot, asset.path)
  const files = []
  const missingFiles = []
  for (const relativePath of asset.requiredFiles ?? []) {
    const path = resolve(root, relativePath)
    if (!await exists(path)) {
      missingFiles.push(relativePath)
      continue
    }
    const sha256 = await sha256File(path)
    const expectedSha256 = asset.expectedSha256?.[relativePath]
    if (expectedSha256 && sha256 !== expectedSha256) throw new Error(`${asset.name}/${relativePath} hash mismatch: expected ${expectedSha256}, got ${sha256}`)
    files.push({ path: relativePath, sha256 })
  }
  return {
    name: asset.name,
    path: asset.path,
    preloadTo: asset.preloadTo,
    status: missingFiles.length === 0 ? 'verified' : 'missing',
    files,
    missingFiles,
  }
}
