/**
 * Pure helpers for the shim-owned write_neo4j_cypher path.
 * Extracted from index.ts so the executeWrite tx body, response synthesis,
 * and value serialization can be unit-tested without the shim's top-level
 * process bootstrap (which spawns uvx and opens stdin/stdout pipes).
 *
 * The shim's index.ts wires these up: rewriter → shared driver → session.
 * executeWrite(tx => runWriteTxBody(...)) → response synth → stdout.
 */

export const ORPHAN_CHECK_CYPHER =
  "MATCH (n) WHERE n.createdBySession = $__autoSession " +
  "AND n.createdAt >= $__autoStartTimestamp " +
  "AND NOT (n)--() " +
  "RETURN collect(elementId(n)) AS orphanIds, " +
  "collect(distinct labels(n)) AS labelSets";

export interface GraphTx {
  run(
    cypher: string,
    params?: Record<string, unknown>,
  ): Promise<{
    records: Array<{
      keys: readonly string[];
      get: (k: string | number) => unknown;
    }>;
    summary: {
      counters: {
        updates(): {
          nodesCreated: number;
          relationshipsCreated: number;
          propertiesSet: number;
        };
      };
    };
  }>;
}

export interface GraphSession {
  executeWrite<T>(work: (tx: GraphTx) => Promise<T>): Promise<T>;
  close(): Promise<void>;
}

export interface GraphDriver {
  session(): GraphSession;
}

export class OrphanRollbackError extends Error {
  constructor(
    public readonly orphanIds: string[],
    public readonly sampleLabels: string[],
  ) {
    super(`orphan rollback: ${orphanIds.length} unattached node(s)`);
  }
}

export interface AutoContext {
  agent: string;
  session: string;
}

export interface WriteTxOutcome {
  nodesCreated: number;
  relsCreated: number;
  propertiesSet: number;
  serializedRecords: Array<Record<string, unknown>>;
}

export function serializeValue(v: unknown): unknown {
  if (v === null || v === undefined) return v;
  if (typeof v === "string" || typeof v === "boolean" || typeof v === "number") {
    return v;
  }
  if (Array.isArray(v)) return v.map(serializeValue);
  if (typeof v === "object") {
    const obj = v as Record<string, unknown>;
    if (
      "low" in obj &&
      "high" in obj &&
      typeof obj.low === "number" &&
      typeof obj.high === "number" &&
      Object.keys(obj).length === 2
    ) {
      if (obj.high === 0 || obj.high === -1) return obj.low;
      return String(obj);
    }
    if ("properties" in obj && "labels" in obj) {
      return {
        labels: obj.labels,
        elementId: obj.elementId,
        properties: serializeValue(obj.properties),
      };
    }
    if ("properties" in obj && "type" in obj && "elementId" in obj) {
      return {
        type: obj.type,
        elementId: obj.elementId,
        properties: serializeValue(obj.properties),
      };
    }
    if (
      obj.constructor &&
      obj.constructor.name &&
      obj.constructor.name !== "Object" &&
      obj.constructor.name !== "Array"
    ) {
      return String(obj);
    }
    const out: Record<string, unknown> = {};
    for (const [k, val] of Object.entries(obj)) {
      out[k] = serializeValue(val);
    }
    return out;
  }
  return v;
}

/**
 * Body of the executeWrite tx. Runs the rewritten cypher, captures counters,
 * runs the orphan check, throws OrphanRollbackError if any orphans remain
 * (executeWrite catches the throw and rolls back the entire tx).
 *
 * Caller is responsible for: (a) opening the session, (b) wrapping this in
 * `session.executeWrite(tx => runWriteTxBody(tx, ...))`, (c) calling close()
 * in finally, (d) translating the outcome / OrphanRollbackError into the
 * synthesised MCP response and audit log lines.
 */
export async function runWriteTxBody(
  tx: GraphTx,
  rewrittenCypher: string,
  operatorParams: Record<string, unknown>,
  autoCtx: AutoContext,
): Promise<WriteTxOutcome> {
  const tsRes = await tx.run("RETURN datetime() AS now");
  const startTs = tsRes.records[0].get("now");

  const merged: Record<string, unknown> = {
    ...operatorParams,
    __autoStartTimestamp: startTs,
    __autoAgent: autoCtx.agent,
    __autoSession: autoCtx.session,
  };

  const writeRes = await tx.run(rewrittenCypher, merged);
  const counters = writeRes.summary.counters.updates();
  const nodesCreated = counters.nodesCreated;
  const relsCreated = counters.relationshipsCreated;
  const propertiesSet = counters.propertiesSet;
  const keys =
    writeRes.records.length > 0 ? writeRes.records[0].keys.map(String) : [];
  const serializedRecords = writeRes.records.map((rec) => {
    const out: Record<string, unknown> = {};
    for (const k of keys) {
      out[k] = serializeValue(rec.get(k));
    }
    return out;
  });

  const orphanRes = await tx.run(ORPHAN_CHECK_CYPHER, merged);
  const orphanIds = (orphanRes.records[0].get("orphanIds") as string[]) ?? [];
  const labelSets =
    (orphanRes.records[0].get("labelSets") as string[][]) ?? [];
  if (orphanIds.length > 0) {
    const flatLabels = Array.from(new Set(labelSets.flat()));
    throw new OrphanRollbackError(orphanIds, flatLabels);
  }

  return { nodesCreated, relsCreated, propertiesSet, serializedRecords };
}

export interface WriteSynthInput {
  nodesCreated: number;
  relsCreated: number;
  propertiesSet: number;
  records: Array<Record<string, unknown>>;
}

export function synthesiseWriteResponse(
  id: string | number,
  w: WriteSynthInput,
): string {
  const parts: string[] = [];
  if (w.records.length > 0) {
    parts.push(
      `${w.records.length} record${w.records.length === 1 ? "" : "s"} returned`,
    );
  }
  parts.push(`${w.nodesCreated} nodes created`);
  parts.push(`${w.relsCreated} relationships created`);
  if (w.propertiesSet > 0) parts.push(`${w.propertiesSet} properties set`);
  if (w.records.length > 0) {
    parts.push("");
    parts.push(JSON.stringify(w.records, null, 2));
  }
  return JSON.stringify({
    jsonrpc: "2.0",
    id,
    result: {
      content: [{ type: "text", text: parts.join("\n") }],
    },
  });
}

export function synthesiseOrphanRollback(
  id: string | number,
  orphanCount: number,
  sampleLabels: string[],
): string {
  const labelLine =
    sampleLabels.length > 0
      ? sampleLabels.slice(0, 5).join(", ")
      : "(unknown — orphans had no labels)";
  const text =
    `orphan rollback — ${orphanCount} node(s) created without an edge in the same transaction.\n` +
    `Sample labels: ${labelLine}.\n\n` +
    `The transaction was aborted; nothing was persisted. Anchor each new node to its semantic parent in the same statement (Graph Stewardship Doctrine Rule 1 in database-operator.md).`;
  return JSON.stringify({
    jsonrpc: "2.0",
    id,
    result: {
      content: [{ type: "text", text }],
      isError: true,
    },
  });
}

export function synthesiseShimError(
  id: string | number,
  prefix: string,
  errMsg: string,
): string {
  return JSON.stringify({
    jsonrpc: "2.0",
    id,
    result: {
      content: [{ type: "text", text: `${prefix}: ${errMsg}` }],
      isError: true,
    },
  });
}
