// session-recorder.js — per-process session lifecycle management
//
// Maintains in-memory state for the active session (id, turn index, pending
// turn row id) and dispatches writes to sf-db.js on hook events. Keeping
// this state here — not in register-hooks.js — lets it be reset cleanly
// across session_switch events and tested in isolation.
//
// Purpose: bridge the Pi/Copilot hook lifecycle into the sf.db session layer
// so every user↔assistant exchange is persisted as a searchable turns row,
// promoted into memory_sources for future LLM extraction + vector embedding,
// and cross-session learning has the structural inputs it needs.
//
// Consumer: register-hooks.js session_start, before_agent_start, agent_end,
//           tool_call, session_shutdown hooks.

import { createMemorySource } from "./memory-source-store.js";
import {
	insertSessionTurn,
	patchTurnResponse,
	recordSessionFileTouch,
	recordSessionRef,
	upsertSession,
} from "./sf-db.js";

// ── Write-class tools whose input.path counts as a file touch ──────────────
const WRITE_TOOL_NAMES = new Set([
	"edit_file",
	"create_file",
	"write_file",
	"str_replace_editor",
	"str_replace_based_edit_tool",
	"rewrite_file",
	"insert_content",
	"delete_file",
]);

// ── Ref patterns extracted from turn text ──────────────────────────────────
const REF_PATTERNS = [
	{ type: "pr", re: /\bPR\s*#(\d+)\b|\bpull.request[/ #]+(\d+)\b/gi },
	{ type: "issue", re: /\bissue\s*#(\d+)\b|\bGH-(\d+)\b/gi },
	{ type: "commit", re: /\b([0-9a-f]{7,40})\b/g },
	{ type: "branch", re: /\borigin\/([^\s"'`]+)\b|\bbranch[: ]+([^\s"'`]+)/gi },
];

// ── Module-level active session state ─────────────────────────────────────
let _sessionId = null;
let _turnIndex = -1;
let _pendingTurnId = null;
/** User message text captured in recordTurnStart — used in promoteTurnToMemorySource. */
let _pendingUserMessage = null;

/**
 * Reset all session state. Called on session_start and session_switch to
 * ensure each host session gets a clean slate.
 *
 * Purpose: prevent stale state from a previous session bleeding into the next.
 * Consumer: initSessionRecorder, session_switch handler.
 */
export function resetSessionRecorder() {
	_sessionId = null;
	_turnIndex = -1;
	_pendingTurnId = null;
	_pendingUserMessage = null;
}

/**
 * Initialize the recorder for a new session. Upserts the sessions row and
 * resets per-turn counters. Safe to call multiple times — upsertSession is
 * idempotent.
 *
 * Purpose: establish the session row that all subsequent turns hang off.
 * Consumer: register-hooks.js session_start.
 */
export function initSessionRecorder(
	sessionId,
	{ mode, cwd, repo, branch } = {},
) {
	resetSessionRecorder();
	if (!sessionId) return;
	_sessionId = sessionId;
	_turnIndex = 0;
	upsertSession({
		sessionId,
		mode: mode ?? "interactive",
		cwd: cwd ?? process.cwd(),
		repo,
		branch,
	});
}

/**
 * Record the start of a new turn (user message). Inserts the turns row with
 * the user_message immediately so the record exists even if agent_end never
 * fires (e.g. crash or interrupt). Also scans for ref mentions.
 *
 * Returns the DB row id so tool-call handlers can link touches to this turn.
 *
 * Purpose: store the user's intent before the agent processes it.
 * Consumer: register-hooks.js before_agent_start.
 */
export function recordTurnStart(userMessage) {
	if (!_sessionId) return null;
	const ts = new Date().toISOString();
	const id = insertSessionTurn({
		sessionId: _sessionId,
		turnIndex: _turnIndex,
		userMessage: userMessage ?? null,
		ts,
	});
	_pendingTurnId = id;
	_pendingUserMessage = userMessage ?? null;
	if (userMessage) extractAndRecordRefs(userMessage, id);
	return id;
}

/**
 * Patch the assistant_response on the pending turn row. Called from agent_end.
 * Also promotes the completed turn into memory_sources so the content is
 * available for `/memory rebuild` (LLM extraction → memories rows) and
 * eventual vector embedding via runEmbeddingBackfill. Advances the turn
 * index so the next turn gets a fresh slot.
 *
 * Purpose: complete the turn record so both halves are searchable as a unit,
 * and make the raw turn text a first-class memory source for retrieval.
 * Consumer: register-hooks.js agent_end.
 */
export function recordTurnEnd(assistantResponse) {
	if (!_sessionId) return;
	if (assistantResponse) {
		patchTurnResponse(_sessionId, _turnIndex, assistantResponse);
		extractAndRecordRefs(assistantResponse, _pendingTurnId);
	}
	// Promote the completed turn into memory_sources for future LLM extraction
	// + vector embedding. Both halves are available here — user message was
	// captured in recordTurnStart and assistant text arrives now.
	promoteTurnToMemorySource(_turnIndex, _pendingUserMessage, assistantResponse);
	_pendingTurnId = null;
	_pendingUserMessage = null;
	_turnIndex += 1;
}

/**
 * Record a file path as touched in the current session. No-op when no active
 * session or when the tool is not a write-class tool.
 *
 * Purpose: build the session_file_touches index without requiring a full
 * audit-event scan.
 *
 * Consumer: register-hooks.js tool_call.
 */
export function recordFileTouch(toolName, filePath) {
	if (!_sessionId || !filePath) return;
	if (!WRITE_TOOL_NAMES.has(toolName)) return;
	recordSessionFileTouch({
		sessionId: _sessionId,
		path: filePath,
		toolName,
		turnId: _pendingTurnId,
		firstSeenAt: new Date().toISOString(),
	});
}

/**
 * Update the session summary (e.g. from a compaction summary). The summary
 * is the most recent compaction digest — useful as a semantic handle for
 * memory-pipeline promotion. Also persists the summary as a memory_source
 * with kind="session" so `/memory rebuild` can extract durable knowledge
 * from it without requiring an LLM call at compaction time.
 *
 * Purpose: give memory retrieval a compact description of each session's work.
 * Consumer: register-hooks.js session_before_compact result.
 */
export function updateSessionSummary(summary) {
	if (!_sessionId || !summary) return;
	upsertSession({ sessionId: _sessionId, summary });
	// Promote into memory_sources for future LLM extraction + embedding.
	// The [session:<id>] prefix enables idempotent detection on re-compact.
	try {
		const content = `[session:${_sessionId}] ${summary.slice(0, 1000)}`;
		createMemorySource({
			kind: "session",
			uri: null,
			title: `Session ${_sessionId.slice(0, 8)} summary`,
			content,
			scope: "project",
			tags: ["session", "summary"],
		});
	} catch {
		/* non-fatal: memory source creation must never block compaction */
	}
}

/**
 * Patch the session's repo + branch when git context becomes available after
 * initial startup (common when cwd is set before the DB is open).
 *
 * Purpose: back-fill attribution data so session→repo queries work.
 * Consumer: register-hooks.js after ensureDbOpen resolves.
 */
export function patchSessionGitContext(repo, branch) {
	if (!_sessionId) return;
	upsertSession({
		sessionId: _sessionId,
		repo: repo ?? null,
		branch: branch ?? null,
	});
}

// ── Helpers ────────────────────────────────────────────────────────────────

/**
 * Synthesize a memory_source entry from a completed turn so the conversation
 * text feeds the memory pipeline. The source can later be processed by
 * `/memory rebuild` (LLM extraction) or embedded via runEmbeddingBackfill.
 *
 * Content format: `[turn:<session_id>:<turn_index>] Q: <user_msg> A: <resp>`
 * The structured prefix enables idempotent detection across rebuilds.
 *
 * Silently no-ops when either half of the turn is absent, the session is not
 * initialised, or createMemorySource fails — turn recording must never block.
 */
function promoteTurnToMemorySource(turnIndex, userMessage, assistantResponse) {
	if (!_sessionId) return;
	// Only promote turns that have at least one non-trivial text half.
	const user = (userMessage ?? "").trim();
	const asst = (assistantResponse ?? "").trim();
	if (!user && !asst) return;
	try {
		const prefix = `[turn:${_sessionId}:${turnIndex}]`;
		const qPart = user ? `Q: ${user.slice(0, 400)}` : "";
		const aPart = asst ? `A: ${asst.slice(0, 400)}` : "";
		const content = [prefix, qPart, aPart].filter(Boolean).join(" ");
		createMemorySource({
			kind: "turn",
			uri: null,
			title: `Turn ${_sessionId.slice(0, 8)}:${turnIndex}`,
			content,
			scope: "project",
			tags: ["turn", "session"],
		});
	} catch {
		/* non-fatal */
	}
}

function extractAndRecordRefs(text, turnId) {
	if (!_sessionId || !text) return;
	const now = new Date().toISOString();
	for (const { type, re } of REF_PATTERNS) {
		re.lastIndex = 0;
		let m;
		while ((m = re.exec(text)) !== null) {
			const value = (m[1] ?? m[2] ?? "").trim();
			// Skip short hex strings that are likely not real commit SHAs
			if (type === "commit" && value.length < 7) continue;
			if (!value) continue;
			recordSessionRef({
				sessionId: _sessionId,
				refType: type,
				refValue: value,
				turnId: turnId ?? null,
				createdAt: now,
			});
		}
	}
}
