/**
 * install/runtime-detect.mjs — discover which runtimes a project has
 * installed (or appears to use) by checking for known directory markers
 * and per-runtime manifests.
 *
 * Two distinct checks:
 *   - `detectInstalledRuntimes(targetDir)` — runtimes that the apt
 *     installer has written into (i.e. an apt-file-manifest.json exists
 *     at the runtime's install root). Used by check-version + statusline
 *     to know what to walk.
 *   - `suggestRuntimes(targetDir)` — runtimes whose CLI/IDE seems to be
 *     in use locally (presence of `.cursor/`, `.codex/` etc.) regardless
 *     of whether apt has installed into them. Used by the `init` TTY
 *     prompt to pre-check the right boxes.
 */

import { existsSync, statSync } from 'node:fs'
import { join } from 'node:path'
import { MANIFEST_FILENAME } from './manifest.mjs'

/**
 * @typedef {Object} RuntimeDescriptor
 * @property {string} id            Stable identifier (`claude`, `cursor`, …).
 * @property {string} flag          CLI flag form (e.g. `--cursor`).
 * @property {string} installRoot   Path under the project where the runtime's
 *                                  files live (relative).
 * @property {string[]} markers     Additional path markers (relative) that
 *                                  hint the runtime is in use even when no
 *                                  apt manifest is present yet.
 * @property {string|null} cliId    The `cli` string `detectHost()` returns
 *                                  when this runtime's CLI is at the prompt.
 *                                  `null` when the runtime has no install
 *                                  target mapped to a host CLI.
 * @property {string} [displayLabel] Optional user-facing label (e.g. "Kilo Code").
 *                                  When unset, callers fall back to the
 *                                  capitalized form of `id`. Use for runtimes
 *                                  whose product branding differs from the
 *                                  internal id (C58: `kilo` → "Kilo Code").
 * @property {string|null} nativeInstructionFile
 *                                  The root-level instruction file this
 *                                  runtime's native CLI reads (e.g. `claude`
 *                                  reads `CLAUDE.md`, `codex` reads
 *                                  `AGENTS.md`). `init` stamps the
 *                                  `<!-- APT:framework-* -->` managed block
 *                                  into every distinct non-null target for the
 *                                  selected runtimes. `null` means the runtime
 *                                  installs to its own surface (e.g. cursor →
 *                                  `.cursor/rules`), NOT a root `.md` — `init`
 *                                  skips null entries. Nullable by design, not
 *                                  a future-work placeholder (ID-01).
 */

/** @type {RuntimeDescriptor[]} */
export const RUNTIMES = Object.freeze([
	{
		id: 'claude',
		flag: '--claude',
		installRoot: '.claude',
		markers: ['.claude/settings.json', '.claude/settings.local.json'],
		cliId: 'claude-code',
		nativeInstructionFile: 'CLAUDE.md',
	},
	{
		id: 'codex',
		flag: '--codex',
		installRoot: '.agents',
		markers: ['.codex/config.toml'],
		cliId: 'codex',
		nativeInstructionFile: 'AGENTS.md',
	},
	{
		id: 'cursor',
		flag: '--cursor',
		installRoot: '.cursor',
		markers: ['.cursor/settings.json'],
		cliId: 'cursor',
		nativeInstructionFile: null,
	},
	{
		id: 'gemini',
		flag: '--gemini',
		installRoot: '.gemini',
		markers: ['.gemini/settings.json'],
		cliId: 'gemini-cli',
		nativeInstructionFile: 'GEMINI.md',
	},
	{
		id: 'opencode',
		flag: '--opencode',
		installRoot: '.opencode',
		markers: ['.opencode/config.json'],
		cliId: 'opencode',
		nativeInstructionFile: 'AGENTS.md',
	},
	{
		id: 'pi',
		flag: '--pi',
		installRoot: '.pi',
		markers: ['.pi/settings.json', '.pi/skills', '.pi/prompts'],
		cliId: 'pi',
		nativeInstructionFile: null,
	},
	{
		id: 'kilo',
		flag: '--kilo',
		installRoot: '.kilo',
		markers: ['.kilo/config.json'],
		cliId: 'kilo',
		// C58 (0.7.2): user-visible label. Internal id stays `kilo` (zero
		// migration per spec ID-01). `--kilocode` is also accepted as a
		// deprecated alias of `--kilo` at the CLI flag parser layer.
		displayLabel: 'Kilo Code',
		nativeInstructionFile: null,
	},
	{
		id: 'copilot',
		flag: '--copilot',
		installRoot: '.github/copilot',
		markers: ['.github/copilot-instructions.md'],
		cliId: 'copilot',
		nativeInstructionFile: null,
	},
	{
		id: 'antigravity',
		flag: '--antigravity',
		installRoot: '.antigravity',
		markers: ['.antigravity/config.json'],
		cliId: 'antigravity',
		nativeInstructionFile: null,
	},
	{
		id: 'windsurf',
		flag: '--windsurf',
		installRoot: '.windsurf',
		markers: ['.windsurfrules'],
		cliId: 'windsurf',
		nativeInstructionFile: null,
	},
	{
		id: 'augment',
		flag: '--augment',
		installRoot: '.augment',
		markers: ['.augment/config.json'],
		cliId: 'augment',
		nativeInstructionFile: null,
	},
	{
		id: 'trae',
		flag: '--trae',
		installRoot: '.trae',
		markers: ['.trae/config.json'],
		cliId: 'trae',
		nativeInstructionFile: null,
	},
	{
		id: 'qwen',
		flag: '--qwen',
		installRoot: '.qwen',
		markers: ['.qwen/config.json'],
		cliId: 'qwen',
		nativeInstructionFile: null,
	},
	{
		id: 'cline',
		flag: '--cline',
		installRoot: '.cline',
		markers: ['.cline/config.json'],
		cliId: 'cline',
		nativeInstructionFile: null,
	},
	{
		id: 'codebuddy',
		flag: '--codebuddy',
		installRoot: '.codebuddy',
		markers: ['.codebuddy/config.json'],
		cliId: 'codebuddy',
		nativeInstructionFile: null,
	},
])

/** @type {Set<string>} */
export const RUNTIME_IDS = new Set(RUNTIMES.map((r) => r.id))

/**
 * Look up a runtime descriptor by id. Returns null when unknown.
 *
 * @param {string} id
 * @returns {RuntimeDescriptor|null}
 */
export function getRuntime(id) {
	return RUNTIMES.find((r) => r.id === id) ?? null
}

/**
 * Find runtimes that appear to be in active use locally — used by the
 * `init` interactive prompt to pre-suggest the right options.
 *
 * A runtime is "suggested" when the install root directory exists OR any
 * of its declared markers exists.
 *
 * @param {string} targetDir
 * @returns {string[]} runtime ids
 */
export function suggestRuntimes(targetDir) {
	const out = []
	for (const r of RUNTIMES) {
		if (existsSync(join(targetDir, r.installRoot))) {
			out.push(r.id)
			continue
		}
		if (r.markers.some((m) => existsSync(join(targetDir, m)))) {
			out.push(r.id)
		}
	}
	return out
}

/**
 * Find runtimes that have been installed into by apt — i.e. carry an
 * `apt-file-manifest.json` at their install root.
 *
 * @param {string} targetDir
 * @returns {RuntimeDescriptor[]}
 */
export function detectInstalledRuntimes(targetDir) {
	const out = []
	for (const r of RUNTIMES) {
		const manifestPath = join(targetDir, r.installRoot, MANIFEST_FILENAME)
		if (!existsSync(manifestPath)) continue
		try {
			if (statSync(manifestPath).isFile()) out.push(r)
		} catch {
			/* manifest disappeared between check and stat — ignore */
		}
	}
	return out
}
