/*
 * SDARC synthetic history check, not a general SDARC or production verifier.
 * Supports only the published v2 Child-signed registration/create/revoke fixture.
 * History signatures authenticate bytes. A separately trusted latest checkpoint
 * supplies freshness; this file does not authenticate production HMAC receipts.
 * No keys, grants, ciphertext or personal data are submitted to a service.
 *
 * Copyright (c) 2026 Neuroclusive. Permission is granted to download, inspect,
 * modify and run this verifier for testing and review. Keep this notice with
 * copies. This permission applies only to this file; repository and protocol
 * package licensing is unchanged. This demonstration is provided as-is.
 */

const CATEGORIES = [
  "cognitive_profile",
  "psychological_assessments",
  "neurodiverse_characteristics",
  "communication_style",
  "productivity_patterns",
  "learning_preferences",
  "accessibility_needs",
  "work_style",
  "work_pattern",
  "neuroprint_index",
];
const EVENT_FIELDS = [
  "event_id",
  "sequence",
  "previous_event_hash",
  "event_type",
  "canonical_context_hash",
  "event_schema_version",
  "semantic_context",
  "grandchild_hash_optional",
  "category_generation_optional",
  "occurred_at",
  "authorisation_proof",
];
const GRANT_FIELDS = [
  "protocol_version",
  "context_schema_version",
  "owner_id",
  "child_id",
  "actor_id",
  "recipient_id",
  "recipient_type",
  "category_id",
  "purpose",
  "operations",
  "permissions",
  "object_id",
  "object_version",
  "session_id",
  "idempotency_commitment",
  "payload_commitment",
  "retention_class",
  "warrant_id",
  "warrant_hash",
  "grant_id",
  "grant_generation",
  "category_generation",
  "issued_at",
  "expires_at",
  "fresh_random_value",
  "issuance_sequence",
  "preceding_history_hash",
  "algorithm_profile",
];
const encoder = new TextEncoder();

function requireCheck(condition, message) {
  if (!condition) throw new Error(message);
}

function exactFields(value, names, label) {
  requireCheck(
    value !== null && typeof value === "object" && !Array.isArray(value),
    `${label} must be an object`,
  );
  const actual = Object.keys(value).sort();
  const expected = [...names].sort();
  requireCheck(
    actual.length === expected.length &&
      actual.every((name, index) => name === expected[index]),
    `${label} has unsupported or missing fields`,
  );
}

function hexBytes(value, length, label) {
  requireCheck(
    typeof value === "string" &&
      new RegExp(`^[0-9a-f]{${length * 2}}$`).test(value),
    `${label} must be ${length}-byte lowercase hex`,
  );
  return Uint8Array.from(value.match(/../g), (part) =>
    Number.parseInt(part, 16),
  );
}

function identifier(value, label) {
  requireCheck(
    typeof value === "string" && value.length > 0 && value.length <= 200,
    `${label} must be a bounded identifier`,
  );
}

function timestamp(value, label) {
  requireCheck(
    typeof value === "string" &&
      /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ$/.test(value),
    `${label} must be canonical UTC seconds`,
  );
  const millis = Date.parse(value);
  requireCheck(
    Number.isFinite(millis) &&
      new Date(millis).toISOString() === value.replace("Z", ".000Z"),
    `${label} is invalid`,
  );
  return millis;
}

function wellFormedUnicode(value) {
  // Avoid requiring String.isWellFormed on browsers that otherwise have the
  // cryptographic APIs needed to run this check.
  for (let index = 0; index < value.length; index++) {
    const code = value.charCodeAt(index);
    if (code >= 0xd800 && code <= 0xdbff) {
      const next = value.charCodeAt(++index);
      if (!(next >= 0xdc00 && next <= 0xdfff)) return false;
    } else if (code >= 0xdc00 && code <= 0xdfff) return false;
  }
  return true;
}

// This restricted canonical JSON profile matches Go's committed history bytes:
// UTF-16 key ordering, minimal JSON escaping, and safe integer numbers only.
export function canonicalJson(value, depth = 0) {
  requireCheck(depth <= 32, "canonical JSON nesting is too deep");
  if (value === null) return "null";
  if (typeof value === "boolean") return value ? "true" : "false";
  if (typeof value === "number") {
    requireCheck(
      Number.isSafeInteger(value),
      "canonical numbers must be safe integers",
    );
    return Object.is(value, -0) ? "0" : String(value);
  }
  if (typeof value === "string") {
    requireCheck(
      wellFormedUnicode(value),
      "canonical strings must be well-formed Unicode",
    );
    return JSON.stringify(value);
  }
  if (Array.isArray(value))
    return `[${value.map((item) => canonicalJson(item, depth + 1)).join(",")}]`;
  requireCheck(
    value !== undefined && typeof value === "object",
    "unsupported canonical JSON value",
  );
  return `{${Object.keys(value)
    .sort()
    .map(
      (key) =>
        `${canonicalJson(key, depth + 1)}:${canonicalJson(value[key], depth + 1)}`,
    )
    .join(",")}}`;
}

async function subtleCrypto() {
  if (globalThis.crypto?.subtle) return globalThis.crypto.subtle;
  // The browser never imports node:crypto. The CLI needs only built-in Node APIs.
  if (typeof process !== "undefined" && process.versions?.node) {
    return (await import("node:crypto")).webcrypto.subtle;
  }
  throw new Error("This browser does not support the required WebCrypto API");
}

export async function sha256Hex(bytes) {
  const digest = await (await subtleCrypto()).digest("SHA-256", bytes);
  return Array.from(new Uint8Array(digest), (value) =>
    value.toString(16).padStart(2, "0"),
  ).join("");
}

async function hashJson(value) {
  return sha256Hex(encoder.encode(canonicalJson(value)));
}

function verifyRegistration(context, keys) {
  exactFields(
    context,
    [
      "protocol_version",
      "owner_id",
      "child_id",
      "parent_lifecycle_public_key_hex",
      "child_history_public_key_hex",
      "recovery_confirmed",
      "categories",
    ],
    "registration context",
  );
  requireCheck(
    context.protocol_version === "sdarc/v1" &&
      context.owner_id === keys.owner_id,
    "registration owner or protocol mismatch",
  );
  hexBytes(context.child_id, 32, "registration Child identity");
  requireCheck(
    context.child_history_public_key_hex ===
      keys.child_history_public_key_hex &&
      context.parent_lifecycle_public_key_hex ===
        keys.parent_lifecycle_public_key_hex,
    "registration differs from supplied trusted public keys",
  );
  requireCheck(
    context.recovery_confirmed === true,
    "unsupported registration recovery state",
  );
  requireCheck(
    Array.isArray(context.categories) &&
      context.categories.length === CATEGORIES.length,
    "registration must contain all ten categories",
  );
  context.categories.forEach((category, index) => {
    exactFields(
      category,
      ["category_id", "generation", "key_ref_commitment"],
      "category commitment",
    );
    requireCheck(
      category.category_id === CATEGORIES[index] && category.generation === 1,
      "registration category order or generation mismatch",
    );
    hexBytes(category.key_ref_commitment, 32, "category commitment");
  });
  return context.child_id;
}

function verifyGrantCreation(context, fields, keys, childID) {
  exactFields(context, GRANT_FIELDS, "grant context");
  requireCheck(
    context.protocol_version === "sdarc/v1" &&
      context.context_schema_version === "sdarc-grant-context/v3" &&
      context.algorithm_profile === "sdarc-v1",
    "unsupported grant protocol",
  );
  requireCheck(
    context.owner_id === keys.owner_id &&
      context.child_id === childID &&
      context.grant_id === keys.grant_id,
    "grant identity differs from registration or target",
  );
  requireCheck(
    context.issuance_sequence === fields.sequence &&
      context.preceding_history_hash === fields.previous_event_hash,
    "grant issuance is not bound to this history position",
  );
  requireCheck(
    CATEGORIES.includes(context.category_id) &&
      context.category_generation === 1 &&
      fields.category_generation_optional === 1 &&
      context.grant_generation === 1,
    "unsupported grant category or generation",
  );
  requireCheck(
    fields.grandchild_hash_optional === keys.grandchild_hash,
    "grant creation targets a different Grandchild",
  );
  identifier(context.actor_id, "grant actor");
  identifier(context.recipient_id, "grant recipient");
  requireCheck(
    context.actor_id === keys.owner_id && context.recipient_type === "service",
    "unsupported fixture actor or recipient type",
  );
  requireCheck(
    context.purpose === "category_summary" &&
      canonicalJson(context.operations) === '["process"]' &&
      canonicalJson(context.permissions) === '["read_derived"]',
    "unsupported fixture grant command",
  );
  // This narrow fixture intentionally contains no D20 warrant, object operation,
  // encryption or production admission proof. Never infer those properties here.
  for (const name of [
    "object_id",
    "session_id",
    "idempotency_commitment",
    "payload_commitment",
    "retention_class",
    "warrant_id",
    "warrant_hash",
  ]) {
    requireCheck(context[name] === "", `unsupported fixture ${name}`);
  }
  requireCheck(
    context.object_version === 0,
    "unsupported fixture object version",
  );
  hexBytes(context.fresh_random_value, 16, "grant fresh randomness");
  const issued = timestamp(context.issued_at, "grant issue time");
  const expires = timestamp(context.expires_at, "grant expiry time");
  requireCheck(
    expires > issued &&
      expires - issued <= 600_000 &&
      context.issued_at === fields.occurred_at,
    "grant lifetime or occurrence binding mismatch",
  );
}

/** Verify only the two- or three-event public fixture history, not live access. */
export async function verifySnapshot(snapshot, keys) {
  exactFields(snapshot, ["sequence", "head", "events"], "snapshot");
  identifier(keys.owner_id, "trusted owner");
  identifier(keys.grant_id, "target grant");
  hexBytes(keys.grandchild_hash, 32, "target Grandchild hash");
  const childPublic = hexBytes(
    keys.child_history_public_key_hex,
    32,
    "trusted Child-history public key",
  );
  hexBytes(
    keys.parent_lifecycle_public_key_hex,
    32,
    "trusted Parent lifecycle public key",
  );
  hexBytes(snapshot.head, 32, "snapshot head");
  requireCheck(
    [2, 3].includes(snapshot.sequence) &&
      Array.isArray(snapshot.events) &&
      snapshot.events.length === snapshot.sequence,
    "unsupported snapshot event count",
  );
  const subtle = await subtleCrypto();
  const publicKey = await subtle.importKey(
    "raw",
    childPublic,
    "Ed25519",
    false,
    ["verify"],
  );
  let previous = "00".repeat(32);
  let previousTime = 0;
  let childID;
  let effective;
  const expectedTypes = [
    "child_created",
    "grandchild_created",
    "grandchild_revoked",
  ];
  for (const [index, event] of snapshot.events.entries()) {
    exactFields(
      event,
      ["fields", "event_hash", "authorisation_type"],
      "history event",
    );
    exactFields(event.fields, EVENT_FIELDS, "signed event fields");
    const fields = event.fields;
    requireCheck(
      event.authorisation_type === "child_history_signature" &&
        fields.event_schema_version === "sdarc-history-v2",
      "unsupported history authorization or schema",
    );
    requireCheck(
      fields.sequence === index + 1 &&
        fields.previous_event_hash === previous &&
        fields.event_type === expectedTypes[index],
      "history sequence, previous hash or event type mismatch",
    );
    requireCheck(
      fields.event_id === `evt-${fields.sequence}-${fields.event_type}`,
      "event identifier mismatch",
    );
    const time = timestamp(fields.occurred_at, "event occurrence time");
    requireCheck(time >= previousTime, "history time moves backwards");
    requireCheck(
      fields.canonical_context_hash ===
        (await hashJson(fields.semantic_context)),
      "signed semantic context hash mismatch",
    );
    const signature = hexBytes(
      fields.authorisation_proof,
      64,
      "Child history signature",
    );
    const preHash = await hashJson({
      ...fields,
      authorisation_proof: "pending",
    });
    requireCheck(
      await subtle.verify(
        "Ed25519",
        publicKey,
        signature,
        hexBytes(preHash, 32, "event prehash"),
      ),
      "Child history signature is invalid",
    );
    requireCheck(
      event.event_hash === (await hashJson(fields)),
      "history event hash mismatch",
    );
    if (index === 0) {
      requireCheck(
        fields.grandchild_hash_optional === null &&
          fields.category_generation_optional === null,
        "registration has unexpected grant bindings",
      );
      childID = verifyRegistration(fields.semantic_context, keys);
    } else if (index === 1) {
      verifyGrantCreation(fields.semantic_context, fields, keys, childID);
      effective = {
        owner_id: keys.owner_id,
        child_id: childID,
        grant_id: keys.grant_id,
        grandchild_hash: keys.grandchild_hash,
        grant_state: "ACTIVE",
        grant_context_hash: fields.canonical_context_hash,
        category_generation: 1,
        issuance_sequence: fields.sequence,
      };
    } else {
      const revoke = fields.semantic_context;
      exactFields(
        revoke,
        [
          "protocol_version",
          "owner_id",
          "grant_id",
          "grandchild_hash",
          "grant_context_hash",
          "issuance_sequence",
        ],
        "grant revocation context",
      );
      requireCheck(
        revoke.protocol_version === "sdarc/v1" &&
          revoke.owner_id === keys.owner_id &&
          revoke.grant_id === effective.grant_id &&
          revoke.grandchild_hash === effective.grandchild_hash &&
          revoke.grant_context_hash === effective.grant_context_hash &&
          revoke.issuance_sequence === effective.issuance_sequence &&
          fields.grandchild_hash_optional === effective.grandchild_hash &&
          fields.category_generation_optional === null,
        "revocation is not bound to the created grant",
      );
      effective.grant_state = "REVOKED";
    }
    previous = event.event_hash;
    previousTime = time;
  }
  requireCheck(
    snapshot.head === previous,
    "snapshot head differs from verified history",
  );
  return effective;
}

/** Compare against a checkpoint trusted separately from the restored snapshot. */
export function verifyCheckpoint(snapshot, checkpoint, ownerID) {
  exactFields(
    checkpoint,
    ["owner_id", "sequence", "head"],
    "trusted checkpoint",
  );
  hexBytes(checkpoint.head, 32, "checkpoint head");
  requireCheck(
    Number.isSafeInteger(checkpoint.sequence) &&
      checkpoint.sequence > 0 &&
      checkpoint.owner_id === ownerID,
    "checkpoint owner or sequence mismatch",
  );
  requireCheck(
    checkpoint.sequence === snapshot.sequence &&
      checkpoint.head === snapshot.head,
    "snapshot differs from the retained trusted checkpoint",
  );
}

/** Compare the limited mutable projection with the signed effective state. */
export function verifyGrantProjection(projection, effective) {
  exactFields(
    projection,
    [
      "grant_id",
      "grandchild_hash",
      "state",
      "context_hash",
      "category_generation",
      "issuance_sequence",
    ],
    "grant projection",
  );
  requireCheck(
    projection.grant_id === effective.grant_id &&
      projection.grandchild_hash === effective.grandchild_hash &&
      projection.state === effective.grant_state &&
      projection.context_hash === effective.grant_context_hash &&
      projection.category_generation === effective.category_generation &&
      projection.issuance_sequence === effective.issuance_sequence,
    "grant projection differs from verified signed history",
  );
}

function projectionOf(effective) {
  return {
    grant_id: effective.grant_id,
    grandchild_hash: effective.grandchild_hash,
    state: effective.grant_state,
    context_hash: effective.grant_context_hash,
    category_generation: effective.category_generation,
    issuance_sequence: effective.issuance_sequence,
  };
}

async function expectDenied(action, message) {
  let denied = false;
  try {
    await action();
  } catch {
    denied = true;
  }
  requireCheck(denied, message);
}

export async function runCheck(fixture) {
  exactFields(
    fixture,
    [
      "schema",
      "owner_id",
      "child_history_public_key_hex",
      "parent_lifecycle_public_key_hex",
      "grant_id",
      "grandchild_hash",
      "snapshots",
      "trusted_checkpoint",
    ],
    "public fixture",
  );
  requireCheck(
    fixture.schema === "sdarc-public-check/v1",
    "unsupported public fixture schema",
  );
  exactFields(
    fixture.snapshots,
    ["before_revocation", "after_revocation"],
    "fixture snapshots",
  );
  const before = fixture.snapshots.before_revocation;
  const after = fixture.snapshots.after_revocation;
  requireCheck(
    before.sequence === 2 && after.sequence === 3,
    "fixture requires one creation and one revocation",
  );
  requireCheck(
    canonicalJson(before.events) === canonicalJson(after.events.slice(0, 2)),
    "current history does not extend the old signed prefix",
  );
  const oldState = await verifySnapshot(before, fixture);
  const currentState = await verifySnapshot(after, fixture);
  requireCheck(
    oldState.grant_state === "ACTIVE" && currentState.grant_state === "REVOKED",
    "fixture grant states do not match the scenario",
  );
  verifyCheckpoint(after, fixture.trusted_checkpoint, fixture.owner_id);
  verifyGrantProjection(projectionOf(currentState), currentState);

  const tampered = structuredClone(after);
  tampered.events[1].fields.semantic_context.recipient_id = "changed-recipient";
  tampered.events[1].fields.canonical_context_hash = await hashJson(
    tampered.events[1].fields.semantic_context,
  );
  tampered.events[1].event_hash = await hashJson(tampered.events[1].fields);
  await expectDenied(
    () => verifySnapshot(tampered, fixture),
    "changed signed command was accepted",
  );

  const badSignature = structuredClone(after);
  const proof = badSignature.events[2].fields.authorisation_proof;
  badSignature.events[2].fields.authorisation_proof =
    (proof[0] === "0" ? "1" : "0") + proof.slice(1);
  await expectDenied(
    () => verifySnapshot(badSignature, fixture),
    "changed signature was accepted",
  );
  await expectDenied(
    () =>
      verifyGrantProjection(
        { ...projectionOf(currentState), state: "ACTIVE" },
        currentState,
      ),
    "mutable projection revived a revoked grant",
  );
  await expectDenied(
    () =>
      verifyCheckpoint(before, fixture.trusted_checkpoint, fixture.owner_id),
    "old signed snapshot matched a newer checkpoint",
  );
  const fork = {
    ...fixture.trusted_checkpoint,
    head: (after.head[0] === "0" ? "1" : "0") + after.head.slice(1),
  };
  await expectDenied(
    () => verifyCheckpoint(after, fork, fixture.owner_id),
    "same-sequence different head was accepted",
  );

  return [
    {
      id: "signed-history",
      label: "Signed history verified",
      outcome: "pass",
      detail:
        "Ed25519 signatures, canonical event hashes, order, registration keys and exact grant revocation bindings verify.",
    },
    {
      id: "context-tamper",
      label: "Changed grant command rejected",
      outcome: "pass",
      detail:
        "Changing the recipient and recomputing ordinary hashes cannot preserve the original Child signature.",
    },
    {
      id: "signature-tamper",
      label: "Changed signature rejected",
      outcome: "pass",
      detail:
        "An altered Child history signature fails cryptographic verification.",
    },
    {
      id: "projection-revival",
      label: "Database row cannot undo signed revocation",
      outcome: "pass",
      detail:
        "An active grant projection conflicts with the revoked state reduced from verified history.",
    },
    {
      id: "older-snapshot",
      label: "Old snapshot rejected by retained checkpoint",
      outcome: "pass",
      detail:
        "The old history remains correctly signed, but its sequence and head differ from the retained newer trusted checkpoint.",
    },
    {
      id: "same-sequence-fork",
      label: "Same sequence with a different head rejected",
      outcome: "pass",
      detail:
        "Matching the checkpoint sequence is insufficient when the history head differs.",
    },
  ];
}

// CLI: node verifier.mjs fixture.json [expected-fixture-sha256]
if (
  typeof process !== "undefined" &&
  process.versions?.node &&
  process.argv[1]
) {
  const { pathToFileURL } = await import("node:url");
  const { realpath } = await import("node:fs/promises");
  // Match Node's canonical module URL when the CLI path contains a symlink.
  const entryPath = await realpath(process.argv[1]).catch(() => null);
  if (entryPath && import.meta.url === pathToFileURL(entryPath).href) {
    try {
      requireCheck(
        process.argv.length >= 3 && process.argv.length <= 4,
        "Usage: node verifier.mjs fixture.json [expected-fixture-sha256]",
      );
      const { readFile } = await import("node:fs/promises");
      const bytes = await readFile(process.argv[2]);
      requireCheck(bytes.length <= 256_000, "fixture is too large");
      if (process.argv[3]) {
        hexBytes(process.argv[3], 32, "expected fixture SHA-256");
        requireCheck(
          (await sha256Hex(bytes)) === process.argv[3],
          "fixture SHA-256 differs from the supplied trusted digest",
        );
      }
      const results = await runCheck(
        JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)),
      );
      for (const result of results)
        process.stdout.write(
          `${result.outcome.toUpperCase()}: ${result.label}\n${result.detail}\n\n`,
        );
    } catch (error) {
      process.stderr.write(
        `Check failed: ${error instanceof Error ? error.message : String(error)}\n`,
      );
      process.exitCode = 1;
    }
  }
}
