import fs from "node:fs";
import path from "node:path";
import { DatabaseSync } from "node:sqlite";
import { Type } from "@sinclair/typebox";
import { complete, getModel } from "@mariozechner/pi-ai";

//#region src/agents/llm-tools/forced-tool-call.ts
/**
* Calls an Anthropic model with a single forced tool definition and returns
* the typed result. toolChoice defaults to { type: "tool", name: <toolName> }
* so the model always invokes the schema tool exactly once.
*
* Pass toolChoice: "required" only for OpenAI/Google callers — Anthropic
* silently rejects that value.
*/
async function forcedToolCall(prompt, schema, options) {
	const toolName = options.toolName ?? "result";
	const model = getModel("anthropic", options.model ?? "claude-haiku-4-5");
	const toolChoice = options.toolChoice ?? {
		type: "tool",
		name: toolName
	};
	const toolCall = (await complete(model, {
		messages: [{
			role: "user",
			content: prompt,
			timestamp: Date.now()
		}],
		tools: [{
			name: toolName,
			description: "Return a structured result.",
			parameters: schema
		}]
	}, {
		apiKey: options.apiKey,
		maxTokens: options.maxTokens ?? 256,
		toolChoice,
		signal: options.signal
	})).content.find((c) => c.type === "toolCall");
	if (!toolCall || toolCall.type !== "toolCall") throw new Error(`NO_TOOL_CALL: model did not call the "${toolName}" tool`);
	return toolCall.arguments;
}

//#endregion
//#region extensions/complaint-listener/src/classifier.ts
const ComplaintSchema = Type.Object({
	is_complaint: Type.Boolean(),
	severity: Type.Union([
		Type.Literal("low"),
		Type.Literal("med"),
		Type.Literal("high")
	]),
	category: Type.Union([
		Type.Literal("product_defect"),
		Type.Literal("billing"),
		Type.Literal("delivery"),
		Type.Literal("service"),
		Type.Literal("other")
	]),
	summary: Type.String()
});
const SYSTEM_PROMPT_BASE = `You are a customer-complaint classifier.
The conversation history below is the most recent context from this customer (oldest first).
Classify whether the LATEST message (the final entry in the history) indicates a genuine new or escalated complaint.

Hard rules:
- The LATEST message is what you classify. Use prior history only to disambiguate that message.
- If the LATEST message is short, vague, a single character, a typo, a greeting, a question, or general chit-chat, it is NOT a complaint — even if earlier messages in the history were complaints.
- Do NOT re-classify an older complaint just because it appears in the context window.
- For an existing complaint thread, only flag a NEW classification if the LATEST message escalates the issue (e.g. "it exploded", "I'm calling my lawyer") or introduces a new grievance.
- Call the result tool exactly once.`;
const LANGUAGE_HINT = {
	es: "Write the \"summary\" field in Spanish (one short sentence summarizing the complaint).",
	en: "Write the \"summary\" field in English (one short sentence summarizing the complaint)."
};
function renderHistory(history) {
	return history.map((h) => `[${h.role}] ${h.content}`).join("\n");
}
async function classifyMessage(history, apiKey, model, signal, language = "es") {
	const raw = await forcedToolCall(`${SYSTEM_PROMPT_BASE}\n${LANGUAGE_HINT[language]}\n\nConversation history:\n${renderHistory(history)}`, ComplaintSchema, {
		apiKey,
		model,
		toolName: "result",
		toolChoice: {
			type: "tool",
			name: "result"
		},
		maxTokens: 256,
		signal
	});
	return {
		is_complaint: Boolean(raw.is_complaint),
		severity: raw.severity,
		category: raw.category,
		summary: String(raw.summary)
	};
}

//#endregion
//#region extensions/complaint-listener/src/context.ts
function fetchContext(db, channel, accountId, chatId, limit) {
	return db.prepare(`SELECT content, sender_id, direction, timestamp
       FROM messages
       WHERE channel = ? AND account_id = ? AND chat_id = ?
       ORDER BY timestamp DESC
       LIMIT ?`).all(channel, accountId, chatId, limit).map((r) => ({
		content: String(r.content ?? ""),
		sender_id: r.sender_id == null ? null : String(r.sender_id),
		direction: String(r.direction ?? "in"),
		timestamp: Number(r.timestamp ?? 0)
	})).reverse().map((r) => ({
		role: r.direction === "out" ? "agent" : "user",
		content: r.content,
		senderId: r.sender_id ?? void 0,
		ts: r.timestamp
	}));
}

//#endregion
//#region extensions/complaint-listener/src/cooldown.ts
const SEVERITY_RANK = {
	low: 0,
	med: 1,
	high: 2
};
const CAPACITY = 5e3;
function createCooldown() {
	const entries = /* @__PURE__ */ new Map();
	const order = [];
	function set(key, entry) {
		if (!entries.has(key)) {
			order.push(key);
			if (order.length > CAPACITY) {
				const evict = order.shift();
				if (evict !== void 0) entries.delete(evict);
			}
		}
		entries.set(key, entry);
	}
	function withinWindow(e, cfg, nowMs) {
		if (e.messagesSinceAlert >= cfg.maxMessages) return false;
		if ((nowMs ?? Date.now()) - e.lastAlertAt >= cfg.windowMinutes * 6e4) return false;
		return true;
	}
	return {
		shouldSuppress(chatKey, cfg, newSeverity, nowMs) {
			if (!cfg.enabled) return false;
			const e = entries.get(chatKey);
			if (!e) return false;
			if (!withinWindow(e, cfg, nowMs)) return false;
			if (cfg.allowEscalation !== false && SEVERITY_RANK[newSeverity] > SEVERITY_RANK[e.lastSeverity]) return false;
			return true;
		},
		markAlerted(chatKey, severity, nowMs) {
			set(chatKey, {
				lastAlertAt: nowMs ?? Date.now(),
				messagesSinceAlert: 0,
				lastSeverity: severity
			});
		},
		recordMessage(chatKey) {
			const e = entries.get(chatKey);
			if (!e) return;
			e.messagesSinceAlert += 1;
		},
		_clear() {
			entries.clear();
			order.length = 0;
		}
	};
}

//#endregion
//#region extensions/complaint-listener/src/dedup.ts
function createDedup(capacity) {
	const order = [];
	const set = /* @__PURE__ */ new Set();
	return { markSeen(id) {
		if (id === void 0) return false;
		if (set.has(id)) return true;
		set.add(id);
		order.push(id);
		if (order.length > capacity) {
			const evict = order.shift();
			if (evict !== void 0) set.delete(evict);
		}
		return false;
	} };
}

//#endregion
//#region extensions/complaint-listener/src/destinations.ts
const CHANNEL_METHOD_NAMES = {
	whatsapp: "sendMessageWhatsApp",
	telegram: "sendMessageTelegram",
	discord: "sendMessageDiscord"
};
function resolveSender(api, channel) {
	const ch = api.runtime.channel[channel];
	if (!ch) return null;
	const fn = ch[CHANNEL_METHOD_NAMES[channel] ?? `sendMessage${channel.charAt(0).toUpperCase() + channel.slice(1)}`];
	return typeof fn === "function" ? fn : null;
}
async function sendAlerts(api, destinations, text) {
	const results = await Promise.all(destinations.map(async (d) => {
		const sender = resolveSender(api, d.channel);
		if (!sender) {
			api.logger.warn(`complaint-listener: unknown channel '${d.channel}' for destination ${d.to}`);
			return {
				destination: d,
				ok: false,
				error: "unknown_channel"
			};
		}
		try {
			const opts = { verbose: false };
			if (d.accountId) opts.accountId = d.accountId;
			if (d.channel === "telegram") opts.textMode = "markdown";
			await sender(d.to, text, opts);
			return {
				destination: d,
				ok: true
			};
		} catch (err) {
			api.logger.error(`complaint-listener: send to ${d.channel}:${d.to} failed: ${String(err)}`);
			return {
				destination: d,
				ok: false,
				error: String(err)
			};
		}
	}));
	return {
		total: destinations.length,
		successCount: results.filter((r) => r.ok).length,
		perDestination: results
	};
}

//#endregion
//#region extensions/complaint-listener/src/store.ts
function initSchema(db) {
	db.exec(`
    CREATE TABLE IF NOT EXISTS complaints (
      id          INTEGER PRIMARY KEY AUTOINCREMENT,
      agent_id    TEXT,
      channel_id  TEXT    NOT NULL,
      chat_id     TEXT    NOT NULL,
      from_sender TEXT    NOT NULL,
      content     TEXT    NOT NULL,
      is_complaint INTEGER NOT NULL DEFAULT 0,
      severity    TEXT    NOT NULL DEFAULT 'low',
      category    TEXT    NOT NULL DEFAULT 'other',
      summary     TEXT    NOT NULL DEFAULT '',
      created_at  INTEGER NOT NULL,
      notified_at INTEGER
    );
    CREATE INDEX IF NOT EXISTS idx_complaints_chat_id ON complaints(chat_id, created_at);
  `);
}
function insertComplaint(db, p) {
	const run = db.prepare(`INSERT INTO complaints
        (agent_id, channel_id, chat_id, from_sender, content, is_complaint, severity, category, summary, created_at)
       VALUES (?,?,?,?,?,?,?,?,?,?)`).run(p.agentId ?? null, p.channelId, p.chatId, p.fromSender, p.content, p.result.is_complaint ? 1 : 0, p.result.severity, p.result.category, p.result.summary, Date.now());
	return Number(run.lastInsertRowid);
}
function markNotified(db, id) {
	db.prepare("UPDATE complaints SET notified_at = ? WHERE id = ?").run(Date.now(), id);
}
function getPendingNotifications(db) {
	return db.prepare("SELECT * FROM complaints WHERE is_complaint = 1 AND notified_at IS NULL ORDER BY created_at ASC").all();
}

//#endregion
//#region extensions/complaint-listener/src/subscriptions.ts
function matchesChannelSub(subs, event) {
	return subs.some((s) => {
		if (s.kind === "channel-catchall") return true;
		if (s.kind !== "channel") return false;
		if (s.channel !== event.channel) return false;
		if (s.accountId !== void 0 && s.accountId !== event.accountId) return false;
		return true;
	});
}
function matchesAgentSub(subs, agentId) {
	if (!agentId) return false;
	return subs.some((s) => s.kind === "agent" && s.agentId === agentId);
}

//#endregion
//#region extensions/complaint-listener/index.ts
const PLUGIN_ID = "complaint-listener";
const SEVERITY_ORDER = {
	low: 0,
	med: 1,
	high: 2
};
function resolveConfig(raw) {
	const c = raw.classifier ?? {};
	const cd = raw.cooldown ?? {};
	const lang = raw.language === "en" ? "en" : "es";
	return {
		subscriptions: raw.subscriptions ?? [],
		destinations: raw.destinations ?? [],
		classifier: {
			model: c.model ?? "claude-haiku-4-5",
			contextWindow: c.contextWindow ?? 5,
			minSeverity: c.minSeverity ?? "low"
		},
		language: lang,
		cooldown: {
			enabled: typeof cd.enabled === "boolean" ? cd.enabled : true,
			maxMessages: typeof cd.maxMessages === "number" ? cd.maxMessages : 12,
			windowMinutes: typeof cd.windowMinutes === "number" ? cd.windowMinutes : 30,
			allowEscalation: typeof cd.allowEscalation === "boolean" ? cd.allowEscalation : true
		}
	};
}
function formatAlert(event, result, history, language) {
	const emoji = result.severity === "high" ? "🔴" : result.severity === "med" ? "🟠" : "🟡";
	if (language === "es") {
		const lines = [
			`${emoji} *Queja detectada* (${{
				low: "BAJA",
				med: "MEDIA",
				high: "ALTA"
			}[result.severity]})`,
			`De: ${event.senderUsername ?? event.senderId ?? "desconocido"} (${event.channel})`,
			`Categoría: ${result.category}`,
			`Mensaje: "${event.content}"`
		];
		if (history.length > 1) {
			lines.push("", `Contexto (últimos ${history.length}):`);
			for (const h of history) lines.push(`  • [${h.role}] ${h.content}`);
		}
		if (result.summary) lines.push("", `Resumen: ${result.summary}`);
		return lines.join("\n");
	}
	const lines = [
		`${emoji} *Complaint detected* (${result.severity.toUpperCase()})`,
		`From: ${event.senderUsername ?? event.senderId ?? "unknown"} (${event.channel})`,
		`Category: ${result.category}`,
		`Latest: "${event.content}"`
	];
	if (history.length > 1) {
		lines.push("", `Context (last ${history.length}):`);
		for (const h of history) lines.push(`  • [${h.role}] ${h.content}`);
	}
	if (result.summary) lines.push("", `Summary: ${result.summary}`);
	return lines.join("\n");
}
var complaint_listener_default = {
	id: PLUGIN_ID,
	name: "Complaint Listener",
	description: "Watches subscribed channels/agents and fans out classifier-detected complaint alerts to configured destinations.",
	configSchema: {
		type: "object",
		additionalProperties: false,
		properties: {}
	},
	register(api) {
		let db = null;
		const dedup = createDedup(1e3);
		const cooldown = createCooldown();
		api.registerService({
			id: PLUGIN_ID,
			async start(ctx) {
				const dbPath = path.join(ctx.stateDir, "plugins", PLUGIN_ID, "complaints.db");
				fs.mkdirSync(path.dirname(dbPath), { recursive: true });
				db = new DatabaseSync(dbPath);
				initSchema(db);
				const cfg = resolveConfig(api.pluginConfig);
				api.logger.info(`${PLUGIN_ID}: subscriptions=${cfg.subscriptions.length} destinations=${cfg.destinations.length}`);
				if (!api.runtime.messageLedger?.getDb?.()) api.logger.warn(`${PLUGIN_ID}: messageLedger disabled — alerts will fire on single-message context only`);
				if (cfg.destinations.length > 0) {
					const pending = getPendingNotifications(db);
					for (const row of pending) {
						const text = formatAlert({
							channel: row.channel_id,
							accountId: "default",
							senderId: row.from_sender,
							content: row.content
						}, {
							is_complaint: true,
							severity: row.severity,
							category: row.category,
							summary: row.summary
						}, [], cfg.language);
						if ((await sendAlerts(api, cfg.destinations, text)).successCount > 0) markNotified(db, row.id);
					}
				}
			},
			stop() {
				db = null;
				api.logger.info(`${PLUGIN_ID}: stopped`);
			}
		});
		async function handle(event, agentId) {
			if (!db) return;
			const cfg = resolveConfig(api.pluginConfig);
			if (dedup.markSeen(event.messageId)) return;
			const chatKey = `${event.channel}:${event.accountId}:${event.chatId}`;
			cooldown.recordMessage(chatKey);
			if (event.content.trim().replace(/[^a-zA-Z0-9À-ɏḀ-ỿ]/g, "").length < 3) return;
			const apiKey = process.env.ANTHROPIC_API_KEY;
			if (!apiKey) {
				api.logger.warn(`${PLUGIN_ID}: ANTHROPIC_API_KEY not set`);
				return;
			}
			const ledger = api.runtime.messageLedger?.getDb?.();
			if (!ledger) {
				api.logger.warn(`${PLUGIN_ID}: ledger disabled — skipping classification`);
				return;
			}
			let history = fetchContext(ledger, event.channel, event.accountId, event.chatId, cfg.classifier.contextWindow);
			if (history.length === 0) history = [{
				role: "user",
				content: event.content,
				ts: Date.now()
			}];
			let result;
			try {
				result = await classifyMessage(history, apiKey, cfg.classifier.model, void 0, cfg.language);
			} catch (err) {
				api.logger.error(`${PLUGIN_ID}: classifier error: ${String(err)}`);
				return;
			}
			if (!result.is_complaint) return;
			if (SEVERITY_ORDER[result.severity] < SEVERITY_ORDER[cfg.classifier.minSeverity]) return;
			if (cooldown.shouldSuppress(chatKey, cfg.cooldown, result.severity)) {
				api.logger.info(`${PLUGIN_ID}: cooldown active for ${chatKey} (severity=${result.severity}) — suppressed`);
				return;
			}
			let rowId = -1;
			try {
				rowId = insertComplaint(db, {
					agentId: agentId ?? null,
					channelId: event.channel,
					chatId: event.chatId,
					fromSender: event.senderId ?? "unknown",
					content: event.content,
					result
				});
			} catch (err) {
				api.logger.error(`${PLUGIN_ID}: store error: ${String(err)}`);
			}
			const text = formatAlert(event, result, history, cfg.language);
			const delivery = await sendAlerts(api, cfg.destinations, text);
			if (delivery.successCount > 0) {
				cooldown.markAlerted(chatKey, result.severity);
				if (rowId > 0) markNotified(db, rowId);
			}
			api.logger.info(`${PLUGIN_ID}: complaint logged (id=${rowId}, severity=${result.severity}, alerts_delivered=${delivery.successCount}/${delivery.total})`);
		}
		api.on("message_inbound", async (event) => {
			if (!matchesChannelSub(resolveConfig(api.pluginConfig).subscriptions, event)) return;
			await handle(event, void 0);
		});
		api.on("message_received", async (event, ctx) => {
			if (!matchesAgentSub(resolveConfig(api.pluginConfig).subscriptions, ctx.agentId)) return;
			await handle({
				channel: ctx.channelId,
				accountId: ctx.accountId ?? "default",
				chatId: ctx.chatId ?? "unknown",
				senderId: event.from,
				content: event.content,
				messageId: event.messageId
			}, ctx.agentId);
		});
	}
};

//#endregion
export { complaint_listener_default as default };