import { createHash } from "node:crypto";
import {
	existsSync,
	mkdirSync,
	readFileSync,
	renameSync,
	writeFileSync,
} from "node:fs";
import { dirname, resolve } from "node:path";
import type { ProjectInfo, ProjectMarker } from "./types.js";

/**
 * Describes one repo row in the server-maintained swarm registry.
 *
 * Purpose: give the SF server a stable, low-trust read model of discovered
 * repos without reading daemon internals or repo DBs.
 *
 * Consumer: web /api/swarms and syncSwarmRegistryFromProjects().
 */
export interface SwarmRegistryEntry {
	id: string;
	name: string;
	path: string;
	markers: ProjectMarker[];
	lastModified: number;
	supervisor: {
		kind: "daemon-scan";
		health: "unknown";
	};
}

/**
 * Stores the full swarm registry document written by sf-server.
 *
 * Purpose: version the SF server's cross-repo discovery file so future schema
 * changes can degrade cleanly instead of breaking the dashboard.
 *
 * Consumer: readSwarmRegistry() and web/lib/swarm-status.ts.
 */
export interface SwarmRegistry {
	schemaVersion: 1;
	updatedAt: string;
	swarms: SwarmRegistryEntry[];
}

/**
 * Summarizes one registry refresh.
 *
 * Purpose: let the daemon log and CLI report what the server scan changed
 * without exposing the full registry document.
 *
 * Consumer: Daemon.syncSwarms() and `sf-server --sync-swarms`.
 */
export interface SyncSwarmRegistryResult {
	registryPath: string;
	discovered: number;
	written: number;
	updatedAt: string;
}

/**
 * Return the stable ID used by the SF server to correlate one repo row.
 *
 * Purpose: keep swarm IDs independent of display names so renaming a folder in
 * the UI does not create a second logical worker row.
 *
 * Consumer: syncSwarmRegistryFromProjects() and the web /api/swarms read model.
 */
export function stableSwarmId(repoPath: string): string {
	return createHash("sha256")
		.update(resolve(repoPath))
		.digest("hex")
		.slice(0, 16);
}

function normalizeEntry(value: unknown): SwarmRegistryEntry | null {
	if (!value || typeof value !== "object") return null;
	const entry = value as Record<string, unknown>;
	const path = typeof entry.path === "string" ? resolve(entry.path) : null;
	if (!path) return null;
	return {
		id: typeof entry.id === "string" ? entry.id : stableSwarmId(path),
		name:
			typeof entry.name === "string"
				? entry.name
				: (path.split(/[\\/]/).filter(Boolean).at(-1) ?? path),
		path,
		markers: Array.isArray(entry.markers)
			? entry.markers.filter((marker): marker is ProjectMarker =>
					["git", "node", "sf", "rust", "python", "go"].includes(
						String(marker),
					),
				)
			: [],
		lastModified:
			typeof entry.lastModified === "number" ? entry.lastModified : 0,
		supervisor: { kind: "daemon-scan", health: "unknown" },
	};
}

/**
 * Read the swarm registry from disk.
 *
 * Purpose: preserve operator-curated rows when the daemon refreshes discovered
 * repo metadata, while tolerating the older array-shaped file during one
 * in-place rewrite.
 *
 * Consumer: syncSwarmRegistryFromProjects() before writing the authoritative
 * web read-model registry.
 */
export function readSwarmRegistry(registryPath: string): SwarmRegistry {
	if (!existsSync(registryPath)) {
		return {
			schemaVersion: 1,
			updatedAt: new Date(0).toISOString(),
			swarms: [],
		};
	}
	const parsed = JSON.parse(readFileSync(registryPath, "utf8")) as unknown;
	const rawEntries = Array.isArray(parsed)
		? parsed
		: parsed && typeof parsed === "object"
			? (parsed as Record<string, unknown>).swarms
			: [];
	const swarms = Array.isArray(rawEntries)
		? rawEntries.flatMap((entry) => {
				const normalized = normalizeEntry(entry);
				return normalized ? [normalized] : [];
			})
		: [];
	const updatedAt =
		parsed &&
		typeof parsed === "object" &&
		typeof (parsed as Record<string, unknown>).updatedAt === "string"
			? String((parsed as Record<string, unknown>).updatedAt)
			: new Date(0).toISOString();
	return { schemaVersion: 1, updatedAt, swarms };
}

function writeAtomicJson(path: string, value: unknown): void {
	mkdirSync(dirname(path), { recursive: true });
	const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
	writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`);
	renameSync(tempPath, path);
}

/**
 * Rewrite the swarm registry from the daemon's project scan.
 *
 * Purpose: make the SF server self-updating: configured scan roots become the
 * source of truth for the SF server swarm list, removing manually tended watchdog and
 * enrollment scripts from the normal M053 path.
 *
 * Consumer: Daemon.start() and its periodic swarm refresh timer.
 */
export function syncSwarmRegistryFromProjects(
	projects: ProjectInfo[],
	registryPath: string,
	now: () => Date = () => new Date(),
): SyncSwarmRegistryResult {
	const existing = readSwarmRegistry(registryPath);
	const existingByPath = new Map(
		existing.swarms.map((entry) => [entry.path, entry]),
	);
	const swarms = projects
		.map((project) => {
			const path = resolve(project.path);
			const existingEntry = existingByPath.get(path);
			return {
				id: existingEntry?.id ?? stableSwarmId(path),
				name: existingEntry?.name ?? project.name,
				path,
				markers: project.markers,
				lastModified: project.lastModified,
				supervisor: { kind: "daemon-scan", health: "unknown" },
			} satisfies SwarmRegistryEntry;
		})
		.sort((a, b) => a.name.localeCompare(b.name));
	const updatedAt = now().toISOString();
	writeAtomicJson(registryPath, { schemaVersion: 1, updatedAt, swarms });
	return {
		registryPath,
		discovered: projects.length,
		written: swarms.length,
		updatedAt,
	};
}
