/**
 * dispatch-layer.js — Unified Dispatch v2 entry point (M010/S02).
 *
 * Implements the 4-dimensional dispatch API from
 * `docs/plans/UNIFIED_DISPATCH_V2_PLAN.md` (Qwen Plan, 2026-05-08).
 *
 * Public API per the plan:
 *   const layer = new DispatchLayer(basePath, options);
 *   const result = await layer.dispatch({
 *     isolation,   // 'full' | 'constrained'
 *     coordination,// 'standalone' | 'managed'
 *     scope,       // 'milestone' | 'slice' | 'task' | 'inline'
 *     mode,        // 'single' | 'parallel' | 'debate' | 'chain'
 *     unitType,
 *     unitId,
 *     ... unit-specific args
 *   });
 *
 * Scope of this slice (M010/S02):
 *   - Define the DispatchLayer class with the documented signature.
 *   - Implement ONLY the `{full, managed, inline, single}` config — this
 *     is the "new headless autonomous in-process" cell per the parameter
 *     matrix (line 152). Delegates to runUnitInline (M010/S01).
 *   - All other matrix rows return a structured "not-implemented-yet" error
 *     pointing to which future slice will land them.
 *
 * Out of scope (deferred):
 *   - M010/S03 wires the autonomous-loop's swarmDispatchAndWait callsite
 *     to use this layer for inline-eligible units.
 *   - M010/S05 migrates validate-milestone + complete-milestone to use
 *     inline scope by default.
 *   - parallel / debate / chain modes (existing parallel-orchestrator
 *     keeps those paths until v2 lands the unified matrix).
 *
 * Refs:
 *   - docs/plans/UNIFIED_DISPATCH_V2_PLAN.md
 *   - .sf/REQUIREMENTS.md R013 (this slice's owning requirement)
 *   - src/resources/extensions/sf/dispatch/run-unit-inline.js (M010/S01)
 */

import { debugLog } from "../debug-logger.js";
import { runUnitInline } from "./run-unit-inline.js";

/**
 * Dispatch options validator. Returns a structured error string on bad
 * input, or null when ok.
 *
 * @param {object} opts
 * @returns {string|null}
 */
function validateDispatchOpts(opts) {
	if (!opts || typeof opts !== "object") return "options must be an object";
	const { isolation, coordination, scope, mode, unitType, unitId } = opts;
	const allowedIso = ["full", "constrained"];
	const allowedCoord = ["standalone", "managed"];
	const allowedScope = ["milestone", "slice", "task", "inline"];
	const allowedMode = ["single", "parallel", "debate", "chain"];
	if (!allowedIso.includes(isolation))
		return `isolation must be one of: ${allowedIso.join(", ")}`;
	if (!allowedCoord.includes(coordination))
		return `coordination must be one of: ${allowedCoord.join(", ")}`;
	if (!allowedScope.includes(scope))
		return `scope must be one of: ${allowedScope.join(", ")}`;
	if (!allowedMode.includes(mode))
		return `mode must be one of: ${allowedMode.join(", ")}`;
	if (!unitType || typeof unitType !== "string") return "unitType required";
	if (!unitId || typeof unitId !== "string") return "unitId required";
	return null;
}

/**
 * Encode a 4D config tuple into a stable key for routing.
 * @returns {string}
 */
function configKey({ isolation, coordination, scope, mode }) {
	return `${isolation}|${coordination}|${scope}|${mode}`;
}

/**
 * The single "implemented" cell in M010/S02's parameter matrix:
 * full + managed + inline + single → the new headless-autonomous-in-process
 * path that retires swarm-dispatch's silent-failure class.
 */
const IMPLEMENTED_CONFIG = configKey({
	isolation: "full",
	coordination: "managed",
	scope: "inline",
	mode: "single",
});

/**
 * Map of unimplemented configs → reason + future-slice owner. Used to
 * produce structured "not implemented yet" errors instead of generic
 * fallbacks, so operators see exactly which slice owns the gap.
 */
const NOT_IMPLEMENTED_OWNERS = {
	[configKey({
		isolation: "full",
		coordination: "managed",
		scope: "milestone",
		mode: "parallel",
	})]: "parallel-orchestrator.js (existing) — M010 does not replace this path",
	[configKey({
		isolation: "full",
		coordination: "managed",
		scope: "slice",
		mode: "parallel",
	})]: "slice-parallel-orchestrator.js (existing) — M010 does not replace this path",
	[configKey({
		isolation: "constrained",
		coordination: "standalone",
		scope: "inline",
		mode: "single",
	})]: "subagent tool (existing) — M010 does not replace this path",
};

export class DispatchLayer {
	/**
	 * @param {string} basePath
	 * @param {object} [options]
	 */
	constructor(basePath, options = {}) {
		if (!basePath || typeof basePath !== "string") {
			throw new Error("DispatchLayer: basePath required");
		}
		this._basePath = basePath;
		this._options = options;
		this._dispatchCount = 0;
		debugLog("dispatch-layer", {
			event: "constructed",
			basePath,
		});
	}

	/**
	 * Dispatch a single unit per the 4D config.
	 *
	 * @param {object} opts
	 * @returns {Promise<object>}
	 */
	async dispatch(opts) {
		const validationError = validateDispatchOpts(opts);
		if (validationError) {
			debugLog("dispatch-layer", {
				event: "validation-failed",
				error: validationError,
			});
			return {
				ok: false,
				exitCode: 1,
				stderr: `DispatchLayer.dispatch: ${validationError}`,
				output: "",
			};
		}

		const key = configKey(opts);
		this._dispatchCount += 1;
		debugLog("dispatch-layer", {
			event: "dispatch",
			dispatchN: this._dispatchCount,
			configKey: key,
			unitType: opts.unitType,
			unitId: opts.unitId,
		});

		if (key === IMPLEMENTED_CONFIG) {
			// The implemented cell: route to runUnitInline (M010/S01).
			return await runUnitInline(opts.unitType, opts.unitId, {
				basePath: this._basePath,
				...(opts.model ? { model: opts.model } : {}),
				...(opts.systemPrompt ? { systemPrompt: opts.systemPrompt } : {}),
				...(opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {}),
				...(opts.noOutputTimeoutMs
					? { noOutputTimeoutMs: opts.noOutputTimeoutMs }
					: {}),
				...(opts.signal ? { signal: opts.signal } : {}),
				...(opts.onEvent ? { onEvent: opts.onEvent } : {}),
				...(opts.extras ? { extras: opts.extras } : {}),
			});
		}

		// Not implemented yet: produce a structured error that names the
		// future-slice owner so operators (and downstream callers) can
		// follow the trail.
		const owner =
			NOT_IMPLEMENTED_OWNERS[key] ??
			"future M010 slice or new milestone — see UNIFIED_DISPATCH_V2_PLAN.md parameter matrix";
		debugLog("dispatch-layer", {
			event: "not-implemented",
			configKey: key,
			owner,
		});
		return {
			ok: false,
			exitCode: 99,
			stderr: `DispatchLayer.dispatch: config "${key}" is not implemented in M010/S02. Owner: ${owner}.`,
			output: "",
		};
	}

	/**
	 * Total dispatch attempts since construction. Useful for tests + audit.
	 * @returns {number}
	 */
	getDispatchCount() {
		return this._dispatchCount;
	}

	/**
	 * The configKey of the only implemented cell. Exposed for tests and
	 * for callers that want to check eligibility before dispatching.
	 * @returns {string}
	 */
	static implementedConfigKey() {
		return IMPLEMENTED_CONFIG;
	}
}
