/**
 * agy-pi — Google Antigravity CLI (agy) come subagent multimodale dentro pi.
 *
 * Espone un set completo di strumenti specializzati per generazione ed editing
 * di immagini, analisi di file, trascrizione audio, analisi video e gestione
 * conversazione. Ogni strumento costruisce un prompt ottimizzato secondo le
 * best practice di Gemini/Nano Banana (Keep+Change+Add+Render, un cambiamento
 * per turno, ri-attacco dell'immagine base, ecc.).
 *
 * L'agente (pi) decide quale strumento usare in base al task.
 */

import { execFile, spawn } from "node:child_process";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { promisify } from "node:util";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";

const execFileAsync = promisify(execFile);

// ---------------------------------------------------------------------------
// Configurazione
// ---------------------------------------------------------------------------
const AGY_CHAT_DIR = path.join(os.homedir(), ".agy-chat");
const STATE_FILE = path.join(AGY_CHAT_DIR, "conversation_id");
const CONV_DIR = path.join(os.homedir(), ".gemini", "antigravity-cli", "conversations");

const DEFAULT_TIMEOUT_MS = 180_000; // 3 min
const IMAGE_TIMEOUT_MS = 300_000; // 5 min

// ---------------------------------------------------------------------------
// Helper: trovare il binario agy
// ---------------------------------------------------------------------------
function findAgy(): string {
	const candidates = [
		process.env.AGY_BIN,
		path.join(os.homedir(), ".local", "bin", "agy"),
		"agy",
	].filter(Boolean) as string[];
	for (const c of candidates) {
		if (c === "agy") return c;
		if (fs.existsSync(c)) return c;
	}
	return "agy";
}

// ---------------------------------------------------------------------------
// Helper: ultimo conversation_id
// ---------------------------------------------------------------------------
function getLatestConversationId(): string | null {
	try {
		if (!fs.existsSync(CONV_DIR)) return null;
		const files = fs
			.readdirSync(CONV_DIR)
			.filter((f) => f.endsWith(".db"))
			.map((f) => path.join(CONV_DIR, f))
			.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
		if (files.length === 0) return null;
		return path.basename(files[0], ".db");
	} catch {
		return null;
	}
}

function readState(): string | null {
	try {
		return fs.readFileSync(STATE_FILE, "utf8").trim() || null;
	} catch {
		return null;
	}
}

function writeState(id: string) {
	try {
		fs.mkdirSync(AGY_CHAT_DIR, { recursive: true });
		fs.writeFileSync(STATE_FILE, id, "utf8");
	} catch {
		/* ignora */
	}
}

function resetState() {
	try {
		fs.rmSync(STATE_FILE, { force: true });
	} catch {
		/* ignora */
	}
}

// ---------------------------------------------------------------------------
// Helper: eseguire agy
// ---------------------------------------------------------------------------
interface RunResult {
	output: string;
	error: string | null;
	exitCode: number;
}

async function runAgy(
	args: string[],
	timeoutMs: number,
	signal?: AbortSignal,
): Promise<RunResult> {
	const bin = findAgy();
	// --output-format json: agy non scrive nulla su stdout quando è piped/redirected
	// (bug #76). Il formato json emette un oggetto con la risposta in .response.
	const fullArgs = [...args, "--output-format", "json"];
	try {
		const { stdout, stderr } = await execFileAsync(bin, fullArgs, {
			timeout: timeoutMs,
			maxBuffer: 20 * 1024 * 1024,
			signal,
			env: { ...process.env, PATH: `${path.join(os.homedir(), ".local", "bin")}:${process.env.PATH ?? ""}` },
		});
		// estrai la risposta dal JSON
		let output = stdout;
		try {
			const parsed = JSON.parse(stdout);
			if (parsed && typeof parsed.response === "string") {
				output = parsed.response;
			}
		} catch {
			/* non-JSON: usa stdout grezzo */
		}
		return { output, error: stderr || null, exitCode: 0 };
	} catch (err: any) {
		const code = typeof err.code === "number" ? err.code : 1;
		return {
			output: err.stdout || "",
			error: err.stderr || err.message || String(err),
			exitCode: code,
		};
	}
}

// ---------------------------------------------------------------------------
// Helper: estrarre il percorso immagine dall'output
// ---------------------------------------------------------------------------
function extractImagePath(text: string): string | undefined {
	const m =
		text.match(/IMAGE_PATH:\s*(\S+)/i) ||
		text.match(/(\/[^\s]+\.(?:png|jpe?g|webp|gif))/i);
	return m ? m[1] : undefined;
}

// ---------------------------------------------------------------------------
// Helper: copiare l'immagine generata in una cartella di output
// ---------------------------------------------------------------------------
function copyImage(src: string | undefined, outputDir?: string): string | undefined {
	if (!src || !outputDir) return src;
	try {
		fs.mkdirSync(outputDir, { recursive: true });
		const dest = path.join(outputDir, path.basename(src));
		fs.copyFileSync(src, dest);
		return dest;
	} catch {
		return src;
	}
}

// ---------------------------------------------------------------------------
// Helper: esecuzione comune di un tool agy
// ---------------------------------------------------------------------------
interface AgyExecOptions {
	prompt: string;
	mode?: "chat" | "image" | "analyze";
	model?: string;
	effort?: "low" | "medium" | "high";
	newConversation?: boolean;
	stateless?: boolean; // non continua la conversazione (ri-attacca l'immagine base)
	addDirs?: string[];
	filePaths?: string[];
	yolo?: boolean;
	timeoutMs?: number;
	outputDir?: string;
	signal?: AbortSignal;
}

async function executeAgy(opts: AgyExecOptions) {
	const args: string[] = ["-p", opts.prompt];

	// add-dir per i file
	const dirs = new Set<string>();
	for (const d of opts.addDirs ?? []) if (d) dirs.add(d);
	for (const f of opts.filePaths ?? []) if (f) dirs.add(path.dirname(f));
	for (const d of dirs) args.push("--add-dir", d);

	// conversazione
	const useState = !opts.stateless && !opts.newConversation;
	let convId: string | null = null;
	if (opts.newConversation) convId = null;
	else if (opts.stateless) convId = null;
	else convId = readState();
	if (convId) args.push("--conversation", convId);

	if (opts.model) args.push("--model", opts.model);
	if (opts.effort) args.push("--effort", opts.effort);
	if (opts.yolo) args.push("--dangerously-skip-permissions");

	const timeout = opts.timeoutMs ?? (opts.mode === "image" ? IMAGE_TIMEOUT_MS : DEFAULT_TIMEOUT_MS);
	const r = await runAgy(args, timeout, opts.signal);

	// aggiorna stato conversazione (solo se non stateless)
	if (r.exitCode === 0 && !opts.stateless) {
		const latest = getLatestConversationId();
		if (latest) writeState(latest);
	}

	let text = r.output.trim();
	if (r.error) {
		const err = r.error
			.split("\n")
			.filter((l) => !/logging before google\.Init/i.test(l))
			.join("\n")
			.trim();
		if (err && !text) text = err;
	}

	const imagePath = copyImage(extractImagePath(text), opts.outputDir);
	if (imagePath && opts.outputDir) {
		text += `\n\n[immagine copiata in: ${imagePath}]`;
	}

	return {
		text,
		imagePath,
		conversationId: convId,
		exitCode: r.exitCode,
	};
}

// ---------------------------------------------------------------------------
// Helper: costruire il suffisso "IMAGE_PATH" per i tool immagine
// ---------------------------------------------------------------------------
const IMG_SUFFIX =
	"\n\nAlla fine della risposta, scrivi su una riga esattamente: IMAGE_PATH: <percorso assoluto dell'immagine generata>";

// ---------------------------------------------------------------------------
// Registrazione microfono (F12) e trascrizione
// ---------------------------------------------------------------------------
const MAX_RECORD_MS = 120_000; // 2 min

interface Recording {
	proc: ReturnType<typeof spawn>;
	file: string;
}

let recording: Recording | null = null;

function startRecording(): string {
	const file = path.join(os.tmpdir(), `agy-rec-${Date.now()}.wav`);
	const proc = spawn(
		"ffmpeg",
		["-y", "-f", "pulse", "-i", "default", "-ac", "1", "-ar", "16000", "-t", "120", file],
		{ stdio: "ignore" },
	);
	recording = { proc, file };
	// se ffmpeg termina da solo (timeout 2 min), azzera lo stato
	proc.on("exit", () => {
		if (recording && recording.proc === proc) recording = null;
	});
	return file;
}

function stopRecording(): Promise<string | null> {
	return new Promise((resolve) => {
		if (!recording) return resolve(null);
		const { proc, file } = recording;
		recording = null;
		let done = false;
		const finish = () => {
			if (!done) {
				done = true;
				resolve(file);
			}
		};
		proc.on("exit", finish);
		proc.kill("SIGINT"); // ffmpeg finalizza il file
		setTimeout(finish, 3000); // fallback
	});
}

// Ottimizza l'audio: taglia il silenzio iniziale/finale e converte in MP3 (più piccolo)
async function optimizeAudio(input: string): Promise<string> {
	const output = input.replace(/\.wav$/, ".mp3");
	try {
		await execFileAsync(
			"ffmpeg",
			[
				"-y",
				"-i",
				input,
				"-af",
				"silenceremove=start_periods=1:start_threshold=-50dB:start_silence=0.5,areverse,silenceremove=start_periods=1:start_threshold=-50dB:start_silence=0.5,areverse",
				"-c:a",
				"libmp3lame",
				"-q:a",
				"4",
				output,
			],
			{ timeout: 30_000 },
		);
		return output;
	} catch {
		return input; // fallback al file originale
	}
}

function extractText(content: any): string {
	if (typeof content === "string") return content;
	if (Array.isArray(content)) {
		return content
			.filter((p: any) => p && p.type === "text" && typeof p.text === "string")
			.map((p: any) => p.text)
			.join(" ");
	}
	return "";
}

function getConversationContext(ctx: any, maxEntries = 8): string {
	try {
		const entries = ctx.sessionManager.getEntries();
		const recent = entries.slice(-maxEntries);
		const lines: string[] = [];
		for (const e of recent) {
			if (e.type !== "message" || !e.message) continue;
			const text = extractText(e.message.content);
			if (!text) continue;
			const who =
				e.message.role === "user"
					? "Utente"
					: e.message.role === "assistant"
						? "Assistente"
						: "Strumento";
			lines.push(`${who}: ${text}`);
		}
		return lines.join("\n");
	} catch {
		return "";
	}
}

// ---------------------------------------------------------------------------
// Estensione
// ---------------------------------------------------------------------------
export default function agyExtension(pi: ExtensionAPI) {
	// =========================================================================
	// TOOL: agy — generico (chat / image / analyze)
	// =========================================================================
	pi.registerTool({
		name: "agy",
		label: "agy (Antigravity subagent)",
		description:
			"Delega un task generico a Google Antigravity CLI (agy), che usa Gemini multimodale. " +
			"Utile per conversazioni di ragionamento multi-turno, generazione immagini e analisi di file. " +
			"Per task specializzati usa agy_generate, agy_edit, agy_inpaint, agy_style_transfer, agy_compose, " +
			"agy_analyze, agy_transcribe, agy_video.",
		parameters: Type.Object({
			prompt: Type.String({ description: "Il task/prompt da dare a agy." }),
			mode: Type.Optional(
				Type.Union([Type.Literal("chat"), Type.Literal("image"), Type.Literal("analyze")], {
					description: "chat (default) | image | analyze",
				}),
			),
			model: Type.Optional(Type.String({ description: "Modello agy (es. 'Gemini 3.1 Pro (High)')." })),
			effort: Type.Optional(
				Type.Union([Type.Literal("low"), Type.Literal("medium"), Type.Literal("high")]),
			),
			newConversation: Type.Optional(Type.Boolean({ description: "true per forzare una nuova conversazione." })),
			addDir: Type.Optional(Type.String({ description: "Cartella da aggiungere al workspace agy." })),
			filePath: Type.Optional(Type.String({ description: "File (immagine/audio/video) da analizzare." })),
			yolo: Type.Optional(Type.Boolean({ description: "true per --dangerously-skip-permissions." })),
		}),
		async execute(toolCallId, params, signal, onUpdate, ctx) {
			const p = params as any;
			onUpdate?.(`agy: ${p.mode === "image" ? "generazione immagine" : "elaborazione"}...`);
			const res = await executeAgy({
				prompt: p.prompt,
				mode: p.mode,
				model: p.model,
				effort: p.effort,
				newConversation: p.newConversation,
				addDirs: p.addDir ? [p.addDir] : [],
				filePaths: p.filePath ? [p.filePath] : [],
				yolo: p.yolo,
				signal,
			});
			return {
				content: [{ type: "text", text: res.text }],
				details: { conversationId: res.conversationId, exitCode: res.exitCode, imagePath: res.imagePath, model: p.model },
			};
		},
	});

	// =========================================================================
	// TOOL: agy_generate — generazione immagine strutturata
	// =========================================================================
	pi.registerTool({
		name: "agy_generate",
		label: "agy generate image",
		description:
			"Genera un'immagine da zero con parametri strutturati (soggetto, azione, luogo, composizione, stile, luce, formato). " +
			"Usa la formula narrativa [Soggetto]+[Azione]+[Luogo]+[Composizione]+[Stile] per il massimo controllo.",
		parameters: Type.Object({
			subject: Type.String({ description: "Soggetto: chi/cosa è nell'immagine. Sii specifico." }),
			action: Type.Optional(Type.String({ description: "Azione: cosa sta succedendo." })),
			location: Type.Optional(Type.String({ description: "Luogo/contesto/sfondo." })),
			composition: Type.Optional(
				Type.String({ description: "Composizione: inquadratura (es. 'close-up', 'wide shot', 'low-angle')." }),
			),
			style: Type.Optional(Type.String({ description: "Stile: estetica (es. 'fotorealistico', 'watercolor', 'film noir')." })),
			lighting: Type.Optional(Type.String({ description: "Illuminazione (es. 'golden hour', 'softbox', 'neon')." })),
			aspectRatio: Type.Optional(
				Type.String({ description: "Formato (es. '1:1', '16:9', '9:16', '4:3', '21:9')." }),
			),
			text: Type.Optional(Type.String({ description: "Testo da includere nell'immagine (tra virgolette)." })),
			negative: Type.Optional(Type.String({ description: "Cosa evitare, in framing positivo (es. 'nessun testo')." })),
			model: Type.Optional(Type.String({ description: "Modello agy." })),
			outputDir: Type.Optional(Type.String({ description: "Cartella dove copiare l'immagine generata." })),
		}),
		async execute(toolCallId, params, signal, onUpdate, ctx) {
			const p = params as any;
			const parts = [`Genera un'immagine: ${p.subject}`];
			if (p.action) parts.push(`Azione: ${p.action}`);
			if (p.location) parts.push(`Luogo/contesto: ${p.location}`);
			if (p.composition) parts.push(`Composizione: ${p.composition}`);
			if (p.style) parts.push(`Stile: ${p.style}`);
			if (p.lighting) parts.push(`Illuminazione: ${p.lighting}`);
			if (p.aspectRatio) parts.push(`Formato/aspect ratio: ${p.aspectRatio}`);
			if (p.text) parts.push(`Includi il testo "${p.text}" nell'immagine.`);
			if (p.negative) parts.push(`Evita: ${p.negative}.`);
			const prompt = parts.join(". ") + IMG_SUFFIX;

			onUpdate?.("agy_generate: generazione immagine...");
			const res = await executeAgy({
				prompt,
				mode: "image",
				model: p.model,
				stateless: true,
				outputDir: p.outputDir,
				signal,
			});
			return {
				content: [{ type: "text", text: res.text }],
				details: { imagePath: res.imagePath, exitCode: res.exitCode },
			};
		},
	});

	// =========================================================================
	// TOOL: agy_edit — editing controllato (Keep + Change + Add + Render)
	// =========================================================================
	pi.registerTool({
		name: "agy_edit",
		label: "agy edit image",
		description:
			"Modifica un'immagine esistente in modo controllato usando la formula Keep+Change+Add+Render. " +
			"Specifica cosa mantenere invariato, cosa cambiare, cosa aggiungere e il target di resa. " +
			"Un solo cambiamento per turno per il massimo controllo.",
		parameters: Type.Object({
			baseImage: Type.String({ description: "Percorso dell'immagine base da modificare." }),
			keep: Type.String({ description: "Cosa mantenere invariato (es. 'soggetto, posa, illuminazione, composizione')." }),
			change: Type.String({ description: "Cosa cambiare (es. 'il colore del divano in blu navy')." }),
			add: Type.Optional(Type.String({ description: "Cosa aggiungere (es. 'un vaso sul tavolo')." })),
			render: Type.Optional(
				Type.String({ description: "Target di resa (es. 'foto premium', 'stile editoriale', 'formato 4:5')." }),
			),
			preserveAspectRatio: Type.Optional(
				Type.Boolean({ description: "true per non cambiare l'aspect ratio dell'input." }),
			),
			model: Type.Optional(Type.String({ description: "Modello agy." })),
			outputDir: Type.Optional(Type.String({ description: "Cartella dove copiare l'immagine risultante." })),
		}),
		async execute(toolCallId, params, signal, onUpdate, ctx) {
			const p = params as any;
			const keep = p.preserveAspectRatio ? `${p.keep}. Non cambiare l'aspect ratio dell'immagine di input.` : p.keep;
			let prompt = `Usando l'immagine al percorso ${p.baseImage} come base, mantieni ${keep} invariato. Cambia ${p.change}.`;
			if (p.add) prompt += ` Aggiungi ${p.add}.`;
			if (p.render) prompt += ` Render come ${p.render}.`;
			prompt += IMG_SUFFIX;

			onUpdate?.("agy_edit: modifica immagine...");
			const res = await executeAgy({
				prompt,
				mode: "image",
				model: p.model,
				stateless: true,
				filePaths: [p.baseImage],
				outputDir: p.outputDir,
				signal,
			});
			return {
				content: [{ type: "text", text: res.text }],
				details: { imagePath: res.imagePath, baseImage: p.baseImage, exitCode: res.exitCode },
			};
		},
	});

	// =========================================================================
	// TOOL: agy_inpaint — editing di una zona specifica (semantic masking)
	// =========================================================================
	pi.registerTool({
		name: "agy_inpaint",
		label: "agy inpaint (edit zona specifica)",
		description:
			"Modifica SOLO una parte specifica dell'immagine lasciando il resto intatto (inpainting / semantic masking). " +
			"Definisci l'elemento target e la sostituzione; tutto il resto resta identico.",
		parameters: Type.Object({
			baseImage: Type.String({ description: "Percorso dell'immagine base." }),
			target: Type.String({ description: "L'elemento specifico da modificare (es. 'la maglietta del soggetto')." }),
			replacement: Type.String({ description: "La nuova descrizione dell'elemento (es. 'una maglietta rossa')." }),
			keepRest: Type.Optional(
				Type.String({ description: "Cosa mantenere identico (default: tutto il resto)." }),
			),
			model: Type.Optional(Type.String({ description: "Modello agy." })),
			outputDir: Type.Optional(Type.String({ description: "Cartella dove copiare l'immagine risultante." })),
		}),
		async execute(toolCallId, params, signal, onUpdate, ctx) {
			const p = params as any;
			const keep = p.keepRest ?? "tutto il resto";
			const prompt =
				`Usando l'immagine al percorso ${p.baseImage}, cambia SOLO ${p.target} in ${p.replacement}. ` +
				`Mantieni ${keep} esattamente identico, preservando stile, illuminazione e composizione originali.` +
				IMG_SUFFIX;

			onUpdate?.("agy_inpaint: modifica zona specifica...");
			const res = await executeAgy({
				prompt,
				mode: "image",
				model: p.model,
				stateless: true,
				filePaths: [p.baseImage],
				outputDir: p.outputDir,
				signal,
			});
			return {
				content: [{ type: "text", text: res.text }],
				details: { imagePath: res.imagePath, baseImage: p.baseImage, exitCode: res.exitCode },
			};
		},
	});

	// =========================================================================
	// TOOL: agy_style_transfer — applica uno stile preservando il contenuto
	// =========================================================================
	pi.registerTool({
		name: "agy_style_transfer",
		label: "agy style transfer",
		description:
			"Applica uno stile artistico a un'immagine preservando il contenuto e la composizione originali " +
			"(style transfer). Es. trasformare una foto in un dipinto Van Gogh.",
		parameters: Type.Object({
			baseImage: Type.String({ description: "Percorso dell'immagine base." }),
			style: Type.String({ description: "Lo stile da applicare (es. 'pittura Van Gogh', 'architectural drawing', 'film noir')." }),
			preserve: Type.Optional(
				Type.String({ description: "Cosa preservare (default: 'la composizione originale')." }),
			),
			model: Type.Optional(Type.String({ description: "Modello agy." })),
			outputDir: Type.Optional(Type.String({ description: "Cartella dove copiare l'immagine risultante." })),
		}),
		async execute(toolCallId, params, signal, onUpdate, ctx) {
			const p = params as any;
			const preserve = p.preserve ?? "la composizione originale";
			const prompt =
				`Usando l'immagine al percorso ${p.baseImage}, trasforma il contenuto nello stile di ${p.style}. ` +
				`Preserva ${preserve} ma renderizzala con lo stile richiesto.` +
				IMG_SUFFIX;

			onUpdate?.("agy_style_transfer: applica stile...");
			const res = await executeAgy({
				prompt,
				mode: "image",
				model: p.model,
				stateless: true,
				filePaths: [p.baseImage],
				outputDir: p.outputDir,
				signal,
			});
			return {
				content: [{ type: "text", text: res.text }],
				details: { imagePath: res.imagePath, baseImage: p.baseImage, exitCode: res.exitCode },
			};
		},
	});

	// =========================================================================
	// TOOL: agy_compose — combina più immagini
	// =========================================================================
	pi.registerTool({
		name: "agy_compose",
		label: "agy compose (combina immagini)",
		description:
			"Combina più immagini in una nuova composizione (multi-image composition / fusion). " +
			"Specifica il ruolo di ciascuna immagine e l'istruzione di fusione.",
		parameters: Type.Object({
			images: Type.Array(Type.String({ description: "Percorsi delle immagini da combinare." }), {
				description: "Lista di percorsi immagine (fino a ~6-14).",
			}),
			instruction: Type.String({
				description: "Istruzione di fusione: cosa prendere da ciascuna immagine e come combinarle.",
			}),
			model: Type.Optional(Type.String({ description: "Modello agy." })),
			outputDir: Type.Optional(Type.String({ description: "Cartella dove copiare l'immagine risultante." })),
		}),
		async execute(toolCallId, params, signal, onUpdate, ctx) {
			const p = params as any;
			const refs = (p.images as string[]).map((img, i) => `Immagine ${i + 1}: ${img}`).join("\n");
			const prompt =
				`Combina le seguenti immagini in una nuova composizione:\n${refs}\n\n` +
				`Istruzione: ${p.instruction}. Specifica il ruolo di ciascuna immagine.` +
				IMG_SUFFIX;

			onUpdate?.("agy_compose: combina immagini...");
			const res = await executeAgy({
				prompt,
				mode: "image",
				model: p.model,
				stateless: true,
				filePaths: p.images,
				outputDir: p.outputDir,
				signal,
			});
			return {
				content: [{ type: "text", text: res.text }],
				details: { imagePath: res.imagePath, images: p.images, exitCode: res.exitCode },
			};
		},
	});

	// =========================================================================
	// TOOL: agy_character — consistenza personaggio
	// =========================================================================
	pi.registerTool({
		name: "agy_character",
		label: "agy character consistency",
		description:
			"Mantiene la consistenza di un personaggio/oggetto attraverso più generazioni o edit. " +
			"Usa un'immagine di riferimento e un token di consistenza per preservare le caratteristiche.",
		parameters: Type.Object({
			referenceImage: Type.String({ description: "Percorso dell'immagine di riferimento del personaggio." }),
			name: Type.String({ description: "Nome/token del personaggio (es. 'Maya-giacca-blu')." }),
			features: Type.String({ description: "Caratteristiche immutabili da preservare (es. 'cicatrice sopracciglio sinistro')." }),
			task: Type.String({ description: "Cosa fare con il personaggio (es. 'mettilo in una scena notturna')." }),
			model: Type.Optional(Type.String({ description: "Modello agy." })),
			outputDir: Type.Optional(Type.String({ description: "Cartella dove copiare l'immagine risultante." })),
		}),
		async execute(toolCallId, params, signal, onUpdate, ctx) {
			const p = params as any;
			const prompt =
				`Usando l'immagine al percorso ${p.referenceImage} come riferimento del personaggio '${p.name}', ` +
				`${p.task}. Mantieni le caratteristiche del personaggio identiche: ${p.features}. ` +
				`Usa il token '${p.name}' per riferirti al personaggio.` +
				IMG_SUFFIX;

			onUpdate?.("agy_character: mantieni consistenza personaggio...");
			const res = await executeAgy({
				prompt,
				mode: "image",
				model: p.model,
				stateless: true,
				filePaths: [p.referenceImage],
				outputDir: p.outputDir,
				signal,
			});
			return {
				content: [{ type: "text", text: res.text }],
				details: { imagePath: res.imagePath, referenceImage: p.referenceImage, exitCode: res.exitCode },
			};
		},
	});

	// =========================================================================
	// TOOL: agy_analyze — analisi di un file (immagine/audio/video)
	// =========================================================================
	pi.registerTool({
		name: "agy_analyze",
		label: "agy analyze file",
		description:
			"Analizza un file (immagine, audio o video) con Gemini multimodale. " +
			"Per immagini: descrizione, OCR, analisi. Per audio: contenuto, trascrizione. Per video: scene, contenuto.",
		parameters: Type.Object({
			filePath: Type.String({ description: "Percorso del file da analizzare." }),
			question: Type.Optional(Type.String({ description: "Domanda specifica sull'analisi." })),
			model: Type.Optional(Type.String({ description: "Modello agy." })),
			yolo: Type.Optional(Type.Boolean({ description: "true per --dangerously-skip-permissions (necessario per audio/video)." })),
		}),
		async execute(toolCallId, params, signal, onUpdate, ctx) {
			const p = params as any;
			const prompt = p.question
				? `Analizza il file al percorso ${p.filePath}. ${p.question}`
				: `Analizza il file al percorso ${p.filePath} e descrivilo in dettaglio.`;

			onUpdate?.("agy_analyze: analisi file...");
			const res = await executeAgy({
				prompt,
				mode: "analyze",
				model: p.model,
				stateless: true,
				filePaths: [p.filePath],
				yolo: p.yolo,
				signal,
			});
			return {
				content: [{ type: "text", text: res.text }],
				details: { filePath: p.filePath, exitCode: res.exitCode },
			};
		},
	});

	// =========================================================================
	// TOOL: agy_transcribe — trascrizione audio
	// =========================================================================
	pi.registerTool({
		name: "agy_transcribe",
		label: "agy transcribe audio",
		description:
			"Trascrive il contenuto di un file audio (voce, discorso) in testo. " +
			"Richiede --yolo (auto-approva gli strumenti).",
		parameters: Type.Object({
			filePath: Type.String({ description: "Percorso del file audio (wav, mp3, m4a, ecc.)." }),
			language: Type.Optional(Type.String({ description: "Lingua del contenuto (es. 'italiano', 'english')." })),
			model: Type.Optional(Type.String({ description: "Modello agy." })),
		}),
		async execute(toolCallId, params, signal, onUpdate, ctx) {
			const p = params as any;
			const lang = p.language ? ` La lingua del contenuto è ${p.language}.` : "";
			const prompt =
				`Trascrivi il contenuto del file audio al percorso ${p.filePath}.` +
				lang +
				` Restituisci la trascrizione testuale completa e fedele.`;

			onUpdate?.("agy_transcribe: trascrizione audio...");
			const res = await executeAgy({
				prompt,
				mode: "analyze",
				model: p.model,
				stateless: true,
				filePaths: [p.filePath],
				yolo: true,
				signal,
			});
			return {
				content: [{ type: "text", text: res.text }],
				details: { filePath: p.filePath, exitCode: res.exitCode },
			};
		},
	});

	// =========================================================================
	// TOOL: agy_video — analisi video
	// =========================================================================
	pi.registerTool({
		name: "agy_video",
		label: "agy analyze video",
		description:
			"Analizza un file video: descrive scene, contenuto, codec, risoluzione, tracce audio. " +
			"Richiede --yolo (auto-approva gli strumenti).",
		parameters: Type.Object({
			filePath: Type.String({ description: "Percorso del file video (mp4, mov, ecc.)." }),
			question: Type.Optional(Type.String({ description: "Domanda specifica sul video." })),
			model: Type.Optional(Type.String({ description: "Modello agy." })),
		}),
		async execute(toolCallId, params, signal, onUpdate, ctx) {
			const p = params as any;
			const prompt = p.question
				? `Analizza il video al percorso ${p.filePath}. ${p.question}`
				: `Analizza il video al percorso ${p.filePath}: descrivi cosa mostra, codec, risoluzione e se contiene audio.`;

			onUpdate?.("agy_video: analisi video...");
			const res = await executeAgy({
				prompt,
				mode: "analyze",
				model: p.model,
				stateless: true,
				filePaths: [p.filePath],
				yolo: true,
				signal,
			});
			return {
				content: [{ type: "text", text: res.text }],
				details: { filePath: p.filePath, exitCode: res.exitCode },
			};
		},
	});

	// =========================================================================
	// TOOL: agy_models — elenca i modelli disponibili
	// =========================================================================
	pi.registerTool({
		name: "agy_models",
		label: "agy list models",
		description: "Elenca i modelli disponibili per agy (Gemini, Claude, ecc.).",
		parameters: Type.Object({}),
		async execute(toolCallId, params, signal) {
			const r = await runAgy(["models"], 30_000, signal);
			return {
				content: [{ type: "text", text: r.output.trim() || r.error || "(nessun output)" }],
				details: { exitCode: r.exitCode },
			};
		},
	});

	// =========================================================================
	// TOOL: agy_conversation — gestione stato conversazione
	// =========================================================================
	pi.registerTool({
		name: "agy_conversation",
		label: "agy conversation state",
		description:
			"Gestisce lo stato della conversazione agy: mostra l'ID corrente, elenca le conversazioni, o azzera lo stato.",
		parameters: Type.Object({
			action: Type.Union(
				[Type.Literal("id"), Type.Literal("list"), Type.Literal("reset")],
				{ description: "id | list | reset" },
			),
		}),
		async execute(toolCallId, params) {
			const action = (params as { action: string }).action;
			if (action === "id") {
				return { content: [{ type: "text", text: readState() ?? "(nessuna conversazione attiva)" }], details: {} };
			}
			if (action === "reset") {
				resetState();
				return { content: [{ type: "text", text: "Stato conversazione azzerato." }], details: {} };
			}
			try {
				if (!fs.existsSync(CONV_DIR)) {
					return { content: [{ type: "text", text: "(nessuna conversazione trovata)" }], details: {} };
				}
				const files = fs
					.readdirSync(CONV_DIR)
					.filter((f) => f.endsWith(".db"))
					.map((f) => path.join(CONV_DIR, f))
					.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)
					.slice(0, 15);
				const lines = files.map((f) => {
					const id = path.basename(f, ".db");
					const mtime = new Date(fs.statSync(f).mtimeMs).toISOString().replace("T", " ").slice(0, 19);
					return `${id}\t${mtime}`;
				});
				return {
					content: [{ type: "text", text: lines.join("\n") || "(nessuna conversazione trovata)" }],
					details: {},
				};
			} catch (e: any) {
				return { content: [{ type: "text", text: `Errore: ${e.message}` }], details: {}, isError: true };
			}
		},
	});

	// =========================================================================
	// Comandi interattivi
	// =========================================================================
	pi.registerCommand("agy", {
		description: "Invia un prompt a agy (subagent Antigravity). Uso: /agy <prompt>",
		handler: async (args, ctx) => {
			if (!args?.trim()) {
				ctx.ui.notify("Uso: /agy <prompt>", "error");
				return;
			}
			ctx.ui.setStatus("agy", "agy: elaborazione...");
			const res = await executeAgy({ prompt: args.trim() });
			ctx.ui.setStatus("agy", "");
			ctx.ui.notify(res.text || "(nessun output)", "info");
		},
	});

	pi.registerCommand("agy:new", {
		description: "Forza una nuova conversazione agy",
		handler: async (_args, ctx) => {
			resetState();
			ctx.ui.notify("Nuova conversazione agy pronta.", "info");
		},
	});

	pi.registerCommand("agy:list", {
		description: "Elenca le conversazioni agy",
		handler: async (_args, ctx) => {
			const id = readState();
			ctx.ui.notify(`Conversazione corrente: ${id ?? "(nessuna)"}`, "info");
		},
	});

	pi.registerCommand("agy:reset", {
		description: "Azzera lo stato conversazione agy",
		handler: async (_args, ctx) => {
			resetState();
			ctx.ui.notify("Stato conversazione azzerato.", "info");
		},
	});

	// =========================================================================
	// Registrazione microfono (F12) + trascrizione via Gemini + prompt su pi
	// =========================================================================
	async function handleRecordToggle(ctx: any) {
		if (!recording) {
			startRecording();
			ctx.ui.notify("🎙️ Registrazione avviata (F12 per fermare, max 2 min)", "info");
			ctx.ui.setStatus("agy-rec", "🔴 REGISTRAZIONE...");
			return;
		}

		ctx.ui.setStatus("agy-rec", "⏹️ Finalizzazione...");
		const file = await stopRecording();
		if (!file) {
			ctx.ui.setStatus("agy-rec", "");
			ctx.ui.notify("Nessuna registrazione attiva", "warning");
			return;
		}

		ctx.ui.notify("Registrazione fermata, ottimizzazione audio...", "info");
		const optimized = await optimizeAudio(file);

		ctx.ui.notify("Trascrizione in corso...", "info");
		const context = getConversationContext(ctx);
		const res = await executeAgy({
			prompt:
				`Trascrivi fedelmente il contenuto del file audio al percorso ${optimized}.` +
				`\n\nContesto della conversazione:\n${context || "(nessuno)"}` +
				`\n\nRestituisci SOLO la trascrizione testuale, senza commenti.`,
			mode: "analyze",
			stateless: true,
			filePaths: [optimized],
			yolo: true,
			model: "Gemini 3.6 Flash (Medium)",
			effort: "low",
		});
		ctx.ui.setStatus("agy-rec", "");

		const transcription = res.text.trim();
		if (!transcription) {
			ctx.ui.notify("Trascrizione vuota o errore", "error");
			return;
		}

		// Inserisci il risultato come prompt su pi
		if (ctx.isIdle()) {
			pi.sendUserMessage(transcription);
		} else {
			pi.sendUserMessage(transcription, { deliverAs: "followUp" });
		}
		ctx.ui.notify("✅ Trascrizione inviata come prompt a pi", "info");
	}

	pi.registerShortcut("f12", {
		description: "Avvia/ferma registrazione microfono (max 2 min) e trascrive via Gemini",
		handler: async (ctx) => {
			await handleRecordToggle(ctx);
		},
	});

	pi.registerCommand("agy:record", {
		description: "Avvia/ferma registrazione microfono (come F12)",
		handler: async (_args, ctx) => {
			await handleRecordToggle(ctx);
		},
	});

	pi.registerCommand("agy:record:stop", {
		description: "Ferma la registrazione microfono in corso",
		handler: async (_args, ctx) => {
			if (!recording) {
				ctx.ui.notify("Nessuna registrazione in corso", "warning");
				return;
			}
			await handleRecordToggle(ctx);
		},
	});
}
