#!/usr/bin/env node
/**
 * agentmemory-session-recall.js
 *
 * SessionStart hook — recalls relevant memories from the agentmemory bridge
 * and injects them as additionalContext so Claude has project history before
 * doing any work.
 *
 * Config (env vars, checked in order):
 *   AGENTMEMORY_BRIDGE_URL    bridge base URL   (default: the live memory-service
 *                             Worker — same host the remember/write path targets,
 *                             so recall reads where the fleet writes)
 *   AGENTMEMORY_BRIDGE_SECRET bearer token      (resolution order below)
 *
 * Bearer resolution (first match wins; no literal is embedded):
 *   1. process.env.AGENTMEMORY_BRIDGE_SECRET
 *   2. process.env.OLAM_AGENT_MEMORY_BEARER
 *   3. file bridge-secret        (~/.olam/secrets/ preferred, then ~/.olam/)
 *   4. file agent-memory-bearer  (~/.olam/secrets/ preferred, then ~/.olam/)
 *   5. else '' → no-op (recall is skipped; session continues normally)
 *
 * The file lookup mirrors agentmemory-recall-trigger.mjs (secrets/ first,
 * then the bare ~/.olam/ path) so BOTH recall hooks resolve the SAME bearer
 * file and can't diverge.
 *
 * Fails open — any error (or a missing bearer) exits 0 with no output; the
 * session continues normally and is never blocked.
 *
 * E2 — task-aware query derivation:
 *   Query is now derived from git branch + recent-changed files + cwd terms
 *   (via `deriveRecallQuery` in src/recall-query.ts, inlined below as CJS).
 *   This replaces the old cwd-last-2-segments slug that was task-blind.
 *   taskText feed (from UserPromptSubmit) is the E2 follow-on; noted at
 *   the bottom of this file.
 */

'use strict';

const fs = require('fs');
const os = require('os');
const path = require('path');

// ---------------------------------------------------------------------------
// Opt-in gate — inline re-implementation of agent-memory-gate.mjs contract
// (CJS cannot import ESM at the top level; inline ensures same fail-closed
// semantics without a dynamic import or a build step). This is a RECALL path,
// so it gates on mode !== off (readonly OR editor); legacy truthy → readonly.
// ---------------------------------------------------------------------------
// Values that enable RECALL (env, then file). Capture (editor-only) is gated
// separately in agentmemory-save.mjs via canWriteAgentMemory().
const _GATE_ENABLED_ENV = new Set(['readonly', 'editor', '1', 'true', 'on']);
const _GATE_ENABLED_FILE = new Set(['readonly', 'editor', 'true', '1']);
function _isAgentMemoryEnabled() {
  const envVal = process.env['OLAM_AGENT_MEMORY'];
  if (envVal !== undefined && envVal.length > 0) {
    return _GATE_ENABLED_ENV.has(envVal.trim().toLowerCase());
  }
  try {
    const fromEnv = process.env['OLAM_HOME'];
    const olamHome = (fromEnv && fromEnv.length > 0) ? fromEnv : path.join(os.homedir(), '.olam');
    const flagPath = path.join(olamHome, 'agent-memory-enabled');
    const raw = fs.readFileSync(flagPath, 'utf8').trim().toLowerCase();
    return _GATE_ENABLED_FILE.has(raw);
  } catch {
    return false;
  }
}
// ---------------------------------------------------------------------------

// Resolve the bridge URL: operator env wins, then the `olam memory connect`
// artifact (~/.olam/memory-connection.json), then the LIVE memory-service
// Worker. The prior default (olam-agent-memory.ernestcodes.workers.dev) is a
// dead host (HTTP 404); the artifact path lets `olam memory connect <url>` take
// effect without reinstalling hooks. Keep in sync with
// packages/cli/src/lib/memory-connection.ts resolveMemoryServiceUrl().
function resolveBridgeUrlDefault() {
  if (process.env.AGENTMEMORY_BRIDGE_URL) return process.env.AGENTMEMORY_BRIDGE_URL;
  try {
    const conn = JSON.parse(
      fs.readFileSync(path.join(os.homedir(), '.olam', 'memory-connection.json'), 'utf8'),
    );
    if (conn && typeof conn.url === 'string' && conn.url) return conn.url;
  } catch {
    // artifact absent / malformed — fall through to the live default
  }
  return 'https://atlas-agent-memory.atlas-kitchen.workers.dev';
}
const BRIDGE_URL = resolveBridgeUrlDefault();

/**
 * Resolve a secret file path: prefers ~/.olam/secrets/<name>, falls back to
 * ~/.olam/<name>. Mirrors resolveSecretPathInline() in
 * agentmemory-recall-trigger.mjs so both recall hooks pick the SAME bearer file.
 */
function resolveSecretPath(name) {
  const olamHome = path.join(os.homedir(), '.olam');
  const newPath = path.join(olamHome, 'secrets', name);
  if (fs.existsSync(newPath)) return newPath;
  return path.join(olamHome, name);
}

/** Read a trimmed secret file by name, or '' if absent/unreadable. */
function readSecretFile(name) {
  try {
    return fs.readFileSync(resolveSecretPath(name), 'utf8').trim();
  } catch {
    return '';
  }
}

// Bearer resolution: env → env → bridge-secret → agent-memory-bearer (each file
// resolved secrets/ first, then bare ~/.olam/) → '' (no-op fail-open). No secret
// literal is embedded in source.
const BEARER =
  process.env.AGENTMEMORY_BRIDGE_SECRET ||
  process.env.OLAM_AGENT_MEMORY_BEARER ||
  readSecretFile('bridge-secret') ||
  readSecretFile('agent-memory-bearer') ||
  '';

// ---------------------------------------------------------------------------
// E2: task-aware query derivation helpers (inlined CJS copy of src/recall-query.ts)
// Kept here to avoid a build step — the hook runs as raw Node.js CJS.
// ---------------------------------------------------------------------------

const PATH_NOISE = new Set([
  'home', 'users', 'projects', 'src', 'packages', 'app', 'apps', 'libs',
  'tmp', 'wt', 'olam-wt', 'work', 'workspace', 'workspaces',
]);
const BRANCH_PREFIX_RE = /^(?:fix|feat|feature|chore|hotfix|release|docs|refactor|test|ci)\//i;
const BRANCH_NOISE_TOKENS = new Set([
  'update', 'patch', 'wip', 'main', 'master', 'develop', 'dev', 'staging',
  'prod', 'production', 'hotfix', 'release', 'draft',
]);

function tokenize(s) {
  return s.toLowerCase().split(/[\s\-_/\\.]+/).filter((t) => t.length > 1);
}
function cwdTokens(cwd) {
  const parts = (cwd || '').replace(/\/$/, '').split('/').slice(-3);
  const tokens = [];
  for (const p of parts) {
    for (const t of tokenize(p)) {
      if (!PATH_NOISE.has(t)) tokens.push(t);
    }
  }
  return tokens;
}
function branchTokens(branch) {
  const stripped = branch.replace(BRANCH_PREFIX_RE, '');
  return tokenize(stripped).filter((t) => !BRANCH_NOISE_TOKENS.has(t));
}
function fileTokens(files) {
  const SKIP = new Set(['index', 'mod', 'main', 'test', 'spec', 'types', '__tests__']);
  const tokens = [];
  for (const f of (files || [])) {
    const base = f.replace(/\.[^.]+$/, '').split('/').pop() || '';
    for (const t of tokenize(base)) {
      if (!SKIP.has(t)) tokens.push(t);
    }
  }
  return tokens;
}
function dedupTokens(tokens) {
  const seen = new Set();
  const out = [];
  for (const t of tokens) {
    if (!seen.has(t)) { seen.add(t); out.push(t); }
  }
  return out;
}

/**
 * Derive a task-aware recall query from the signals available at SessionStart.
 * Priority: taskText (verbatim) > gitBranch > recentFiles > cwd terms.
 * Mirrors the exported `deriveRecallQuery` in src/recall-query.ts.
 *
 * @param {object} context
 * @param {string}          context.cwd
 * @param {string|undefined} context.gitBranch
 * @param {string[]|undefined} context.recentFiles
 * @param {string|undefined} context.taskText   — E2 follow-on; not yet fed at SessionStart
 * @returns {string}
 */
function deriveRecallQuery(context) {
  const { cwd, gitBranch, recentFiles, taskText } = context;
  if (taskText && taskText.trim().length > 0) return taskText.trim();
  const allTokens = [];
  if (gitBranch && gitBranch.trim().length > 0) allTokens.push(...branchTokens(gitBranch.trim()));
  if (recentFiles && recentFiles.length > 0) allTokens.push(...fileTokens(recentFiles));
  allTokens.push(...cwdTokens(cwd || ''));
  const tokens = dedupTokens(allTokens).slice(0, 12);
  return tokens.length === 0 ? 'recent work decisions architecture' : tokens.join(' ');
}

/**
 * Gather git signals at SessionStart (best-effort; fail-open).
 * Returns { gitBranch, recentFiles } — either may be undefined if the
 * git commands fail (not a git repo, git not installed, etc.).
 *
 * NOTE: taskText is not yet available at SessionStart (no UserPromptSubmit
 * context here). Feeding taskText is the E2 follow-on hook; noted in the PR.
 *
 * @param {string} cwd
 * @returns {{ gitBranch?: string, recentFiles?: string[] }}
 */
function gatherGitSignals(cwd) {
  const { execSync } = require('child_process');
  const opts = { cwd, stdio: ['pipe', 'pipe', 'pipe'], timeout: 3000 };
  let gitBranch;
  let recentFiles;
  try {
    gitBranch = execSync('git branch --show-current', opts).toString().trim() || undefined;
  } catch (_) { /* not a git repo or git unavailable — ignore */ }
  try {
    const raw = execSync('git diff --name-only HEAD~3 2>/dev/null || git diff --name-only HEAD~1', opts)
      .toString().trim();
    recentFiles = raw ? raw.split('\n').map((f) => f.trim()).filter(Boolean).map((f) => f.split('/').pop() || f) : undefined;
  } catch (_) { /* ignore */ }
  return { gitBranch, recentFiles };
}

// ---------------------------------------------------------------------------
// (End E2 helpers)
// ---------------------------------------------------------------------------

// ---------------------------------------------------------------------------
// Phase D: recall-linkage log (inlined CJS copy of hooks/recall-log.mjs).
// Persists the surfaced set so the session-end citation hook can detect which
// surfaced memories were actually used and reinforce them. Inlined for the same
// no-build reason as deriveRecallQuery above; kept structurally identical to
// recall-log.mjs (the .mjs is the source of truth + carries the unit tests).
// ---------------------------------------------------------------------------

const RECALL_LOG_MAX_ENTRIES = 50;
const RECALL_LOG_MAX_SURFACED = 20;
const RECALL_LOG_MAX_EXCERPT = 240;

function recallLogSlug(s) {
  return String(s).replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80);
}

function sessionKeyFor(event, now = new Date()) {
  const sid = event && typeof event.session_id === 'string' ? event.session_id.trim() : '';
  if (sid) return recallLogSlug(sid);
  const cwd = event && typeof event.cwd === 'string' ? event.cwd : '';
  const cwdSlug = recallLogSlug(cwd.replace(/\/+$/, '').split('/').slice(-2).join('-')) || 'session';
  return `${cwdSlug}-${now.toISOString().slice(0, 10)}`;
}

function normalizeSurfaced(surfaced) {
  const out = [];
  for (const m of Array.isArray(surfaced) ? surfaced : []) {
    if (!m || typeof m !== 'object') continue;
    const id = typeof m.id === 'string' ? m.id : typeof m.obsId === 'string' ? m.obsId : '';
    if (!id) continue;
    const title = typeof m.title === 'string' ? m.title : '';
    const rawExcerpt =
      (typeof m.excerpt === 'string' && m.excerpt) ||
      (typeof m.contentExcerpt === 'string' && m.contentExcerpt) ||
      (typeof m.content === 'string' && m.content) || '';
    out.push({ id, title, excerpt: String(rawExcerpt).slice(0, RECALL_LOG_MAX_EXCERPT) });
    if (out.length >= RECALL_LOG_MAX_SURFACED) break;
  }
  return out;
}

function appendRecallLog(sessionKey, surfaced, now = Date.now()) {
  const normalized = normalizeSurfaced(surfaced);
  if (normalized.length === 0) return;
  try {
    const dir = path.join(os.homedir(), '.olam', 'recall-log');
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
    const final = path.join(dir, `${recallLogSlug(sessionKey)}.json`);
    let log = { entries: [] };
    try {
      const parsed = JSON.parse(fs.readFileSync(final, 'utf8'));
      if (parsed && Array.isArray(parsed.entries)) log = parsed;
    } catch (_) { /* fresh log */ }
    log.entries.push({ ts: now, surfaced: normalized });
    if (log.entries.length > RECALL_LOG_MAX_ENTRIES) {
      log.entries = log.entries.slice(log.entries.length - RECALL_LOG_MAX_ENTRIES);
    }
    const tmp = `${final}.${require('crypto').randomBytes(6).toString('hex')}.tmp`;
    fs.writeFileSync(tmp, JSON.stringify(log), 'utf8');
    fs.renameSync(tmp, final);
  } catch (_) {
    // Fail-open: the linkage log is best-effort and must never block recall.
  }
}

async function recall(query, limit = 5) {
  const res = await fetch(`${BRIDGE_URL}/agentmemory/mcp/call`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${BEARER}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'memory_recall', arguments: { query, limit, format: 'compact' } }),
    signal: AbortSignal.timeout(5000),
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const body = await res.json();
  const text = body?.content?.[0]?.text;
  return text ? JSON.parse(text) : null;
}

async function main() {
  // Gate: memory is off by default — exit silently when not enabled.
  if (!_isAgentMemoryEnabled()) return;

  const raw = require('fs').readFileSync(0, 'utf8');
  let event = {};
  try { event = JSON.parse(raw); } catch (_) {}

  // No bearer → no-op fail-open. Never throw, never block the session.
  if (!BEARER) return;

  const cwd = event?.cwd || process.cwd();
  // E2: gather git signals (best-effort) then derive a task-aware query.
  const { gitBranch, recentFiles } = gatherGitSignals(cwd);
  const query = deriveRecallQuery({ cwd, gitBranch, recentFiles });
  // taskText is not yet available at SessionStart; it will be fed in the
  // E2 follow-on (a UserPromptSubmit hook that updates a per-session context
  // file that this hook can read on the next session open).
  let result;
  try {
    result = await recall(query);
  } catch (err) {
    // Fail-open: one-line breadcrumb to stderr, then continue with no output.
    process.stderr.write(`[agentmemory-session-recall] recall failed: ${err?.message ?? err}\n`);
    return;
  }

  if (!result?.results?.length) return;

  // Phase D: persist the surfaced set so the session-end citation hook can
  // detect which of these memories were actually used and reinforce them.
  // Best-effort — never blocks recall.
  appendRecallLog(sessionKeyFor(event), result.results);

  const lines = result.results
    .map((r, i) => `${i + 1}. [id ${r.obsId || r.id}] [score ${r.score?.toFixed(2) ?? '?'}] ${r.title || r.obsId}`)
    .join('\n');

  // Phase D nudge: close the agent-native reinforcement loop. If the agent acts
  // on a recalled memory, it should call memory_reinforce(id) so the system
  // learns the memory was useful (writes last_reinforced_at → the usage/recency
  // ranking lift engages next session). Kept to one line to avoid bloating the
  // injected context.
  const nudge =
    'If you act on any memory above, call `memory_reinforce` with its id so the system learns it was useful.';

  const additionalContext =
    `## Recalled from agent memory (query: "${query}")\n\n${lines}\n\n${nudge}`;

  process.stderr.write(
    `\x1b[34m[🧠⇣ Memory recalled]\x1b[0m (${result.results.length} memories · "${query}")\n`
  );

  process.stdout.write(JSON.stringify({
    hookSpecificOutput: {
      hookEventName: 'SessionStart',
      additionalContext,
    }
  }));
}

main().catch(() => process.exit(0));
