/**
 * @internal
 * agent-identity.mjs — per-agent identity record (R4 precursor).
 *
 * This module is part of the @internal CoordinationStore seam (R8). Do NOT
 * import from `bin/apt-tools.mjs` or add to any package `exports` map.
 *
 * V1 returns `{ agentId, pid, hostname, incarnation }` — enough for the
 * team-status directory in R2 to key per-agent files. R4 (subtask 6) fills in
 * TTL-based liveness filtering on top of this record.
 *
 * Incarnation is a monotonic counter stored at
 * `.aperant/team-status/.incarnation`. Incremented once per process via
 * `nextIncarnation(projectRoot)` during identity resolution and cached in
 * module scope so repeated calls within the same process are stable.
 */

import { existsSync, mkdirSync, readFileSync } from 'node:fs'
import { hostname as osHostname } from 'node:os'
import { join } from 'node:path'
import { detectWorktree } from '../git/worktree-detect.mjs'
import { withFileLock } from '../util/fs-lock.mjs'
import { atomicWriteJson } from '../util/io.mjs'

// Cached identity per project root (rooted at the shared .aperant/ dir).
const cache = new Map()

/**
 * Resolve the shared coordination root — always the main repo's root, even
 * when called from a worktree. Mirrors commands/event.mjs.
 */
export function resolveSharedRoot(projectDir) {
	const wt = detectWorktree(projectDir)
	return wt.is_worktree && wt.main_repo_root ? wt.main_repo_root : projectDir
}

/**
 * Read and increment the per-project incarnation counter atomically. Stored
 * as JSON `{ value: <int> }` under `.aperant/team-status/.incarnation` so the
 * normal atomicWriteJson pipeline handles it.
 *
 * If the counter file is missing/malformed, we start from 1.
 */
function nextIncarnation(projectRoot) {
	const sharedRoot = resolveSharedRoot(projectRoot)
	const dir = join(sharedRoot, '.aperant', 'team-status')
	mkdirSync(dir, { recursive: true })
	const counterPath = join(dir, '.incarnation')
	let next = 1
	withFileLock(
		counterPath,
		() => {
			let current = 0
			if (existsSync(counterPath)) {
				try {
					const raw = readFileSync(counterPath, 'utf-8').trim()
					const parsed = JSON.parse(raw)
					if (typeof parsed?.value === 'number' && Number.isFinite(parsed.value)) {
						current = parsed.value
					}
				} catch {
					/* malformed — restart from 0 */
				}
			}
			next = current + 1
			atomicWriteJson(counterPath, { value: next })
		},
		{ projectRoot: sharedRoot },
	)
	return next
}

/**
 * Return the current agent's identity. Cached per project root so repeated
 * callers within a single process see a stable incarnation.
 *
 * `APT_AGENT_ID` overrides the default `<hostname>-<pid>` assembly — used by
 * tests to force a deterministic id.
 */
export function getAgentIdentity(projectDir) {
	const sharedRoot = resolveSharedRoot(projectDir)
	if (cache.has(sharedRoot)) return cache.get(sharedRoot)

	const pid = process.pid
	const host = osHostname()
	const rawAgentId = process.env.APT_AGENT_ID || `${host}-${pid}`
	// SEC-001: Disallow path traversal characters in agentId since it's used as a filesystem path component.
	const SAFE_AGENT_ID = /^[A-Za-z0-9._-]+$/
	if (!SAFE_AGENT_ID.test(rawAgentId)) {
		throw new Error(
			`Invalid APT_AGENT_ID: must match [A-Za-z0-9._-]+. Got: ${JSON.stringify(rawAgentId)}`,
		)
	}
	const agentId = rawAgentId
	const incarnation = nextIncarnation(projectDir)
	const identity = { agentId, pid, hostname: host, incarnation }
	cache.set(sharedRoot, identity)
	return identity
}

/**
 * Test-only helper: reset the process-local identity cache so a fresh
 * getAgentIdentity() call picks up a new incarnation. Not part of the public
 * surface.
 *
 * @internal
 */
export function __resetAgentIdentityCacheForTests() {
	cache.clear()
}

/**
 * Test-only helper — uniformly-named alias for the in-process cmd harness
 * (run-cmd.mjs), which imports `__resetCaches` from each cache host with a
 * single consistent name. Behaviour is identical to
 * `__resetAgentIdentityCacheForTests`.
 *
 * @internal
 */
export function __resetCaches() {
	cache.clear()
}
