import { randomUUID } from "node:crypto";
import {
	lstatSync,
	mkdirSync,
	readFileSync,
	readlinkSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { join, resolve } from "node:path";

const LOCK_FILE = "interactive.lock";
const LOCK_DIR = "interactive.lock.d";

export type InteractiveSessionLockConflict = {
	acquired: false;
	lockPath: string;
	pid: number | null;
	startedAt: string | null;
	sessionFile: string | null;
	message: string;
};

export type InteractiveSessionLockHandle = {
	acquired: true;
	lockPath: string;
	release(): void;
};

export type InteractiveSessionLockResult =
	| InteractiveSessionLockHandle
	| InteractiveSessionLockConflict;

type InteractiveSessionLockData = {
	pid: number;
	ppid: number;
	cwd: string;
	startedAt: string;
	sessionFile?: string;
	token: string;
};

/**
 * Acquire the per-repository interactive-session lock.
 *
 * Purpose: prevent two TUI sessions from mutating the same repo-scoped SF state
 * at the same time while still recovering automatically from dead lock owners.
 *
 * Consumer: cli.ts before InteractiveMode starts.
 */
export function acquireInteractiveSessionLock(
	basePath: string,
	sessionFile?: string,
): InteractiveSessionLockResult {
	const cwd = resolve(basePath);
	const sfDir = join(cwd, ".sf");
	const lockDir = join(sfDir, LOCK_DIR);
	const lockPath = join(sfDir, LOCK_FILE);
	const token = randomUUID();
	const data: InteractiveSessionLockData = {
		pid: process.pid,
		ppid: process.ppid,
		cwd,
		startedAt: new Date().toISOString(),
		...(sessionFile ? { sessionFile } : {}),
		token,
	};

	mkdirSync(sfDir, { recursive: true });
	for (let attempt = 0; attempt < 2; attempt++) {
		try {
			mkdirSync(lockDir);
			writeFileSync(lockPath, JSON.stringify(data, null, 2), "utf-8");
			const release = () => releaseInteractiveSessionLock(basePath, token);
			registerInteractiveLockExitHandler(release);
			return { acquired: true, lockPath, release };
		} catch (err) {
			if ((err as NodeJS.ErrnoException)?.code !== "EEXIST") throw err;
			const existing = readInteractiveSessionLockData(lockPath);
			if (!isInteractiveLockOwnerAlive(existing, cwd)) {
				removeInteractiveLock(lockPath, lockDir);
				continue;
			}
			return {
				acquired: false,
				lockPath,
				pid: existing?.pid ?? null,
				startedAt: existing?.startedAt ?? null,
				sessionFile: existing?.sessionFile ?? null,
				message: formatInteractiveSessionLockConflict(existing, lockPath),
			};
		}
	}

	const existing = readInteractiveSessionLockData(lockPath);
	return {
		acquired: false,
		lockPath,
		pid: existing?.pid ?? null,
		startedAt: existing?.startedAt ?? null,
		sessionFile: existing?.sessionFile ?? null,
		message: formatInteractiveSessionLockConflict(existing, lockPath),
	};
}

/**
 * Release a per-repository interactive-session lock if it is still ours.
 *
 * Purpose: avoid deleting another live TUI session's lock after a fast restart
 * or PID reuse.
 *
 * Consumer: cli.ts finally blocks and the process exit handler registered by
 * acquireInteractiveSessionLock().
 */
export function releaseInteractiveSessionLock(
	basePath: string,
	token: string,
): void {
	const cwd = resolve(basePath);
	const lockPath = join(cwd, ".sf", LOCK_FILE);
	const lockDir = join(cwd, ".sf", LOCK_DIR);
	const existing = readInteractiveSessionLockData(lockPath);
	if (existing?.token !== token) return;
	removeInteractiveLock(lockPath, lockDir);
}

function readInteractiveSessionLockData(
	lockPath: string,
): InteractiveSessionLockData | null {
	try {
		const parsed = JSON.parse(readFileSync(lockPath, "utf-8"));
		if (
			parsed &&
			typeof parsed === "object" &&
			typeof parsed.pid === "number" &&
			typeof parsed.cwd === "string" &&
			typeof parsed.startedAt === "string" &&
			typeof parsed.token === "string"
		) {
			return parsed as InteractiveSessionLockData;
		}
	} catch {
		/* stale or corrupt lock metadata */
	}
	return null;
}

function isInteractiveLockOwnerAlive(
	data: InteractiveSessionLockData | null,
	expectedCwd: string,
): boolean {
	if (!data || data.pid <= 0 || data.pid === process.pid) return false;
	try {
		process.kill(data.pid, 0);
	} catch {
		return false;
	}
	const ownerCwd = readProcessCwd(data.pid);
	if (ownerCwd && ownerCwd !== expectedCwd) return false;
	return true;
}

function readProcessCwd(pid: number): string | null {
	try {
		const link = readlinkSync(`/proc/${pid}/cwd`);
		return resolve(link);
	} catch {
		return null;
	}
}

function removeInteractiveLock(lockPath: string, lockDir: string): void {
	try {
		rmSync(lockPath, { force: true });
	} catch {
		/* best-effort */
	}
	try {
		const stat = lstatSync(lockDir);
		if (stat.isDirectory()) rmSync(lockDir, { recursive: true, force: true });
	} catch {
		/* best-effort */
	}
}

function formatInteractiveSessionLockConflict(
	data: InteractiveSessionLockData | null,
	lockPath: string,
): string {
	const pid = data?.pid ? String(data.pid) : "unknown";
	const startedAt = data?.startedAt ? ` started ${data.startedAt}` : "";
	const session = data?.sessionFile
		? `\n[sf] Active session: ${data.sessionFile}`
		: "";
	return (
		`[sf] Another interactive sf session is already running for this repo (PID ${pid}${startedAt}).` +
		session +
		`\n[sf] Close that terminal first, or remove stale lock ${lockPath} if the process is gone.`
	);
}

function registerInteractiveLockExitHandler(release: () => void): void {
	process.once("exit", release);
	for (const signal of ["SIGINT", "SIGTERM"] as const) {
		process.once(signal, () => {
			release();
			process.exit(signal === "SIGINT" ? 130 : 143);
		});
	}
}
