/**
 * a2a-transport.js — A2A protocol transport for swarm dispatch.
 *
 * Purpose: when SF_A2A_ENABLED=1, SwarmDispatchLayer uses this module instead
 * of the in-process SQLite MessageBus to dispatch DispatchEnvelopes to separate
 * agent processes over the Agent2Agent HTTP protocol. SQLite coordination state
 * is retained for durability; A2A is strictly the wire transport.
 *
 * Consumer: SwarmDispatchLayer._a2aDispatch() when SF_A2A_ENABLED is set.
 */

import { spawn } from "node:child_process";
import { randomUUID } from "node:crypto";

const A2A_AGENT_SERVER_PATH = new URL("./a2a-agent-server.js", import.meta.url)
	.pathname;

const AGENT_READY_TIMEOUT_MS = 15_000;
const BASE_PORT = 34500;
let _portCounter = BASE_PORT;

function nextPort() {
	return ++_portCounter;
}

/**
 * Build an A2A AgentCard descriptor for a named swarm role agent.
 *
 * Purpose: produce the minimal AgentCard structure required by the A2A SDK so
 * the server endpoint can advertise its identity and capabilities to clients
 * without requiring hand-authored config files.
 *
 * Consumer: A2ATransport.spawnAgent and a2a-agent-server.js at startup.
 *
 * @param {string} name - stable agent name (e.g. 'worker-1')
 * @param {string} role - agent role (coordinator|worker|scout|reviewer|planner|verifier|scribe|adversary)
 * @param {number} port - HTTP port the agent server will listen on
 * @returns {object} A2A AgentCard
 */
export function buildAgentCard(name, role, port) {
	const baseUrl = `http://localhost:${port}`;
	const roleSkillMap = {
		coordinator: {
			description: "Orchestrates tasks across worker pool",
			tags: ["coordinate", "route"],
		},
		worker: {
			description: "Executes build and repair tasks",
			tags: ["build", "repair"],
		},
		scout: {
			description: "Discovers and surfaces information",
			tags: ["research", "discover"],
		},
		reviewer: {
			description: "Critiques worker output against intent",
			tags: ["review", "critique"],
		},
		planner: {
			description: "Generates milestone and task contracts",
			tags: ["plan", "design"],
		},
		verifier: {
			description: "Runs gate checks and evidence validation",
			tags: ["verify", "validate"],
		},
		scribe: {
			description: "Writes and exports documentation",
			tags: ["document", "export"],
		},
		adversary: {
			description: "Red-teams plans and decisions",
			tags: ["challenge", "critique"],
		},
	};

	const skill = roleSkillMap[role] ?? {
		description: `SF agent: ${role}`,
		tags: [role],
	};

	return {
		name: `SF ${name}`,
		description: `Singularity Forge swarm agent — role: ${role}. ${skill.description}.`,
		protocolVersion: "0.3.0",
		version: "1.0.0",
		url: `${baseUrl}/a2a/jsonrpc`,
		skills: [
			{
				id: role,
				name: role,
				description: skill.description,
				tags: skill.tags,
			},
		],
		capabilities: {
			pushNotifications: false,
		},
		defaultInputModes: ["application/json"],
		defaultOutputModes: ["application/json"],
	};
}

/**
 * A2ATransport — spawns agent subprocesses and dispatches envelopes via HTTP A2A protocol.
 *
 * Purpose: provide real cross-process dispatch when SF_A2A_ENABLED=1 so each
 * swarm agent runs in an isolated Node.js process with its own context window.
 * Process lifecycle is managed here; callers interact only through dispatch().
 *
 * Consumer: SwarmDispatchLayer._a2aDispatch() in swarm-dispatch.js.
 */
export class A2ATransport {
	constructor() {
		/** @type {Map<string, { url: string, pid: number, process: import('child_process').ChildProcess }>} */
		this._registry = new Map();
	}

	/**
	 * Spawn a new agent subprocess running an A2A HTTP server.
	 *
	 * Purpose: launch an isolated pi/sf agent process that serves A2A JSON-RPC
	 * requests on a dynamically allocated port, wait for it to signal readiness,
	 * and register it in the local process registry.
	 *
	 * Consumer: getOrSpawnAgent() when no cached entry exists for the agent name.
	 *
	 * @param {string} agentName - stable routing name (e.g. 'worker-1')
	 * @param {string} role - agent role
	 * @param {string} basePath - project root for SQLite state
	 * @returns {Promise<string>} the agent's A2A JSON-RPC endpoint URL
	 */
	async spawnAgent(agentName, role, basePath) {
		const port = nextPort();
		const env = {
			...process.env,
			SF_A2A_AGENT_NAME: agentName,
			SF_A2A_AGENT_ROLE: role,
			SF_A2A_PORT: String(port),
			SF_A2A_BASE_PATH: basePath,
		};

		const child = spawn(process.execPath, [A2A_AGENT_SERVER_PATH], {
			env,
			cwd: basePath,
			stdio: ["ignore", "pipe", "pipe"],
		});

		const url = await new Promise((resolve, reject) => {
			const timeout = setTimeout(() => {
				child.kill("SIGTERM");
				reject(
					new Error(
						`A2ATransport: agent ${agentName} did not signal readiness within ${AGENT_READY_TIMEOUT_MS}ms`,
					),
				);
			}, AGENT_READY_TIMEOUT_MS);

			let buffer = "";
			child.stdout.on("data", (chunk) => {
				buffer += chunk.toString();
				const lines = buffer.split("\n");
				buffer = lines.pop() ?? "";
				for (const line of lines) {
					const trimmed = line.trim();
					if (!trimmed) continue;
					try {
						const msg = JSON.parse(trimmed);
						if (msg.ready && msg.port) {
							clearTimeout(timeout);
							resolve(`http://localhost:${msg.port}/a2a/jsonrpc`);
						}
					} catch {
						// non-JSON stdout line — ignore
					}
				}
			});

			child.on("error", (err) => {
				clearTimeout(timeout);
				reject(
					new Error(
						`A2ATransport: failed to spawn agent ${agentName}: ${err.message}`,
					),
				);
			});

			child.on("exit", (code) => {
				clearTimeout(timeout);
				this._registry.delete(agentName);
				if (code !== 0 && code !== null) {
					reject(
						new Error(
							`A2ATransport: agent ${agentName} exited with code ${code} before signalling readiness`,
						),
					);
				}
			});
		});

		this._registry.set(agentName, { url, pid: child.pid, process: child });

		child.stderr.on("data", (chunk) => {
			// Suppress stderr from agent subprocesses unless debugging
			if (process.env.SF_A2A_DEBUG) {
				process.stderr.write(`[a2a:${agentName}] ${chunk}`);
			}
		});

		return url;
	}

	/**
	 * Return the cached agent endpoint URL, spawning the process if needed.
	 *
	 * Purpose: ensure each named agent has at most one live process so dispatch
	 * calls share a persistent agent context rather than forking a new process
	 * per envelope.
	 *
	 * Consumer: dispatch() on every envelope send.
	 *
	 * @param {string} agentName
	 * @param {string} role
	 * @param {string} basePath
	 * @returns {Promise<string>} endpoint URL
	 */
	async getOrSpawnAgent(agentName, role, basePath) {
		const entry = this._registry.get(agentName);
		if (entry) return entry.url;
		return this.spawnAgent(agentName, role, basePath);
	}

	/**
	 * Send a DispatchEnvelope to a live agent endpoint via A2A JSON-RPC.
	 *
	 * Purpose: deliver a single envelope over the A2A wire protocol and return
	 * the agent's JSON response body so the SwarmDispatchLayer can record the
	 * result without knowing the HTTP transport details.
	 *
	 * Consumer: SwarmDispatchLayer._a2aDispatch() after resolving the agent URL.
	 *
	 * @param {string} agentUrl - the agent's JSON-RPC endpoint URL
	 * @param {import('./swarm-dispatch.js').DispatchEnvelope} envelope
	 * @returns {Promise<object>} task result from the agent
	 */
	async dispatch(agentUrl, envelope) {
		// Dynamically import A2A client to avoid loading it unless A2A mode is active.
		const { ClientFactory } = await import("@a2a-js/sdk/client");

		// Derive base URL by stripping the /a2a/jsonrpc suffix for agent card resolution.
		const baseUrl = agentUrl.replace(/\/a2a\/jsonrpc$/, "");
		const factory = new ClientFactory();
		const client = await factory.createFromUrl(baseUrl);

		const messageId = randomUUID();
		const sendParams = {
			message: {
				messageId,
				role: "user",
				kind: "message",
				parts: [
					{
						kind: "data",
						data: envelope,
						metadata: {
							contentType: "application/json",
						},
					},
				],
			},
		};

		const response = await client.sendMessage(sendParams);
		return response;
	}

	/**
	 * Terminate all spawned agent processes.
	 *
	 * Purpose: clean up child processes on shutdown so they don't linger after
	 * the parent process exits.
	 *
	 * Consumer: process exit handlers and test teardown.
	 */
	shutdown() {
		for (const [_name, entry] of this._registry) {
			try {
				entry.process.kill("SIGTERM");
			} catch {
				// already dead
			}
		}
		this._registry.clear();
	}
}
