// Offline upgrade contract: real bus verbs, pinned proxy, no real credentials.
import fs from 'node:fs';
import path from 'node:path';
import net from 'node:net';
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { createHash } from 'node:crypto';
import { getPriority } from 'node:os';
import { initialConfig } from '../../../../packages/model-proxy/src/settings.mjs';
import { PIN } from '../../../../packages/model-proxy/src/common.mjs';
import { ManagementClient } from '../../../../packages/model-proxy/src/client.mjs';
import { ProxyPage } from '../../../../packages/bus/src/proxy-page.mjs';
import { Broker } from '../../../../packages/bus/src/broker.mjs';
import { Store } from '../../../../packages/bus/src/store.mjs';
const [root, binary] = process.argv.slice(2);
if (process.env.P4_UPGRADE_ISOLATED !== 'yes' || !root?.startsWith('/mnt/storage/scratch/tmp/')) throw Error('wrapper required');
if (createHash('sha256').update(fs.readFileSync(binary)).digest('hex') !== PIN.binary.sha256) throw Error('binary does not match PIN');
const reserve = net.createServer(); await new Promise(r => reserve.listen(0, '127.0.0.1', r));
const port = reserve.address().port; await new Promise(r => reserve.close(r));
if (port === 8317) throw Error('forbidden port');
const management = 'p4-management', canary = 'p4-upstream';
const config = initialConfig({ dataRoot: root, port, key: management, sentinel: 'msk-fixture-sentinel-x' });
config.management['disable-control-panel'] = true; config.observability.logs['logging-to-file'] = false;
fs.mkdirSync(path.join(root, 'proxy/auth'), { recursive: true, mode: 0o700 });
fs.mkdirSync(path.join(root, 'home'));
fs.writeFileSync(path.join(root, 'config.yaml'), JSON.stringify(config), { mode: 0o600 });
const child = spawn(binary, ['-config', path.join(root, 'config.yaml'), '-local-model'], {
  cwd: root, env: { HOME: path.join(root, 'home'), PATH: '/usr/bin:/bin' }, stdio: 'ignore',
});
// Diagnostic-only (sage-4432): observe the same run conditions, never change
// priority or print exception values, fixture credentials or upstream content.
let stage = 'binary-start', lastClientErrorCode = null;
const clientErrorCodes = ['proxy-client-down', 'proxy-client-response', 'proxy-client-refused',
  'proxy-client-redirect', 'proxy-client-invalid'];
const errorCodes = [...clientErrorCodes, 'ERR_ASSERTION', 'ERR_SQLITE_ERROR', 'SQLITE_ERROR',
  'proxy-down', 'not-configured', 'human-required', 'invalid-request', 'invalid-business',
  'priority-invalid', 'priority-unconfirmed', 'priority-write-refused', 'login-state-unknown'];
function schedulingPolicy(pid) {
  try {
    // Only our Node process or the still-live child we spawned. Never print
    // raw /proc content; retain just Linux's numeric scheduling-policy field.
    const policy = fs.readFileSync(`/proc/${pid}/sched`, 'utf8').match(/^policy\s*:\s*(\d+)\s*$/m)?.[1];
    return policy === undefined ? null : Number(policy);
  } catch { return null; }
}
function observe() {
  let nodePriority = null, childPriority = null, childSchedulingPolicy = null;
  try { nodePriority = getPriority(); } catch {}
  if (Number.isInteger(child.pid) && child.exitCode === null && child.signalCode === null) {
    try { childPriority = getPriority(child.pid); } catch {}
    childSchedulingPolicy = schedulingPolicy(child.pid);
  }
  console.log('P4 diagnostic ' + JSON.stringify({ stage, nodePriority, childPriority, lastClientErrorCode,
    nodeSchedulingPolicy: schedulingPolicy(process.pid), childSchedulingPolicy,
    childExitCode: Number.isInteger(child.exitCode) ? child.exitCode : null,
    childSignal: /^SIG[A-Z0-9]{1,16}$/.test(child.signalCode ?? '') ? child.signalCode : null }));
}
child.once('exit', observe);
let store;
const pause = ms => new Promise(r => setTimeout(r, ms));
const receipts = [], answers = [];
const client = new ManagementClient({ baseUrl: `http://127.0.0.1:${port}`, key: management, receipt: r => receipts.push(r) });
try {
  console.log('P4 offline upgrade version=' + PIN.version + ' nice=' + getPriority());
  observe();
  let ready = false;
  for (let n = 0; n < 100; n++) {
    try { await client.getCredentials(); ready = true; break; }
    catch (error) { lastClientErrorCode = clientErrorCodes.includes(error.code) ? error.code : 'unclassified'; await pause(50); }
  }
  assert.equal(ready, true, 'fixture listener starts');
  stage = 'management-ready'; observe();
  stage = 'store-open';
  store = new Store(root);
  observe();
  const page = new ProxyPage({ client, instanceTag: 'fixture' });
  const broker = new Broker({ store, proxyPage: (...args) => page.request(...args), businesses: {
    demo: { id: 'demo', human: 'owner', roles: { coder: { authority: { withinRole: [], crossRole: [] } } }, arbiters: { technical: 'coder', delivery: 'coder' } },
  } });
  const cap = broker.bindHuman({ business: 'demo', human: 'owner', via: 'cli', outsideAgent: true });
  stage = 'broker-bound'; observe();
  const call = async (verb, args = {}) => { const answer = await broker.request(cap, { verb, args }); answers.push(answer); return answer; };
  stage = 'initial-accounts'; observe();
  // Diagnostic-only (sage-4448/4452, mos-1683): one direct empty-provider
  // read, types/counts only. Never print field names, values or error text.
  const receiptCount = receipts.length;
  try {
    const groups = await client.getUpstreamGroups();
    const isArray = Array.isArray(groups);
    console.log('P4 empty-provider diagnostic ' + JSON.stringify({
      httpStatus: receipts.length === receiptCount + 1 ? receipts.at(-1).status : null,
      type: groups === null ? 'null' : typeof groups, isArray,
      count: isArray ? groups.length : groups !== null && typeof groups === 'object' ? Object.keys(groups).length : 0,
    }));
  } catch (error) {
    console.log('P4 empty-provider diagnostic ' + JSON.stringify({
      httpStatus: receipts.length === receiptCount + 1 ? receipts.at(-1).status : null,
      code: clientErrorCodes.includes(error.code) ? error.code : 'proxy-client-error',
    }));
  }
  assert.deepEqual((await call('proxy.accounts')).accounts, []);
  console.log('proxy.accounts initial empty; GET /credentials + GET /config/api-keys/claude');
  for (const provider of ['claude', 'codex']) {
    const login = await call('proxy.login.start', { provider });
    assert.equal(typeof login.url, 'string'); assert.equal(typeof login.state, 'string');
    console.log('proxy.login.start ' + provider + ' GET /oauth/auth-url; fields=url,state only');
    const redirect = provider === 'claude' ? 'http://localhost:54545/callback' : 'http://localhost:1455/auth/callback';
    const submitted = await call('proxy.login.submit', { redirect_url: redirect + '?state=' + encodeURIComponent(login.state) + '&code=p4-fake-code' });
    assert.notEqual(submitted.status, 'ok');
    const status = await call('proxy.login.status', { state: login.state });
    assert.notEqual(status.status, 'ok');
    console.log('proxy.login.submit POST /oauth/callback; fake code never ok; GET /oauth/status=' + status.status);
    if (status.status === 'wait') {
      assert.equal((await call('proxy.login.cancel', { state: login.state })).status, 'cancelled');
      console.log('proxy.login.cancel DELETE /oauth/session confirmed');
    }
  }
  await call('proxy.apikey.add', { provider: 'z.ai', key: canary }); await pause(200);
  const accounts = (await call('proxy.accounts')).accounts;
  assert.equal(accounts.length, 1); assert.equal(accounts[0].provider, 'z.ai');
  assert.deepEqual(Object.keys(accounts[0]).sort(), ['accountId', 'displayName', 'priority', 'provider', 'status']);
  console.log('proxy.apikey.add PUT /config/api-keys/claude; account fields=accountId,provider,displayName,status,priority; id from auth_index');
  await call('proxy.priority.set', { accountId: accounts[0].accountId, priority: 7 });
  assert.equal((await call('proxy.accounts')).accounts[0].priority, 7);
  console.log('proxy.priority.set API-key PUT /config/api-keys/claude; priority read-back=7');
  const keys = await call('proxy.keys');
  assert.equal(keys.keys.length, 1); assert.equal(keys.keys[0].management, 'sentinel'); assert.match(keys.keys[0].keyId, /^[a-f0-9]{12}$/);
  console.log('proxy.keys GET /config/access/api-keys; fields=keyId,management,agent,issuedAt,state; sentinel labelled; values absent');
  // Fixture-only upload exercises the OAuth priority API relied on by the page.
  const uploaded = await fetch(`http://127.0.0.1:${port}/v8/management/credentials?name=p4-fixture.json`, {
    method: 'POST', headers: { Authorization: `Bearer ${management}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ type: 'claude', access_token: 'p4-access', refresh_token: 'p4-refresh', expired: '2099-01-01T00:00:00Z' }),
    signal: AbortSignal.timeout(5000), redirect: 'error',
  });
  assert.equal(uploaded.status, 200); await uploaded.body?.cancel(); await pause(200);
  const oauth = (await call('proxy.accounts')).accounts.find(a => a.provider === 'claude');
  assert.ok(oauth);
  await call('proxy.priority.set', { accountId: oauth.accountId, priority: 8 });
  assert.equal((await call('proxy.accounts')).accounts.find(a => a.accountId === oauth.accountId).priority, 8);
  console.log('proxy.priority.set OAuth PATCH /credentials/fields body={name,priority}; priority read-back=8');
  for (const value of [canary, management, 'p4-access', 'p4-refresh', 'p4-fake-code']) {
    assert.equal(JSON.stringify(receipts).includes(value), false, 'secret absent from receipts');
    assert.equal(JSON.stringify(store.all('SELECT body FROM events')).includes(value), false, 'secret absent from bus events');
    assert.equal(JSON.stringify(answers).includes(value), false, 'credential/code absent from bus replies');
  }
  console.log('PASS offline native-page bus contract and secret-free receipts');
} catch (error) {
  // Never print assertion actual/expected values or a raw upstream exception.
  observe();
  const errorClass = ['Error', 'AssertionError', 'ProxyError', 'BusError', 'TypeError', 'RangeError'].includes(error.name) ? error.name : 'Error';
  console.error('P4 upgrade FAILED ' + JSON.stringify({ stage, errorClass,
    code: errorCodes.includes(error.code) ? error.code : 'acceptance-failed' }));
  process.exitCode = 1;
} finally {
  store?.close(); child.kill('SIGTERM');
  await Promise.race([new Promise(r => child.once('exit', r)), pause(3000).then(() => child.kill('SIGKILL'))]);
}
