/**
 * pi-coop extension：让 pi 适配 blackboard 协作协议。
 *
 * 做两件事：
 * 1. 注册 "coop" provider —— 从环境变量 BASE_URL / API_KEY / PROTOCOL / MODEL
 *    构造一个 OpenAI 兼容或 Anthropic 兼容的 provider，使 supervisor 注入的
 *    模型配置（与 pigo-coop 完全相同的环境变量）能直接被 pi 使用。
 * 2. 注册 "blackboard" 工具 —— 复刻 pigo 的 blackboard 工具（read / post / done），
 *    让 agent 通过 action=done 原子创建 /blackboard/DONE 标记，supervisor 据此判定
 *    任务完成。AGENTS.md 协议因此无需改动。
 *
 * 与 pigo blackboard_tool.go 行为对齐：post 用 O_APPEND 单次写（原子），
 * done 用 O_CREAT|O_EXCL（'wx'，保证唯一创建），路径全部做 traversal 校验。
 */
import { Type } from "@earendil-works/pi-ai";
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { openSync, closeSync, writeFileSync, appendFileSync, readFileSync, readdirSync, statSync, mkdirSync, existsSync } from "node:fs";
import { join, resolve, sep, isAbsolute, clean, basename } from "node:path";

// 单条 post / 单文件 read 上限：放开到 128KB，复杂任务的脚本与日志能整段读写。
const MAX_POST_BYTES = 128 * 1024;
const MAX_READ_BYTES = 128 * 1024;

// provider 协议 → pi api 类型映射。PROTOCOL 由 supervisor 从外部注入
// （取值 openai | anthropic，见 agent api_config.go Provider()）。
function protocolToApi(protocol: string): "openai-completions" | "anthropic-messages" {
	return protocol === "anthropic" ? "anthropic-messages" : "openai-completions";
}

export default function (pi: ExtensionAPI) {
	const baseUrl = process.env.BASE_URL ?? "";
	const apiKey = process.env.API_KEY ?? "";
	const protocol = process.env.PROTOCOL ?? "openai";
	const modelId = process.env.MODEL ?? "";
	const bbRoot = process.env.BB ?? "/blackboard";

	if (!baseUrl || !apiKey || !modelId) {
		// supervisor 已在前置校验里保证这三个变量非空，这里只做兜底：
		// 若 provider 注册失败，pi 启动时会报 model 无法解析，比这里静默更好。
		throw new Error(`coop extension: BASE_URL / API_KEY / MODEL 环境变量必须同时提供`);
	}

	// 注册 coop provider。models 数组在工厂里动态构造（id 取自 MODEL），
	// 这样 --provider coop --model "$MODEL" 即可命中。
	pi.registerProvider("coop", {
		baseUrl,
		apiKey,
		api: protocolToApi(protocol),
		models: [
			{
				id: modelId,
				name: modelId,
				reasoning: true,
				input: ["text", "image"],
				cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
			// 完全放开上下文与输出限制，让复杂任务能充分展开思考与多轮工具调用。
			contextWindow: 128000,
			maxTokens: 65536,
			},
		],
	});

	// ---- blackboard 工具 ----
	const blackboardTool = defineTool({
		name: "blackboard",
		label: "Blackboard",
		description:
			"Read and write the task blackboard used by the coop runner. This is the ONLY tool that may touch shared blackboard files atomically. " +
			"Actions: read (no path: global snapshot of task.md, workspace listing, DONE state; with path like \"workspace/exploit.py\": contents of that one file); " +
			"post (atomically append a progress note; file must be a bare .md name under messages/, e.g. \"round-1-a.md\"); " +
			"done (atomically create the DONE marker with a final delivery summary, only when the deliverable is truly complete; fails if DONE already exists). " +
			"The blackboard root, current round and your name are available in the environment as BB, ROUND, NAME.",
		parameters: Type.Object({
			action: Type.String({ description: "read | post | done" }),
			path: Type.Optional(Type.String({ description: 'For read: a path under the blackboard root to fetch (e.g. messages/round-1-a.md). Omit for the global snapshot.' })),
			file: Type.Optional(Type.String({ description: 'For post: bare file name under messages/, must end in .md (e.g. round-1-a.md).' })),
			content: Type.Optional(Type.String({ description: "For post: the message body (max 128 KiB)." })),
			summary: Type.Optional(Type.String({ description: "For done: final delivery summary written into DONE." })),
		}),

		async execute(_toolCallId, params) {
			const action = String(params.action ?? "");
			try {
				switch (action) {
					case "read":
						return read(params.path);
					case "post":
						return post(params.file, params.content);
					case "done":
						return done(params.summary);
					default:
						return err(`blackboard: unknown action "${action}" (want read|post|done)`);
				}
			} catch (e) {
				return err(`blackboard ${action}: ${e instanceof Error ? e.message : String(e)}`);
			}
		},
	});
	pi.registerTool(blackboardTool);

	// ---- 实现部分 ----

	// safePath：把相对路径解析到 blackboard 根内，拒绝绝对路径与 .. 越界。
	// 与 pigo blackboard_tool.go safePath 行为一致。
	function safePath(p: string): string {
		const trimmed = (p ?? "").trim();
		if (trimmed === "") throw new Error("empty path");
		const c = clean(trimmed);
		if (isAbsolute(c)) throw new Error(`path "${p}" must be relative to the blackboard root`);
		const rootClean = clean(bbRoot);
		const full = join(rootClean, c);
		if (full !== rootClean && !full.startsWith(rootClean + sep)) {
			throw new Error(`path "${p}" escapes the blackboard root`);
		}
		return full;
	}

	function ok(text: string) {
		return { content: [{ type: "text" as const, text }] };
	}
	function err(text: string) {
		return { content: [{ type: "text" as const, text }], isError: true };
	}

	function truncate(s: string, max: number): string {
		return s.length > max ? s.slice(0, max) + "\n...<truncated>" : s;
	}

	// read：无 path → 全局快照（task.md / messages 列表 / workspace 列表 / DONE 状态）；
	// 有 path → 单文件内容。
	function read(pathParam: unknown) {
		const p = pathParam != null ? String(pathParam) : "";
		if (p.trim() !== "") {
			return readFile(p);
		}

		const b: string[] = [];
		// task.md
		try {
			const data = readFileSync(join(bbRoot, "task.md"), "utf8");
			b.push("# task.md\n" + truncate(data, MAX_READ_BYTES) + "\n");
		} catch {
			b.push("# task.md\n<missing>\n");
		}

		// messages/
		b.push("\n# messages/ (" + listNames("messages").join(", ") + ")\n");
		for (const name of listListing("messages")) b.push("  - " + name + "\n");

		// workspace/
		b.push("\n# workspace/ (your workspace)\n");
		for (const name of listListing("workspace")) b.push("  - " + name + "\n");

		// DONE
		let doneText = "";
		try {
			doneText = truncate(readFileSync(join(bbRoot, "DONE"), "utf8"), MAX_READ_BYTES);
		} catch {}
		b.push("\n# DONE\n");
		b.push(doneText ? "EXISTS:\n" + doneText + "\n" : "<not created yet — cooperation is still in progress>\n");

		// environment
		const env: string[] = [];
		for (const k of ["BB", "ROUND", "NAME"]) {
			const v = (process.env[k] ?? "").trim();
			if (v) env.push(`  ${k}=${v}`);
		}
		if (env.length) b.push("\n# environment\n" + env.join("\n") + "\n");

		return ok(b.join(""));
	}

	function readFile(p: string) {
		const full = safePath(p);
		let info;
		try {
			info = statSync(full);
		} catch (e) {
			return err(`blackboard read: ${p}: ${e instanceof Error ? e.message : String(e)}`);
		}
		if (info.isDirectory()) return err(`blackboard read: ${p} is a directory; only files can be read`);
		const data = readFileSync(full, "utf8");
		return ok("# " + p + "\n" + truncate(data, MAX_READ_BYTES));
	}

	// post：原子追加一条消息到 messages/<file>。file 必须是裸 *.md 名。
	function post(fileParam: unknown, contentParam: unknown) {
		const name = String(fileParam ?? "").trim();
		if (!validMessageName(name)) {
			return err('blackboard post: file must be a bare name ending in .md (e.g. "round-1-a.md"), no path separators, no ".."');
		}
		const content = String(contentParam ?? "").trim();
		if (content === "") return err("blackboard post: content must not be empty");
		if (Buffer.byteLength(content) > MAX_POST_BYTES) {
			return err(`blackboard post: content too large (${Buffer.byteLength(content)} bytes, max ${MAX_POST_BYTES})`);
		}
		const dir = join(bbRoot, "messages");
		mkdirSync(dir, { recursive: true });
		// O_APPEND 单次写：POSIX 下并发 post 不会交错字节。
		appendFileSync(join(dir, name), content + "\n");
		return ok(`Message appended to messages/${name}`);
	}

	// done：原子创建 DONE 标记。'wx' = O_CREAT|O_EXCL|O_WRONLY，保证唯一创建。
	function done(summaryParam: unknown) {
		const summary = String(summaryParam ?? "").trim();
		if (summary === "") return err("blackboard done: summary must not be empty (include the final delivery summary)");
		const header = "Blackboard cooperation DONE\ncreated: " + new Date().toISOString() + "\n\n";
		const target = join(bbRoot, "DONE");
		let fd: number;
		try {
			fd = openSync(target, "wx");
		} catch (e) {
			if (existsSync(target)) {
				const existing = readFileSync(target, "utf8");
				return err("blackboard done: DONE already exists — cooperation already finished:\n" + truncate(existing, MAX_READ_BYTES));
			}
			return err("blackboard done: " + (e instanceof Error ? e.message : String(e)));
		}
		try {
			writeFileSync(fd, header + summary);
		} finally {
			closeSync(fd);
		}
		return ok("DONE marker created. Cooperation finished.");
	}

	// 校验 post 文件名：裸 *.md，无分隔符，无 . / ..
	function validMessageName(name: string): boolean {
		if (name === "" || !name.endsWith(".md")) return false;
		if (/[\\/]/.test(name) || name === "." || name === "..") return false;
		const base = name.slice(0, -3);
		if (base === "" || base.startsWith(".") || base.includes("..")) return false;
		return true;
	}

	function listNames(sub: string): string[] {
		try {
			return readdirSync(join(bbRoot, sub)).sort();
		} catch {
			return [];
		}
	}

	function listListing(sub: string): string[] {
		try {
			return readdirSync(join(bbRoot, sub))
				.sort()
				.map((name) => {
					try {
						const info = statSync(join(bbRoot, sub, name));
						if (info.isDirectory()) return `${name} (dir)`;
						return `${name} (${info.size} bytes, ${info.mtime.toISOString().slice(11, 19)})`;
					} catch {
						return name;
					}
				});
		} catch {
			return [];
		}
	}
}
