/**
 * PersistentAgent — Persistent named agent with durable inbox, 3-tier memory, and UOK integration.
 *
 * Purpose: provide persistent agents as first-class citizens in the SF
 * swarm system. Each agent has a stable identity that survives restarts, a SQLite-backed
 * inbox, and three memory tiers (core blocks, recall, archival).
 *
 * Consumer: AgentSwarm orchestrator, swarm role agents (CoordinatorAgent, WorkerAgent etc),
 * and direct use in multi-agent dispatch flows.
 *
 * ## Runner
 * The `agent-runner.js` module (sibling to this file) implements the LLM execution half:
 *   1. Reads pending messages from this agent's inbox (`receive(true)`)
 *   2. Assembles a prompt from core memory blocks + inbox messages
 *   3. Dispatches to SF headless (`node dist/loader.js headless --print <prompt>`)
 *   4. Writes the LLM response back into the bus as a reply
 *   5. Limits context window to the most recent N turns
 *
 * `AgentSwarm.run()` optionally enables LLM-backed turns via `opts.enableLLM`.
 * When disabled, the swarm acts as a pure message router (useful for tests).
 *
 * See: Codex `codex-rs/core/src/agent/control.rs` for the reference implementation of
 * typed parallel subagents (explorer/worker roles) with forked rollout history.
 */

import { randomUUID } from "node:crypto";
import { mkdirSync } from "node:fs";
import { join } from "node:path";
import { sfRoot } from "../paths.js";
import { getDatabase, openDatabase } from "../sf-db.js";
import { UokCoordinationStore } from "./coordination-store.js";
import { MessageBus } from "./message-bus.js";

const VALID_ROLES = [
	"coordinator",
	"worker",
	"scout",
	"reviewer",
	"planner",
	"verifier",
	"scribe",
	"adversary",
];
const DEFAULT_ROLE = "worker";
const DEFAULT_MAX_INBOX_SIZE = 200;
const DEFAULT_REFRESH_INTERVAL_MS = 30_000;
const DEFAULT_SEND_AND_WAIT_TIMEOUT_MS = 30_000;
const DEFAULT_SEND_AND_WAIT_POLL_INTERVAL_MS = 500;

function ensureDbOpen(basePath) {
	const dir = sfRoot(basePath);
	mkdirSync(dir, { recursive: true });
	const dbPath = join(dir, "sf.db");
	if (!getDatabase()) {
		openDatabase(dbPath);
	}
	const db = getDatabase();
	if (!db)
		throw new Error(`PersistentAgent: failed to open database at ${dbPath}`);
	return db;
}

/**
 * Persistent named agent with durable inbox, 3-tier memory, and UOK integration.
 *
 * Purpose: first-class swarm citizen with Letta-style identity, memory, and messaging.
 * Survives restarts via SQLite-backed identity and CoordinationStore memory.
 *
 * Consumer: AgentSwarm orchestrator and role agents (coordinator, worker, scout, etc).
 */
export class PersistentAgent {
	/**
	 * Create or resume a named agent.
	 *
	 * Purpose: establish stable agent identity on first init and reload it on
	 * subsequent runs, so agent state (memory blocks, archival, tags) persists
	 * across process restarts without re-registration.
	 *
	 * Consumer: AgentSwarm when spawning or resuming swarm role agents.
	 *
	 * @param {string} basePath - project root used to locate `.sf/sf.db`
	 * @param {object} options
	 * @param {string} options.name - stable human-readable routing key (required)
	 * @param {string} [options.role='worker'] - agent role
	 * @param {string[]} [options.tags=[]] - routing metadata tags
	 * @param {number} [options.maxInboxSize=200]
	 * @param {number} [options.refreshIntervalMs=30000]
	 */
	constructor(basePath, options = {}) {
		const {
			name,
			role = DEFAULT_ROLE,
			tags = [],
			maxInboxSize = DEFAULT_MAX_INBOX_SIZE,
			refreshIntervalMs = DEFAULT_REFRESH_INTERVAL_MS,
		} = options;
		if (!name) throw new Error("PersistentAgent: options.name is required");
		if (!VALID_ROLES.includes(role))
			throw new Error(
				`PersistentAgent: invalid role "${role}". Must be one of: ${VALID_ROLES.join(", ")}`,
			);

		this._basePath = basePath;
		this._name = name;
		this._maxInboxSize = maxInboxSize;
		this._refreshIntervalMs = refreshIntervalMs;

		const db = ensureDbOpen(basePath);
		this._store = new UokCoordinationStore(db);
		this._bus = new MessageBus(basePath, { maxInboxSize, refreshIntervalMs });

		this._identity = this._loadOrCreateIdentity({ name, role, tags });
		this._inbox = this._bus.getInbox(`agent:${name}`);
	}

	// ─── Identity ─────────────────────────────────────────────────────────────

	/**
	 * Returns the agent's stable identity record.
	 *
	 * Purpose: provide callers a single authoritative snapshot of who this agent
	 * is (id, name, role, tags) without exposing mutable internals.
	 *
	 * Consumer: AgentSwarm roster queries and routing logic.
	 */
	get identity() {
		return { ...this._identity };
	}

	/**
	 * Returns the agent's durable inbox instance.
	 *
	 * Purpose: expose the AgentInbox so callers can subscribe or integrate
	 * with the inbox directly when the messaging helpers are insufficient.
	 *
	 * Consumer: AgentSwarm orchestrator and advanced swarm coordination flows.
	 */
	get inbox() {
		return this._inbox;
	}

	_identityKey() {
		return `agent:${this._name}:identity`;
	}

	_loadOrCreateIdentity({ name, role, tags }) {
		const existing = this._store.get(this._identityKey());
		if (existing) return existing;
		const identity = {
			agentId: `agent-${randomUUID()}`,
			name,
			role,
			tags,
			createdAt: new Date().toISOString(),
		};
		this._store.set(this._identityKey(), identity);
		return identity;
	}

	// ─── Core Blocks (in-context memory) ──────────────────────────────────────

	/**
	 * Set a named core memory block.
	 *
	 * Purpose: persist agent in-context slabs (persona, human profile, system
	 * state) so they survive restarts and can be injected into prompts via
	 * getContextBlocks().
	 *
	 * Consumer: agent setup code and AgentSwarm role initialization.
	 *
	 * @param {string} label - block label (e.g. "persona", "human")
	 * @param {*} value - serializable block content
	 * @param {number} [ttlMs] - optional TTL in milliseconds
	 */
	setCoreBlock(label, value, ttlMs) {
		const key = `agent:${this._name}:block:${label}`;
		this._store.set(key, value, ttlMs != null ? { ttlMs } : undefined);
	}

	/**
	 * Retrieve a named core memory block.
	 *
	 * Purpose: load agent in-context state for prompt injection or decision logic.
	 *
	 * Consumer: getContextBlocks and agent reasoning loops.
	 *
	 * @param {string} label
	 * @returns {*} stored value or null if absent/expired
	 */
	getCoreBlock(label) {
		return this._store.get(`agent:${this._name}:block:${label}`);
	}

	/**
	 * Return all core blocks as a formatted markdown string for prompt injection.
	 *
	 * Purpose: produce a ready-to-inject prompt section from all non-expired
	 * core memory blocks so LLM callers don't need to know the block schema.
	 *
	 * Consumer: prompt builders in AgentSwarm dispatch and task execution flows.
	 *
	 * @returns {string} formatted markdown section
	 */
	getContextBlocks() {
		const prefix = `agent:${this._name}:block:`;
		const entries = this._store.entries(prefix);
		if (entries.length === 0) return "";
		const lines = ["## Agent Memory Blocks", ""];
		for (const { key, value } of entries) {
			const label = key.slice(prefix.length);
			const rendered =
				typeof value === "string" ? value : JSON.stringify(value);
			lines.push(`**${label}**: ${rendered}`);
		}
		return lines.join("\n");
	}

	// ─── Archival Memory (long-term KV) ───────────────────────────────────────

	/**
	 * Store a value in archival memory.
	 *
	 * Purpose: persist long-term agent knowledge (learned facts, task history,
	 * resource pointers) that outlives individual task runs.
	 *
	 * Consumer: agent reasoning loops when surfacing durable conclusions.
	 *
	 * @param {string} key
	 * @param {*} value
	 * @param {number} [ttlMs] - optional TTL in milliseconds
	 */
	remember(key, value, ttlMs) {
		const storeKey = `agent:${this._name}:archive:${key}`;
		this._store.set(storeKey, value, ttlMs != null ? { ttlMs } : undefined);
	}

	/**
	 * Retrieve a value from archival memory.
	 *
	 * Purpose: load previously stored long-term knowledge for decision or context.
	 *
	 * Consumer: agent reasoning loops and task context assembly.
	 *
	 * @param {string} key
	 * @returns {*} stored value or null if absent/expired
	 */
	recall(key) {
		return this._store.get(`agent:${this._name}:archive:${key}`);
	}

	/**
	 * Delete a value from archival memory.
	 *
	 * Purpose: remove stale or invalidated archival knowledge to keep agent
	 * context clean and prevent outdated facts from contaminating reasoning.
	 *
	 * Consumer: agent cleanup routines and explicit memory invalidation paths.
	 *
	 * @param {string} key
	 */
	forget(key) {
		this._store.delete(`agent:${this._name}:archive:${key}`);
	}

	// ─── Recall (conversation history) ────────────────────────────────────────

	/**
	 * Return the last N inbox messages in chronological order.
	 *
	 * Purpose: provide agents with recent conversation context so they can
	 * reason over message history without loading the full inbox.
	 *
	 * Consumer: agent prompt builders and response generation loops.
	 *
	 * @param {number} [limit=20]
	 * @returns {object[]} messages sorted oldest-first
	 */
	getRecall(limit = 20) {
		const all = this._inbox.list(false);
		return all.slice(-limit);
	}

	// ─── Messaging ────────────────────────────────────────────────────────────

	/**
	 * Send a message to another agent by name.
	 *
	 * Purpose: route a message to a named agent's durable inbox so it is
	 * delivered even if the recipient is not currently running.
	 *
	 * Consumer: agent coordination flows and swarm task delegation.
	 *
	 * @param {string} toAgentName - recipient agent name (not agentId)
	 * @param {*} body
	 * @param {object} [metadata={}]
	 * @returns {string} message ID
	 */
	send(toAgentName, body, metadata = {}) {
		return this._bus.send(
			`agent:${this._name}`,
			`agent:${toAgentName}`,
			body,
			metadata,
		);
	}

	/**
	 * Send a message and wait for a reply on this agent's inbox.
	 *
	 * Purpose: implement synchronous request-reply across agent boundaries
	 * without coupling sender and receiver to a shared call stack. The reply
	 * is identified by metadata.replyTo matching the sent message ID.
	 *
	 * Consumer: coordinator-to-worker task delegation where a result is
	 * needed before the coordinator can proceed.
	 *
	 * @param {string} toAgentName
	 * @param {*} body
	 * @param {object} [opts]
	 * @param {number} [opts.timeoutMs=30000]
	 * @param {number} [opts.pollIntervalMs=500]
	 * @returns {Promise<*>} reply body
	 */
	sendAndWait(toAgentName, body, opts = {}) {
		const {
			timeoutMs = DEFAULT_SEND_AND_WAIT_TIMEOUT_MS,
			pollIntervalMs = DEFAULT_SEND_AND_WAIT_POLL_INTERVAL_MS,
		} = opts;
		const sentId = this.send(toAgentName, body, opts.metadata ?? {});
		return new Promise((resolve, reject) => {
			const deadline = Date.now() + timeoutMs;
			const interval = setInterval(() => {
				this._inbox.refresh();
				const messages = this._inbox.list(false);
				const reply = messages.find((m) => m.metadata?.replyTo === sentId);
				if (reply) {
					clearInterval(interval);
					resolve(reply.body);
					return;
				}
				if (Date.now() >= deadline) {
					clearInterval(interval);
					reject(
						new Error(
							`PersistentAgent.sendAndWait: timeout after ${timeoutMs}ms waiting for reply to ${sentId}`,
						),
					);
				}
			}, pollIntervalMs);
		});
	}

	/**
	 * Receive messages from this agent's inbox.
	 *
	 * Purpose: drain incoming messages for agent processing, with an option to
	 * filter to unread-only to avoid reprocessing.
	 *
	 * Consumer: agent polling loops and event-driven dispatch handlers.
	 *
	 * @param {boolean} [unreadOnly=true]
	 * @returns {object[]}
	 */
	receive(unreadOnly = true) {
		return this._inbox.list(unreadOnly);
	}

	/**
	 * Mark a message as read in this agent's inbox.
	 *
	 * Purpose: advance the inbox read cursor so subsequent receive(true) calls
	 * do not re-deliver already-processed messages.
	 *
	 * Consumer: agent processing loops after handling each message.
	 *
	 * @param {string} messageId
	 */
	markRead(messageId) {
		this._inbox.markRead(messageId);
	}

	/**
	 * Broadcast a message to multiple agents by name.
	 *
	 * Purpose: fan-out a notification or task to a set of agents in one call
	 * without the caller managing individual send loops.
	 *
	 * Consumer: coordinator agents distributing work to worker pools.
	 *
	 * @param {*} body
	 * @param {object} metadata
	 * @param {string[]} recipientNames - agent names (not IDs)
	 * @returns {string[]} message IDs
	 */
	broadcast(body, metadata, recipientNames) {
		return this._bus.broadcast(
			`agent:${this._name}`,
			recipientNames.map((n) => `agent:${n}`),
			body,
			metadata,
		);
	}
}
