import {
	type AgentModelEntry,
	getTab,
	isReasoningEffort,
	NotificationDispatcher,
} from "@dispatch/core";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { AgentManager } from "./agent-manager.js";
import { PermissionManager } from "./permission-manager.js";
import { agentsRoutes } from "./routes/agents.js";
import { configRoutes } from "./routes/config.js";
import { modelsRoutes, startWakeScheduler } from "./routes/models.js";
import { notificationsRoutes } from "./routes/notifications.js";
import { skillsRoutes } from "./routes/skills.js";
import { tabsRoutes } from "./routes/tabs.js";

/**
 * Validate and normalise the `agentModels` fallback chain coming from the
 * frontend. Each entry must carry string `key_id`/`model_id`; an `effort` is
 * kept only when it's a recognised level (otherwise dropped so the per-tab /
 * default effort applies). Returns `undefined` when the input isn't an array.
 */
function sanitizeAgentModels(raw: unknown): AgentModelEntry[] | undefined {
	if (!Array.isArray(raw)) return undefined;
	const out: AgentModelEntry[] = [];
	for (const m of raw) {
		if (!m || typeof m !== "object") continue;
		const entry = m as Record<string, unknown>;
		if (typeof entry.key_id !== "string" || typeof entry.model_id !== "string") continue;
		out.push({
			key_id: entry.key_id,
			model_id: entry.model_id,
			...(isReasoningEffort(entry.effort) ? { effort: entry.effort } : {}),
		});
	}
	return out;
}

export const permissionManager = new PermissionManager();
export const agentManager = new AgentManager(permissionManager);

// ntfy.sh push notifications. The dispatcher reads its config from the
// `settings` table on every send, so config changes apply immediately —
// no restart, no re-attach needed.
export const notificationDispatcher = new NotificationDispatcher({
	getTabTitle: (tabId) => {
		try {
			return getTab(tabId)?.title ?? null;
		} catch {
			return null;
		}
	},
	getTabParentId: (tabId) => {
		try {
			// `undefined` when the lookup fails (tab not found / DB unavailable)
			// so the dispatcher falls back to "treat as top-level" rather than
			// silently dropping notifications.
			const row = getTab(tabId);
			return row ? row.parentTabId : undefined;
		} catch {
			return undefined;
		}
	},
});
notificationDispatcher.attachToAgentManager(agentManager);
notificationDispatcher.attachToPermissionManager(permissionManager);

export const app = new Hono();

app.use(
	"*",
	cors({
		origin: (origin) => origin || "*",
		credentials: true,
		allowHeaders: ["Content-Type", "Authorization"],
		allowMethods: ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"],
	}),
);

app.get("/health", (c) => {
	return c.json({ ok: true });
});

app.get("/status", (c) => {
	return c.json({
		status: agentManager.getStatus(),
		messageCount: agentManager.getMessageCount(),
		statuses: agentManager.getAllStatuses(),
	});
});

app.post("/chat", async (c) => {
	const body = await c.req.json<{
		tabId?: unknown;
		message?: unknown;
		keyId?: unknown;
		modelId?: unknown;
		agentModels?: unknown;
		reasoningEffort?: unknown;
		workingDirectory?: unknown;
		queueId?: unknown;
	}>();
	const { tabId, message } = body;

	if (typeof tabId !== "string" || tabId.trim() === "") {
		return c.json({ error: "tabId must be a non-empty string" }, 400);
	}

	if (typeof message !== "string" || message.trim() === "") {
		return c.json({ error: "message must be a non-empty string" }, 400);
	}

	const keyId = typeof body.keyId === "string" ? body.keyId : undefined;
	const modelId = typeof body.modelId === "string" ? body.modelId : undefined;
	const agentModels = sanitizeAgentModels(body.agentModels);
	const workingDirectory =
		typeof body.workingDirectory === "string" ? body.workingDirectory : undefined;
	const queueId = typeof body.queueId === "string" ? body.queueId : undefined;
	const reasoningEffort = isReasoningEffort(body.reasoningEffort)
		? body.reasoningEffort
		: undefined;

	// Single routing decision (queue if busy, new turn if idle) shared with the
	// `send_to_tab` tool via `AgentManager.deliverMessage`. Non-blocking — a
	// started turn runs in the background.
	const outcome = agentManager.deliverMessage(tabId, message, {
		...(keyId ? { keyId } : {}),
		...(modelId ? { modelId } : {}),
		...(agentModels ? { agentModels } : {}),
		...(reasoningEffort ? { reasoningEffort } : {}),
		...(workingDirectory !== undefined ? { workingDirectory } : {}),
		...(queueId ? { queueId } : {}),
	});

	if (outcome.status === "queued") {
		return c.json({ status: "queued", messageId: outcome.messageId });
	}
	return c.json({ status: "ok" });
});

app.route("/config", configRoutes);

app.post("/chat/cancel", async (c) => {
	const body = await c.req.json();
	if (typeof body.tabId !== "string" || typeof body.messageId !== "string") {
		return c.json({ error: "tabId and messageId are required strings" }, 400);
	}
	const tabId = body.tabId;
	const messageId = body.messageId;
	const cancelled = agentManager.cancelQueuedMessage(tabId, messageId);
	return c.json({ success: cancelled });
});

app.post("/chat/stop", async (c) => {
	const body = await c.req.json();
	if (typeof body.tabId !== "string") {
		return c.json({ error: "tabId is required" }, 400);
	}
	agentManager.stopTab(body.tabId);
	return c.json({ success: true });
});

app.route("/skills", skillsRoutes);
app.route("/models", modelsRoutes);
app.route("/tabs", tabsRoutes);
app.route("/agents", agentsRoutes);
app.route("/notifications", notificationsRoutes);

// Start the wake scheduler on boot (restores persisted schedule)
startWakeScheduler();
