#!/usr/bin/env node
/**
 * agentmemory-save.mjs — PostToolUse hook: persist agent-written memory notes.
 *
 * AM_SENTINEL=olam-agent-memory-hook-v2
 *
 * This is the FILE-BASED port of the legacy inline python SAVE blob that used
 * to live directly in ~/.claude/settings.json under
 * `AM_SENTINEL=olam-agent-memory-hook-v1; python3 -c '…'`. The installer
 * (`olam hooks install-all`, invoked by `olam skills sync`) copies this file to
 * ~/.claude/scripts/hooks/ and registers a settings.json entry pointing at it,
 * REPLACING the inline blob. Keeping the logic in a real source file means it
 * is version-controlled, testable, bundled into @pleri/olam-cli, and owned by
 * sync — not stranded as a string in the operator's settings.
 *
 * Behaviour (byte-faithful port of the v1 blob):
 *   - Reads the PostToolUse JSON payload from stdin.
 *   - Acts ONLY when tool_name === "Write" AND the written file_path matches
 *     /\/\.claude\/projects\/.*\/memory\/.*\.md$/ ; otherwise bails (exit 0).
 *   - Requires OLAM_AGENT_MEMORY_BEARER (trimmed, non-empty); else bails.
 *   - Parses YAML-ish frontmatter delimited by leading "---":
 *       description: → title (default "memory")   ← keyed on description, per v1
 *       type:        → type  (default "fact")
 *       remainder    → body
 *   - POSTs {title, content: body, type} to
 *       <OLAM_AGENT_MEMORY_URL|default>/agentmemory/remember
 *     with Authorization: Bearer <bearer>, ~3s timeout.
 *   - On success prints to STDERR the green status line:
 *       [🧠⇡ Memory saved] "<title first 60 chars>"
 *   - Fail-open: ANY error → silent, exit 0. Never throws to the caller.
 *
 * Node built-ins only (global fetch + AbortController, available on Node ≥18).
 */

import { readFileSync } from 'node:fs';
import { canWriteAgentMemory } from './agent-memory-gate.mjs';
import { resolveAgentMemoryBearer } from './agent-memory-bearer.mjs';

const DEFAULT_URL = 'https://atlas-agent-memory.atlas-kitchen.workers.dev';
const MEMORY_PATH_RE = /\/\.claude\/projects\/.*\/memory\/.*\.md$/;
const TIMEOUT_MS = 3000;

/** Read all of stdin as a UTF-8 string. */
function readStdin() {
  try {
    return readFileSync(0, 'utf8');
  } catch {
    return '';
  }
}

/**
 * Parse leading "---"-delimited frontmatter. Returns {title, type, body}.
 * Mirrors the v1 blob: title comes from `description:`, type from `type:`.
 */
function parseNote(raw) {
  let title = 'memory';
  let type = 'fact';
  let body = raw;
  if (raw.startsWith('---')) {
    // Split into [before-first-delim, frontmatter, …rest]. JS's limited split
    // truncates the remainder, so slice manually to keep the full body.
    const firstDelimEnd = 3; // length of leading "---"
    const secondDelim = raw.indexOf('---', firstDelimEnd);
    if (secondDelim !== -1) {
      const fm = raw.slice(firstDelimEnd, secondDelim);
      body = raw.slice(secondDelim + 3).trim();
      for (const ln of fm.split('\n')) {
        const s = ln.trim();
        if (s.startsWith('description:')) {
          title = stripQuotes(s.slice(s.indexOf(':') + 1).trim());
        } else if (s.startsWith('type:')) {
          type = stripQuotes(s.slice(s.indexOf(':') + 1).trim());
        }
      }
    }
  }
  return { title, type, body };
}

function stripQuotes(v) {
  return v.replace(/^["']/, '').replace(/["']$/, '');
}

async function main() {
  const payloadRaw = readStdin();
  let payload;
  try {
    payload = JSON.parse(payloadRaw);
  } catch {
    return; // not parseable → fail-open
  }

  const toolName = payload?.tool_name ?? '';
  const filePath = payload?.tool_input?.file_path ?? '';
  if (toolName !== 'Write' || !MEMORY_PATH_RE.test(filePath)) return;

  // Capture is gated to EDITOR mode. off/readonly never write to the store.
  if (!canWriteAgentMemory()) return;

  // Resolve via the SHARED chain the recall hook uses: env
  // AGENTMEMORY_BRIDGE_SECRET → env OLAM_AGENT_MEMORY_BEARER → file
  // bridge-secret → file agent-memory-bearer (secrets/ preferred, OLAM_HOME
  // aware). After `olam credentials pull` writes the file, capture works with
  // NO env set. Empty → fail-open no-op.
  const bearer = resolveAgentMemoryBearer();
  if (!bearer) return;

  let raw;
  try {
    raw = readFileSync(filePath, 'utf8');
  } catch {
    return;
  }

  const { title, type, body } = parseNote(raw);
  const base = (process.env['OLAM_AGENT_MEMORY_URL'] ?? DEFAULT_URL).replace(/\/$/, '');
  const url = `${base}/agentmemory/remember`;

  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
  try {
    const resp = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${bearer}`,
        'User-Agent': 'olam-agent-memory-hook/2',
      },
      body: JSON.stringify({ title, content: body, type }),
      signal: controller.signal,
    });
    // Treat any non-throwing response as delivered (the v1 blob did the same —
    // it printed on a non-exception urlopen regardless of status code).
    if (resp) {
      const label = title.slice(0, 60);
      process.stderr.write(`\x1b[32m[\u{1F9E0}\u{21E1} Memory saved]\x1b[0m "${label}"\n`);
    }
  } catch {
    // Fail-open: network error / timeout / abort → silent.
  } finally {
    clearTimeout(timer);
  }
}

main().catch(() => {
  /* fail-open: never propagate to the caller */
});
