#!/usr/bin/env node

import { createHash } from 'node:crypto'
import dns from 'node:dns'
import http from 'node:http'
import https from 'node:https'
import net from 'node:net'
import tls from 'node:tls'
import { lstatSync, readFileSync, readdirSync, realpathSync } from 'node:fs'
import { join, relative, resolve, sep } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { canonical, parseJsonStrict, sha256, verifyPacket } from './verify-packet.mjs'

const REPORT_PROFILE = 'glyph.retained-evidence-drill-report/0.1-experimental'
const JUDGMENT_PROFILE = 'glyph.retained-evidence-judgment/1'
const HASH_RE = /^sha256:[0-9a-f]{64}$/
let networkRequests = 0

function denyNetwork() {
  const blocked = () => {
    networkRequests += 1
    throw new Error('network access is denied by the retained-evidence launcher')
  }
  globalThis.fetch = blocked
  for (const [object, names] of [
    [http, ['get', 'request']],
    [https, ['get', 'request']],
    [net, ['connect', 'createConnection']],
    [tls, ['connect']],
    [dns, ['lookup', 'resolve', 'resolve4', 'resolve6', 'reverse']],
  ]) for (const name of names) object[name] = blocked
}

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

function readJson(path, label) {
  const raw = readFileSync(path)
  return { raw, value: parseJsonStrict(raw, label) }
}

function loadDirectory(root) {
  const rootReal = realpathSync(root)
  const rootStat = lstatSync(root)
  if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new Error('state dossier must be a real directory')
  const bundle = new Map()
  function visit(directory) {
    for (const name of readdirSync(directory).sort()) {
      const absolute = join(directory, name)
      const stat = lstatSync(absolute)
      if (stat.isSymbolicLink()) throw new Error('state dossier contains a symlink')
      const foundReal = realpathSync(absolute)
      if (foundReal !== rootReal && !foundReal.startsWith(`${rootReal}${sep}`)) throw new Error('state dossier escapes its root')
      if (stat.isDirectory()) visit(absolute)
      else if (stat.isFile()) bundle.set(relative(root, absolute).split(sep).join('/'), new Uint8Array(readFileSync(absolute)))
      else throw new Error('state dossier contains a non-regular member')
    }
  }
  visit(root)
  return bundle
}

function digestJson(value) {
  return sha256(Buffer.from(canonical(value)))
}

function plainObject(value) {
  return value !== null && typeof value === 'object' && !Array.isArray(value)
}

function compareValues(left, right, path, changed, unchanged, leftPresent = true, rightPresent = true) {
  if (!leftPresent || !rightPresent) {
    changed.push({
      path,
      before: leftPresent ? { present: true, value: left } : { present: false },
      after: rightPresent ? { present: true, value: right } : { present: false },
    })
    return
  }
  if (plainObject(left) && plainObject(right)) {
    const keys = [...new Set([...Object.keys(left), ...Object.keys(right)])].sort()
    for (const key of keys) compareValues(left[key], right[key], path ? `${path}.${key}` : key, changed, unchanged, Object.hasOwn(left, key), Object.hasOwn(right, key))
    return
  }
  if (canonical(left) === canonical(right)) unchanged.push(path)
  else changed.push({ path, before: left, after: right })
}

export function deriveDelta(from, to, newEvidence, propagationAuthority) {
  const changed = []
  const unchanged = []
  compareValues(from, to, '', changed, unchanged)
  return { from: from.state, to: to.state, new_evidence: newEvidence, changed, unchanged, propagation_authority: propagationAuthority }
}

function receiverRootDigest(publishedContext, correctedContext, faultContext) {
  if (canonical(publishedContext.accepted_issuers) !== canonical(correctedContext.accepted_issuers)) throw new Error('buyer and provider receiver roots changed across states')
  if (canonical(publishedContext.witness_context) !== canonical(correctedContext.witness_context) || canonical(publishedContext.witness_context) !== canonical(faultContext)) throw new Error('witness receiver roots changed across states')
  return digestJson({ accepted_issuers: publishedContext.accepted_issuers, witness_context: publishedContext.witness_context })
}

function commonJudgment(state, report, transactionDigest, receiverRoots, verificationContext, relianceContext, baseline) {
  return {
    profile: JUDGMENT_PROFILE,
    state,
    transaction_record_digest: transactionDigest,
    receiver_roots_digest: receiverRoots,
    verification_context_digest: digestJson(verificationContext),
    reliance_context_digest: relianceContext === null ? null : digestJson(relianceContext),
    buyer_policy: report.buyer_policy ?? baseline.buyer_policy,
    selected_receipt: report.selected_receipt ?? baseline.selected_receipt,
    receipt_inclusion: report.receipt_inclusion ?? baseline.receipt_inclusion,
    history_consistency: report.history_consistency,
    same_size_equivocation: report.same_size_equivocation,
    challenge: report.challenge,
    witness_remedy: report.witness_remedy,
    settlement_authorization: report.settlement_authorization,
    test_ledger_attempt: report.test_ledger_attempt,
  }
}

function assertReport(actual, expected, label) {
  if (canonical(actual) !== canonical(expected)) throw new Error(`${label} disagrees with its frozen expected report`)
  if (actual.exit_code !== 0 || actual.errors?.length !== 0) throw new Error(`${label} did not verify cleanly`)
}

export async function computeReport(packetRoot) {
  const verified = verifyPacket(packetRoot)
  denyNetwork()
  const answerabilityModule = await import(pathToFileURL(join(verified.root, 'verifiers/answerability-network/kernel.mjs')).href)
  const witnessModule = await import(pathToFileURL(join(verified.root, 'verifiers/witness-covenant/kernel.mjs')).href)

  const publishedContext = readJson(join(verified.root, 'states/published/context.json'), 'published context').value
  const faultContext = readJson(join(verified.root, 'states/fault-observed/context.json'), 'fault context').value
  const correctedContext = readJson(join(verified.root, 'states/corrected/context.json'), 'corrected context').value
  const receiverRoots = receiverRootDigest(publishedContext, correctedContext, faultContext)

  const published = await answerabilityModule.verifyAnswerabilityNetwork(loadDirectory(join(verified.root, 'states/published/dossier')), new Uint8Array(readFileSync(join(verified.root, 'states/published/context.json'))))
  const publishedExpected = readJson(join(verified.root, 'states/published/expected-report.json'), 'published expected report').value
  assertReport(published, publishedExpected, 'published report')

  const fault = await witnessModule.verifyWitnessCovenant(loadDirectory(join(verified.root, 'states/fault-observed/dossier')), new Uint8Array(readFileSync(join(verified.root, 'states/fault-observed/context.json'))))
  const faultExpected = readJson(join(verified.root, 'states/fault-observed/expected-report.json'), 'fault expected report').value
  assertReport(fault, faultExpected, 'fault-observed report')
  if (fault.same_size_equivocation !== 'ESTABLISHED' || fault.history_consistency !== 'EQUIVOCATION_ESTABLISHED') throw new Error('fault evidence does not establish the frozen same-size equivocation')

  const corrected = await answerabilityModule.verifyAnswerabilityNetwork(loadDirectory(join(verified.root, 'states/corrected/dossier')), new Uint8Array(readFileSync(join(verified.root, 'states/corrected/context.json'))))
  const correctedExpected = readJson(join(verified.root, 'states/corrected/expected-report.json'), 'corrected expected report').value
  assertReport(corrected, correctedExpected, 'corrected report')
  if (corrected.reliance === null) throw new Error('corrected report did not compute declared reliance')

  const transactionFiles = ['states/published/transaction-record.json', 'states/fault-observed/transaction-record.json', 'states/corrected/transaction-record.json']
  const transactionDigests = transactionFiles.map((path) => sha256(readFileSync(join(verified.root, path))))
  if (!transactionDigests.every((value) => value === transactionDigests[0])) throw new Error('transaction record changed across drill states')
  const receipt = readJson(join(verified.root, transactionFiles[0]), 'transaction record').value
  if (receipt.hashes?.attestation !== corrected.reliance.selected_receipt_attestation || !HASH_RE.test(receipt.hashes.attestation)) throw new Error('transaction record does not bind the corrected reliance report')

  const correctionLedger = readJson(join(verified.root, 'states/corrected/dossier/reliance/correction-ledger.json'), 'correction ledger').value
  exact(correctionLedger, ['profile', 'corrections'], 'correction ledger')
  if (correctionLedger.corrections.length !== 1) throw new Error('corrected state must carry exactly one correction')
  const correctionReceipt = correctionLedger.corrections[0]
  const correctionSubject = correctionReceipt.action?.subject
  exact(correctionSubject, ['authority_epoch', 'previous_correction', 'profile', 'reason_digest', 'replacement_digest', 'sequence', 'target_digest'], 'correction subject')
  for (const path of ['covenant-core.json', 'evidence/head-a.json', 'evidence/head-b.json']) {
    const faultBytes = readFileSync(join(verified.root, 'states/fault-observed/dossier', path))
    const correctedBytes = readFileSync(join(verified.root, 'states/corrected/dossier/witness-covenant', path))
    if (!faultBytes.equals(correctedBytes)) throw new Error(`fault-observed and corrected states disagree on ${path}`)
  }
  const faultHeadA = readJson(join(verified.root, 'states/fault-observed/dossier/evidence/head-a.json'), 'fault head A').value
  const faultHeadB = readJson(join(verified.root, 'states/fault-observed/dossier/evidence/head-b.json'), 'fault head B').value
  const faultCheckpoints = new Set([faultHeadA.checkpoint_hash, faultHeadB.checkpoint_hash])
  if (correctionSubject.target_digest !== corrected.reliance.selected_checkpoint || !faultCheckpoints.has(correctionSubject.target_digest)) throw new Error('accepted correction does not target an exact fault-observed checkpoint')

  const common = {
    transactionDigest: transactionDigests[0],
    receiverRoots,
  }
  const judgmentA = {
    ...commonJudgment('published', published, common.transactionDigest, common.receiverRoots, publishedContext, publishedContext.reliance_context, published),
    correction_authority: 'NOT_APPLICABLE',
    correction: null,
    reliance: 'NOT_COMPUTED',
  }
  const judgmentB = {
    ...commonJudgment('fault-observed', fault, common.transactionDigest, common.receiverRoots, faultContext, null, published),
    correction_authority: 'NOT_SUPPLIED',
    correction: null,
    reliance: 'NOT_COMPUTED',
  }
  const judgmentC = {
    ...commonJudgment('corrected', corrected, common.transactionDigest, common.receiverRoots, correctedContext, correctedContext.reliance_context, published),
    correction_authority: 'VERIFIED',
    correction: {
      issuer: correctionReceipt.authorization?.issuer,
      authority_epoch: correctionSubject.authority_epoch,
      target_digest: correctionSubject.target_digest,
      replacement_digest: correctionSubject.replacement_digest,
      attestation: correctionReceipt.hashes?.attestation,
    },
    reliance: corrected.reliance,
  }

  for (const judgment of [judgmentA, judgmentB, judgmentC]) exact(judgment, ['profile', 'state', 'transaction_record_digest', 'receiver_roots_digest', 'verification_context_digest', 'reliance_context_digest', 'buyer_policy', 'selected_receipt', 'receipt_inclusion', 'history_consistency', 'same_size_equivocation', 'challenge', 'witness_remedy', 'settlement_authorization', 'test_ledger_attempt', 'correction_authority', 'correction', 'reliance'], `${judgment.state} judgment`)
  const judgmentDigests = [judgmentA, judgmentB, judgmentC].map(digestJson)
  if (new Set(judgmentDigests).size !== 3) throw new Error('judgment digests did not change across all three states')

  const deltaAB = deriveDelta(judgmentA, judgmentB, [{ kind: 'conflicting_checkpoint_head', checkpoint_hash: faultHeadB.checkpoint_hash, operator: faultHeadB.operator }], 'ABSENT')
  const deltaBC = deriveDelta(judgmentB, judgmentC, [{ kind: 'accepted_correction_notice', attestation: correctionReceipt.hashes.attestation, issuer: correctionReceipt.authorization.issuer }], 'VERIFIED')
  if (!deltaAB.changed.some((entry) => entry.path === 'history_consistency') || deltaAB.changed.some((entry) => entry.path.startsWith('reliance.'))) throw new Error('fault-observed delta has the wrong jurisdiction')
  if (!deltaBC.changed.some((entry) => entry.path === 'correction_authority') || !deltaBC.changed.some((entry) => entry.path.startsWith('reliance'))) throw new Error('corrected delta does not derive propagation from the accepted correction')

  const summary = corrected.reliance.summary
  exact(summary, ['affected', 'declared_decisions', 'graph_edges', 'graph_nodes', 'not_affected', 'undetermined'], 'corrected reliance summary')
  const reviewControl = {
    review_all_control: summary.declared_decisions,
    required_review: summary.affected + summary.undetermined,
    declared_review_reduction: summary.not_affected,
    incomplete_lineage_cleared: 0,
  }
  if (canonical(reviewControl) !== canonical({ review_all_control: 10000, required_review: 5000, declared_review_reduction: 5000, incomplete_lineage_cleared: 0 })) throw new Error('review-all control no longer matches the frozen fixture')

  const report = {
    profile: REPORT_PROFILE,
    states: {
      published: { source_report: published, transaction_record_digest: common.transactionDigest, judgment: judgmentA, judgment_digest: judgmentDigests[0] },
      fault_observed: { source_report: fault, transaction_record_digest: common.transactionDigest, correction_authority: 'NOT_SUPPLIED', reliance: 'NOT_COMPUTED', judgment: judgmentB, judgment_digest: judgmentDigests[1] },
      corrected: { source_report: corrected, transaction_record_digest: common.transactionDigest, judgment: judgmentC, judgment_digest: judgmentDigests[2] },
    },
    transaction_record_invariant: {
      published_equals_fault_observed: transactionDigests[0] === transactionDigests[1],
      fault_observed_equals_corrected: transactionDigests[1] === transactionDigests[2],
    },
    judgment_deltas: [deltaAB, deltaBC],
    review_control: reviewControl,
    retained_execution: {
      historical_verification: 'REPRODUCED',
      input_scope: 'PACKET_MEMBERS_ONLY',
      network_policy: 'COMMON_NODE_INTERFACES_BLOCKED',
      network_request_attempts_observed: networkRequests,
      glyph_standard_routes: 'NOT_REQUESTED',
      repository_input: 'NOT_SUPPLIED',
      installed_bulla: 'NOT_USED',
      prior_receiver_state: 'NOT_SUPPLIED',
      freshness: 'NOT_ESTABLISHED',
      current_external_state: 'NOT_ESTABLISHED',
    },
    boundaries: [
      'fault evidence does not supply correction authority',
      'declared separation is not proof of organizational or causal independence',
      'the application-level network guard is not evidence of operating-system network isolation',
      'historical reproduction does not establish freshness or current external state',
      'local reproduction increments no external counter',
    ],
  }
  if (networkRequests !== 0) throw new Error('the drill attempted network access')
  return report
}

export async function main(argv = process.argv.slice(2)) {
  if (argv.length !== 1) throw new Error('usage: node run-network-denied.mjs PACKET_ROOT')
  const root = resolve(argv[0])
  const report = await computeReport(root)
  const expected = readJson(join(root, 'expected-report.json'), 'expected drill report').value
  if (canonical(report) !== canonical(expected)) throw new Error('computed drill report disagrees with expected-report.json')
  process.stdout.write(`${JSON.stringify(report, null, 2)}\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
  })
}
