/**
 * voice-ingest-session-text — write operator turns the agent read from its
 * own session context as :Message {format:'text', source:'admin-pty'} corpus
 * nodes.
 *
 * Invoked on demand by the voice-mirror skill (Task 644): the operator asks to
 * capture this session's voice, the admin agent extracts its own operator
 * turns from the live conversation context, and passes them here as `turns`.
 * Re-running with the same turns is a no-op — MERGE on (contentHash, accountId)
 * is the deduplication primitive.
 *
 * After ingest, triggers voice-distil-profile {format:'text'} so the cadence
 * guard decides whether distillation runs.
 *
 * "Real operator text" filter (each supplied turn is excluded when):
 *   - text is empty after trimming                      → nothing to ingest
 *   - text starts with "/"                              → slash command
 *   - text contains "<system-reminder>"                 → harness injection
 *   - text.length >= 4096                              → likely pasted content
 */
import { createHash } from "node:crypto";
import { getSession } from "../lib/neo4j.js";
import { voiceDistilProfile } from "./voice-distil-profile.js";

export interface VoiceIngestSessionTextParams {
  turns: string[];
  accountId: string;
  userId: string;
  sessionId?: string;
}

export interface VoiceIngestSessionTextResult {
  ingestedCount: number;
  skippedCount: number;
  sessionId: string | null;
}

/**
 * Applies the "real operator text" exclusions to a single supplied turn.
 * Returns the trimmed text to ingest, or null when the turn should be skipped.
 * Exported for testability (no DB required).
 */
export function filterOperatorText(text: unknown): string | null {
  if (typeof text !== "string") return null;
  const trimmed = text.trim();
  if (trimmed.length === 0) return null;
  if (trimmed.startsWith("/")) return null; // slash command
  if (trimmed.includes("<system-reminder>")) return null; // harness injection
  if (trimmed.length >= 4096) return null; // paste block
  return trimmed;
}

export async function voiceIngestSessionText(
  params: VoiceIngestSessionTextParams,
): Promise<VoiceIngestSessionTextResult> {
  const { turns, accountId, userId, sessionId = null } = params;

  if (!accountId || !userId) {
    throw new Error("voice-ingest-session-text: accountId and userId required");
  }
  if (!Array.isArray(turns)) {
    throw new Error("voice-ingest-session-text: turns array required");
  }

  // 1. Apply the operator-text filter to each supplied turn.
  const allTurns: string[] = [];
  for (const turn of turns) {
    const text = filterOperatorText(turn);
    if (text !== null) allTurns.push(text);
  }

  if (allTurns.length === 0) {
    process.stderr.write(
      `[voice-ingest-session-text] sessionId=${sessionId ?? "unknown"} adminUser=${userId} turns=0 bytes=0\n`,
    );
    return { ingestedCount: 0, skippedCount: 0, sessionId };
  }

  // 2. Write each turn as a :Message node. MERGE on contentHash+accountId dedupes.
  const session = getSession();
  const now = new Date().toISOString();
  let ingestedCount = 0;
  let skippedCount = 0;
  let totalBytes = 0;

  try {
    for (const body of allTurns) {
      const contentHash = createHash("sha256")
        .update(body, "utf-8")
        .digest("hex");

      const result = await session.run(
        `MATCH (a:AdminUser {accountId: $accountId, userId: $userId})
         MERGE (n:Message {contentHash: $contentHash, accountId: $accountId})
         ON CREATE SET
           n.userId = $userId,
           n.body = $body,
           n.occurredAt = $now,
           n.sessionId = $sessionId,
           n.source = 'admin-pty',
           n.authorshipMode = 'human-only',
           n.format = 'text',
           n.createdAt = $now
         MERGE (a)-[:AUTHORED]->(n)
         RETURN n.createdAt = $now AS isNew`,
        {
          accountId,
          userId,
          contentHash,
          body,
          now,
          sessionId,
        },
      );

      if (result.records.length === 0) {
        // :AdminUser not found — abort rather than orphan nodes.
        throw new Error(
          `voice-ingest-session-text: :AdminUser {accountId='${accountId}', userId='${userId}'} not found. Onboarding must promote the operator to :AdminUser before ingest.`,
        );
      }

      const isNew = result.records[0].get("isNew") === true;
      if (isNew) {
        ingestedCount++;
        totalBytes += body.length;
      } else {
        skippedCount++;
      }
    }
  } finally {
    await session.close();
  }

  process.stderr.write(
    `[voice-ingest-session-text] sessionId=${sessionId ?? "unknown"} adminUser=${userId} turns=${ingestedCount} bytes=${totalBytes} skipped=${skippedCount}\n`,
  );

  // 3. Trigger text-format distillation (cadence guard decides whether to run).
  if (ingestedCount > 0) {
    try {
      await voiceDistilProfile({ accountId, userId, format: "text" });
    } catch (err) {
      // Distil failure must not fail the ingest.
      process.stderr.write(
        `[voice-ingest-session-text] distil-after-ingest error: ${err instanceof Error ? err.message : String(err)}\n`,
      );
    }
  }

  return { ingestedCount, skippedCount, sessionId };
}
