// Sachin RAG permissions, part 6. Node.js 20+. Fictional fixtures; fixed answers.
// @ts-check
/** Fictional teaching fixtures. This simulation is not an authorization boundary.
 * @typedef {{id: string, name: string, role: string, tenantId: string, groups: readonly string[]}} Principal
 * @typedef {{id: string, title: string, team: string, tenantId: string, allowedGroups: readonly string[], allowedUsers: readonly string[], match: number, fact: string}} Evidence
 */

/** @type {readonly Principal[]} */
export const PEOPLE = [
  { id: "maya", name: "Maya", role: "Support", tenantId: "sachin", groups: ["support"] },
  { id: "arun", name: "Arun", role: "Engineering", tenantId: "sachin", groups: ["engineering"] },
];

export const QUESTION = "Why is the Orion launch delayed?";

/** @type {readonly Evidence[]} */
export const DOCUMENTS = [
  {
    id: "D1", title: "Customer launch update", team: "Shared", tenantId: "sachin",
    allowedGroups: ["support", "engineering"], allowedUsers: [], match: 87,
    fact: "Orion’s launch has moved to 14 October while the team completes reliability checks.",
  },
  {
    id: "D2", title: "Engineering incident note", team: "Engineering", tenantId: "sachin",
    allowedGroups: ["engineering"], allowedUsers: [], match: 98,
    fact: "Sign-in requests time out under peak load. The identity team is fixing the session refresh path.",
  },
  {
    id: "D3", title: "Customer support FAQ", team: "Support", tenantId: "sachin",
    allowedGroups: ["support"], allowedUsers: [], match: 82,
    fact: "Existing workspaces stay available. Customers do not need to take any action.",
  },
  {
    id: "D4", title: "Launch cost review", team: "Finance", tenantId: "sachin",
    allowedGroups: ["finance"], allowedUsers: [], match: 91,
    fact: "The delay adds $24,000 to the internal launch budget.",
  },
];

// @example:policy
/** Identity must already come from a trusted session; this function only checks permission.
 * @param {Principal | null} user @param {Evidence} chunk */
export function checkAccess(user, chunk) {
  const hasIdentity = user !== null;
  const sameCompany = hasIdentity && user.tenantId === chunk.tenantId;
  const directGrant = hasIdentity && chunk.allowedUsers.includes(user.id);
  const matchingGroups = user?.groups.filter(group => chunk.allowedGroups.includes(group)) ?? [];
  const groupGrant = matchingGroups.length > 0;
  const allowed = sameCompany && (directGrant || groupGrant);
  const reason = !hasIdentity ? "No verified identity"
    : !sameCompany ? "Different company"
    : directGrant ? "Direct user grant"
    : groupGrant ? `${matchingGroups.join(", ")} group grants access`
    : "No matching permission";

  return { hasIdentity, sameCompany, directGrant, groupGrant, matchingGroups, allowed, reason };
}
// @example:end-policy

/** @param {Principal | null} user @param {readonly Evidence[]} documents */
export function retrieveEvidence(user, documents = DOCUMENTS) {
  return documents
    .filter(chunk => checkAccess(user, chunk).allowed)
    .sort((a, b) => b.match - a.match);
}

/** @param {Principal | null} user @param {readonly Evidence[]} documents */
export function buildAnswer(user, documents = DOCUMENTS) {
  const evidence = retrieveEvidence(user, documents);
  const context = evidence.map(chunk => `[${chunk.id}] ${chunk.title}\n${chunk.fact}`).join("\n\n");
  // These are example request messages. This lesson makes no model call.
  const messages = evidence.length ? [
    { role: "system", content: "Use the supplied evidence to answer the question. Cite each source ID. If the evidence is insufficient, say so." },
    { role: "user", content: `Question: ${QUESTION}\n\nPermitted context:\n${context}` },
  ] : [];

  return {
    evidence,
    context,
    messages,
    // Assemble fixed facts to make the experiment reproducible, without a model.
    sentences: evidence.map(({ id, fact }) => ({ sourceId: id, text: fact })),
    emptyMessage: evidence.length ? null : "I don’t have accessible evidence to answer this question.",
  };
}


/** Fictional, deterministic teaching models shared by the diagrams and downloads.
 * @typedef {import('./rag-permissions.mjs').Principal} Principal
 * @typedef {import('./rag-permissions.mjs').Evidence} Evidence
 */

export function hasPolicy(record) {
  const ids = value => Array.isArray(value) && value.every(id => typeof id === "string" && id.length > 0);
  return typeof record?.tenantId === "string" && record.tenantId.length > 0
    && ids(record.allowedGroups) && ids(record.allowedUsers);
}

export function ingestDocument(document, paragraphs, aclVersion = 1) {
  if (!hasPolicy(document)) throw new Error("Missing or invalid permission metadata");
  if (!Number.isInteger(aclVersion) || aclVersion < 1) throw new Error("Invalid policy version");
  if (!Array.isArray(paragraphs) || paragraphs.some(text => typeof text !== "string" || !text.trim())) {
    throw new Error("Chunks must contain nonempty text");
  }
  return paragraphs.map((fact, index) => ({
    ...document, id: `${document.id}.${index + 1}`, parentId: document.id,
    fact, aclVersion, allowedGroups: [...document.allowedGroups], allowedUsers: [...document.allowedUsers],
  }));
}

// Ten fictional passages extend the same four documents. Scores are illustrative, not vector distances.
/** @type {Array<[string, number, number, string]>} */
const rankingRows = [
  ["D2", 1, 98, "Sign-in requests time out under peak load."],
  ["D4", 1, 96, "The delay adds $24,000 to the internal launch budget."],
  ["D1", 1, 94, "Orion’s launch has moved to 14 October."],
  ["D2", 2, 92, "The identity team is fixing the session refresh path."],
  ["D3", 1, 90, "Existing workspaces stay available."],
  ["D1", 2, 88, "The team is completing reliability checks."],
  ["D3", 2, 86, "Customers do not need to take any action."],
  ["D1", 3, 84, "The launch update will be revised after the next readiness review."],
  ["D4", 2, 82, "Finance will reconcile launch costs after release."],
  ["D3", 3, 80, "Support will share the next approved launch update."],
];
export const RANKED_CHUNKS = rankingRows.map(([parentId, ordinal, match, fact]) => {
  const parent = DOCUMENTS.find(document => document.id === parentId);
  if (!parent) throw new Error("Unknown parent document");
  return { ...parent, id: `${parentId}.${ordinal}`, parentId, match, fact, aclVersion: 1 };
});

/** Exact ranked-list experiment; it does not simulate an ANN graph. */
export function compareRetrieval(user = PEOPLE[0], k = 5, fetchCount = 5) {
  const ranked = [...RANKED_CHUNKS].sort((a, b) => b.match - a.match);
  const isAllowed = chunk => hasPolicy(chunk) && checkAccess(user, chunk).allowed;
  const eligible = ranked.filter(isAllowed);
  const ideal = eligible.slice(0, k);
  const fetched = ranked.slice(0, fetchCount);
  const after = fetched.filter(isAllowed).slice(0, k);
  const recovered = after.filter(chunk => ideal.some(target => target.id === chunk.id)).length;
  return { ranked, eligible, ideal, fetched, after, missed: ideal.filter(chunk => !after.some(hit => hit.id === chunk.id)),
    recall: ideal.length ? recovered / ideal.length : null };
}

/** Each stage is a new snapshot; moving backwards never mutates the fixtures. */
export function revocationSnapshot(stage = 0, enforceCurrent = true) {
  const original = DOCUMENTS[1];
  const source = { ...original, allowedGroups: stage >= 1 ? [] : [...original.allowedGroups], aclVersion: stage >= 1 ? 2 : 1 };
  const indexed = stage >= 2 ? { ...source } : { ...original, aclVersion: 1 };
  const indexAllows = checkAccess(PEOPLE[1], indexed).allowed;
  const currentAllows = checkAccess(PEOPLE[1], source).allowed;
  const oldCacheExists = stage < 3;
  const cacheVersion = 1;
  const cacheUsable = oldCacheExists && cacheVersion === source.aclVersion && currentAllows;
  return { source, indexed, indexAllows, currentAllows, oldCacheExists, cacheUsable,
    contextIncludesD2: indexAllows && (!enforceCurrent || currentAllows),
    cacheVersion, policyVersion: source.aclVersion };
}

/** Source IDs are looked up and authorized again on every source request. */
export function readSource(user, id, documents = DOCUMENTS) {
  const document = documents.find(item => item.id === id && item.tenantId === user?.tenantId);
  if (!document || !hasPolicy(document) || !checkAccess(user, document).allowed) {
    return { status: 404, body: { message: "Source unavailable" } };
  }
  return { status: 200, body: { id: document.id, title: document.title, text: document.fact } };
}

export function inspectSurface(personId = "maya", surface = "citation", enforce = true) {
  const person = PEOPLE.find(user => user.id === personId) ?? PEOPLE[0];
  const permitted = buildAnswer(person);
  const restricted = DOCUMENTS[1];
  const safe = surface === "citation" ? readSource(person, "D2")
    : surface === "preview" ? { status: 200, body: permitted.evidence.map(({ id, title }) => ({ id, title })) }
    : surface === "shared-answer" ? { status: 200, body: permitted.sentences }
    : { status: 200, body: { requestId: "demo-1", outcome: "allowed", returnedIds: permitted.evidence.map(chunk => chunk.id) } };
  const unsafe = surface === "citation" ? { status: 200, body: { id: restricted.id, title: restricted.title, text: restricted.fact } }
    : surface === "preview" ? { status: 200, body: DOCUMENTS.map(({ id, title }) => ({ id, title })) }
    : surface === "shared-answer" ? { status: 200, body: buildAnswer(PEOPLE[1]).sentences }
    : { status: 200, body: { question: QUESTION, candidates: DOCUMENTS, sessionToken: "fictional-demo-token" } };
  return { person, response: enforce ? safe : unsafe, protected: enforce };
}

export function createServiceState() {
  return {
    // Public exercise credentials, not secrets or a real login system.
    sessions: { "support-demo": structuredClone(PEOPLE[0]), "engineering-demo": structuredClone(PEOPLE[1]),
      "other-tenant-demo": { ...structuredClone(PEOPLE[0]), tenantId: "other" } },
    documents: structuredClone(DOCUMENTS), policyVersion: 1, policyAvailable: true,
    cache: new Map(), audit: [],
  };
}

export function revokeD2(state) {
  const document = state.documents.find(item => item.id === "D2");
  document.allowedGroups = [];
  document.allowedUsers = [];
  state.policyVersion += 1;
}

export function prepareServiceRequest(state, token, body = {}) {
  const user = Object.hasOwn(state.sessions, token) ? structuredClone(state.sessions[token]) : null;
  const base = { user, sessionKey: token, evidence: [], version: state.policyVersion, key: "", query: "", trace: ["Resolve identity from the server’s session table"] };
  if (!user) return { ...base, error: 401, message: "Sign in required" };
  if (typeof body?.question !== "string" || body.question.trim() !== QUESTION) {
    return { ...base, error: 400, message: "This exercise supports the Orion launch question only" };
  }
  if (!state.policyAvailable) return { ...base, error: 503, message: "Permission check unavailable" };
  if (state.documents.some(document => !hasPolicy(document))) {
    return { ...base, error: 503, message: "Permission metadata unavailable" };
  }
  // Body-supplied groups and tenantId never participate in the decision.
  const evidence = buildAnswer(user, state.documents).evidence;
  const key = JSON.stringify(["fixed-generator-v1", state.policyVersion, user.tenantId, user.id, [...user.groups].sort(), QUESTION]);
  return { ...base, error: 0, message: "", evidence, query: QUESTION, key,
    trace: [...base.trace, "Ignore client permission claims", "Check tenant and current document grants", "Prepare permitted evidence"] };
}

export function finishServiceRequest(state, plan) {
  const reply = (status, body, cacheHit = false) => {
    // This audit record stays on the server. No token, prompt, title, or document body.
    state.audit.push({ requestId: `request-${state.audit.length + 1}`, status,
      policyVersion: state.policyVersion, returnedIds: status === 200 ? (body.sources ?? []).map(source => source.id) : [] });
    return { status, body, cacheHit, trace: plan.trace };
  };
  if (plan.error) return reply(plan.error, { message: plan.message });
  if (!state.policyAvailable) return reply(503, { message: "Permission check unavailable" });
  if (plan.version !== state.policyVersion) return reply(409, { message: "Permissions changed; retry the request" });
  // Recheck current membership too. The version check covers policy changes through revokeD2.
  const current = Object.hasOwn(state.sessions, plan.sessionKey) ? state.sessions[plan.sessionKey] : null;
  if (!current || current.id !== plan.user.id || current.tenantId !== plan.user.tenantId
    || JSON.stringify([...current.groups].sort()) !== JSON.stringify([...plan.user.groups].sort())
    || plan.evidence.some(chunk => {
    const source = state.documents.find(document => document.id === chunk.id);
    return !source || !hasPolicy(source) || !checkAccess(current, source).allowed;
  })) return reply(409, { message: "Permissions changed; retry the request" });
  if (!plan.evidence.length) return reply(200, { answer: "I don’t have accessible evidence to answer this question.", sources: [], modelCalled: false });
  const cached = state.cache.get(plan.key);
  // Compare dependencies too: a missed revision update must not restore an excluded source.
  const selectedIds = plan.evidence.map(chunk => chunk.id);
  if (cached && JSON.stringify(cached.sources.map(source => source.id)) === JSON.stringify(selectedIds)) {
    return reply(200, structuredClone(cached), true);
  }
  // Deterministic generation seam: a real model integration needs separate output evaluation.
  const response = { answer: plan.evidence.map(chunk => `${chunk.fact} [${chunk.id}]`).join("\n\n"),
    sources: plan.evidence.map(({ id, title }) => ({ id, title })), modelCalled: false };
  state.cache.set(plan.key, structuredClone(response));
  return reply(200, response);
}

export function runServiceScenario(scenario = "maya") {
  const state = createServiceState();
  let token = scenario === "arun" || scenario === "in-flight" || scenario === "revoked" ? "engineering-demo" : "support-demo";
  if (scenario === "missing-session") token = "unknown";
  if (scenario === "other-tenant") token = "other-tenant-demo";
  if (scenario === "outage") state.policyAvailable = false;
  const body = { question: QUESTION, ...(scenario === "forged-groups" ? { groups: ["engineering"], tenantId: "other" } : {}) };
  if (scenario === "revoked") {
    finishServiceRequest(state, prepareServiceRequest(state, token, body));
    revokeD2(state);
  }
  const plan = prepareServiceRequest(state, token, body);
  if (scenario === "in-flight") revokeD2(state);
  const response = finishServiceRequest(state, plan);
  return { body, plan, response, audit: state.audit, cacheEntries: state.cache.size, policyVersion: state.policyVersion };
}

import { createServer } from "node:http";

/** Loopback-only teaching server. Demo session tokens are public exercise fixtures. */
export function createPermissionServer(state = createServiceState()) {
  return createServer(async (request, response) => {
    const send = (status, body) => {
      response.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
      response.end(JSON.stringify(body));
    };
    const authorization = request.headers.authorization ?? "";
    const token = authorization.startsWith("Bearer ") ? authorization.slice(7) : "";
    const user = Object.hasOwn(state.sessions, token) ? state.sessions[token] : null;
    if (!user) { request.resume(); return send(401, { message: "Sign in required" }); }
    if (!state.policyAvailable) { request.resume(); return send(503, { message: "Permission check unavailable" }); }
    let url;
    try { url = new URL(request.url ?? "/", "http://127.0.0.1"); }
    catch { request.resume(); return send(400, { message: "Invalid request target" }); }
    if (request.method === "GET" && url.pathname.startsWith("/sources/")) {
      request.resume();
      const result = readSource(user, url.pathname.slice("/sources/".length), state.documents);
      return send(result.status, result.body);
    }
    if (request.method !== "POST" || url.pathname !== "/ask") {
      request.resume(); return send(404, { message: "Route unavailable" });
    }
    try {
      let input = "";
      let bytes = 0;
      for await (const chunk of request) {
        bytes += chunk.length;
        if (bytes > 8192) { request.resume(); return send(413, { message: "Request too large" }); }
        input += chunk.toString();
      }
      const body = JSON.parse(input);
      const plan = prepareServiceRequest(state, token, body);
      // A real asynchronous generator belongs between prepare and finish.
      // Its returned claims need evaluation; do not blindly trust model-supplied source IDs.
      await Promise.resolve();
      const result = finishServiceRequest(state, plan);
      send(result.status, result.body);
    } catch {
      send(400, { message: "Invalid request" });
    }
  });
}

import assert from "node:assert/strict";
const expectations = {
  maya: [200, ["D1", "D3"]], arun: [200, ["D2", "D1"]],
  "forged-groups": [200, ["D1", "D3"]], "other-tenant": [200, []],
  "missing-session": [401, []], outage: [503, []], revoked: [200, ["D1"]], "in-flight": [409, []],
};
for (const [scenario, [status, ids]] of Object.entries(expectations)) {
  const result = runServiceScenario(scenario);
  assert.equal(result.response.status, status);
  assert.deepEqual((result.response.body.sources ?? []).map(source => source.id), ids);
  console.log(scenario + ": " + status + " [" + ids.join(", ") + "]");
}
const state = createServiceState();
const request = { question: QUESTION };
const ask = token => finishServiceRequest(state, prepareServiceRequest(state, token, request));
ask("engineering-demo");
assert.equal(ask("engineering-demo").cacheHit, true);
assert.equal(ask("support-demo").cacheHit, false);
revokeD2(state);
assert.equal(ask("engineering-demo").cacheHit, false);
assert.ok(!JSON.stringify(state.audit).includes("engineering-demo"));
assert.ok(!JSON.stringify(state.audit).includes(DOCUMENTS[1].fact));
console.log("Cache and audit checks passed.");

if (process.argv.includes("--serve")) {
  const server = createPermissionServer();
  server.listen(4317, "127.0.0.1", () => console.log("Exercise server: http://127.0.0.1:4317 (Ctrl+C to stop)"));
}
