/**
 * 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, execFileSync, spawn, spawnSync } 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, ExtensionContext } 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

// ---------------------------------------------------------------------------
// Configurazione persistente (~/.config/agy-pi/config.json)
// ---------------------------------------------------------------------------
const CONFIG_DIR = path.join(os.homedir(), ".config", "agy-pi");
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");

interface AgyConfig {
	geminiApiKey?: string;
	enne2ApiKey?: string;
	sttBackend?: string; // gemini | enne2
	sttUrl?: string;
	sttModel?: string;
	sttMaxDuration?: number; // secondi
	ttsBackend?: string; // gemini | enne2
	ttsNotify?: boolean;
	ttsModel?: string;
	agyBin?: string;
	agyDefaultModel?: string;
	agyTimeoutMs?: number;
	// Fase 2 — Context Injection
	contextInject?: boolean; // on|off
	contextTokens?: number; // token cap per il contesto iniettato
	webSearch?: string; // auto|on|off — Strada A: pi cerca + inietta
	vocalPlanningMode?: boolean; // Opzione 4: piano + conferma prima di eseguire
}

const CONFIG_DEFAULTS: AgyConfig = {
	sttBackend: "gemini",
	sttUrl: "https://ai.enne2.net",
	sttModel: "gemma4:E4B",
	sttMaxDuration: 120,
	ttsBackend: "gemini",
	ttsNotify: true,
	ttsModel: "gemini-2.5-flash-preview-tts",
	agyTimeoutMs: 180_000,
	contextInject: true,
	contextTokens: 1500,
	webSearch: "auto",
	vocalPlanningMode: true,
};

function loadConfig(): AgyConfig {
	try {
		return { ...CONFIG_DEFAULTS, ...JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8")) };
	} catch {
		return { ...CONFIG_DEFAULTS };
	}
}

function saveConfig(cfg: AgyConfig) {
	try {
		fs.mkdirSync(CONFIG_DIR, { recursive: true });
		fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), { mode: 0o600 });
	} catch {
		/* ignora */
	}
}

// Legge una chiave: config file → env var → default
function getConfig(key: keyof AgyConfig): string | undefined {
	const cfg = loadConfig();
	const v = cfg[key];
	if (v === undefined || v === "") return undefined;
	return String(v);
}

function setConfig(key: keyof AgyConfig, value: string) {
	const cfg = loadConfig();
	const numKeys: (keyof AgyConfig)[] = ["sttMaxDuration", "agyTimeoutMs", "contextTokens"];
	const boolKeys: (keyof AgyConfig)[] = ["ttsNotify", "contextInject", "vocalPlanningMode"];
	if (numKeys.includes(key)) {
		(cfg as any)[key] = Number(value);
	} else if (boolKeys.includes(key)) {
		(cfg as any)[key] = value === "true" || value === "1" || value === "yes";
	} else {
		(cfg as any)[key] = value;
	}
	saveConfig(cfg);
}

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

// ---------------------------------------------------------------------------
// Helper: trovare il binario agy
// ---------------------------------------------------------------------------
function findAgy(): string {
	const candidates = [
		getConfig("agyBin"),
		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;
	}
}

// Esegue uno script Python/OpenCV sull'immagine per metriche oggettive:
// dimensioni, aspect ratio, luminosità, densità bordi e colori dominanti.
// Restituisce JSON (stringa) o stringa vuota in caso di errore.
async function runOpenCV(imagePath: string): Promise<string> {
	const script = `
import cv2, json, sys, collections
img = cv2.imread(sys.argv[1])
if img is None:
    print(json.dumps({"error": "cannot read image"}))
    sys.exit(1)
h, w = img.shape[:2]
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
brightness = round(float(gray.mean()), 1)
small = cv2.resize(img, (32, 32), interpolation=cv2.INTER_AREA)
colors = [tuple(int(x) for x in p) for p in small.reshape(-1, 3)]
counts = collections.Counter(colors)
dominant = [{"rgb": list(c), "count": n} for c, n in counts.most_common(3)]
edges = cv2.Canny(gray, 100, 200)
edge_density = round(float(edges.mean() / 255.0), 3)
print(json.dumps({
  "width": w, "height": h,
  "aspect_ratio": round(w / h, 3),
  "brightness": brightness,
  "edge_density": edge_density,
  "dominant_colors": dominant
}))
`;
	try {
		const { stdout } = await execFileAsync("python3", ["-c", script, imagePath], {
			timeout: 15_000,
		});
		return stdout.trim();
	} catch {
		return "";
	}
}

// ---------------------------------------------------------------------------
// 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;
	contextBlock?: string; // Fase 2: contesto iniettato prima della richiesta
}

// ===========================================================================
// Fase 2 — Context Injection (pi → agy)
// Best practice: tag XML, richiesta per ultima (anti lost-in-the-middle),
// prefisso stabile/cache, token cap, memoria durevole automatica, niente segreti.
// =========================================================================
const MEMORY_FILE = path.join(CONFIG_DIR, "memory.json");

interface DurableMemory {
	goal?: string;
	decisions: string[];
	conventions: string[];
	openQuestions: string[];
	updatedAt: string;
}

function loadMemory(): DurableMemory {
	try {
		const raw = JSON.parse(fs.readFileSync(MEMORY_FILE, "utf8"));
		return {
			decisions: Array.isArray(raw.decisions) ? raw.decisions : [],
			conventions: Array.isArray(raw.conventions) ? raw.conventions : [],
			openQuestions: Array.isArray(raw.openQuestions) ? raw.openQuestions : [],
			goal: typeof raw.goal === "string" ? raw.goal : undefined,
			updatedAt: raw.updatedAt ?? "",
		};
	} catch {
		return { decisions: [], conventions: [], openQuestions: [], updatedAt: "" };
	}
}

function saveMemory(mem: DurableMemory) {
	try {
		fs.mkdirSync(CONFIG_DIR, { recursive: true });
		fs.writeFileSync(
			MEMORY_FILE,
			JSON.stringify({ ...mem, updatedAt: new Date().toISOString() }, null, 2),
			{ mode: 0o600 },
		);
	} catch {
		/* ignora */
	}
}

// Auto-update: quando la conversazione pi cresce oltre la soglia, condensa i
// punti chiave recenti in durable facts invece di rigirare tutto il transcript.
function autoUpdateMemory(ctx: any, force = false) {
	try {
		const mem = loadMemory();
		const entries = ctx?.sessionManager?.getEntries?.() ?? [];
		if (force || entries.length > 24) {
			const userTexts: string[] = [];
			for (const e of entries) {
				if (e.type === "message" && e.message?.role === "user") {
					const c = e.message.content;
					const text =
						typeof c === "string"
							? c
							: Array.isArray(c)
								? c.map((b: any) => b.text ?? "").join(" ")
								: "";
					if (text && text.length > 20) userTexts.push(text.slice(0, 160));
				}
			}
			if (!mem.goal && userTexts.length) mem.goal = userTexts[0];
			// ultima decisione/azione chiave (ultimo user prompt significativo)
			if (userTexts.length > 1) {
				const last = userTexts[userTexts.length - 1];
				const lastShort = last.length > 100 ? last.slice(0, 100) : last;
				if (!mem.decisions.includes(lastShort)) {
					mem.decisions.push(lastShort);
					if (mem.decisions.length > 8) mem.decisions.shift();
				}
			}
			saveMemory(mem);
		}
	} catch {
		/* ignora */
	}
}

function getGitInfo(cwd: string): { branch: string; dirty: boolean; modified: string[] } {
	try {
		const branch =
			execFileSync("git", ["-C", cwd, "rev-parse", "--abbrev-ref", "HEAD"], {
				encoding: "utf8",
			})
				.trim() || "(nessun branch)";
		const porcelain = execFileSync("git", ["-C", cwd, "status", "--porcelain"], {
			encoding: "utf8",
		})
			.split("\n")
			.filter(Boolean);
		return {
			branch,
			dirty: porcelain.length > 0,
			modified: porcelain.slice(0, 8).map((l) => l.slice(0, 70)),
		};
	} catch {
		return { branch: "(no git)", dirty: false, modified: [] };
	}
}

// Estrae lo stato recente della conversazione pi (ultimi N messaggi utente)
// e lo compatta — NON il transcript grezzo (lost-in-the-middle, token bloat).
function extractSessionState(ctx: any): string {
	try {
		const entries = ctx?.sessionManager?.getEntries?.() ?? [];
		const userTexts: string[] = [];
		for (let i = entries.length - 1; i >= 0 && userTexts.length < 3; i--) {
			const e = entries[i];
			if (e.type === "message" && e.message?.role === "user") {
				const c = e.message.content;
				const text =
					typeof c === "string"
						? c
						: Array.isArray(c)
							? c.map((b: any) => b.text ?? "").join(" ")
							: "";
				if (text) userTexts.push(text.replace(/\s+/g, " ").trim().slice(0, 200));
			}
		}
		return userTexts.reverse().join("\n");
	} catch {
		return "";
	}
}

// Cerca sul web tramite Gemini API (googleSearch grounding) e restituisce i
// risultati iniettabili. È la Strada A: l'estensione cerca, poi inietta.
async function webSearchGemini(query: string, signal?: AbortSignal): Promise<string> {
	let key = getConfig("geminiApiKey") ?? process.env.GEMINI_API_KEY ?? "";
	if (!key) {
		try {
			key = fs.readFileSync(path.join(AGY_CHAT_DIR, "gemini-key"), "utf8").trim();
		} catch {
			/* ignora */
		}
	}
	if (!key) return "";
	const model = process.env.AGY_GEMINI_MODEL ?? "gemini-3.5-flash";
	try {
		const res = await fetch(
			`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(key)}`,
			{
				method: "POST",
				signal,
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({
					contents: [{ role: "user", parts: [{ text: query }] }],
					tools: [{ googleSearch: {} }],
				}),
			},
		);
		if (!res.ok) return "";
		const data: any = await res.json();
		const text = data?.candidates?.[0]?.content?.parts?.map((p: any) => p.text ?? "").join(" ") ?? "";
		return text.trim().slice(0, 600);
	} catch {
		return "";
	}
}

// Cache anti-ridondanza: evita ricerche web ripetute sullo stesso topic in una
// finestra breve (il multi-step reasoning lancia prompt simili in sequenza).
let lastSearch = { topic: "", time: 0 };

// Euristica per webSearch=auto. Determina se il prompt beneficia di dati
// aggiornati dalla ricerca web. Filtri negativi (immagini, codice locale,
// comandi) per non sprecare chiamate API/token su task che non servono.
function needsWebSearch(prompt: string, mode?: string): boolean {
	if (mode === "image" || mode === "analyze") return false;
	const ws = getConfig("webSearch") ?? "auto";
	if (ws === "on") return true;
	if (ws === "off") return false;

	const p = prompt.trim().toLowerCase();
	if (!p || p.length < 14) return false;

	// --- Filtri negativi: task che NON beneficiano della ricerca web ---
	// generazione / editing immagini
	if (/(genera|crea|disegna|produrr|dipin|realizz).{0,30}(immagine|logo|icona|poster|banner|ritratto|illustraz|foto|meme|sfondo)/i.test(p))
		return false;
	// analisi locale / OCR / trascrizione / traduzione / voce
	if (/(analizza questo|analizza il|\bocr\b|trascri|leggi il file|traduci|riassumi il file)/.test(p)) return false;
	// task di codice puramente locale
	if (/^(scrivi|rifattorizza|correggi|implementa|aggiungi|rimuovi|crea|modifica|fix|refactor|aggiorna il file|genera il codice)/.test(p))
		return false;
	// comandi / gestione conversazione
	if (/^(continua|ricordati|ignora|vai avanti|mostra|riassumi la conversazione|non)/.test(p)) return false;

	// --- Trigger positivi ---
	// segnali di recency / tempo corrente
	if (/(oggi|stamattina|adesso|al momento|quest'anno|del 20\d\d|nel 20\d\d|recent|ultim|appena|di recente|attuale|latest)/i.test(p))
		return true;
	// versioni / package / tech aggiornati
	if (/(versione|version|release|changelog|release notes|npm|package|dipendenz|compatibil|lts|stabile|beta|deprecat|\beol\b|end of life|release date)/i.test(p))
		return true;
	// confronti / alternative / raccomandazioni
	if (/(differenza|confronta|\bvs\b|alternativ|migliore|best|top|consigli|oppure)/i.test(p)) return true;
	// domande informative / definizioni
	if (/(cos'?è|che cos|chi è|quanto|quando|dove|perché|come funzion|cosa significa|definizion|significat|prezzo|costo|storia di)/i.test(p))
		return true;
	// news / novità / fatti correnti
	if (/(news|notizie|novità|novita|ultime notizie|accadut|succes|eventi|mercato|azienda|lancio|annunci)/i.test(p))
		return true;

	return false;
}

const estTokens = (s: string) => Math.ceil(s.length / 4);
function truncToTokens(s: string, limit: number): string {
	if (estTokens(s) <= limit) return s;
	return s.slice(0, limit * 4);
}

// Assemblea del blocco di contesto con tag XML e token cap (~1500).
// La richiesta utente va SEMPRE dopo (anti lost-in-the-middle).
async function buildContextBlock(
	ctx: any,
	prompt: string,
	signal?: AbortSignal,
	mode?: string,
): Promise<string> {
	if (getConfig("contextInject") === "off") return "";
	const budget = Number(getConfig("contextTokens") ?? 1500) || 1500;
	const cwd = ctx?.sessionManager?.getCwd?.() ?? process.cwd();
	const git = getGitInfo(cwd);
	const now = new Date().toISOString();
	const os = `${process.platform} ${process.arch}`;

	const parts: { label: string; xml: string; body: string }[] = [];

	// --- <environment_snapshot> ---
	const envBody = [
		`cwd: ${cwd}`,
		`os: ${os}`,
		`time: ${now}`,
		`git_branch: ${git.branch}${git.dirty ? " (dirty)" : ""}`,
		git.modified.length ? `modified: ${git.modified.join(" | ")}` : "",
	]
		.filter(Boolean)
		.join("\n");
	parts.push({ label: "env", xml: "environment_snapshot", body: envBody });

	// --- <session_state> ---
	const sessionState = extractSessionState(ctx);
	if (sessionState) {
		parts.push({ label: "session", xml: "session_state", body: sessionState });
	}

	// --- <durable_memory> ---
	const mem = loadMemory();
	const memLines = [
		mem.goal ? `goal: ${mem.goal.slice(0, 120)}` : "",
		mem.decisions.length ? `decisions: ${mem.decisions.join(" | ")}` : "",
		mem.conventions.length ? `conventions: ${mem.conventions.join(" | ")}` : "",
		mem.openQuestions.length ? `open_questions: ${mem.openQuestions.join(" | ")}` : "",
	].filter(Boolean);
	if (memLines.length) {
		parts.push({ label: "memory", xml: "durable_memory", body: memLines.join("\n") });
	}

	// --- <web_context> (Strada A) ---
	if (needsWebSearch(prompt, mode)) {
		// anti-ridondanza: non ricercare lo stesso topic entro 60s
		const topic = prompt.trim().toLowerCase().slice(0, 60);
		const now = Date.now();
		if (topic !== lastSearch.topic || now - lastSearch.time > 60_000) {
			lastSearch = { topic, time: now };
			const web = await webSearchGemini(prompt.slice(0, 300), signal);
			if (web) parts.push({ label: "web", xml: "web_context", body: web });
		}
	}

	// Token cap: budget totale ~1500. Assegna in modo proporzionale.
	const blocks: string[] = [];
	let used = 0;
	const envBudget = Math.min(300, budget);
	const sessionBudget = Math.min(500, budget);
	const memBudget = Math.min(400, budget);
	const webBudget = Math.min(300, budget);
	const budgets: Record<string, number> = {
		env: envBudget,
		session: sessionBudget,
		memory: memBudget,
		web: webBudget,
	};
	for (const p of parts) {
		const cap = budgets[p.label] ?? 200;
		if (used >= budget) break;
		const body = truncToTokens(p.body, cap);
		if (!body) continue;
		blocks.push(`<${p.xml}>
${body}
</${p.xml}>`);
		used += estTokens(body) + 6;
	}
	if (!blocks.length) return "";
	return `[CONTEXT_AGENTE] Contesto ambiente e stato corrente per la risposta. Usa solo se pertinente; la richiesta dell'utente è sotto.
${blocks.join("\n\n")}`;
}

async function executeAgy(opts: AgyExecOptions) {
	const finalPrompt = opts.contextBlock ? `${opts.contextBlock}\n\n${opts.prompt}` : opts.prompt;
	const args: string[] = ["-p", finalPrompt];

	// 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);

	const model = opts.model ?? getConfig("agyDefaultModel");
	if (model) args.push("--model", model);
	if (opts.effort) args.push("--effort", opts.effort);
	if (opts.yolo) args.push("--dangerously-skip-permissions");

	const timeout =
		opts.timeoutMs ??
		Number(getConfig("agyTimeoutMs") ?? DEFAULT_TIMEOUT_MS) ??
		(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;
	startTime: number;
	timer?: NodeJS.Timeout;
}

let recording: Recording | null = null;

function startRecording(ctx?: any): string {
	const maxDur = Number(getConfig("sttMaxDuration") ?? 120);
	const file = path.join(os.tmpdir(), `agy-rec-${Date.now()}.wav`);
	const proc = spawn(
		"ffmpeg",
		["-hide_banner", "-loglevel", "error", "-y", "-f", "pulse", "-i", "default", "-ac", "1", "-ar", "16000", "-t", String(maxDur), file],
		{ stdio: "ignore" },
	);
	const startTime = Date.now();
	let timer: NodeJS.Timeout | undefined;

	if (ctx) {
		const updateStatus = () => {
			const elapsedSec = Math.floor((Date.now() - startTime) / 1000);
			const elapsedMinStr = String(Math.floor(elapsedSec / 60)).padStart(2, "0");
			const elapsedSecStr = String(elapsedSec % 60).padStart(2, "0");
			const maxMinStr = String(Math.floor(maxDur / 60)).padStart(2, "0");
			const maxSecStr = String(maxDur % 60).padStart(2, "0");
			ctx.ui.setStatus(
				"agy-rec",
				`🔴 REGISTRAZIONE... ${elapsedMinStr}:${elapsedSecStr} / ${maxMinStr}:${maxSecStr} (Esc per annullare)`,
			);
		};
		updateStatus();
		timer = setInterval(updateStatus, 500);
	}

	recording = { proc, file, startTime, timer };
	// se ffmpeg termina da solo (timeout max durata), azzera lo stato e avvisa
	proc.on("exit", (code) => {
		if (recording && recording.proc === proc) {
			if (recording.timer) clearInterval(recording.timer);
			recording = null;
			playSound("timeout");
			if (ctx) {
				ctx.ui.setStatus("agy-rec", "");
				ctx.ui.notify("⏰ Tempo massimo di registrazione raggiunto", "warning");
			}
		}
	});
	return file;
}

function stopRecording(): Promise<string | null> {
	return new Promise((resolve) => {
		if (!recording) return resolve(null);
		if (recording.timer) clearInterval(recording.timer);
		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
	});
}

function cancelRecording(): Promise<void> {
	return new Promise((resolve) => {
		if (!recording) return resolve();
		if (recording.timer) clearInterval(recording.timer);
		const { proc, file } = recording;
		recording = null;
		let done = false;
		const cleanup = () => {
			if (!done) {
				done = true;
				try {
					if (fs.existsSync(file)) fs.unlinkSync(file);
				} catch {
					/* ignora */
				}
				resolve();
			}
		};
		proc.on("exit", cleanup);
		proc.kill("SIGKILL");
		setTimeout(cleanup, 1000);
	});
}

// ---------------------------------------------------------------------------
// Feedback sonori sci-fi (sintesi procedurale ffmpeg)
// ---------------------------------------------------------------------------
type SoundType = "start" | "stop" | "cancel" | "timeout" | "done";

// Espressioni aevalsrc per effetti fantascientifici
const SOUNDS: Record<SoundType, { expr: string; dur: string }> = {
	// Power up: sweep ascendente 440→1760Hz
	start: { expr: "sin(2*PI*(440+1320*t/0.15)*t)*(1-t/0.15)", dur: "0.15" },
	// Deactivate: sweep discendente 1200→300Hz
	stop: { expr: "sin(2*PI*(1200-900*t/0.18)*t)*(1-t/0.18)", dur: "0.18" },
	// Error/Abort: modulato discendente a bassa freq
	cancel: { expr: "sin(2*PI*(350-200*t/0.35)*t)*exp(-2*t)*(1+0.3*sin(2*PI*8*t))", dur: "0.35" },
	// Warning beacon: pulsazione bi-tono (radar)
	timeout: { expr: "if(lt(mod(t,0.5),0.15),sin(2*PI*700*t),if(lt(mod(t,0.5),0.35),0,sin(2*PI*500*t)))*0.8", dur: "1.2" },
	// Success chime: arpeggio Do-Mi-Sol acuto
	done: { expr: "if(lt(t,0.08),sin(2*PI*523*t),if(lt(t,0.16),sin(2*PI*659*t),sin(2*PI*784*t)))*(1-0.3*t)", dur: "0.28" },
};

function playSound(type: SoundType) {
	const cfg = SOUNDS[type];
	if (!cfg) return;
	const wav = path.join(os.tmpdir(), `agy-sound-${type}-${Date.now()}.wav`);
	// genera il tono sci-fi con ffmpeg (silenzioso) e riproduce con paplay/aplay/ffplay
	try {
		execFileAsync(
			"ffmpeg",
			[
				"-hide_banner",
				"-loglevel",
				"error",
				"-y",
				"-f",
				"lavfi",
				"-i",
				`aevalsrc='${cfg.expr}':d=${cfg.dur}:s=44100`,
				wav,
			],
			{ timeout: 10_000 },
		)
			.then(() => {
				execFileAsync("paplay", [wav], { timeout: 5000 }).catch(() => {
					// fallback su aplay/ffplay se paplay non disponibile
					execFileAsync("aplay", ["-q", wav], { timeout: 5000 }).catch(() => {
						execFileAsync("ffplay", ["-nodisp", "-autoexit", wav], { timeout: 5000 }).catch(() => {});
					});
				});
				// pulizia
				setTimeout(() => {
					try {
						fs.rmSync(wav, { force: true });
					} catch {
						/* ignora */
					}
				}, 3000);
			})
			.catch(() => {});
	} catch {
		/* ignora */
	}
}

// Ottimizza l'audio: taglia il silenzio iniziale/finale e converte in WAV 16kHz mono
async function optimizeAudio(input: string): Promise<string> {
	const output = input.replace(/\.wav$/, "-opt.wav");
	try {
		await execFileAsync(
			"ffmpeg",
			[
				"-hide_banner",
				"-loglevel",
				"error",
				"-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",
				"-ac",
				"1",
				"-ar",
				"16000",
				output,
			],
			{ timeout: 30_000 },
		);
		return output;
	} catch {
		return input; // fallback al file originale
	}
}

// Trascrizione affidabile e veloce via Gemini API diretta
// (agy CLI non supporta audio; la API supporta audio/wav nativamente)
// Rileva se l'audio contiene parlato reale (non solo silenzio)
// Ritorna true se c'è segnale, false se è silenzio
function audioHasSpeech(file: string): boolean {
	try {
		const res = spawnSync(
			"ffmpeg",
			["-hide_banner", "-i", file, "-af", "volumedetect", "-f", "null", "-"],
			{ timeout: 30_000, encoding: "utf8" },
		);
		const stderr = res.stderr || "";
		const maxMatch = stderr.match(/max_volume: ([\-0-9.]+) dB/);
		const meanMatch = stderr.match(/mean_volume: ([\-0-9.]+) dB/);
		if (maxMatch) {
			const max = parseFloat(maxMatch[1]);
			// max < -35dB ≈ silenzio quasi totale
			if (max < -35) return false;
		}
		if (meanMatch) {
			const mean = parseFloat(meanMatch[1]);
			if (mean < -45) return false;
		}
		return true;
	} catch {
		return true; // se non possiamo verificare, assumiamo che ci sia parlato
	}
}

// Risultato trascrizione con dettaglio errore
interface TranscriptResult {
	text: string;
	error?: string;
}

// Trascrizione affidabile e veloce via Gemini API diretta
// (agy CLI non supporta audio; la API supporta audio/mpeg nativamente)
async function transcribeWithGeminiAPI(file: string): Promise<TranscriptResult> {
	// key da config file o env var
	let key = getConfig("geminiApiKey") ?? process.env.GEMINI_API_KEY ?? "";
	if (!key) {
		try {
			key = fs.readFileSync(path.join(AGY_CHAT_DIR, "gemini-key"), "utf8").trim();
		} catch {
			/* ignora */
		}
	}
	if (!key) return { text: "", error: "Key Gemini mancante (imposta con /agy:config set geminiApiKey <chiave>)" };

	// rileva silenzio prima di chiamare l'API (evita allucinazioni su audio vuoto)
	if (!audioHasSpeech(file)) {
		return { text: "", error: "Nessun parlato rilevato nell'audio (silenzio o volume troppo basso)" };
	}

	const model = process.env.AGY_GEMINI_MODEL ?? "gemini-3.5-flash";
	try {
		// Comprimi in Opus/OGG (audio/ogg) — molto più efficiente dell'MP3 per il parlato,
		// riduce il payload di ~60% evitando HTTP 413 su registrazioni lunghe
		let audioFile = file;
		let mimeType = "audio/wav";
		if (file.toLowerCase().endsWith(".wav")) {
			const ogg = file.replace(/\.wav$/i, ".ogg");
			try {
				await execFileAsync(
					"ffmpeg",
					["-hide_banner", "-loglevel", "error", "-y", "-i", file, "-ac", "1", "-ar", "16000", "-c:a", "libopus", "-b:a", "16k", ogg],
					{ timeout: 60_000 },
				);
				audioFile = ogg;
				mimeType = "audio/ogg";
			} catch (e: any) {
				return { text: "", error: `Conversione Opus fallita: ${e.message}` };
			}
		}
		const b64 = fs.readFileSync(audioFile).toString("base64");
		const body = {
			contents: [
				{
					parts: [
						{ text: "Trascrivi fedelmente il contenuto di questo audio in italiano. Restituisci SOLO la trascrizione testuale." },
						{ inline_data: { mime_type: mimeType, data: b64 } },
					],
				},
			],
		};

		// retry su errori transitori (429, 500, 503)
		let lastErr = "";
		for (let attempt = 0; attempt < 3; attempt++) {
			const res = await fetch(
				`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`,
				{
					method: "POST",
					headers: { "Content-Type": "application/json" },
					body: JSON.stringify(body),
					signal: AbortSignal.timeout(120_000),
				},
			);
		if (res.ok) {
			const data: any = await res.json();
			const text = data?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? "";
			if (!text) {
				return { text: "", error: "La Gemini API ha restituito una risposta vuota" };
			}
			return { text };
		}
			lastErr = `HTTP ${res.status}`;
			const errBody = await res.text().catch(() => "");
			try {
				const e = JSON.parse(errBody);
				lastErr = `${lastErr}: ${e?.error?.message ?? ""}`.trim();
			} catch {
				/* ignora */
			}
			if (res.status !== 429 && res.status !== 500 && res.status !== 503) break;
			await new Promise((r) => setTimeout(r, 1500 * (attempt + 1)));
		}
		return { text: "", error: lastErr || "Errore API Gemini sconosciuto" };
	} catch (e: any) {
		return { text: "", error: `Errore API Gemini: ${e?.message ?? String(e)}` };
	}
}

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 "";
	}
}

// ===========================================================================
// Opzione 4 — Workflow vocale a 2 fasi: overlay TUI con trascrizione + piano
// e conferma utente prima dell'esecuzione (Enter esegui, Esc annulla, E modifica,
// F12 registra di nuovo). Riusa l'infrastruttura overlay di /agy:key e /agy:status.
// =========================================================================
interface VoicePlanDecision {
	action: "send" | "cancel" | "record";
	text: string;
}

async function showVoicePlanOverlay(
	ctx: ExtensionContext,
	transcript: string,
	plan: string,
): Promise<VoicePlanDecision> {
	const { Container, Text, matchesKey, Key } = await import("@earendil-works/pi-tui");
	const { DynamicBorder } = await import("@earendil-works/pi-coding-agent");

	return ctx.ui.custom<VoicePlanDecision>(
		(tui, theme, _keybindings, done) => {
			let text = plan;
			let editing = false;
			const container = new Container();

			const render = () => {
				container.clear();
				container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
				container.addChild(
					new Text(theme.fg("accent", theme.bold("🎙️ Conferma vocale")), 1, 1),
				);
				container.addChild(new Text("", 0, 0));
				container.addChild(new Text(theme.fg("dim", "Trascrizione:"), 1, 0));
				container.addChild(new Text(theme.fg("text", transcript.slice(0, 250)), 1, 0));
				container.addChild(new Text("", 0, 0));
				container.addChild(new Text(theme.fg("accent", "📋 Piano proposto:"), 1, 0));
				container.addChild(new Text(theme.fg("text", text), 1, 0));
				container.addChild(new Text("", 0, 0));
				container.addChild(
					new Text(
						theme.fg(
							"dim",
							editing
								? "✏️ Modifica: digitando cambia il testo • Enter applica • Esc annulla"
								: "Enter esegui • Esc annulla • E modifica • F12 registra di nuovo",
						),
						1,
						0,
					),
				);
				container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
			};
			render();

			return {
				render: (w) => {
					render();
					return container.render(w);
				},
				invalidate: () => container.invalidate(),
				handleInput: (data) => {
					if (editing) {
						if (matchesKey(data, Key.enter) || matchesKey(data, Key.escape)) {
							editing = false;
							tui.requestRender();
							return;
						}
						if (matchesKey(data, Key.backspace)) {
							text = text.slice(0, -1);
							tui.requestRender();
							return;
						}
						if (data.length === 1 && data.charCodeAt(0) >= 32) {
							text += data;
							tui.requestRender();
						}
						return;
					}
					if (matchesKey(data, Key.enter)) {
						done({ action: "send", text });
						return;
					}
					if (matchesKey(data, Key.escape)) {
						done({ action: "cancel", text });
						return;
					}
					if (matchesKey(data, "e") || data === "E") {
						editing = true;
						tui.requestRender();
						return;
					}
					if (matchesKey(data, Key.f12)) {
						done({ action: "record", text });
						return;
					}
				},
			};
		},
		{
			overlay: true,
			overlayOptions: { width: "60%", minWidth: 60, anchor: "center" },
		},
	);
}

// Trascrizione via server locale ai.enne2.net (gemma4:E4B supporta audio)
async function transcribeWithEnne2(file: string): Promise<TranscriptResult> {
	const baseUrl = getConfig("sttUrl") ?? "https://ai.enne2.net";
	const model = getConfig("sttModel") ?? "gemma4:E4B";
	const apiKey = getConfig("enne2ApiKey") ?? process.env.ENNE2_API_KEY ?? "";

	// rileva silenzio
	if (!audioHasSpeech(file)) {
		return { text: "", error: "Nessun parlato rilevato nell'audio (silenzio o volume troppo basso)" };
	}

	try {
		// Comprimi in MP3 per evitare HTTP 413 su audio lunghi (come il backend gemini)
		let audioFile = file;
		let audioFormat = "wav";
		if (file.toLowerCase().endsWith(".wav")) {
			const mp3 = file.replace(/\.wav$/i, ".mp3");
			try {
				await execFileAsync(
					"ffmpeg",
					["-hide_banner", "-loglevel", "error", "-y", "-i", file, "-ac", "1", "-ar", "16000", "-c:a", "libmp3lame", "-q:a", "5", mp3],
					{ timeout: 60_000 },
				);
				audioFile = mp3;
				audioFormat = "mp3";
			} catch {
				/* fallback al WAV */
			}
		}
		const b64 = fs.readFileSync(audioFile).toString("base64");
		const body = {
			model,
			messages: [
				{
					role: "user",
					content: [
						{ type: "text", text: "Trascrivi fedelmente il parlato in questo audio. Rispondi solo con la trascrizione." },
						{ type: "input_audio", input_audio: { data: b64, format: audioFormat } },
					],
				},
			],
			max_tokens: 2000,
		};
		const headers: Record<string, string> = { "Content-Type": "application/json" };
		if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
		const res = await fetch(`${baseUrl}/v1/chat/completions`, {
			method: "POST",
			headers,
			body: JSON.stringify(body),
			signal: AbortSignal.timeout(120_000),
		});
		if (!res.ok) {
			return { text: "", error: `Errore server enne2: HTTP ${res.status}` };
		}
		const data: any = await res.json();
		const text = data?.choices?.[0]?.message?.content?.trim() ?? "";
		return { text };
	} catch (e: any) {
		return { text: "", error: `Errore server enne2: ${e?.message ?? String(e)}` };
	}
}

// Dispatcher: sceglie il backend di trascrizione (gemini | enne2)
async function transcribeAudio(file: string): Promise<TranscriptResult> {
	const backend = getConfig("sttBackend") ?? "gemini";
	if (backend === "enne2" || backend === "local") {
		return transcribeWithEnne2(file);
	}
	return transcribeWithGeminiAPI(file);
}

// ---------------------------------------------------------------------------
// TTS via Gemini API (nessun engine esterno)
// ---------------------------------------------------------------------------
async function ttsSpeak(text: string, outputDir?: string): Promise<string | null> {
	let key = getConfig("geminiApiKey") ?? process.env.GEMINI_API_KEY ?? "";
	if (!key) {
		try {
			key = fs.readFileSync(path.join(AGY_CHAT_DIR, "gemini-key"), "utf8").trim();
		} catch {
			/* ignora */
		}
	}
	if (!key) return null;

	const model = getConfig("ttsModel") ?? "gemini-2.5-flash-preview-tts";
	try {
		const body = {
			contents: [{ parts: [{ text }] }],
			generationConfig: { responseModalities: ["AUDIO"] },
		};
		const res = await fetch(
			`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`,
			{
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify(body),
				signal: AbortSignal.timeout(60_000),
			},
		);
		if (!res.ok) return null;
		const data: any = await res.json();
		const parts = data?.candidates?.[0]?.content?.parts ?? [];
		const audioPart = parts.find((p: any) => p.inlineData);
		if (!audioPart) return null;

		// salva PCM raw (16-bit, 24kHz) e converti in WAV
		const pcmFile = path.join(os.tmpdir(), `tts-${Date.now()}.pcm`);
		fs.writeFileSync(pcmFile, Buffer.from(audioPart.inlineData.data, "base64"));
		const wavFile = pcmFile.replace(".pcm", ".wav");
		await execFileAsync(
			"ffmpeg",
			["-hide_banner", "-loglevel", "error", "-y", "-f", "s16le", "-ar", "24000", "-ac", "1", "-i", pcmFile, wavFile],
			{ timeout: 30_000 },
		);
		fs.rmSync(pcmFile, { force: true });

		// copia in outputDir se richiesto
		let finalFile = wavFile;
		if (outputDir) {
			try {
				fs.mkdirSync(outputDir, { recursive: true });
				const dest = path.join(outputDir, `tts-${Date.now()}.wav`);
				fs.copyFileSync(wavFile, dest);
				finalFile = dest;
			} catch {
				/* ignora */
			}
		}

		// riproduci (fire-and-forget)
		execFileAsync("paplay", [finalFile], { timeout: 60_000 }).catch(() => {});

		return finalFile;
	} catch {
		return null;
	}
}

// ---------------------------------------------------------------------------
// Helper: onUpdate nel formato corretto (oggetto con content, non stringa)
// ---------------------------------------------------------------------------
function toolUpdate(onUpdate: any, text: string) {
	onUpdate?.({ content: [{ type: "text", text }] });
}

// ---------------------------------------------------------------------------
// 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." })),
			injectContext: Type.Optional(
				Type.Boolean({ description: "Inietta contesto ambiente/sessione/web nel prompt (default true)." }),
			),
			webSearch: Type.Optional(
				Type.Boolean({ description: "Forza/non forzare la ricerca web (override di webSearch config)." }),
			),
		}),
		async execute(toolCallId, params, signal, onUpdate, ctx) {
			const p = params as any;
			toolUpdate(onUpdate, `agy: ${p.mode === "image" ? "generazione immagine" : "elaborazione"}...`);
			// Fase 2: costruzione contesto iniettato (ambiente + sessione + memoria + web)
			const wantInject = p.injectContext ?? true;
			const prevWs = getConfig("webSearch");
			if (typeof p.webSearch === "boolean") setConfig("webSearch", p.webSearch ? "on" : "off");
			const contextBlock = wantInject ? await buildContextBlock(ctx, p.prompt, signal, p.mode) : "";
			if (typeof p.webSearch === "boolean" && prevWs) setConfig("webSearch", prevWs);
			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,
				contextBlock,
			});
			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;

			toolUpdate(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;

			toolUpdate(onUpdate, "agy_edit: modifica immagine...");
			const res = await executeAgy({
				prompt,
				mode: "image",
				model: p.model,
				stateless: true,
				filePaths: [p.baseImage],
				outputDir: p.outputDir,
				yolo: true,
				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;

			toolUpdate(onUpdate, "agy_inpaint: modifica zona specifica...");
			const res = await executeAgy({
				prompt,
				mode: "image",
				model: p.model,
				stateless: true,
				filePaths: [p.baseImage],
				outputDir: p.outputDir,
				yolo: true,
				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;

			toolUpdate(onUpdate, "agy_style_transfer: applica stile...");
			const res = await executeAgy({
				prompt,
				mode: "image",
				model: p.model,
				stateless: true,
				filePaths: [p.baseImage],
				outputDir: p.outputDir,
				yolo: true,
				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;

			toolUpdate(onUpdate, "agy_compose: combina immagini...");
			const res = await executeAgy({
				prompt,
				mode: "image",
				model: p.model,
				stateless: true,
				filePaths: p.images,
				outputDir: p.outputDir,
				yolo: true,
				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;

			toolUpdate(onUpdate, "agy_character: mantieni consistenza personaggio...");
			const res = await executeAgy({
				prompt,
				mode: "image",
				model: p.model,
				stateless: true,
				filePaths: [p.referenceImage],
				outputDir: p.outputDir,
				yolo: true,
				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.`;

			toolUpdate(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_create_verified — generazione iterativa con verifica
	// Loop self-contained: genera immagine → analizza con visione → verifica
	// OpenCV → rigenera se non rispetta i requisiti (fino a maxIterations).
	// =========================================================================
	pi.registerTool({
		name: "agy_create_verified",
		label: "agy create verified image",
		description:
			"Genera un'immagine che soddisfi i requisiti, la verifica iterativamente con visione " +
			"testuale e metriche OpenCV, e la rigenera finché non rispetta i requisiti (o fino a un limite). " +
			"Ideale quando l'orchestratore non ha visione e serve un'immagine conforme a specifiche precise.",
		parameters: Type.Object({
			requirements: Type.String({ description: "Requisiti precisi che l'immagine deve soddisfare." }),
			outputDir: Type.Optional(Type.String({ description: "Cartella dove salvare l'immagine finale." })),
			maxIterations: Type.Optional(Type.Number({ description: "Limite massimo di iterazioni (default 3, max 5)." })),
			useOpenCV: Type.Optional(Type.Boolean({ description: "Esegui metriche OpenCV oggettive (default true)." })),
			model: Type.Optional(Type.String({ description: "Modello agy." })),
		}),
		async execute(toolCallId, params, signal, onUpdate, ctx) {
			const p = params as any;
			const requirements = String(p.requirements ?? "").trim();
			const maxIter = Math.min(Math.max(Number(p.maxIterations ?? 3) || 3, 1), 5);
			const useCV = p.useOpenCV ?? true;

			toolUpdate(onUpdate, "agy_create_verified: avvio loop di creazione verificata...");

			let currentImage: string | undefined;
			let lastIssues = "";
			const report: {
				iteration: number;
				pass: boolean;
				imagePath: string;
				verdict: string;
				opencv: string;
			}[] = [];
			let finalText = "";

			for (let iter = 1; iter <= maxIter; iter++) {
				toolUpdate(
					onUpdate,
					`agy_create_verified: iterazione ${iter}/${maxIter} (${iter === 1 ? "generazione" : "rigenerazione"})...`,
				);

				// 1) Genera (o rigenera) con prompt di correzione se non è la prima
				const genPrompt = currentImage
					? `L'immagine precedente (${currentImage}) non rispetta i requisiti per questi motivi: ${lastIssues}. ` +
						`Rigenera/rettifica l'immagine per soddisfare esattamente: ${requirements}.`
					: `Genera un'immagine che soddisfi esattamente questi requisiti: ${requirements}.`;
				const genRes = await executeAgy({
					prompt: genPrompt + IMG_SUFFIX,
					mode: "image",
					model: p.model,
					stateless: true,
					filePaths: currentImage ? [currentImage] : [],
					outputDir: p.outputDir,
					signal,
				});
				const imgPath = genRes.imagePath;
				if (!imgPath) {
					finalText = `Generazione fallita all'iterazione ${iter}: nessuna immagine prodotta.\n${genRes.text}`;
					break;
				}
				currentImage = imgPath;

				// 2) Analisi visione (giudice): PASS/FAIL + problemi rispetto ai requisiti
				const judgePrompt =
					`Analizza il file al percorso ${imgPath}. Requisiti richiesti: ${requirements}. ` +
					`Verifica se l'immagine li rispetta. Rispondi iniziando con \"PASS:\" o \"FAIL:\", ` +
					`poi elenca sinteticamente (max 3 punti) le deviazioni rispetto ai requisiti in caso di FAIL.`;
				const judgeRes = await executeAgy({
					prompt: judgePrompt,
					mode: "analyze",
					model: p.model,
					stateless: true,
					filePaths: [imgPath],
					yolo: true,
					signal,
				});
				const verdict = judgeRes.text.trim();
				const pass = /^\s*PASS:?/i.test(verdict);
				lastIssues = verdict;

				// 3) Metriche OpenCV oggettive (opzionale)
				let cvMetrics = "";
				if (useCV) {
					cvMetrics = await runOpenCV(imgPath);
				}

				report.push({ iteration: iter, pass, imagePath: imgPath, verdict, opencv: cvMetrics });

				if (pass) {
					finalText =
						`✅ Immagine verificata dopo ${iter} iterazione/i.\nPercorso: ${imgPath}` +
						(cvMetrics ? `\nMetriche OpenCV: ${cvMetrics}` : "") +
						`\nVerdetto visione:\n${verdict}`;
					break;
				}
				if (iter === maxIter) {
					finalText =
						`⚠️ Requisiti non soddisfatti dopo ${maxIter} iterazioni.\nPercorso: ${imgPath}` +
						(cvMetrics ? `\nMetriche OpenCV: ${cvMetrics}` : "") +
						`\nUltimo verdetto visione:\n${verdict}`;
				}
			}

			return {
				content: [{ type: "text", text: finalText || "Nessun output." }],
				details: {
					iterations: report.length,
					imagePath: currentImage,
					report,
				},
			};
		},
	});

	// =========================================================================
	// TOOL: agy_transcribe — trascrizione audio (via Gemini API diretta)
	// =========================================================================
	pi.registerTool({
		name: "agy_transcribe",
		label: "agy transcribe audio",
		description:
			"Trascrive il contenuto di un file audio (voce, discorso) in testo usando la Gemini API diretta " +
			"(veloce e affidabile; agy CLI non supporta audio). Richiede la key Gemini in ~/.agy-chat/gemini-key o GEMINI_API_KEY.",
		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 Gemini (default: gemini-3.5-flash)." })),
		}),
		async execute(toolCallId, params, signal, onUpdate, ctx) {
			const p = params as any;
			toolUpdate(onUpdate, "agy_transcribe: trascrizione audio...");
			const tr = await transcribeAudio(p.filePath);
			if (!tr.text) {
				return {
					content: [{ type: "text", text: `Trascrizione fallita: ${tr.error ?? "vuota"}` }],
					details: { filePath: p.filePath, error: tr.error },
					isError: true,
				};
			}
			const transcript = tr.text;
			return {
				content: [{ type: "text", text: transcript }],
				details: { filePath: p.filePath, model: p.model ?? "gemini-3.5-flash" },
			};
		},
	});

	// =========================================================================
	// 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.`;

			toolUpdate(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_tts — text-to-speech via Gemini API
	// =========================================================================
	pi.registerTool({
		name: "agy_tts",
		label: "agy TTS (text to speech)",
		description:
			"Converte un testo in audio (TTS) usando la Gemini API (gemini-2.5-flash-preview-tts) e lo riproduce. " +
			"Utile per notifiche vocali, sintesi parlate e aggiornamenti di stato. " +
			"Richiede la key Gemini in ~/.agy-chat/gemini-key o GEMINI_API_KEY.",
		parameters: Type.Object({
			text: Type.String({ description: "Il testo da pronunciare." }),
			play: Type.Optional(Type.Boolean({ description: "true per riprodurre l'audio (default: true)." })),
			outputDir: Type.Optional(Type.String({ description: "Cartella dove salvare il file audio." })),
		}),
		async execute(toolCallId, params, signal, onUpdate, ctx) {
			const p = params as any;
			toolUpdate(onUpdate, "agy_tts: sintesi vocale...");
			return ttsSpeak(p.text, p.outputDir).then((file) => {
				if (!file) {
					return {
						content: [{ type: "text", text: "TTS fallito: key Gemini mancante o errore API." }],
						details: {},
						isError: true,
					};
				}
				return {
					content: [{ type: "text", text: `Audio TTS generato: ${file}` }],
					details: { audioFile: file },
				};
			});
		},
	});

	// =========================================================================
	// 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);
			playSound("start");
			ctx.ui.notify("🎙️ Registrazione avviata (F12 per fermare, Esc per annullare)", "info");
			return;
		}

		playSound("stop");
		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 tr = await transcribeAudio(optimized);
		if (!tr.text) {
			ctx.ui.setStatus("agy-rec", "");
			ctx.ui.notify(`Trascrizione fallita: ${tr.error ?? "vuota"}`, "error");
			playSound("cancel");
			return;
		}
		const transcript = tr.text;

		// Legge il testo scritto a mano nel campo editor prima della registrazione
		// e lo combina con la trascrizione audio (comportamento "multimodale").
		const editorText = (ctx.ui.getEditorText?.() ?? "").trim();

		// Interpreta con Gemini (testo) — direttiva plan-first: solo piano, no esecuzione
		ctx.ui.notify("Interpretazione con Gemini...", "info");
		const context = getConversationContext(ctx);
		const res = await executeAgy({
			prompt:
				`Ecco la trascrizione di un messaggio vocale dell'utente:\n\n${transcript}` +
				(editorText
					? `\n\nTesto scritto dall'utente nel campo di input (da combinare con la voce):\n${editorText}`
					: "") +
				`\n\nContesto della conversazione:\n${context || "(nessuno)"}` +
				`\n\nProduce SOLO: 1) la trascrizione corretta, 2) un piano d'azione sintetico.` +
				`\nIMPORTANTE: NON eseguire alcuno strumento o azione. Attendi la conferma dell'utente.`,
			stateless: true,
			model: "Gemini 3.6 Flash (Medium)",
			yolo: true,
		});
		ctx.ui.setStatus("agy-rec", "");

		let finalText = res.text.trim() || transcript;

		// Il testo dell'editor è stato consumato: lo svuota per evitare reinvii duplicati.
		if (editorText) {
			try {
				ctx.ui.setEditorText?.("");
			} catch {
				/* ignora */
			}
		}

		// Opzione 4: piano + conferma in overlay TUI prima di eseguire (se abilitata)
		const planningMode = getConfig("vocalPlanningMode") ?? "true";
		if (planningMode !== "false") {
			const decision = await showVoicePlanOverlay(ctx, transcript, finalText);
			if (decision.action === "cancel") {
				ctx.ui.notify("Vocale annullato — nessuna azione eseguita.", "info");
				playSound("cancel");
				return;
			}
			if (decision.action === "record") {
				ctx.ui.notify("Registra di nuovo con F12.", "info");
				return;
			}
			finalText = decision.text;
		}

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

		// notifica vocale (config ttsNotify o AGY_TTS_NOTIFY=0 per disattivare)
		const ttsNotify = getConfig("ttsNotify") ?? "true";
		if (ttsNotify !== "false" && process.env.AGY_TTS_NOTIFY !== "0") {
			ttsSpeak("Trascrizione completata e inviata.").catch(() => {});
		}
	}

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

	pi.registerShortcut("escape", {
		description: "Annulla la registrazione microfono in corso",
		handler: async (ctx) => {
			if (recording) {
				await cancelRecording();
				ctx.ui.setStatus("agy-rec", "");
				ctx.ui.notify("❌ Registrazione annullata", "info");
				playSound("cancel");
			}
		},
	});

	pi.registerShortcut("esc", {
		description: "Annulla la registrazione microfono in corso",
		handler: async (ctx) => {
			if (recording) {
				await cancelRecording();
				ctx.ui.setStatus("agy-rec", "");
				ctx.ui.notify("❌ Registrazione annullata", "info");
				playSound("cancel");
			}
		},
	});

	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);
		},
	});

	pi.registerCommand("agy:record:cancel", {
		description: "Annulla la registrazione microfono in corso",
		handler: async (_args, ctx) => {
			if (!recording) {
				ctx.ui.notify("Nessuna registrazione in corso da annullare", "warning");
				return;
			}
			await cancelRecording();
			ctx.ui.setStatus("agy-rec", "");
			ctx.ui.notify("❌ Registrazione annullata", "info");
			playSound("cancel");
		},
	});

	pi.registerCommand("agy:speak", {
		description: "Pronuncia un testo con TTS Gemini. Uso: /agy:speak <testo>",
		handler: async (args, ctx) => {
			if (!args?.trim()) {
				ctx.ui.notify("Uso: /agy:speak <testo>", "error");
				return;
			}
			ctx.ui.setStatus("agy-tts", "🔊 sintesi vocale...");
			const file = await ttsSpeak(args.trim());
			ctx.ui.setStatus("agy-tts", "");
			if (file) ctx.ui.notify(`🔊 Audio: ${file}`, "info");
			else ctx.ui.notify("TTS fallito (key Gemini mancante?)", "error");
		},
	});

	pi.registerCommand("agy:vocal", {
		description: "Attiva/disattiva il feedback vocale TTS. Uso: /agy:vocal [on|off|status]",
		handler: async (args, ctx) => {
			const arg = (args ?? "").trim().toLowerCase();
			const current = getConfig("ttsNotify") ?? "true";

			if (arg === "on") {
				setConfig("ttsNotify", "true");
				ctx.ui.notify("🔊 Feedback vocale ATTIVATO", "info");
				ttsSpeak("Feedback vocale attivato.").catch(() => {});
				return;
			}
			if (arg === "off") {
				setConfig("ttsNotify", "false");
				ctx.ui.notify("🔇 Feedback vocale DISATTIVATO", "info");
				return;
			}
			if (arg === "status" || !arg) {
				const on = current !== "false";
				ctx.ui.notify(on ? "🔊 Feedback vocale: ATTIVO" : "🔇 Feedback vocale: DISATTIVO", "info");
				return;
			}

			// toggle
			if (current === "false") {
				setConfig("ttsNotify", "true");
				ctx.ui.notify("🔊 Feedback vocale ATTIVATO", "info");
				ttsSpeak("Feedback vocale attivato.").catch(() => {});
			} else {
				setConfig("ttsNotify", "false");
				ctx.ui.notify("🔇 Feedback vocale DISATTIVATO", "info");
			}
		},
	});

	// =========================================================================
	// Comando: /agy:config — gestione configurazione persistente
	// =========================================================================
	const CONFIG_KEYS: { key: keyof AgyConfig; desc: string }[] = [
		{ key: "geminiApiKey", desc: "Chiave API Google Gemini (STT/TTS)" },
		{ key: "enne2ApiKey", desc: "Token per il server proxy ai.enne2.net (opzionale)" },
		{ key: "sttBackend", desc: "Backend trascrizione: gemini | enne2" },
		{ key: "sttUrl", desc: "URL base backend enne2" },
		{ key: "sttModel", desc: "Modello STT backend enne2" },
		{ key: "sttMaxDuration", desc: "Durata max registrazione (secondi)" },
		{ key: "ttsBackend", desc: "Backend TTS: gemini | enne2" },
		{ key: "ttsNotify", desc: "Notifiche vocali automatiche: true | false" },
		{ key: "ttsModel", desc: "Modello TTS Gemini" },
		{ key: "agyBin", desc: "Path del binario agy" },
		{ key: "agyDefaultModel", desc: "Modello predefinito per le chiamate agy" },
		{ key: "agyTimeoutMs", desc: "Timeout esecuzione agy (ms)" },
		{ key: "contextInject", desc: "Iniezione contesto nel prompt agy: true | false" },
		{ key: "contextTokens", desc: "Token cap per il contesto iniettato (default 1500)" },
		{ key: "webSearch", desc: "Ricerca web Strada A: auto | on | off" },
		{ key: "vocalPlanningMode", desc: "Opzione 4: piano+conferma dopo il vocale (true|false)" },
	];

	pi.registerCommand("agy:config", {
		description:
			"Gestisce la configurazione dell'estensione. Uso: /agy:config [get|set|reset] [chiave] [valore]",
		handler: async (args, ctx) => {
			const parts = (args ?? "").trim().split(/\s+/);
			const action = parts[0] ?? "";
			const key = parts[1] as keyof AgyConfig | undefined;
			const value = parts.slice(2).join(" ");

			// /agy:config — elenca tutto
			if (!action) {
				const cfg = loadConfig();
				const lines = CONFIG_KEYS.map(({ key: k, desc }) => {
					const v = (cfg as any)[k];
					const masked =
						k === "geminiApiKey" || k === "enne2ApiKey"
							? v
								? `${String(v).slice(0, 4)}...${String(v).slice(-4)}`
								: "(non impostata)"
							: v ?? "(non impostata)";
					return `${k} = ${masked}  — ${desc}`;
				});
				ctx.ui.notify(`Config agy-pi (${CONFIG_FILE}):\n${lines.join("\n")}`, "info");
				return;
			}

			// /agy:config get <key>
			if (action === "get" && key) {
				const v = getConfig(key);
				ctx.ui.notify(`${key} = ${v ?? "(non impostata)"}`, "info");
				return;
			}

			// /agy:config set <key> <value>
			if (action === "set" && key && value) {
				if (!CONFIG_KEYS.some((c) => c.key === key)) {
					ctx.ui.notify(`Chiave sconosciuta: ${key}`, "error");
					return;
				}
				setConfig(key, value);
				ctx.ui.notify(`✅ ${key} impostato. File: ${CONFIG_FILE}`, "info");
				return;
			}

			// /agy:config reset
			if (action === "reset") {
				resetConfig();
				ctx.ui.notify("Configurazione azzerata (default ripristinati).", "info");
				return;
			}

			ctx.ui.notify(
				"Uso: /agy:config | /agy:config get <chiave> | /agy:config set <chiave> <valore> | /agy:config reset",
				"warning",
			);
		},
	});

	// =========================================================================
	// Comando: /agy:key — dialog overlay TUI per inserire/modificare la chiave
	// API Gemini in modo interattivo (campo mascherato, Enter conferma, Esc
	// annulla). Usa i componenti TUI di pi (overlay in sovrimpressione).
	// =========================================================================
	pi.registerCommand("agy:key", {
		description:
			"Apre un dialog overlay per inserire/modificare la chiave API Gemini (campo mascherato)",
		handler: async (_args, ctx) => {
			// Import dinamico: se pi-tui non fosse disponibile, fallisce solo questo
			// comando senza rompere il caricamento dell'intera estensione.
			const {
				Container,
				Text,
				matchesKey,
				Key,
			} = await import("@earendil-works/pi-tui");
			const { DynamicBorder } = await import("@earendil-works/pi-coding-agent");

			const result = await ctx.ui.custom<string | null>(
				(tui, theme, _keybindings, done) => {
					let value = "";
					const currentKey = getConfig("geminiApiKey") ?? "";
					const maskCurrent = currentKey
						? `${currentKey.slice(0, 4)}...${currentKey.slice(-4)}`
						: "(non impostata)";
					const mask = (v: string) => "•".repeat(v.length);

					const container = new Container();
					const renderDialog = () => {
						container.clear();
						container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
						container.addChild(
							new Text(theme.fg("accent", theme.bold("🔑 Chiave API Gemini")), 1, 1),
						);
						container.addChild(
							new Text(theme.fg("dim", `Attuale: ${maskCurrent}`), 1, 0),
						);
						container.addChild(new Text("", 0, 0));
						const field = value ? mask(value) : "(vuota)";
						container.addChild(
							new Text(
								theme.fg("text", "Nuova chiave: ") + theme.fg("warning", field),
								1,
								0,
								(s) => theme.bg("toolPendingBg", s),
							),
						);
						container.addChild(new Text("", 0, 0));
						container.addChild(
							new Text(
								theme.fg("dim", "Digita la chiave • Enter conferma • Esc annulla"),
								1,
								0,
							),
						);
						container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
					};
					renderDialog();

					return {
						render: (w) => {
							renderDialog();
							return container.render(w);
						},
						invalidate: () => container.invalidate(),
						handleInput: (data) => {
							if (matchesKey(data, Key.enter)) {
								const trimmed = value.trim();
								if (trimmed) {
									setConfig("geminiApiKey", trimmed);
									// Aggiorna anche ~/.agy-chat/gemini-key per coerenza con gli script TTS/STT
									try {
										fs.mkdirSync(AGY_CHAT_DIR, { recursive: true });
										fs.writeFileSync(
											path.join(AGY_CHAT_DIR, "gemini-key"),
											trimmed,
											{ mode: 0o600 },
										);
									} catch {
										/* ignora */
									}
									done(trimmed);
								}
								return;
							}
							if (matchesKey(data, Key.escape)) {
								done(null);
								return;
							}
							if (matchesKey(data, Key.backspace)) {
								value = value.slice(0, -1);
								tui.requestRender();
								return;
							}
							// Caratteri stampabili permessi per una chiave API (A-Z a-z 0-9 _ . -)
							if (data.length === 1 && data.charCodeAt(0) >= 32) {
								if (/^[A-Za-z0-9._-]+$/.test(data)) {
									value += data;
								}
								tui.requestRender();
							}
						},
					};
				},
				{
					overlay: true,
					overlayOptions: {
						width: "50%",
						minWidth: 54,
						anchor: "center",
					},
				},
			);

			if (result) {
				ctx.ui.notify(
					`✅ Chiave API Gemini aggiornata: ${result.slice(0, 4)}...${result.slice(-4)}`,
					"info",
				);
				try {
					playSound("done");
				} catch {
					/* ignora */
				}
			} else {
				ctx.ui.notify("Chiave non modificata.", "info");
			}
		},
	});

	// =========================================================================
	// Comando: /agy:status — diagnostica estensione in un overlay TUI
	// Mostra: binario agy, versione, chiave Gemini, modello attivo, ultimo
	// errore dal log, stato config. Chiuso con Enter o Esc.
	// =========================================================================
	pi.registerCommand("agy:status", {
		description: "Mostra lo stato dell'estensione (agi, chiave, modello, ultimo errore)",
		handler: async (_args, ctx) => {
			const { Container, Text, matchesKey, Key } = await import("@earendil-works/pi-tui");
			const { DynamicBorder } = await import("@earendil-works/pi-coding-agent");

			// ---- raccolta diagnostica ----
			const agyBin = findAgy();
			const agyExists =
				agyBin === "agy" ? fs.existsSync("/home/enne2/.local/bin/agy") : fs.existsSync(agyBin);
			let agyVersion = "(sconosciuta)";
			if (agyExists) {
				try {
					agyVersion =
						(
							await execFileAsync(agyBin, ["--version"], {
								timeout: 8000,
								env: { ...process.env, NO_COLOR: "1" },
							})
						).stdout.trim() || "(sconosciuta)";
				} catch {
					agyVersion = "(errore esecuzione)";
				}
			}

			const cfgKey = getConfig("geminiApiKey") ?? "";
			const fileKey = (() => {
				try {
					const f = path.join(AGY_CHAT_DIR, "gemini-key");
					return fs.existsSync(f) ? fs.readFileSync(f, "utf8").trim() : "";
				} catch {
					return "";
				}
			})();
			const key = cfgKey || fileKey;
			const keyStatus = key ? "✅ valida" : "❌ mancante (usa /agy:key)";
			const keyMask = key ? `${key.slice(0, 4)}...${key.slice(-4)}` : "—";
			const model = getConfig("agyDefaultModel") || "(default agy)";
			const sttBackend = getConfig("sttBackend") ?? "gemini";
			const ttsNotify = getConfig("ttsNotify") ?? "true";

			// ultimo errore dal log agy (ultima riga con 'ERROR'/'denied'/'quota')
			let lastError = "(nessuno)";
			try {
				const logDir = path.join(os.homedir(), ".gemini", "antigravity-cli", "log");
				if (fs.existsSync(logDir)) {
					const logs = fs
						.readdirSync(logDir)
						.map((f) => path.join(logDir, f))
						.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
					const logFile = logs[0];
					if (logFile) {
						const lines = fs.readFileSync(logFile, "utf8").split("\n");
						const meaningful = [...lines]
							.reverse()
							.filter((l) => /error|denied|quota|failed|timed out/i.test(l))
							.filter((l) => !/^ERROR: logging before google\.Init:/i.test(l.trim()));
						if (meaningful.length) {
							const raw = meaningful[0];
							// estrai il messaggio dopo il pattern glog: "go:file.go:NN] msg" oppure dopo ":"
							const m = raw.match(/\][\s]*\s*\s*(.*)$/) || raw.match(/:\s*(.*)$/i);
							lastError = (m ? m[1] : raw).replace(/^ERROR:|^WARN:/i, "").trim().slice(0, 90) || "(vedi log)";
						}
					}
				}
			} catch {
				lastError = "(non disponibile)";
			}

			// ---- overlay TUI ----
			await ctx.ui.custom<void>(
				(tui, theme, _keybindings, done) => {
					const container = new Container();
					const render = () => {
						container.clear();
						container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
						container.addChild(
							new Text(theme.fg("accent", theme.bold("📊 Stato agy-pi")), 1, 1),
						);
						container.addChild(new Text("", 0, 0));
							const line = (label: string, v: string, color: "success" | "error" | "text" | "muted" | "accent" | "warning" | "dim") =>
								container.addChild(
									new Text(theme.fg("dim", `${label}: `) + theme.fg(color, v), 1, 0),
								);
						line("Binario agy", agyExists ? `trovato ${agyBin}` : "❌ NON trovato",
							agyExists ? "success" : "error");
						line("Versione", agyVersion, "text");
						line("Chiave Gemini", keyStatus, key ? "success" : "error");
						line("Chiave (mask)", keyMask, "muted");
						line("Modello attivo", model, "accent");
						line("Backend STT", sttBackend, "text");
						line("Notifiche TTS", ttsNotify, "text");
						container.addChild(new Text("", 0, 0));
						container.addChild(
							new Text(theme.fg("warning", "Ultimo errore:") + " " + theme.fg("text", lastError), 1, 0),
						);
						container.addChild(new Text("", 0, 0));
						container.addChild(
							new Text(theme.fg("dim", "Enter o Esc per chiudere"), 1, 0),
						);
						container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
					};
					render();
					return {
						render: (w) => {
							render();
							return container.render(w);
						},
						invalidate: () => container.invalidate(),
						handleInput: (data) => {
							if (matchesKey(data, Key.enter) || matchesKey(data, Key.escape)) done();
						},
					};
				},
				{
					overlay: true,
					overlayOptions: { width: "55%", minWidth: 56, anchor: "center" },
				},
			);
		},
	});
}
