/**
 * fs-lock.mjs — file-lock helpers extracted from apt-tools.mjs (C29).
 *
 * Depends on project.mjs (findProjectRootFromPath) and the `proper-lockfile`
 * dependency. Behavior must match the originals byte-for-byte.
 */

import { closeSync, existsSync, mkdirSync, openSync, writeFileSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import properLockfileImport from 'proper-lockfile'
import { findProjectRootFromPath } from './project.mjs'

// proper-lockfile exposes lock/unlock/check on the default export in CJS.
const properLockfile = properLockfileImport?.default ?? properLockfileImport

/**
 * Acquire an exclusive file lock via `proper-lockfile`, run `fn`, then
 * release. Kept synchronous to match the existing call sites (atomic JSON
 * writers) that expect a value back from the callback.
 *
 * proper-lockfile manages its own `.lock` sidecar and handles stale locks
 * via mtime polling, so C28 retires the ad-hoc `writeFileSync(...wx)`
 * pattern. On acquisition failure we emit a structured event to
 * `.aperant/events/lock_timeout_*.json` so operators can spot contention.
 */
export function withFileLock(targetPath, fn, options = {}) {
	const maxRetries = options.maxRetries ?? 20
	const retryDelayMs = options.retryDelayMs ?? 50
	const maxRetryDelayMs = options.maxRetryDelayMs ?? 500
	const stale = options.stale ?? 5000
	const realpath = options.realpath ?? false
	mkdirSync(dirname(targetPath), { recursive: true })
	if (!existsSync(targetPath)) {
		writeFileSync(targetPath, '', { flag: 'a' })
	}
	// proper-lockfile's sync API does not accept the full retries object —
	// only a number. Implement our own retry loop around lockSync.
	let release = null
	let lastErr = null
	for (let attempt = 0; attempt <= maxRetries; attempt++) {
		try {
			release = properLockfile.lockSync(targetPath, {
				stale,
				realpath,
			})
			break
		} catch (err) {
			lastErr = err
			// Retry only on "already locked" style errors. Fatal errors (bad path,
			// permission) should fail fast.
			const msg = String(err?.message ?? err)
			if (!/lock|ELOCKED|already held|EEXIST/i.test(msg) || attempt === maxRetries) {
				break
			}
			const jitter =
				retryDelayMs + Math.random() * Math.min(maxRetryDelayMs, retryDelayMs * (attempt + 1))
			// Block this thread without spinning the CPU. Atomics.wait on a
			// non-shared int just parks the event loop for the duration.
			try {
				Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, jitter)
			} catch {
				// Fallback for runtimes without SharedArrayBuffer — fall back to
				// a spin with a short bound so we don't burn CPU indefinitely.
				const startedAt = Date.now()
				while (Date.now() - startedAt < Math.min(jitter, 100)) {
					/* spin */
				}
			}
		}
	}
	if (!release) {
		recordLockTimeoutEvent(targetPath, options.projectRoot, lastErr)
		throw new Error(`Could not acquire lock at ${targetPath}.lock: ${lastErr?.message ?? lastErr}`)
	}
	try {
		return fn()
	} finally {
		try {
			release?.()
		} catch {
			/* already released or stale */
		}
	}
}

export function recordLockTimeoutEvent(targetPath, projectRoot, err) {
	try {
		const root = projectRoot ? resolve(projectRoot) : findProjectRootFromPath(targetPath)
		if (!root) return
		const eventsDir = join(root, '.aperant', 'events')
		mkdirSync(eventsDir, { recursive: true })
		const ts = new Date().toISOString().replace(/[:.]/g, '-')
		const file = join(eventsDir, `lock_timeout_${ts}_${process.pid}.json`)
		writeFileSync(
			file,
			JSON.stringify(
				{
					type: 'lock_timeout',
					target: targetPath,
					reason: err?.message ?? String(err),
					pid: process.pid,
					at: new Date().toISOString(),
				},
				null,
				2,
			),
		)
	} catch {
		/* best-effort only */
	}
}

export function openSyncExclusive(path) {
	return openSync(path, 'wx')
}

export function closeSyncFd(fd) {
	closeSync(fd)
}
