#!/usr/bin/env node

import { createHash } from 'node:crypto'
import { lstatSync, readFileSync, readdirSync, realpathSync } from 'node:fs'
import { basename, join, relative, resolve, sep } from 'node:path'
import { fileURLToPath } from 'node:url'

export const PACKET_PROFILE = 'glyph.compute-contract-drill-packet/0.1-experimental'
const HASH_RE = /^sha256:[0-9a-f]{64}$/
const PATH_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/
const MAX_MEMBERS = 220
const MAX_MEMBER_BYTES = 8 * 1024 * 1024
const MAX_TOTAL_BYTES = 48 * 1024 * 1024
const MAX_DEPTH = 12

export function canonical(value) {
  if (value === null || typeof value !== 'object') return JSON.stringify(value)
  if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`
  const keys = Object.keys(value).sort((left, right) => left < right ? -1 : left > right ? 1 : 0)
  return `{${keys.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(',')}}`
}

export function sha256(raw) {
  return `sha256:${createHash('sha256').update(raw).digest('hex')}`
}

export function parseJsonStrict(raw, label = 'JSON input') {
  let text
  try { text = new TextDecoder('utf-8', { fatal: true }).decode(raw) } catch { throw new Error(`${label} is not UTF-8 JSON`) }
  let offset = 0
  const whitespace = () => { while (/\s/u.test(text[offset] ?? '')) offset += 1 }
  const string = () => {
    if (text[offset] !== '"') throw new Error(`${label} contains an invalid object member`)
    const start = offset
    offset += 1
    while (offset < text.length) {
      if (text[offset] === '"') {
        offset += 1
        try { return JSON.parse(text.slice(start, offset)) } catch { throw new Error(`${label} contains an invalid string`) }
      }
      if (text[offset] === '\\') {
        offset += 1
        if (text[offset] === 'u') offset += 4
      }
      offset += 1
    }
    throw new Error(`${label} contains an unterminated string`)
  }
  const value = () => {
    whitespace()
    if (text[offset] === '{') {
      offset += 1
      whitespace()
      const keys = new Set()
      if (text[offset] === '}') { offset += 1; return }
      while (offset < text.length) {
        const key = string()
        if (keys.has(key)) throw new Error(`${label} contains duplicate member ${JSON.stringify(key)}`)
        keys.add(key)
        whitespace()
        if (text[offset] !== ':') throw new Error(`${label} contains an invalid object member`)
        offset += 1
        value()
        whitespace()
        if (text[offset] === '}') { offset += 1; return }
        if (text[offset] !== ',') throw new Error(`${label} contains an invalid object separator`)
        offset += 1
        whitespace()
      }
      throw new Error(`${label} contains an unterminated object`)
    }
    if (text[offset] === '[') {
      offset += 1
      whitespace()
      if (text[offset] === ']') { offset += 1; return }
      while (offset < text.length) {
        value()
        whitespace()
        if (text[offset] === ']') { offset += 1; return }
        if (text[offset] !== ',') throw new Error(`${label} contains an invalid array separator`)
        offset += 1
      }
      throw new Error(`${label} contains an unterminated array`)
    }
    if (text[offset] === '"') { string(); return }
    const start = offset
    while (offset < text.length && !/[\s,\]}]/u.test(text[offset])) offset += 1
    if (offset === start) throw new Error(`${label} contains an invalid value`)
  }
  value()
  whitespace()
  if (offset !== text.length) throw new Error(`${label} contains trailing data`)
  try { return JSON.parse(text) } catch { throw new Error(`${label} is invalid JSON`) }
}

function exact(value, keys, label) {
  if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must be an object`)
  const actual = Object.keys(value).sort()
  const expected = [...keys].sort()
  if (JSON.stringify(actual) !== JSON.stringify(expected)) throw new Error(`${label} has the wrong fields`)
  return value
}

function safePath(path) {
  if (typeof path !== 'string' || path.includes('\\') || path.includes('..') || path.startsWith('/')) return false
  if (!PATH_RE.test(path)) return false
  const parts = path.split('/')
  return parts.length <= MAX_DEPTH && parts.every((part) => part && part !== '.' && part !== '..')
}

function walk(root) {
  const rootReal = realpathSync(root)
  const rootStat = lstatSync(root)
  if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new Error('packet root must be a real directory')
  const members = []
  let total = 0
  function visit(directory, depth) {
    if (depth > MAX_DEPTH) throw new Error('packet path depth exceeds the limit')
    for (const name of readdirSync(directory).sort()) {
      const absolute = join(directory, name)
      const stat = lstatSync(absolute)
      if (stat.isSymbolicLink()) throw new Error('packet contains a symlink')
      const foundReal = realpathSync(absolute)
      if (foundReal !== rootReal && !foundReal.startsWith(`${rootReal}${sep}`)) throw new Error('packet path escapes its root')
      if (stat.isDirectory()) visit(absolute, depth + 1)
      else if (stat.isFile()) {
        const path = relative(root, absolute).split(sep).join('/')
        if (path === 'manifest.json') continue
        if (!safePath(path)) throw new Error(`unsafe packet path: ${path}`)
        if (stat.size > MAX_MEMBER_BYTES) throw new Error(`packet member is oversized: ${path}`)
        total += stat.size
        if (total > MAX_TOTAL_BYTES) throw new Error('packet aggregate size exceeds the limit')
        members.push({ path, absolute, byte_length: stat.size })
        if (members.length > MAX_MEMBERS) throw new Error('packet member count exceeds the limit')
      } else throw new Error('packet contains a non-regular member')
    }
  }
  visit(root, 0)
  return members.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0)
}

export function verifyPacket(packetRoot) {
  const root = resolve(packetRoot)
  const manifestPath = join(root, 'manifest.json')
  const manifestStat = lstatSync(manifestPath)
  if (!manifestStat.isFile() || manifestStat.isSymbolicLink() || manifestStat.size > MAX_MEMBER_BYTES) throw new Error('packet manifest must be a bounded regular file')
  const manifestRaw = readFileSync(manifestPath)
  const manifest = exact(parseJsonStrict(manifestRaw, 'packet manifest'), ['profile', 'root', 'members'], 'packet manifest')
  if (manifest.profile !== PACKET_PROFILE || !HASH_RE.test(manifest.root) || !Array.isArray(manifest.members) || manifest.members.length > MAX_MEMBERS) throw new Error('unsupported packet manifest')
  const actual = walk(root).map((entry) => {
    const raw = readFileSync(entry.absolute)
    if (entry.path.endsWith('.json')) parseJsonStrict(raw, entry.path)
    return { path: entry.path, byte_length: entry.byte_length, sha256: sha256(raw) }
  })
  const expected = manifest.members.map((entry, index) => {
    exact(entry, ['path', 'byte_length', 'sha256'], `packet member ${index}`)
    if (!safePath(entry.path) || !Number.isSafeInteger(entry.byte_length) || entry.byte_length < 0 || entry.byte_length > MAX_MEMBER_BYTES || !HASH_RE.test(entry.sha256)) throw new Error(`invalid packet member ${index}`)
    if (index > 0 && manifest.members[index - 1].path >= entry.path) throw new Error('packet members are not strictly ASCII ordered')
    return entry
  })
  const exactPresentation = {
    profile: manifest.profile,
    root: manifest.root,
    members: expected.map((entry) => ({ path: entry.path, byte_length: entry.byte_length, sha256: entry.sha256 })),
  }
  if (!manifestRaw.equals(Buffer.from(`${JSON.stringify(exactPresentation, null, 2)}\n`))) throw new Error('packet manifest is not in its exact canonical presentation')
  if (canonical(actual) !== canonical(expected)) throw new Error('packet members do not match the manifest')
  const computedRoot = sha256(Buffer.from(canonical({ profile: manifest.profile, members: expected })))
  if (computedRoot !== manifest.root) throw new Error('packet root does not match its members')
  if (basename(root) !== computedRoot.slice(7)) throw new Error('packet directory is not named by its root')
  return { root, manifest, members: actual }
}

export async function main(argv = process.argv.slice(2)) {
  if (argv.length !== 1) throw new Error('usage: node verify-packet.mjs PACKET_ROOT')
  const verified = verifyPacket(argv[0])
  process.stdout.write(`${verified.manifest.root}\n`)
  return 0
}

if (process.argv[1] && realpathSync(fileURLToPath(import.meta.url)) === realpathSync(resolve(process.argv[1]))) {
  main().then((code) => { process.exitCode = code }).catch((error) => {
    process.stderr.write(`${error.message}\n`)
    process.exitCode = 1
  })
}
