/**
 * swarm-dispatch.js — SwarmDispatchLayer routing UOK DispatchEnvelopes through the agent swarm.
 *
 * Purpose: bridge the UOK dispatch model (envelopes with unitType, workMode, unitId) to the
 * swarm agent topology, routing each envelope to the appropriate role agent and returning
 * a structured result. Provides cached swarm access so the same AgentSwarm instance is
 * reused across dispatch calls within a process.
 *
 * Consumer: UOK kernel dispatch path, parallel orchestrators, and /sf autonomous controller
 * when SF_A2A_ENABLED is set.
 *
 * ## Current state — enqueue only, no runner
 * `_busDispatch` routes an envelope to a role agent's inbox via the MessageBus. It does NOT
 * wait for a response — the `DispatchResult` contains only `messageId` and `targetAgent`,
 * not LLM output. Nothing currently drains agent inboxes and runs LLM calls.
 *
 * This layer is ready to use once `PersistentAgent` gains a runner (see persistent-agent.js
 * module comment for the runner design). At that point `dispatch()` can be extended to
 * optionally block until the runner posts a reply to the bus.
 *
 * Callers outside uok/: none currently. The autonomous loop uses AgentSwarm directly for
 * the sleeptime memory path. Wire this in when building the autonomous orchestrator.
 */

import { AgentSwarm } from "./agent-swarm.js";
import { MessageBus } from "./message-bus.js";
import { createDefaultSwarm } from "./swarm-roles.js";
import {
	formatMemoriesForPrompt,
	getActiveMemoriesRanked,
} from "../memory-store.js";

// Module-level cache keyed by `${basePath}:${swarmName}`
const _cache = new Map();

// Lazy singleton A2ATransport — only loaded when SF_A2A_ENABLED is set.
let _a2aTransport = null;
async function getA2ATransport() {
	if (!_a2aTransport) {
		const { A2ATransport } = await import("./a2a-transport.js");
		_a2aTransport = new A2ATransport();
	}
	return _a2aTransport;
}

/**
 * @typedef {object} DispatchEnvelope
 * @property {string} unitId
 * @property {string} unitType    - 'task' | 'slice' | 'milestone' | 'eval' | 'research'
 * @property {string} workMode    - 'build' | 'repair' | 'review' | 'research' | 'coordinate'
 * @property {object} payload     - arbitrary task data
 * @property {number} priority    - 0-10
 * @property {string} scope       - project scope string
 */

/**
 * @typedef {object} DispatchResult
 * @property {string} messageId
 * @property {string} targetAgent
 * @property {string} swarmName
 * @property {DispatchEnvelope} envelope
 */

/**
 * SwarmDispatchLayer — routes DispatchEnvelopes through the agent swarm topology.
 *
 * Purpose: bridge the UOK dispatch model to the swarm agent topology, routing each
 * envelope to the appropriate role agent and returning a structured result.
 *
 * Consumer: UOK kernel dispatch path, parallel orchestrators, and /sf autonomous
 * controller when SF_A2A_ENABLED is set.
 */
export class SwarmDispatchLayer {
	/**
	 * @param {string} basePath - project root used to locate `.sf/sf.db`
	 * @param {object} [options={}]
	 * @param {string} [options.swarmName='default']
	 * @param {boolean} [options.autoCreate=true]
	 */
	constructor(basePath, options = {}) {
		this._basePath = basePath;
		this._swarmName = options.swarmName ?? "default";
		this._autoCreate = options.autoCreate !== false;
		/** @type {AgentSwarm | null} */
		this._swarm = null;
		this._bus = new MessageBus(basePath);
	}

	/**
	 * Return the cached AgentSwarm, loading or creating it as needed.
	 *
	 * Purpose: ensure a single AgentSwarm instance is reused across dispatch
	 * calls within a process to avoid redundant DB round-trips and agent
	 * re-registration on every envelope.
	 *
	 * Consumer: dispatch() and dispatchBatch() before routing envelopes.
	 *
	 * @returns {Promise<AgentSwarm>}
	 */
	async getOrCreateSwarm() {
		if (this._swarm) return this._swarm;

		let swarm = AgentSwarm.load(this._basePath, this._swarmName);

		// load() returns a new empty swarm when no saved registry exists.
		// If it has no agents and autoCreate is on, bootstrap a default topology.
		if (swarm.getAll().length === 0 && this._autoCreate) {
			const result = await createDefaultSwarm(this._basePath, {
				swarmName: this._swarmName,
			});
			swarm = result.swarm;
		}

		this._swarm = swarm;
		return this._swarm;
	}

	/**
	 * Route a single DispatchEnvelope to the appropriate swarm agent.
	 *
	 * Purpose: select the right role agent for the envelope's workMode/unitType,
	 * deliver the payload to that agent's durable inbox, and return a structured
	 * result so callers can track message delivery without knowing the swarm topology.
	 * When SF_A2A_ENABLED is set, delivery uses the A2A HTTP transport instead of
	 * the in-process SQLite MessageBus.
	 *
	 * Consumer: UOK kernel dispatch path and swarmDispatch() convenience function.
	 *
	 * @param {DispatchEnvelope} envelope
	 * @returns {Promise<DispatchResult>}
	 */
	async dispatch(envelope) {
		if (process.env.SF_A2A_ENABLED) {
			return this._a2aDispatch(envelope);
		}
		return this._busDispatch(envelope);
	}

	/**
	 * Deliver an envelope via the SQLite MessageBus (default in-process path).
	 *
	 * Purpose: preserve the existing bus-backed delivery path so all existing
	 * callers continue working unchanged when SF_A2A_ENABLED is not set.
	 *
	 * Consumer: dispatch() when SF_A2A_ENABLED is unset.
	 *
	 * @param {DispatchEnvelope} envelope
	 * @returns {Promise<DispatchResult>}
	 */
	async _busDispatch(envelope) {
		const swarm = await this.getOrCreateSwarm();
		const target = swarm.route(envelope);

		if (!target) {
			throw new Error(
				`SwarmDispatchLayer: no agent available to handle envelope unitType=${envelope.unitType} workMode=${envelope.workMode}`,
			);
		}

		// ── Memory inheritance: snapshot the parent's active memory view ──────
		// Fetch the top-30 ranked memories at dispatch time so the member agent
		// starts with the same knowledge context as the orchestrator. Snapshot
		// semantics: the member sees the world as it was when dispatched and does
		// not receive live updates mid-task. Fail-open: if getActiveMemoriesRanked
		// throws or returns nothing the dispatch still proceeds without context.
		let memoryContext = "";
		try {
			const memories = getActiveMemoriesRanked(30);
			if (memories.length > 0) {
				memoryContext = formatMemoriesForPrompt(memories, 2000, false);
			}
		} catch {
			// Memory context is additive — never block dispatch on a read failure.
		}

		const from = `dispatch:${envelope.scope}:${envelope.unitId}`;
		const to = `agent:${target.identity.name}`;
		const metadata = {
			unitId: envelope.unitId,
			unitType: envelope.unitType,
			workMode: envelope.workMode,
			...(memoryContext ? { memoryContext } : {}),
		};

		const messageId = this._bus.send(from, to, envelope.payload, metadata);

		return {
			messageId,
			targetAgent: target.identity.name,
			swarmName: this._swarmName,
			envelope,
			...(memoryContext ? { memoryContext } : {}),
		};
	}

	/**
	 * Deliver an envelope to a spawned agent process over the A2A HTTP protocol.
	 *
	 * Purpose: route the envelope to the correct role agent running in a separate
	 * process, using A2ATransport to spawn the process on first use and the A2A
	 * JSON-RPC client for delivery. SQLite state is updated by the remote process.
	 *
	 * Consumer: dispatch() when SF_A2A_ENABLED is set.
	 *
	 * @param {DispatchEnvelope} envelope
	 * @returns {Promise<DispatchResult>}
	 */
	async _a2aDispatch(envelope) {
		const swarm = await this.getOrCreateSwarm();
		const target = swarm.route(envelope);

		if (!target) {
			throw new Error(
				`SwarmDispatchLayer(a2a): no agent available to handle envelope unitType=${envelope.unitType} workMode=${envelope.workMode}`,
			);
		}

		const { name: agentName, role } = target.identity;
		const transport = await getA2ATransport();
		const agentUrl = await transport.getOrSpawnAgent(
			agentName,
			role,
			this._basePath,
		);

		const response = await transport.dispatch(agentUrl, envelope);

		// Extract messageId from the A2A response if present.
		let messageId = `a2a-${Date.now()}`;
		try {
			const parts = response?.parts ?? [];
			for (const part of parts) {
				const data =
					part?.data ??
					(part?.kind === "text" ? JSON.parse(part.text ?? "{}") : null);
				if (data?.messageId) {
					messageId = data.messageId;
					break;
				}
			}
		} catch {
			// leave messageId as default
		}

		return {
			messageId,
			targetAgent: agentName,
			swarmName: this._swarmName,
			envelope,
			transport: "a2a",
			agentUrl,
		};
	}

	/**
	 * Route multiple DispatchEnvelopes through the swarm in parallel.
	 *
	 * Purpose: fan-out a batch of work units to the swarm concurrently so
	 * throughput scales with the number of envelopes rather than serializing them.
	 *
	 * Consumer: parallel orchestrators and autonomous dispatch batching loops.
	 *
	 * @param {DispatchEnvelope[]} envelopes
	 * @returns {Promise<DispatchResult[]>}
	 */
	async dispatchBatch(envelopes) {
		return Promise.all(envelopes.map((envelope) => this.dispatch(envelope)));
	}

	/**
	 * Return a structured view of the swarm topology by role.
	 *
	 * Purpose: expose the swarm role snapshot so callers can inspect routing
	 * capacity without reaching into the AgentSwarm directly.
	 *
	 * Consumer: diagnostic tooling, health checks, and /sf swarm status.
	 *
	 * @returns {Promise<{ coordinator: import('./persistent-agent.js').PersistentAgent[], workers: import('./persistent-agent.js').PersistentAgent[], scouts: import('./persistent-agent.js').PersistentAgent[], reviewers: import('./persistent-agent.js').PersistentAgent[], all: import('./persistent-agent.js').PersistentAgent[] }>}
	 */
	async getTopology() {
		const swarm = await this.getOrCreateSwarm();
		return swarm.getTopology();
	}

	/**
	 * Return or create a SwarmDispatchLayer for the given basePath and swarmName.
	 *
	 * Purpose: provide a process-level singleton factory so callers that don't
	 * manage a SwarmDispatchLayer instance directly share the same cached swarm
	 * and avoid redundant initialization on repeated dispatch calls.
	 *
	 * Consumer: swarmDispatch() convenience function and any caller that wants
	 * a default dispatch layer without explicit lifecycle management.
	 *
	 * @param {string} basePath
	 * @param {object} [options={}]
	 * @param {string} [options.swarmName='default']
	 * @param {boolean} [options.autoCreate=true]
	 * @returns {SwarmDispatchLayer}
	 */
	static getOrCreate(basePath, options = {}) {
		const key = `${basePath}:${options.swarmName ?? "default"}`;
		if (!_cache.has(key)) {
			_cache.set(key, new SwarmDispatchLayer(basePath, options));
		}
		return _cache.get(key);
	}
}

/**
 * Route a single DispatchEnvelope through the default swarm for basePath.
 *
 * Purpose: one-liner entry point for dispatch callers that don't need to manage
 * a SwarmDispatchLayer instance directly.
 *
 * Consumer: UOK kernel dispatch integration, test helpers.
 *
 * @param {string} basePath
 * @param {DispatchEnvelope} envelope
 * @returns {Promise<DispatchResult>}
 */
export async function swarmDispatch(basePath, envelope) {
	return SwarmDispatchLayer.getOrCreate(basePath).dispatch(envelope);
}
