/**
 * MiActionExecutor — Executes parsed MI actions against the live canvas
 * and module APIs.
 *
 * Canvas actions use `window.__canvasApi` exposed by canvas.html.
 * Module actions (create-content, etc.) make authenticated fetch calls to
 * the module's API endpoints via the routing map in mi-module-routes.ts.
 */

import type { MiAction } from "./mi-actions";
import { resolveModuleRoute } from "./mi-module-routes";

export interface ExecutionResult {
	action: MiAction;
	ok: boolean;
	shapeId?: string;
	/** ID returned by module APIs (e.g. created event ID) */
	contentId?: string;
	error?: string;
}

/** Progress callback for scaffold/batch execution. */
export type ProgressCallback = (current: number, total: number, label: string) => void;

interface CanvasApi {
	newShape: (tagName: string, props?: Record<string, any>) => any;
	findFreePosition: (width: number, height: number) => { x: number; y: number };
	SHAPE_DEFAULTS: Record<string, { width: number; height: number }>;
	setupShapeEventListeners: (shape: any) => void;
	sync: any;
	canvasContent: HTMLElement;
}

function getCanvasApi(): CanvasApi | null {
	return (window as any).__canvasApi || null;
}

/** Resolve `$N` backreferences in an id string. */
function resolveRef(id: string, refMap: Map<string, string>): string {
	if (id.startsWith("$")) {
		return refMap.get(id) || id;
	}
	return id;
}

export class MiActionExecutor {
	static instance: MiActionExecutor | null = null;
	#space = "";
	#token = "";

	constructor() {
		if (MiActionExecutor.instance) return MiActionExecutor.instance;
		MiActionExecutor.instance = this;
	}

	/** Set the current space and auth token for module API calls. */
	setContext(space: string, token: string): void {
		this.#space = space;
		this.#token = token;
	}

	execute(actions: MiAction[], onProgress?: ProgressCallback): ExecutionResult[] {
		const results: ExecutionResult[] = [];
		const refMap = new Map<string, string>();

		for (let i = 0; i < actions.length; i++) {
			const action = actions[i];
			onProgress?.(i + 1, actions.length, this.#actionLabel(action));
			try {
				const result = this.#executeOne(action, refMap);
				results.push(result);
			} catch (e: any) {
				results.push({ action, ok: false, error: e.message });
			}
		}
		return results;
	}

	/** Execute actions that may include async module API calls. */
	async executeAsync(actions: MiAction[], onProgress?: ProgressCallback): Promise<ExecutionResult[]> {
		const results: ExecutionResult[] = [];
		const refMap = new Map<string, string>();

		for (let i = 0; i < actions.length; i++) {
			const action = actions[i];
			onProgress?.(i + 1, actions.length, this.#actionLabel(action));
			try {
				const result = await this.#executeOneAsync(action, refMap);
				results.push(result);
			} catch (e: any) {
				results.push({ action, ok: false, error: e.message });
			}
		}
		return results;
	}

	#actionLabel(action: MiAction): string {
		switch (action.type) {
			case "create-shape": return `Creating ${action.tagName}`;
			case "create-content": return `Creating ${action.contentType} in ${action.module}`;
			case "scaffold": return `Running "${action.name}"`;
			case "batch": return `Executing batch`;
			default: return action.type;
		}
	}

	#executeOne(action: MiAction, refMap: Map<string, string>): ExecutionResult {
		switch (action.type) {
			case "create-shape":
			case "update-shape":
			case "delete-shape":
			case "connect":
			case "move-shape":
			case "transform":
			case "navigate":
				return this.#executeCanvasAction(action, refMap);

			// Module/composite actions need async — return immediately with a note
			case "create-content":
			case "update-content":
			case "delete-content":
			case "enable-module":
			case "disable-module":
			case "configure-module":
			case "scaffold":
			case "batch":
				return { action, ok: false, error: "Use executeAsync() for module actions" };

			default:
				return { action, ok: false, error: "Unknown action type" };
		}
	}

	async #executeOneAsync(action: MiAction, refMap: Map<string, string>): Promise<ExecutionResult> {
		switch (action.type) {
			// Canvas actions are sync
			case "create-shape":
			case "update-shape":
			case "delete-shape":
			case "connect":
			case "move-shape":
			case "transform":
			case "navigate":
				return this.#executeCanvasAction(action, refMap);

			// Module content actions
			case "create-content":
				return this.#executeCreateContent(action, refMap);
			case "update-content":
				return this.#executeUpdateContent(action);
			case "delete-content":
				return this.#executeDeleteContent(action);

			// Server-side actions (media gen, data queries) — delegate to server
			case "generate-image":
			case "generate-video":
			case "query-content":
				return this.#executeServerAction(action);

			// Admin actions
			case "enable-module":
			case "disable-module":
			case "configure-module":
				return { action, ok: false, error: "Admin actions not yet implemented" };

			// Composite
			case "scaffold":
				return this.#executeScaffold(action, refMap);
			case "batch":
				return this.#executeBatch(action, refMap);

			default:
				return { action, ok: false, error: "Unknown action type" };
		}
	}

	/** Delegate server-side actions to the MI API (fallback for non-agentic path). */
	async #executeServerAction(action: MiAction): Promise<ExecutionResult> {
		try {
			const res = await fetch("/api/mi/execute-server-action", {
				method: "POST",
				headers: {
					"Content-Type": "application/json",
					...(this.#token ? { Authorization: `Bearer ${this.#token}` } : {}),
				},
				body: JSON.stringify({ action, space: this.#space }),
			});

			if (!res.ok) {
				const err = await res.json().catch(() => ({ error: "Request failed" }));
				return { action, ok: false, error: err.error || `HTTP ${res.status}` };
			}

			const data = await res.json();
			return { action, ok: data.ok !== false, contentId: data.url || data.summary };
		} catch (e: any) {
			return { action, ok: false, error: e.message };
		}
	}

	#executeCanvasAction(action: MiAction, refMap: Map<string, string>): ExecutionResult {
		const api = getCanvasApi();
		if (!api) {
			return { action, ok: false, error: "Canvas API not available" };
		}

		switch (action.type) {
			case "create-shape": {
				const shape = api.newShape(action.tagName, action.props || {});
				if (!shape) {
					return { action, ok: false, error: `Failed to create ${action.tagName}` };
				}
				if (action.ref) {
					refMap.set(action.ref, shape.id);
				}
				return { action, ok: true, shapeId: shape.id };
			}

			case "update-shape": {
				const el = api.canvasContent.querySelector(`#${CSS.escape(action.shapeId)}`);
				if (!el) {
					return { action, ok: false, error: `Shape ${action.shapeId} not found` };
				}
				for (const [key, value] of Object.entries(action.fields)) {
					(el as any)[key] = value;
				}
				api.sync.updateShape?.(action.shapeId);
				return { action, ok: true, shapeId: action.shapeId };
			}

			case "delete-shape": {
				const el = api.canvasContent.querySelector(`#${CSS.escape(action.shapeId)}`);
				if (!el) {
					return { action, ok: false, error: `Shape ${action.shapeId} not found` };
				}
				api.sync.deleteShape(action.shapeId);
				el.remove();
				return { action, ok: true, shapeId: action.shapeId };
			}

			case "connect": {
				const sourceId = resolveRef(action.sourceId, refMap);
				const targetId = resolveRef(action.targetId, refMap);

				const arrow = document.createElement("folk-arrow");
				const arrowId = `arrow-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
				arrow.id = arrowId;
				(arrow as any).sourceId = sourceId;
				(arrow as any).targetId = targetId;
				if (action.color) (arrow as any).color = action.color;

				api.canvasContent.appendChild(arrow);
				api.sync.registerShape(arrow);
				return { action, ok: true, shapeId: arrowId };
			}

			case "move-shape": {
				const el = api.canvasContent.querySelector(`#${CSS.escape(action.shapeId)}`);
				if (!el) {
					return { action, ok: false, error: `Shape ${action.shapeId} not found` };
				}
				(el as any).x = action.x;
				(el as any).y = action.y;
				api.sync.updateShape?.(action.shapeId);
				return { action, ok: true, shapeId: action.shapeId };
			}

			case "transform": {
				const transforms = (window as any).__miSelectionTransforms;
				if (transforms && transforms[action.transform]) {
					const shapeEls = action.shapeIds
						.map((id) => api.canvasContent.querySelector(`#${CSS.escape(id)}`))
						.filter(Boolean) as HTMLElement[];
					if (shapeEls.length === 0) {
						return { action, ok: false, error: "No matching shapes found" };
					}
					transforms[action.transform](shapeEls);
					for (const el of shapeEls) {
						api.sync.updateShape?.(el.id);
					}
					return { action, ok: true };
				}
				return { action, ok: false, error: `Unknown transform: ${action.transform}` };
			}

			case "navigate": {
				window.location.href = action.path;
				return { action, ok: true };
			}

			default:
				return { action, ok: false, error: "Unknown canvas action" };
		}
	}

	// ── Module content actions ──

	async #executeCreateContent(
		action: Extract<MiAction, { type: "create-content" }>,
		refMap: Map<string, string>,
	): Promise<ExecutionResult> {
		const route = resolveModuleRoute(action.module, action.contentType, this.#space);
		if (!route) {
			return { action, ok: false, error: `No route for ${action.module}/${action.contentType}` };
		}

		try {
			const res = await fetch(route.url, {
				method: route.method,
				headers: {
					"Content-Type": "application/json",
					...(this.#token ? { Authorization: `Bearer ${this.#token}` } : {}),
				},
				body: JSON.stringify(action.body),
			});

			if (!res.ok) {
				const err = await res.json().catch(() => ({ error: "Request failed" }));
				return { action, ok: false, error: err.error || `HTTP ${res.status}` };
			}

			const data = await res.json().catch(() => ({}));
			const contentId = data.id || data.wizardId || data._id || undefined;
			if (action.ref && contentId) {
				refMap.set(action.ref, contentId);
			}

			// Navigate to wizard if requested (rsocials campaign creation)
			if (action.body?.navigateToWizard && data.url) {
				window.location.href = data.url;
			}

			return { action, ok: true, contentId };
		} catch (e: any) {
			return { action, ok: false, error: e.message };
		}
	}

	async #executeUpdateContent(
		action: Extract<MiAction, { type: "update-content" }>,
	): Promise<ExecutionResult> {
		const route = resolveModuleRoute(action.module, action.contentType, this.#space);
		if (!route) {
			return { action, ok: false, error: `No route for ${action.module}/${action.contentType}` };
		}

		try {
			const url = `${route.url}/${encodeURIComponent(action.id)}`;
			const res = await fetch(url, {
				method: "PATCH",
				headers: {
					"Content-Type": "application/json",
					...(this.#token ? { Authorization: `Bearer ${this.#token}` } : {}),
				},
				body: JSON.stringify(action.body),
			});

			if (!res.ok) {
				const err = await res.json().catch(() => ({ error: "Request failed" }));
				return { action, ok: false, error: err.error || `HTTP ${res.status}` };
			}
			return { action, ok: true, contentId: action.id };
		} catch (e: any) {
			return { action, ok: false, error: e.message };
		}
	}

	async #executeDeleteContent(
		action: Extract<MiAction, { type: "delete-content" }>,
	): Promise<ExecutionResult> {
		const route = resolveModuleRoute(action.module, action.contentType, this.#space);
		if (!route) {
			return { action, ok: false, error: `No route for ${action.module}/${action.contentType}` };
		}

		try {
			const url = `${route.url}/${encodeURIComponent(action.id)}`;
			const res = await fetch(url, {
				method: "DELETE",
				headers: {
					...(this.#token ? { Authorization: `Bearer ${this.#token}` } : {}),
				},
			});

			if (!res.ok) {
				const err = await res.json().catch(() => ({ error: "Request failed" }));
				return { action, ok: false, error: err.error || `HTTP ${res.status}` };
			}
			return { action, ok: true, contentId: action.id };
		} catch (e: any) {
			return { action, ok: false, error: e.message };
		}
	}

	// ── Composite actions ──

	async #executeScaffold(
		action: Extract<MiAction, { type: "scaffold" }>,
		refMap: Map<string, string>,
	): Promise<ExecutionResult> {
		const stepResults: ExecutionResult[] = [];
		for (const step of action.steps) {
			const result = await this.#executeOneAsync(step, refMap);
			stepResults.push(result);
			if (!result.ok) {
				return {
					action,
					ok: false,
					error: `Scaffold "${action.name}" failed at step: ${result.error}`,
				};
			}
		}
		return { action, ok: true };
	}

	async #executeBatch(
		action: Extract<MiAction, { type: "batch" }>,
		refMap: Map<string, string>,
	): Promise<ExecutionResult> {
		const results: ExecutionResult[] = [];
		for (const sub of action.actions) {
			results.push(await this.#executeOneAsync(sub, refMap));
		}
		const failures = results.filter((r) => !r.ok);
		if (failures.length) {
			return {
				action,
				ok: false,
				error: `${failures.length}/${results.length} actions failed`,
			};
		}
		return { action, ok: true };
	}
}
