/**
 * Backlog Aggregator Server
 *
 * Real-time multi-project task aggregator that:
 * - Scans directories for backlog projects
 * - Watches task files for changes
 * - Provides unified WebSocket-based real-time updates
 * - Serves aggregated task data with project metadata
 */

import { type FSWatcher, watch } from "node:fs";
import { mkdir, readdir, readFile, stat, unlink, writeFile } from "node:fs/promises";
import { basename, dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { $, type Server, type ServerWebSocket } from "bun";
import { parseTask } from "../markdown/parser.ts";
import type { Task } from "../types/index.ts";
import { sortByTaskId } from "../utils/task-sorting.ts";

// @ts-expect-error - Bun file import
import favicon from "../web/favicon.png" with { type: "file" };

// Get the directory of this file
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

interface ProjectConfig {
	path: string;
	name: string;
	color: string;
}

interface AggregatedTask extends Task {
	projectName: string;
	projectColor: string;
	projectPath: string;
}

interface AggregatorConfig {
	scanPaths: string[];
	port: number;
	colors: string[];
}

const DEFAULT_COLORS = [
	"#8b5cf6", // purple - canvas-website
	"#22c55e", // green - hyperindex
	"#3b82f6", // blue - mycofi
	"#f97316", // orange - decolonize-time
	"#ef4444", // red - ai-orchestrator
	"#ec4899", // pink
	"#14b8a6", // teal
	"#eab308", // yellow
	"#6366f1", // indigo
	"#84cc16", // lime
];

export class BacklogAggregator {
	private server: Server<unknown> | null = null;
	private sockets = new Set<ServerWebSocket<unknown>>();
	private projects = new Map<string, ProjectConfig>();
	private tasks = new Map<string, AggregatedTask>();
	private watchers = new Map<string, FSWatcher>();
	private colorIndex = 0;
	private config: AggregatorConfig;
	private debounceTimers = new Map<string, Timer>();

	constructor(config: Partial<AggregatorConfig> = {}) {
		this.config = {
			scanPaths: config.scanPaths || ["/opt/websites", "/opt/apps", "/opt/ops"],
			port: config.port || 6420,
			colors: config.colors || DEFAULT_COLORS,
		};
	}

	private getNextColor(): string {
		const colors = this.config.colors.length > 0 ? this.config.colors : DEFAULT_COLORS;
		// DEFAULT_COLORS always has elements, so this is safe
		const color = colors[this.colorIndex % colors.length] as string;
		this.colorIndex++;
		return color;
	}

	async start(): Promise<void> {
		console.log("Starting Backlog Aggregator...");

		// Initial scan for projects
		await this.scanForProjects();

		// Start the HTTP/WebSocket server
		this.server = Bun.serve({
			port: this.config.port,
			development: process.env.NODE_ENV === "development",
			routes: {
				// NOTE: "/" route is handled in fetch() to allow WebSocket upgrade
				// IMPORTANT: Static routes must come BEFORE parameterized routes to avoid `:project` matching literal paths
				"/api/projects": {
					GET: async () => this.handleGetProjects(),
				},
				"/api/tasks": {
					GET: async (req: Request) => this.handleGetTasks(req),
				},
				"/api/tasks/update": {
					PATCH: async (req: Request) => this.handleUpdateTask(req),
				},
				"/api/tasks/create": {
					POST: async (req: Request) => this.handleCreateTask(req),
				},
				"/api/tasks/archive": {
					POST: async (req: Request) => this.handleArchiveTask(req),
				},
				"/api/tasks/delete": {
					DELETE: async (req: Request) => this.handleDeleteTask(req),
				},
				"/api/tasks/:project/:id": {
					GET: async (req: Request & { params: { project: string; id: string } }) =>
						this.handleGetTask(req.params.project, req.params.id),
				},
				"/api/statuses": {
					GET: async () => this.handleGetStatuses(),
				},
				"/api/health": {
					GET: async () => Response.json({ status: "ok", projects: this.projects.size, tasks: this.tasks.size }),
				},
			},
			fetch: async (req: Request, server: Server<unknown>) => {
				const url = new URL(req.url);

				// Handle WebSocket upgrade
				if (req.headers.get("upgrade") === "websocket") {
					const success = server.upgrade(req, { data: undefined });
					if (success) {
						return new Response(null, { status: 101 });
					}
					return new Response("WebSocket upgrade failed", { status: 400 });
				}

				// Serve favicon
				if (url.pathname.startsWith("/favicon")) {
					const faviconFile = Bun.file(favicon);
					return new Response(faviconFile, {
						headers: { "Content-Type": "image/png" },
					});
				}

				// Serve the HTML app for root path
				if (url.pathname === "/" || url.pathname === "") {
					const htmlPath = join(__dirname, "web", "index.html");
					const htmlFile = Bun.file(htmlPath);
					return new Response(htmlFile, { headers: { "Content-Type": "text/html" } });
				}

				// Handle API routes with non-GET methods (Bun routes object has issues with POST/PATCH/DELETE)
				if (url.pathname.startsWith("/api/")) {
					const method = req.method.toUpperCase();
					if (url.pathname === "/api/tasks/update" && method === "PATCH") {
						return this.handleUpdateTask(req);
					}
					if (url.pathname === "/api/tasks/create" && method === "POST") {
						return this.handleCreateTask(req);
					}
					if (url.pathname === "/api/tasks/archive" && method === "POST") {
						return this.handleArchiveTask(req);
					}
					if (url.pathname === "/api/tasks/delete" && method === "DELETE") {
						return this.handleDeleteTask(req);
					}
				}

				return new Response("Not Found", { status: 404 });
			},
			websocket: {
				open: (ws: ServerWebSocket) => {
					this.sockets.add(ws);
					// Send initial state
					ws.send(
						JSON.stringify({
							type: "init",
							projects: Array.from(this.projects.values()),
							tasks: Array.from(this.tasks.values()),
						}),
					);
				},
				message: (ws: ServerWebSocket, message: string | Buffer) => {
					const data = typeof message === "string" ? message : message.toString();
					if (data === "ping") {
						ws.send("pong");
					}
				},
				close: (ws: ServerWebSocket) => {
					this.sockets.delete(ws);
				},
			},
		});

		const url = `http://localhost:${this.config.port}`;
		console.log(`Backlog Aggregator running at ${url}`);
		console.log(`Watching ${this.projects.size} projects with ${this.tasks.size} tasks`);

		// Set up periodic rescan for new projects
		setInterval(() => this.scanForProjects(), 60000); // Every minute

		// Set up periodic task polling as fallback for Docker environments
		// where inotify may not work reliably across bind mounts
		setInterval(() => this.pollAllTasks(), 5000); // Every 5 seconds
	}

	private async pollAllTasks(): Promise<void> {
		let hasChanges = false;
		const previousTaskCount = this.tasks.size;

		for (const [projectPath] of this.projects) {
			const project = this.projects.get(projectPath);
			if (!project) continue;

			const tasksPath = join(projectPath, "backlog", "tasks");
			try {
				const entries = await readdir(tasksPath, { withFileTypes: true });
				for (const entry of entries) {
					if (!entry.isFile() || !entry.name.endsWith(".md")) continue;

					const taskPath = join(tasksPath, entry.name);
					try {
						const content = await readFile(taskPath, "utf-8");
						const task = parseTask(content);
						if (task) {
							const key = `${projectPath}:${task.id}`;
							const existing = this.tasks.get(key);

							// Check if task is new or changed
							if (!existing || existing.rawContent !== task.rawContent || existing.status !== task.status) {
								const aggregatedTask: AggregatedTask = {
									...task,
									projectName: project.name,
									projectColor: project.color,
									projectPath: projectPath,
									filePath: taskPath,
								};
								this.tasks.set(key, aggregatedTask);
								hasChanges = true;
							}
						}
					} catch {
						// File read error
					}
				}
			} catch {
				// Directory read error
			}
		}

		if (hasChanges || this.tasks.size !== previousTaskCount) {
			this.broadcastUpdate();
		}
	}

	async stop(): Promise<void> {
		// Stop all watchers
		for (const [path, watcher] of this.watchers) {
			watcher.close();
		}
		this.watchers.clear();

		// Clear debounce timers
		for (const timer of this.debounceTimers.values()) {
			clearTimeout(timer);
		}
		this.debounceTimers.clear();

		// Stop server
		if (this.server) {
			await this.server.stop();
			this.server = null;
		}

		console.log("Aggregator stopped");
	}

	private async scanForProjects(): Promise<void> {
		const foundProjects = new Set<string>();

		for (const scanPath of this.config.scanPaths) {
			try {
				const entries = await readdir(scanPath, { withFileTypes: true });
				for (const entry of entries) {
					if (!entry.isDirectory()) continue;

					const projectPath = join(scanPath, entry.name);
					const backlogPath = join(projectPath, "backlog");

					try {
						const backlogStat = await stat(backlogPath);
						if (backlogStat.isDirectory()) {
							foundProjects.add(projectPath);
							await this.addProject(projectPath);
						}
					} catch {
						// No backlog directory
					}
				}
			} catch (error) {
				console.warn(`Could not scan ${scanPath}:`, error);
			}
		}

		// Remove projects that no longer exist
		for (const [path] of this.projects) {
			if (!foundProjects.has(path)) {
				this.removeProject(path);
			}
		}
	}

	private async addProject(projectPath: string): Promise<void> {
		if (this.projects.has(projectPath)) return;

		// Try to load project name from config
		let projectName = basename(projectPath);
		const configPath = join(projectPath, "backlog", "config.yml");
		try {
			const configContent = await readFile(configPath, "utf-8");
			const nameMatch = configContent.match(/project_name:\s*["']?([^"'\n]+)["']?/);
			if (nameMatch?.[1]) {
				projectName = nameMatch[1].trim();
			}
		} catch {
			// Use directory name
		}

		// Skip if a project with this name already exists (prevents duplicates from symlinks/mirrors)
		const existingProjectWithName = Array.from(this.projects.values()).find((p) => p.name === projectName);
		if (existingProjectWithName) {
			console.log(
				`Skipping duplicate project: ${projectName} (${projectPath}) - already exists at ${existingProjectWithName.path}`,
			);
			return;
		}

		const project: ProjectConfig = {
			path: projectPath,
			name: projectName,
			color: this.getNextColor(),
		};

		this.projects.set(projectPath, project);
		console.log(`Added project: ${projectName} (${projectPath})`);

		// Load initial tasks
		await this.loadProjectTasks(projectPath);

		// Set up file watcher
		this.watchProject(projectPath);
	}

	private removeProject(projectPath: string): void {
		const project = this.projects.get(projectPath);
		if (!project) return;

		// Remove tasks
		for (const [key] of this.tasks) {
			if (key.startsWith(projectPath + ":")) {
				this.tasks.delete(key);
			}
		}

		// Stop watcher
		const watcher = this.watchers.get(projectPath);
		if (watcher) {
			watcher.close();
			this.watchers.delete(projectPath);
		}

		this.projects.delete(projectPath);
		console.log(`Removed project: ${project.name}`);

		this.broadcastUpdate();
	}

	private async loadProjectTasks(projectPath: string): Promise<void> {
		const project = this.projects.get(projectPath);
		if (!project) return;

		const tasksPath = join(projectPath, "backlog", "tasks");

		try {
			const entries = await readdir(tasksPath, { withFileTypes: true });
			for (const entry of entries) {
				if (!entry.isFile() || !entry.name.endsWith(".md")) continue;

				const taskPath = join(tasksPath, entry.name);
				await this.loadTask(projectPath, taskPath);
			}
		} catch (error) {
			console.warn(`Could not load tasks from ${tasksPath}:`, error);
		}
	}

	private async loadTask(projectPath: string, taskPath: string): Promise<void> {
		const project = this.projects.get(projectPath);
		if (!project) return;

		try {
			const content = await readFile(taskPath, "utf-8");
			const task = parseTask(content);
			if (task) {
				const aggregatedTask: AggregatedTask = {
					...task,
					projectName: project.name,
					projectColor: project.color,
					projectPath: projectPath,
					filePath: taskPath,
				};
				const key = `${projectPath}:${task.id}`;
				this.tasks.set(key, aggregatedTask);
			}
		} catch (error) {
			console.warn(`Could not load task ${taskPath}:`, error);
		}
	}

	private watchProject(projectPath: string): void {
		const tasksPath = join(projectPath, "backlog", "tasks");

		try {
			const watcher = watch(tasksPath, { recursive: true }, (eventType, filename) => {
				if (!filename || !filename.endsWith(".md")) return;

				// Debounce rapid changes
				const key = `${projectPath}:${filename}`;
				const existing = this.debounceTimers.get(key);
				if (existing) {
					clearTimeout(existing);
				}

				this.debounceTimers.set(
					key,
					setTimeout(async () => {
						this.debounceTimers.delete(key);
						const taskPath = join(tasksPath, filename);

						try {
							await stat(taskPath);
							await this.loadTask(projectPath, taskPath);
							console.log(`Task updated: ${filename} in ${this.projects.get(projectPath)?.name}`);
						} catch {
							// File deleted - remove from tasks
							const taskId = basename(filename, ".md").replace(/^task-/, "task-");
							const taskKey = `${projectPath}:task-${taskId.replace(/^task-/, "")}`;
							// Try various key formats
							for (const [k] of this.tasks) {
								if (k.startsWith(`${projectPath}:`) && k.includes(basename(filename, ".md"))) {
									this.tasks.delete(k);
									break;
								}
							}
							console.log(`Task removed: ${filename} from ${this.projects.get(projectPath)?.name}`);
						}

						this.broadcastUpdate();
					}, 100),
				);
			});

			watcher.on("error", (error) => {
				console.warn(`Watcher error for ${projectPath}:`, error);
			});

			this.watchers.set(projectPath, watcher);
		} catch (error) {
			console.warn(`Could not watch ${tasksPath}:`, error);
		}
	}

	private broadcastUpdate(): void {
		const message = JSON.stringify({
			type: "update",
			projects: Array.from(this.projects.values()),
			tasks: Array.from(this.tasks.values()),
			timestamp: Date.now(),
		});

		for (const ws of this.sockets) {
			try {
				ws.send(message);
			} catch {
				// Socket closed
			}
		}
	}

	// HTTP Handlers
	private handleGetProjects(): Response {
		return Response.json(Array.from(this.projects.values()));
	}

	private handleGetTasks(req: Request): Response {
		const url = new URL(req.url);
		const project = url.searchParams.get("project");
		const status = url.searchParams.get("status");
		const priority = url.searchParams.get("priority");

		let tasks = Array.from(this.tasks.values());

		if (project) {
			tasks = tasks.filter((t) => t.projectName === project || t.projectPath.includes(project));
		}
		if (status) {
			tasks = tasks.filter((t) => t.status.toLowerCase() === status.toLowerCase());
		}
		if (priority) {
			tasks = tasks.filter((t) => (t.priority ?? "").toLowerCase() === priority.toLowerCase());
		}

		// Sort: due-date tasks first (soonest/overdue at top), then by project, then by task ID
		const now = new Date().toISOString().slice(0, 10);
		tasks = tasks.sort((a, b) => {
			const aDue = a.dueDate || "";
			const bDue = b.dueDate || "";
			// Tasks with due dates come before tasks without
			if (aDue && !bDue) return -1;
			if (!aDue && bDue) return 1;
			// Both have due dates: sort soonest first
			if (aDue && bDue) return aDue.localeCompare(bDue);
			// Neither has due date: group by project, then by task ID
			const projectCompare = a.projectName.localeCompare(b.projectName);
			if (projectCompare !== 0) return projectCompare;
			return sortByTaskId([a, b])[0] === a ? -1 : 1;
		});

		return Response.json(tasks);
	}

	private handleGetTask(project: string, id: string): Response {
		for (const [key, task] of this.tasks) {
			if ((task.projectName === project || task.projectPath.includes(project)) && task.id === id) {
				return Response.json(task);
			}
		}
		return Response.json({ error: "Task not found" }, { status: 404 });
	}

	private handleGetStatuses(): Response {
		// Collect unique statuses from all projects
		const statuses = new Set<string>();
		for (const task of this.tasks.values()) {
			statuses.add(task.status);
		}
		// Return common defaults plus any found
		const common = ["To Do", "In Progress", "Done"];
		for (const s of common) statuses.add(s);
		return Response.json(Array.from(statuses));
	}

	// Task modification handlers
	private async handleUpdateTask(req: Request): Promise<Response> {
		try {
			const body = await req.json();
			const { projectPath, taskId, status, title, description, priority, labels, assignee } = body as {
				projectPath: string;
				taskId: string;
				status?: string;
				title?: string;
				description?: string;
				priority?: string;
				labels?: string[];
				assignee?: string[];
			};

			if (!projectPath || !taskId) {
				return Response.json({ error: "Missing required fields: projectPath, taskId" }, { status: 400 });
			}

			// Find the task
			const taskKey = `${projectPath}:${taskId}`;
			const task = this.tasks.get(taskKey);
			if (!task) {
				return Response.json({ error: "Task not found" }, { status: 404 });
			}

			// Use the stored file path from when the task was loaded
			const taskFilePath = task.filePath;
			if (!taskFilePath) {
				return Response.json({ error: "Task file path not found" }, { status: 404 });
			}

			// Read the current task file
			let content: string;
			try {
				content = await readFile(taskFilePath, "utf-8");
			} catch {
				return Response.json({ error: "Task file not found" }, { status: 404 });
			}

			// Update the fields in the frontmatter
			let updatedContent = content;
			if (status !== undefined) {
				updatedContent = this.updateTaskField(updatedContent, "status", status);
			}
			if (title !== undefined) {
				updatedContent = this.updateTaskField(updatedContent, "title", title);
			}
			if (priority !== undefined) {
				updatedContent = this.updateTaskField(updatedContent, "priority", priority);
			}
			if (labels !== undefined) {
				updatedContent = this.updateTaskField(updatedContent, "labels", `[${labels.join(", ")}]`);
			}
			if (assignee !== undefined) {
				updatedContent = this.updateTaskField(updatedContent, "assignee", `[${assignee.join(", ")}]`);
			}
			if (description !== undefined) {
				updatedContent = this.updateTaskDescription(updatedContent, description);
			}

			// Add updated_date
			updatedContent = this.addUpdatedDate(updatedContent);

			// Write the updated content back
			await writeFile(taskFilePath, updatedContent, "utf-8");

			const updatedFields = [];
			if (status !== undefined) updatedFields.push(`status="${status}"`);
			if (title !== undefined) updatedFields.push(`title="${title}"`);
			if (priority !== undefined) updatedFields.push(`priority="${priority}"`);
			if (labels !== undefined) updatedFields.push(`labels=[${labels.join(", ")}]`);
			if (assignee !== undefined) updatedFields.push(`assignee=[${assignee.join(", ")}]`);
			if (description !== undefined) updatedFields.push("description updated");

			console.log(`Task ${taskId} updated (${updatedFields.join(", ")}) in ${this.projects.get(projectPath)?.name}`);

			// The file watcher will pick up the change and broadcast update
			// But we can also force an immediate update
			await this.loadTask(projectPath, taskFilePath);
			this.broadcastUpdate();

			return Response.json({ success: true, taskId, updated: updatedFields });
		} catch (error) {
			console.error("Error updating task:", error);
			return Response.json({ error: "Failed to update task" }, { status: 500 });
		}
	}

	private async handleCreateTask(req: Request): Promise<Response> {
		try {
			const body = await req.json();
			const { projectPath, title, description, priority, status, labels } = body as {
				projectPath: string;
				title: string;
				description?: string;
				priority?: string;
				status?: string;
				labels?: string[];
			};

			if (!projectPath || !title) {
				return Response.json({ error: "Missing required fields: projectPath, title" }, { status: 400 });
			}

			// Verify project exists
			if (!this.projects.has(projectPath)) {
				return Response.json({ error: "Project not found" }, { status: 404 });
			}

			// Generate next task ID
			const taskId = await this.getNextTaskId(projectPath);

			// Generate the task markdown content
			const now = new Date();
			const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")} ${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;

			let frontmatter = `---
id: ${taskId}
title: ${title}
status: ${status || "To Do"}
assignee: []
created_date: '${dateStr}'
`;

			if (labels && labels.length > 0) {
				frontmatter += `labels: [${labels.join(", ")}]\n`;
			} else {
				frontmatter += "labels: []\n";
			}

			if (priority) {
				frontmatter += `priority: ${priority}\n`;
			}

			frontmatter += "dependencies: []\n---\n\n";

			let content = frontmatter;
			content += `## Description\n\n${description || "No description provided."}\n`;

			// Ensure tasks directory exists
			const tasksDir = join(projectPath, "backlog", "tasks");
			try {
				await mkdir(tasksDir, { recursive: true });
			} catch {
				// Directory might already exist
			}

			// Write the task file
			const taskFilePath = join(tasksDir, `${taskId}.md`);
			await writeFile(taskFilePath, content, "utf-8");

			console.log(`Created task ${taskId} in ${this.projects.get(projectPath)?.name}`);

			// Load the new task and broadcast
			await this.loadTask(projectPath, taskFilePath);
			this.broadcastUpdate();

			return Response.json({ success: true, taskId, projectPath });
		} catch (error) {
			console.error("Error creating task:", error);
			return Response.json({ error: "Failed to create task" }, { status: 500 });
		}
	}

	private async handleArchiveTask(req: Request): Promise<Response> {
		try {
			const body = await req.json();
			const { projectPath, taskId } = body as {
				projectPath: string;
				taskId: string;
			};

			if (!projectPath || !taskId) {
				return Response.json({ error: "Missing required fields: projectPath, taskId" }, { status: 400 });
			}

			// Find the task
			const taskKey = `${projectPath}:${taskId}`;
			const task = this.tasks.get(taskKey);
			if (!task) {
				return Response.json({ error: "Task not found" }, { status: 404 });
			}

			// Use the stored file path from when the task was loaded
			const taskFilePath = task.filePath;
			if (!taskFilePath) {
				return Response.json({ error: "Task file path not found" }, { status: 404 });
			}

			// Build archive path preserving the original filename
			const archiveDir = join(projectPath, "backlog", "archive", "tasks");
			const archiveFilePath = join(archiveDir, basename(taskFilePath));

			// Ensure archive directory exists
			try {
				await mkdir(archiveDir, { recursive: true });
			} catch {
				// Directory might already exist
			}

			// Read the task file
			let content: string;
			try {
				content = await readFile(taskFilePath, "utf-8");
			} catch {
				return Response.json({ error: "Task file not found" }, { status: 404 });
			}

			// Move to archive (write to archive, delete from tasks)
			await writeFile(archiveFilePath, content, "utf-8");
			await unlink(taskFilePath);

			// Remove from in-memory tasks
			this.tasks.delete(taskKey);

			console.log(`Task ${taskId} archived in ${this.projects.get(projectPath)?.name}`);

			this.broadcastUpdate();

			return Response.json({ success: true, taskId, archived: true });
		} catch (error) {
			console.error("Error archiving task:", error);
			return Response.json({ error: "Failed to archive task" }, { status: 500 });
		}
	}

	private async handleDeleteTask(req: Request): Promise<Response> {
		try {
			const body = await req.json();
			const { projectPath, taskId } = body as {
				projectPath: string;
				taskId: string;
			};

			if (!projectPath || !taskId) {
				return Response.json({ error: "Missing required fields: projectPath, taskId" }, { status: 400 });
			}

			// Find the task
			const taskKey = `${projectPath}:${taskId}`;
			const task = this.tasks.get(taskKey);
			if (!task) {
				return Response.json({ error: "Task not found" }, { status: 404 });
			}

			// Use the stored file path from when the task was loaded
			const taskFilePath = task.filePath;
			if (!taskFilePath) {
				return Response.json({ error: "Task file path not found" }, { status: 404 });
			}

			// Delete the file
			try {
				await unlink(taskFilePath);
			} catch {
				return Response.json({ error: "Task file not found" }, { status: 404 });
			}

			// Remove from in-memory tasks
			this.tasks.delete(taskKey);

			console.log(`Task ${taskId} deleted from ${this.projects.get(projectPath)?.name}`);

			this.broadcastUpdate();

			return Response.json({ success: true, taskId, deleted: true });
		} catch (error) {
			console.error("Error deleting task:", error);
			return Response.json({ error: "Failed to delete task" }, { status: 500 });
		}
	}

	private updateTaskStatus(content: string, newStatus: string): string {
		return this.updateTaskField(content, "status", newStatus);
	}

	private updateTaskField(content: string, field: string, value: string): string {
		// Parse frontmatter and update the field
		// Handle both Windows (\r\n) and Unix (\n) line endings
		const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
		if (!frontmatterMatch || !frontmatterMatch[1]) {
			return content;
		}

		// Detect the line ending style used in the file
		const lineEnding = content.includes("\r\n") ? "\r\n" : "\n";
		const frontmatter = frontmatterMatch[1];
		const fieldRegex = new RegExp(`^${field}:\\s*.+$`, "m");

		let updatedFrontmatter: string;
		if (fieldRegex.test(frontmatter)) {
			// Update existing field
			updatedFrontmatter = frontmatter.replace(fieldRegex, `${field}: ${value}`);
		} else {
			// Add new field
			updatedFrontmatter = frontmatter + `${lineEnding}${field}: ${value}`;
		}

		return content.replace(/^---\r?\n[\s\S]*?\r?\n---/, `---${lineEnding}${updatedFrontmatter}${lineEnding}---`);
	}

	private updateTaskDescription(content: string, newDescription: string): string {
		// Detect the line ending style used in the file
		const lineEnding = content.includes("\r\n") ? "\r\n" : "\n";

		// Find and replace the description section
		// Handle both Windows (\r\n) and Unix (\n) line endings
		const descriptionRegex = /## Description\r?\n\r?\n[\s\S]*?(?=\r?\n## |\r?\n---|Z)/;
		const newDescriptionSection = `## Description${lineEnding}${lineEnding}${newDescription}${lineEnding}`;

		if (descriptionRegex.test(content)) {
			return content.replace(descriptionRegex, newDescriptionSection);
		}
		// If no description section, add one after frontmatter
		return content.replace(/^(---\r?\n[\s\S]*?\r?\n---\r?\n\r?\n?)/, `$1${newDescriptionSection}${lineEnding}`);
	}

	private addUpdatedDate(content: string): string {
		const now = new Date();
		const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")} ${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;

		// Handle both Windows (\r\n) and Unix (\n) line endings
		const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
		if (!frontmatterMatch || !frontmatterMatch[1]) {
			return content;
		}

		// Detect the line ending style used in the file
		const lineEnding = content.includes("\r\n") ? "\r\n" : "\n";
		const frontmatter = frontmatterMatch[1];
		let updatedFrontmatter: string;

		if (frontmatter.includes("updated_date:")) {
			updatedFrontmatter = frontmatter.replace(/^updated_date:\s*.+$/m, `updated_date: '${dateStr}'`);
		} else {
			updatedFrontmatter = frontmatter + `${lineEnding}updated_date: '${dateStr}'`;
		}

		return content.replace(/^---\r?\n[\s\S]*?\r?\n---/, `---${lineEnding}${updatedFrontmatter}${lineEnding}---`);
	}

	private async getNextTaskId(projectPath: string): Promise<string> {
		// Find the highest task ID in the project
		const tasksPath = join(projectPath, "backlog", "tasks");
		let maxId = 0;

		try {
			const entries = await readdir(tasksPath, { withFileTypes: true });
			for (const entry of entries) {
				if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
				const match = entry.name.match(/^task-(\d+)\.md$/);
				if (match?.[1]) {
					const id = Number.parseInt(match[1], 10);
					if (id > maxId) maxId = id;
				}
			}
		} catch {
			// Directory might not exist yet
		}

		// Return next ID with zero-padding
		return `task-${String(maxId + 1).padStart(3, "0")}`;
	}
}

// CLI entry point
if (import.meta.main) {
	const args = process.argv.slice(2);
	const portIndex = args.indexOf("--port");
	const portArg = portIndex !== -1 ? args[portIndex + 1] : undefined;
	const port = portArg ? Number.parseInt(portArg, 10) : 6420;

	const pathsIndex = args.indexOf("--paths");
	const pathsArg = pathsIndex !== -1 ? args[pathsIndex + 1] : undefined;
	const paths = pathsArg ? pathsArg.split(",") : ["/opt/websites", "/opt/apps", "/opt/ops"];

	const aggregator = new BacklogAggregator({ port, scanPaths: paths });

	process.on("SIGINT", async () => {
		await aggregator.stop();
		process.exit(0);
	});

	process.on("SIGTERM", async () => {
		await aggregator.stop();
		process.exit(0);
	});

	aggregator.start().catch((error) => {
		console.error("Failed to start aggregator:", error);
		process.exit(1);
	});
}
