/* Browser and Node wrapper for the actual Go SDARC synthetic lifecycle.
 * Copyright (c) 2026 Neuroclusive. Permission is granted to download, inspect,
 * modify and run this wrapper for testing and review. Keep this notice with
 * copies. Other repository and protocol source licensing is unchanged. */

const RESULT_IDS = [
  "encrypted_processing",
  "single_grant_revocation",
  "sibling_survives_grant_revocation",
  "child_revocation",
  "parent_restore_same_child",
  "individual_revocation_survives_restore",
  "sibling_resumes_after_restore",
  "encrypted_data_replay_denied",
  "grant_projection_tamper_denied",
  "grant_context_tamper_denied",
  "history_proof_tamper_denied",
];
const RUNTIME_FILES = [
  "sdarc-lifecycle.wasm",
  "wasm_exec.js",
  "lifecycle-worker.mjs",
];
const MAX_BYTES = {
  "sdarc-lifecycle.wasm": 64 * 1024 * 1024,
  "wasm_exec.js": 128_000,
  "lifecycle-worker.mjs": 128_000,
};
const decoder = new TextDecoder("utf-8", { fatal: true });
const isNode =
  typeof process !== "undefined" && Boolean(process.versions?.node);

function ensure(condition, message) {
  if (!condition) throw new Error(message);
}
function object(value) {
  return value !== null && typeof value === "object" && !Array.isArray(value);
}
function boundedText(value, limit = 3000) {
  return typeof value === "string" && value.length > 0 && value.length <= limit;
}
function digestString(value) {
  return typeof value === "string" && /^[0-9a-f]{64}$/.test(value);
}
function knownFields(value, fields, label) {
  ensure(
    object(value) && Object.keys(value).every((key) => fields.includes(key)),
    `${label} contains unsupported fields`,
  );
}

const ROW_EVIDENCE = [
  [
    "grant_a_id",
    "grant_b_id",
    "warrant_a_id",
    "warrant_b_id",
    "warrant_a_hash",
    "warrant_b_hash",
    "derived_a",
    "derived_b",
  ],
  ["grant_id", "grandchild_hash", "denial_error"],
  ["grant_id"],
  ["old_child_handle_usable", "grant_a_denial_error", "grant_b_denial_error"],
  ["original_child_id", "restored_child_id", "old_child_handle_usable"],
  ["grant_id", "denial_error"],
  ["grant_id", "derived_result"],
  [
    "object_ids",
    "ciphertext_hashes",
    "valid_sibling_processed",
    "denial_error",
  ],
  ["grant_id", "denial_error"],
  ["grant_id", "denial_error"],
  ["grant_id", "denial_error"],
];

function validateDerived(value) {
  knownFields(
    value,
    [
      "schema_version",
      "category_id",
      "purpose",
      "evidence_band",
      "signal_mix",
      "preferences",
    ],
    "Derived result",
  );
  ensure(
    boundedText(value.schema_version, 200) &&
      ["cognitive_profile", "psychological_assessments"].includes(
        value.category_id,
      ) &&
      value.purpose === "category_summary",
    "Derived result scope is invalid",
  );
  if (value.preferences !== undefined) {
    knownFields(value.preferences, ["known_fields"], "Derived preferences");
    knownFields(
      value.preferences.known_fields,
      [
        "attention_patterns",
        "learning_style",
        "memory_preferences",
        "processing_speed",
      ],
      "Derived field presence",
    );
    ensure(
      Object.values(value.preferences.known_fields).every(
        (status) => status === "absent" || status === "present",
      ),
      "Derived field presence is invalid",
    );
  }
}

export function validateManifest(manifest) {
  ensure(
    object(manifest) &&
      manifest.schema === "sdarc-public-lifecycle-manifest/v1",
    "Unsupported SDARC lifecycle manifest",
  );
  ensure(
    manifest.scope === "Runs the SDARC engine with synthetic data." &&
      object(manifest.artifacts),
    "Lifecycle manifest scope or artifacts are invalid",
  );
  for (const file of RUNTIME_FILES) {
    const artifact = manifest.artifacts[file];
    ensure(
      object(artifact) &&
        digestString(artifact.sha256) &&
        Number.isSafeInteger(artifact.size_bytes) &&
        artifact.size_bytes > 0 &&
        artifact.size_bytes <= MAX_BYTES[file],
      `Invalid artifact declaration: ${file}`,
    );
  }
  return manifest;
}

export async function sha256Hex(bytes) {
  const subtle =
    globalThis.crypto?.subtle ||
    (isNode ? (await import("node:crypto")).webcrypto.subtle : undefined);
  ensure(subtle, "This browser does not provide the required SHA-256 API");
  const digest = await subtle.digest("SHA-256", bytes);
  return Array.from(new Uint8Array(digest), (value) =>
    value.toString(16).padStart(2, "0"),
  ).join("");
}

export async function verifyArtifact(bytes, artifact, name) {
  ensure(
    bytes instanceof Uint8Array && bytes.byteLength === artifact.size_bytes,
    `Artifact size differs from manifest: ${name}`,
  );
  ensure(
    (await sha256Hex(bytes)) === artifact.sha256,
    `Artifact SHA-256 differs from manifest: ${name}`,
  );
  return bytes;
}

export function validateLifecycleResult(result) {
  ensure(
    object(result) && result.schema === "sdarc-public-lifecycle/v1",
    "SDARC engine returned an unsupported result",
  );
  if (result.ok !== true) {
    const message = boundedText(result.error?.message, 1000)
      ? result.error.message
      : "SDARC engine did not complete its checks";
    throw new Error(message);
  }
  knownFields(
    result,
    ["schema", "ok", "results", "evidence", "scope"],
    "Engine result",
  );
  ensure(
    Array.isArray(result.results) &&
      result.results.length === RESULT_IDS.length,
    "SDARC engine returned an incomplete check sequence",
  );
  for (const [index, row] of result.results.entries()) {
    ensure(
      object(row) &&
        row.id === RESULT_IDS[index] &&
        row.outcome === "pass" &&
        boundedText(row.label, 200) &&
        boundedText(row.detail) &&
        object(row.evidence),
      `SDARC engine returned an invalid check at position ${index + 1}`,
    );
    knownFields(
      row,
      ["id", "label", "outcome", "detail", "evidence"],
      "Check result",
    );
    knownFields(row.evidence, ROW_EVIDENCE[index], "Check evidence");
    ensure(
      ROW_EVIDENCE[index].every((key) => Object.hasOwn(row.evidence, key)),
      "Check evidence is incomplete",
    );
    for (const key of ["derived_a", "derived_b", "derived_result"])
      if (row.evidence[key] !== undefined) validateDerived(row.evidence[key]);
  }
  const evidence = result.evidence;
  knownFields(
    evidence,
    [
      "owner_id",
      "child_id",
      "parent_lifecycle_public_key_hex",
      "child_history_public_key_hex",
      "mint_warrant_mode",
      "grant_a_id",
      "grant_b_id",
      "grandchild_a_hash",
      "grandchild_b_hash",
      "object_ids",
      "object_ciphertext_hashes",
      "history_sequence",
      "history_head",
      "checkpoint_sequence",
      "checkpoint_head",
    ],
    "Lifecycle evidence",
  );
  ensure(
    object(evidence) &&
      boundedText(evidence.owner_id, 200) &&
      digestString(evidence.child_id) &&
      digestString(evidence.parent_lifecycle_public_key_hex) &&
      digestString(evidence.child_history_public_key_hex) &&
      boundedText(evidence.grant_a_id, 200) &&
      boundedText(evidence.grant_b_id, 200) &&
      evidence.grant_a_id !== evidence.grant_b_id &&
      digestString(evidence.grandchild_a_hash) &&
      digestString(evidence.grandchild_b_hash) &&
      Array.isArray(evidence.object_ids) &&
      evidence.object_ids.length > 0 &&
      evidence.object_ids.every((id) => boundedText(id, 200)) &&
      Number.isSafeInteger(evidence.history_sequence) &&
      evidence.history_sequence > 0 &&
      digestString(evidence.history_head),
    "SDARC engine returned incomplete lifecycle evidence",
  );
  ensure(
    evidence.mint_warrant_mode === "required" &&
      Array.isArray(evidence.object_ciphertext_hashes) &&
      evidence.object_ciphertext_hashes.length === evidence.object_ids.length &&
      evidence.object_ciphertext_hashes.every(digestString) &&
      evidence.checkpoint_sequence === evidence.history_sequence &&
      evidence.checkpoint_head === evidence.history_head,
    "SDARC engine returned inconsistent checkpoint or encryption evidence",
  );
  ensure(
    Array.isArray(result.scope) &&
      result.scope.length > 0 &&
      result.scope.every((line) => boundedText(line)),
    "SDARC engine did not identify its check scope",
  );
  return result;
}

async function browserArtifact(file, manifest, baseUrl, signal) {
  const response = await fetch(new URL(file, new URL(baseUrl, location.href)), {
    cache: "no-store",
    signal,
  });
  ensure(response.ok, `Unable to load SDARC artifact: ${file}`);
  const bytes = new Uint8Array(await response.arrayBuffer());
  return verifyArtifact(bytes, manifest.artifacts[file], file);
}

async function nodeArtifact(file, manifest, directory) {
  const { readFile } = await import("node:fs/promises");
  const { resolve } = await import("node:path");
  const bytes = new Uint8Array(await readFile(resolve(directory, file)));
  return verifyArtifact(bytes, manifest.artifacts[file], file);
}

function runWorker(worker, wasm, { timeoutMs, signal }, browser) {
  return new Promise((resolve, reject) => {
    let finished = false;
    const finish = (error, result) => {
      if (finished) return;
      finished = true;
      clearTimeout(timer);
      signal?.removeEventListener("abort", abort);
      worker.terminate();
      if (error) reject(error);
      else resolve(result);
    };
    const abort = () => finish(new Error("SDARC check cancelled"));
    const timer = setTimeout(
      () =>
        finish(
          new Error("SDARC engine did not complete within the time limit"),
        ),
      timeoutMs,
    );
    const onMessage = (message) => {
      try {
        ensure(object(message), "SDARC worker returned an invalid message");
        if (message.type === "error")
          throw new Error(
            boundedText(message.error) ? message.error : "SDARC worker failed",
          );
        ensure(
          message.type === "result",
          "SDARC worker returned an unexpected message",
        );
        finish(null, validateLifecycleResult(message.result));
      } catch (error) {
        finish(error);
      }
    };
    if (browser) {
      worker.onmessage = (event) => onMessage(event.data);
      worker.onerror = (event) => {
        event.preventDefault();
        finish(new Error(event.message || "SDARC worker could not start"));
      };
    } else {
      worker.once("message", onMessage);
      worker.once("error", (error) => finish(error));
      worker.once("exit", () => {
        if (!finished)
          finish(new Error("SDARC worker exited before returning its results"));
      });
    }
    signal?.addEventListener("abort", abort, { once: true });
    if (signal?.aborted) {
      abort();
      return;
    }
    const buffer = wasm.buffer.slice(
      wasm.byteOffset,
      wasm.byteOffset + wasm.byteLength,
    );
    worker.postMessage({ wasmBytes: buffer }, [buffer]);
  });
}

/** Run freshly instantiated native Go code in a dedicated, disposable worker. */
export async function runLifecycle(manifest, options = {}) {
  validateManifest(manifest);
  const timeoutMs = options.timeoutMs ?? 90_000;
  ensure(
    Number.isSafeInteger(timeoutMs) && timeoutMs >= 100 && timeoutMs <= 180_000,
    "Invalid SDARC check timeout",
  );
  if (isNode) {
    const directory = options.directory ?? ".";
    const [wasm, runtime, workerSource] = await Promise.all(
      RUNTIME_FILES.map((file) => nodeArtifact(file, manifest, directory)),
    );
    const { Worker } = await import("node:worker_threads");
    const prefix =
      'const { webcrypto } = await import("node:crypto"); globalThis.crypto ??= webcrypto;\n';
    const source =
      prefix + decoder.decode(runtime) + "\n" + decoder.decode(workerSource);
    const url = new URL(
      `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`,
    );
    return runWorker(
      new Worker(url),
      wasm,
      { timeoutMs, signal: options.signal },
      false,
    );
  }
  ensure(
    typeof Worker === "function" && typeof WebAssembly === "object",
    "This browser does not support the SDARC engine runner",
  );
  const baseUrl = options.baseUrl ?? "/sdarc/check/";
  const [wasm, runtime, workerSource] = await Promise.all(
    RUNTIME_FILES.map((file) =>
      browserArtifact(file, manifest, baseUrl, options.signal),
    ),
  );
  // All code is hashed before execution. Combining the matching Go shim and
  // worker into one Blob needs only the existing worker-src blob: policy;
  // there is no dynamic runtime-script import or eval on the page.
  const url = URL.createObjectURL(
    new Blob([decoder.decode(runtime), "\n", decoder.decode(workerSource)], {
      type: "text/javascript",
    }),
  );
  try {
    return await runWorker(
      new Worker(url, { type: "module" }),
      wasm,
      { timeoutMs, signal: options.signal },
      true,
    );
  } finally {
    URL.revokeObjectURL(url);
  }
}

// Download lifecycle.mjs, manifest.json and its three runtime files together.
// CLI: node lifecycle.mjs manifest.json [trusted-manifest-sha256]
if (isNode && process.argv[1]) {
  const { pathToFileURL } = await import("node:url");
  const { realpath } = await import("node:fs/promises");
  // Node resolves the imported file's symlinks, including macOS /var aliases.
  // Resolve the launch path too so a downloaded CLI cannot silently do nothing.
  const entryPath = await realpath(process.argv[1]).catch(() => null);
  if (entryPath && import.meta.url === pathToFileURL(entryPath).href) {
    try {
      ensure(
        process.argv.length >= 3 && process.argv.length <= 4,
        "Usage: node lifecycle.mjs manifest.json [trusted-manifest-sha256]",
      );
      const { readFile } = await import("node:fs/promises");
      const { dirname, resolve } = await import("node:path");
      const manifestPath = resolve(process.argv[2]);
      const bytes = await readFile(manifestPath);
      ensure(bytes.length <= 128_000, "SDARC manifest is too large");
      if (process.argv[3]) {
        ensure(
          digestString(process.argv[3]),
          "Expected manifest SHA-256 is invalid",
        );
        ensure(
          (await sha256Hex(bytes)) === process.argv[3],
          "Manifest SHA-256 differs from the supplied trusted digest",
        );
      }
      const manifest = validateManifest(JSON.parse(decoder.decode(bytes)));
      const result = await runLifecycle(manifest, {
        directory: dirname(manifestPath),
      });
      process.stdout.write(JSON.stringify(result, null, 2) + "\n");
    } catch (error) {
      process.stderr.write(
        `Check failed: ${error instanceof Error ? error.message : String(error)}\n`,
      );
      process.exitCode = 1;
    }
  }
}
