#!/usr/bin/env node
import { createHash } from 'node:crypto'
import { spawnSync } from 'node:child_process'
import { cp, lstat, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'

const here = dirname(fileURLToPath(import.meta.url))
const platformRoot = resolve(here, '../..')
const workspaceRoot = resolve(platformRoot, '..')
const engineRoot = resolve(workspaceRoot, 'NODEDC_ENGINE_INFRA')
const artifactRoot = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || join(workspaceRoot, 'deploy-artifacts'))
const [patchId = '', ...extra] = process.argv.slice(2)
const previouslyIssuedPatchIds = new Set([
  'engine-data-product-publish-grant-20260716-001',
  'engine-data-product-publish-grant-20260717-001',
  'engine-data-product-publish-grant-20260717-002',
])
const compositeProviderCatalogTargetSha256 = '9c931f9abfcadb5b34a8a854c2efb8fd79913e000eecf0967d1b7c500bf9a56a'

if (extra.length || !/^engine-data-product-publish-grant-\d{8}-\d{3}$/.test(patchId)) {
  throw new Error('usage: build-engine-data-product-publish-grant-artifact.mjs <fresh-patch-id>')
}
if (previouslyIssuedPatchIds.has(patchId)) throw new Error('engine_publish_grant_patch_id_already_issued')

// Deliberately exclude frontend/dist, runtime data, tests, every credential
// value, and the already-installed credential-sink implementation. The sink
// remains mounted and byte-frozen as the predecessor domain; this artifact
// adds the exact Data Product Publish grant flow beside it.
const entries = Object.freeze([
  'nodedc-source/server/assets/provider-packages/v1/catalog.json',
  'nodedc-source/server/dataProductPublishGrant',
  'nodedc-source/server/engineAgents/store.js',
  'nodedc-source/server/routes/engineAgentGateway.js',
  'nodedc-source/server/routes/n8n.js',
  'nodedc-source/services/backend/data-product-publish-grant/docker-compose.immutable-runtime.yml',
])
const ignoredBasenames = new Set(['.DS_Store', '.git', 'node_modules'])
const credentialSinkPredecessorSha256 = Object.freeze({
  'docker-compose.yml': '258cebb64ff1943c939655cc55bdce00fc5c4dced67ec291d84d6df066ace50e',
  'nodedc-source/server/credentialPolicies/ndcPrivateNode.js': '723874a02dc7b8a68b22ff2304431cfe64f28933cedf5f1e6cda79b2e1cf704a',
  'nodedc-source/server/credentialSink/core.js': '9f0facc41fd398fcd955cffdd486abb126cdcd756c87ffde667fcbe00e2c41d3',
  'nodedc-source/server/credentialSink/requestAuth.js': 'f8c9237c3e6f4219dee0d4f956d6e97f76f5bd7ba8a6fd0b4fb21aceffa38138',
  'nodedc-source/server/credentialSink/store.js': 'c78dc285a973b6acd8a2330f0310935ad08720b2905454492f292d235abf12c0',
  'nodedc-source/server/credentialSink/vendor/engine-credential-sink.mjs': 'b4800eead9bf94793ff1280d06e34aad6b37ef055a9d7f8d83d7fb5f9bd66d8b',
  'nodedc-source/server/index.js': 'b2b790b02839570d967a2ca68b00e2724485a99389ac9b3a589a1b22302a36b8',
  'nodedc-source/server/routes/engineCredentialSink.js': '9cbb69dbc8cbe6181cd5b0170fe9c4d717b0a173866ca98cba3e3766c0bab94e',
  'nodedc-source/server/routes/ndcAgentMcp.js': '534e2c85e1faecc72a00da7ad32d0584ee09b6bd89eeec2221c3b23d80e1e962',
  'nodedc-source/services/backend/credential-sink/docker-compose.immutable-runtime.yml': '944fa64b08255eb8207b93fd327aebb98ecd9400d37d25fcfa8e3a040ee44afe',
})
const publishGrantRuntimeOverride = [
  'services:',
  '  nodedc-backend:',
  '    user: "0:0"',
  '    environment:',
  '      ENGINE_DATA_PLANE_BASE_URL: http://external-data-plane:18106',
  '      ENGINE_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE: /run/nodedc-secrets/engine-edp-managed-provisioner-private.pem',
  '      ENGINE_CONTROL_PLANE_PUBLISH_GRANT_ROOT: /var/lib/nodedc-control-plane/publish-grants',
  '    volumes:',
  '      - type: bind',
  '        source: /volume2/nodedc-demo/nodedc-control-plane/publish-grants',
  '        target: /var/lib/nodedc-control-plane/publish-grants',
  '        bind:',
  '          create_host_path: false',
  '      - type: bind',
  '        source: /volume1/docker/nodedc-platform/secrets/engine-edp-managed-provisioner/private-key.pem',
  '        target: /run/nodedc-secrets/engine-edp-managed-provisioner-private.pem',
  '        read_only: true',
  '        bind:',
  '          create_host_path: false',
  '',
].join('\n')

await assertSourceBoundary()
await mkdir(artifactRoot, { recursive: true })
const artifact = join(artifactRoot, `nodedc-${patchId}.tgz`)
await assertArtifactTargetFresh(artifact)
const stage = await mkdtemp(join(tmpdir(), 'nodedc-engine-publish-grant-artifact-'))
const payload = join(stage, 'payload')

try {
  await mkdir(payload, { recursive: true })
  for (const entry of entries) {
    await copySafe(resolve(engineRoot, entry), join(payload, entry))
  }
  await writeFile(
    join(stage, 'manifest.env'),
    `id=${patchId}\ncomponent=engine\ntype=app-overlay\n`,
    'utf8',
  )
  await writeFile(join(stage, 'files.txt'), `${entries.join('\n')}\n`, 'utf8')
  run('python3', ['-c', canonicalTarScript(), artifact, stage])
  const sha256 = digest(await readFile(artifact))
  console.log(JSON.stringify({
    ok: true,
    patchId,
    artifact,
    sha256,
    entries,
    excluded: [
      'docker-compose.yml',
      'nodedc-source/server/index.js',
      'nodedc-source/server/credentialPolicies/ndcPrivateNode.js',
      'nodedc-source/server/credentialSink',
      'nodedc-source/server/routes/engineCredentialSink.js',
      'nodedc-source/server/middleware/demoAccess.js',
      'nodedc-source/server/routes/ndcAgentMcp.js',
      'nodedc-source/server/data',
      'nodedc-source/dist',
      'nodedc-source/server/tests',
    ],
    providerPackage: 'gelios.provider.v4',
    providerRequests: [
      'https://api.geliospro.com/api/v1/users/me/monitoring-config',
      'https://api.geliospro.com/api/v1/units?incltrip=true',
    ],
    dataProductId: 'fleet.positions.current.v3',
    credentialValues: 'preserved',
    preservedPredecessor: Object.keys(credentialSinkPredecessorSha256),
  }, null, 2))
} finally {
  await rm(stage, { recursive: true, force: true })
}

async function assertSourceBoundary() {
  // These files are deliberately excluded from the artifact. Their exact
  // installed predecessor is verified by the root-owned runner during plan
  // and apply; the local builder only proves that it cannot package a link or
  // another unsafe filesystem object in their place.
  for (const relativePath of Object.keys(credentialSinkPredecessorSha256)) {
    const info = await lstat(join(engineRoot, relativePath))
    if (!info.isFile() || info.isSymbolicLink()) {
      throw new Error(`engine_credential_sink_predecessor_unsafe:${relativePath}`)
    }
  }
  const runtimeOverridePath = join(
    engineRoot,
    'nodedc-source/services/backend/data-product-publish-grant/docker-compose.immutable-runtime.yml',
  )
  if (await readFile(runtimeOverridePath, 'utf8') !== publishGrantRuntimeOverride) {
    throw new Error('engine_publish_grant_runtime_override_mismatch')
  }
  const catalogPath = join(
    engineRoot,
    'nodedc-source/server/assets/provider-packages/v1/catalog.json',
  )
  if (digest(await readFile(catalogPath)) !== compositeProviderCatalogTargetSha256) {
    throw new Error('engine_composite_provider_catalog_target_mismatch')
  }

  const indexSource = await readFile(join(engineRoot, 'nodedc-source/server/index.js'), 'utf8')
  if (!indexSource.includes("app.use('/api/engine-agent-mcp', engineAgentMcpRouter)")) {
    throw new Error('engine_agent_mcp_mount_missing')
  }
  if (
    !indexSource.includes("import engineCredentialSinkRouter from './routes/engineCredentialSink.js'")
    || !indexSource.includes("app.use('/internal/engine-credential-sink', express.json({")
  ) throw new Error('engine_credential_sink_predecessor_mount_missing')

  const gateway = await readFile(join(engineRoot, 'nodedc-source/server/routes/engineAgentGateway.js'), 'utf8')
  for (const tool of [
    'engine_plan_data_product_publish_grant',
    'engine_apply_data_product_publish_grant',
    'engine_accept_data_product_publish_grant',
    'engine_rollback_data_product_publish_grant',
  ]) {
    if (!gateway.includes(tool)) throw new Error(`engine_publish_grant_tool_missing:${tool}`)
  }

  const n8nRoute = await readFile(join(engineRoot, 'nodedc-source/server/routes/n8n.js'), 'utf8')
  if (n8nRoute.includes("from '../credentialSink/")) {
    throw new Error('engine_publish_grant_depends_on_legacy_sink')
  }
  if (!n8nRoute.includes('engineDataProductPublishGrantN8nAdapter')) {
    throw new Error('engine_publish_grant_native_adapter_missing')
  }
  if (!n8nRoute.includes('engineCredentialSinkN8nAdapter')) {
    throw new Error('engine_credential_sink_native_adapter_missing')
  }

  const grantDirectory = join(engineRoot, 'nodedc-source/server/dataProductPublishGrant')
  const grantFiles = (await readdir(grantDirectory, { withFileTypes: true }))
    .filter((entry) => entry.isFile())
    .map((entry) => entry.name)
    .sort()
  const expectedGrantFiles = [
    'acceptance.js',
    'providerCatalog.js',
    'service.js',
    'signedDataPlaneClient.js',
    'store.js',
  ]
  if (JSON.stringify(grantFiles) !== JSON.stringify(expectedGrantFiles)) {
    throw new Error('engine_publish_grant_source_set_mismatch')
  }
  const providerCatalogSource = await readFile(join(grantDirectory, 'providerCatalog.js'), 'utf8')
  const grantServiceSource = await readFile(join(grantDirectory, 'service.js'), 'utf8')
  const grantStoreSource = await readFile(join(grantDirectory, 'store.js'), 'utf8')
  for (const marker of [
    'function capabilityRequests(capability)',
    'function exactHttpRequestUrl(n8n)',
    'capabilityIds',
    'providerRequestNodeIds',
    'providerCredentialRefs.size !== 1',
  ]) {
    if (!providerCatalogSource.includes(marker)) {
      throw new Error(`engine_composite_provider_resolver_missing:${marker}`)
    }
  }
  if (
    !grantServiceSource.includes('capabilities: descriptor.capabilityIds')
    || !grantServiceSource.includes('providerRequestNodeIds: descriptor.providerRequestNodeIds')
  ) {
    throw new Error('engine_composite_provider_plan_projection_missing')
  }
  if (
    !grantStoreSource.includes('Array.isArray(raw.capabilityIds)')
    || !grantStoreSource.includes('Array.isArray(raw.providerRequestNodeIds)')
  ) {
    throw new Error('engine_composite_provider_store_projection_missing')
  }
  for (const entry of entries) {
    const source = resolve(engineRoot, entry)
    const info = await lstat(source)
    if (info.isSymbolicLink() || (!info.isFile() && !info.isDirectory())) {
      throw new Error(`engine_artifact_source_unsafe:${entry}`)
    }
  }
  for (const source of [
    ...expectedGrantFiles.map((name) => join(grantDirectory, name)),
    join(engineRoot, 'nodedc-source/server/routes/engineAgentGateway.js'),
    join(engineRoot, 'nodedc-source/server/routes/n8n.js'),
    join(engineRoot, 'nodedc-source/server/routes/ndcAgentMcp.js'),
    join(engineRoot, 'nodedc-source/server/engineAgents/store.js'),
  ]) run('node', ['--check', source])
}

async function assertArtifactTargetFresh(target) {
  try {
    await lstat(target)
  } catch (error) {
    if (error?.code === 'ENOENT') return
    throw error
  }
  throw new Error('engine_publish_grant_artifact_already_exists')
}

async function copySafe(source, destination) {
  const info = await lstat(source)
  if (info.isSymbolicLink()) throw new Error(`source_symlink_rejected:${source}`)
  if (info.isFile()) {
    await mkdir(dirname(destination), { recursive: true })
    await cp(source, destination, { force: false, verbatimSymlinks: true })
    return
  }
  if (!info.isDirectory()) throw new Error(`source_type_rejected:${source}`)
  await mkdir(destination, { recursive: true })
  for (const entry of await readdir(source, { withFileTypes: true })) {
    if (ignoredBasenames.has(entry.name) || entry.name.startsWith('.env')) continue
    const childSource = join(source, entry.name)
    if (entry.isSymbolicLink()) {
      throw new Error(`source_symlink_rejected:${relative(engineRoot, childSource)}`)
    }
    await copySafe(childSource, join(destination, entry.name))
  }
}

function canonicalTarScript() {
  return [
    'import gzip, io, pathlib, sys, tarfile',
    'root=pathlib.Path(sys.argv[2])',
    "with open(sys.argv[1], 'wb') as out:",
    "  with gzip.GzipFile(filename='', mode='wb', fileobj=out, compresslevel=9, mtime=0) as gz:",
    "    with tarfile.open(fileobj=gz, mode='w', format=tarfile.PAX_FORMAT) as tar:",
    "      for top in ('manifest.env','files.txt','payload'):",
    "        p=root/top; paths=[p] + (sorted(p.rglob('*')) if p.is_dir() else [])",
    '        for x in paths:',
    "          info=tar.gettarinfo(str(x), arcname=x.relative_to(root).as_posix())",
    "          info.uid=info.gid=0; info.uname=info.gname='root'; info.mtime=0; info.mode=0o755 if info.isdir() else 0o644",
    "          with (open(x,'rb') if info.isfile() else io.BytesIO()) as src: tar.addfile(info, src if info.isfile() else None)",
  ].join('\n')
}

function run(command, args) {
  const result = spawnSync(command, args, {
    encoding: 'utf8',
    stdio: ['ignore', 'pipe', 'pipe'],
    maxBuffer: 128 * 1024 * 1024,
  })
  if (result.status !== 0) throw new Error(`${command}_failed:${result.stderr || result.stdout}`)
  return result
}

function digest(bytes) {
  return createHash('sha256').update(bytes).digest('hex')
}
