/**
 * upgrade-gitignore.mjs — rewrite `.aperant/.gitignore` to the post-C31
 * template (R6), now policy-driven (C35).
 *
 * C31/R6 rationale: state.json, team-status/, events/ are per-machine
 * coordination data — committing them leaks PIDs + hostnames + incarnation
 * counters and causes merge conflicts in the normal dev loop. INDEX.md and
 * ROADMAP.md are derived from JSON source of truth; committing them creates
 * a two-way write path that drifts. R6 pulled all of them behind the
 * deny-by-default wall.
 *
 * C35 layered a share-policy on top of that baseline so users can deliberately
 * opt into sharing roadmap/**, team.json, decisions/, digests/, and per-task
 * artifacts. See:
 *   - spec.md §4 for the artifact taxonomy (ephemeral / derived / shared-
 *     source / durable-source / local-private).
 *   - spec.md §5 for the signature change: buildAperantGitignoreTemplate now
 *     takes a resolved policy and emits the matching allow rules inline.
 *   - share-policy.mjs for the resolver + matcher that drive this template
 *     and audit in lockstep. If you add a new shared-source file, add it in
 *     BOTH places or the audit will report ghost violations.
 *
 * C36 completed the folder reshape: the durable-source context payload now
 * lives under `context/installed/` (the template's allow-rule below) and
 * runtime decision ledgers live under `context/notes/` (deny-by-default —
 * no rule emitted, the wildcard ignore catches them). The retired `plans/`
 * directory is not referenced anywhere. Solo default still emits the same
 * skeleton of durable-source allow rules — the legacy plural-contexts rule
 * was replaced in place by `!context/installed/**`, not layered on.
 *
 * This helper is NON-DESTRUCTIVE:
 *   - Backs up the old file to `.aperant/.gitignore.pre-c31.backup`.
 *   - Writes the new template.
 *   - Greps `git ls-files` for any currently-tracked files that WOULD become
 *     ignored and prints the suggested `git rm --cached -- <file>` list.
 *   - Never runs git commands on the user's behalf; the user decides when to
 *     untrack the files.
 *
 * The init.mjs writer consumes `buildAperantGitignoreTemplate(policy)` for
 * new installs so the two surfaces can never drift.
 */

import { execFileSync } from 'node:child_process'
import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { DEFAULT_SHARE_POLICY, resolveSharePolicy } from './share-policy.mjs'

/**
 * The canonical .aperant/.gitignore template for the R6 layout, specialized
 * by a resolved share policy (C35).
 *
 * No-arg form expands DEFAULT_SHARE_POLICY (the `solo` preset) so existing
 * callers that don't know about the policy surface still produce the exact
 * bytes the R6 tests pin.
 *
 * @param {ReturnType<typeof resolveSharePolicy>} [policy]
 * @returns {string}
 *
 * @internal
 */
export function buildAperantGitignoreTemplate(policy = DEFAULT_SHARE_POLICY) {
	const lines = [
		'# ━━━ Aperant Framework — deny by default, allow durable/reviewable files ━━━',
		'#',
		'# Everything is ignored unless explicitly allowed below. New files',
		'# default to private — safer than allowlisting ignores.',
		'#',
		'# File classes (see spec §13.3 for the full taxonomy — C35):',
		'#   durable-source (always committed): config.json, .gitignore, axes.json,',
		'#     features/, prompts/, templates/, workflows/*.md, context/installed/, standards/ —',
		'#     framework scaffolding that ships with the repo. Runtime workflow run',
		'#     files (workflows/*.json + workflows/archive/) are ephemeral.',
		'#   shared-source (committed iff matching share.* toggle is on):',
		'#     team.json, roadmap/**, decisions/*.md, digests/*.md, tasks/*/** —',
		'#     the allow rules below are emitted only when the policy opts in.',
		'#   ephemeral/coordination (always ignored): state.json, active.json (legacy, removed in C34 — rule kept as defense in depth), events/,',
		'#     locks/, team-status/, paused/, pending-effects.jsonl — per-machine',
		'#     lifecycle data; committing leaks PIDs + hostnames.',
		'#   derived/regenerated (always ignored): tasks/INDEX.md, roadmap/**/ROADMAP.md,',
		'#     decisions/INDEX.md — humans read the Markdown, machines write the JSON.',
		'',
		'# Ignore everything by default',
		'*',
		'',
		'# ── Durable / reviewable (committed — installed by apt-tools init) ─',
		'',
		'!.gitignore',
		'!config.json',
		'!axes.json',
		'',
		'# Feature registry (created by /apt:scan)',
		'!features/',
		'!features/**',
		'',
		'# Agent prompts and templates',
		'!prompts/',
		'!prompts/**',
		'!templates/',
		'!templates/**',
		'',
		'# Workflow definitions (framework-distributed markdown only — runtime',
		'# workflow run files are ephemeral. apt-tools workflow create writes',
		'# .aperant/workflows/{id}.json per-review/per-task; those must stay ignored.',
		'# A bare `!workflows/**` would un-ignore them and pollute git status.)',
		'!workflows/',
		'!workflows/*.md',
		'',
		'# Planning context (installed payload — runtime notes under context/notes/ stay ignored)',
		'# Re-include the parent `context/` directory so git descends into it; the',
		'# `!context/installed/**` pattern only works when its parent is not ignored.',
		'# `context/notes/` stays deny-by-default — no rule below negates it.',
		'!context/',
		'!context/installed/',
		'!context/installed/**',
		'',
		'# Standards',
		'!standards/',
		'!standards/**',
		'',
		'# ── Shared team files (committed to git) ──────────────────────────',
		'',
	]

	// ── Shared-source: team.json (share.team.roster) ────────────────────────
	if (policy.team.roster) {
		lines.push('# Team configuration (members auto-detected from git)')
		lines.push('!team.json # class: shared-source')
		lines.push('')
	}

	// ── Shared-source: tasks (share.tasks) ──────────────────────────────────
	// When tasks are shared, expose per-task directories but keep the derived
	// INDEX.md explicitly denied (it is regenerated from task records).
	if (policy.tasks) {
		lines.push('# Task specs + plans (the per-task directory content, not the INDEX)')
		lines.push('!tasks/')
		lines.push('tasks/*')
		lines.push('# Re-allow per-task directories; INDEX.md is derived and stays ignored.')
		lines.push('!tasks/*/ # class: shared-source')
		lines.push('!tasks/*/** # class: shared-source')
		lines.push('')
	}

	// ── Shared-source: roadmap (share.roadmap) ──────────────────────────────
	if (policy.roadmap) {
		lines.push('# Roadmap JSON (source of truth — ROADMAP.md is derived, below)')
		lines.push('!roadmap/')
		lines.push('!roadmap/** # class: shared-source')
		lines.push('# Derived markdown views of roadmap JSON — regenerated on every sync.')
		lines.push('roadmap/**/ROADMAP.md')
		lines.push('')
	}

	// ── Shared-source: decisions (share.decisions) ──────────────────────────
	if (policy.decisions) {
		lines.push('# Decision records (output of /apt:discuss + /apt:roundtable)')
		lines.push('!decisions/')
		lines.push('!decisions/*.md # class: shared-source')
		lines.push('# INDEX.md is derived and regenerated on every drain.')
		lines.push('decisions/INDEX.md')
		lines.push('')
	}

	// ── Shared-source: digests (share.digests, already inherited) ──────────
	if (policy.digests) {
		lines.push('# Weekly narrative digests (apt-team-docs-narrator output)')
		lines.push('!digests/')
		lines.push('!digests/*.md # class: shared-source')
		lines.push('# Ephemeral helpers stay denied regardless of share.digests.')
		lines.push('digests/.last-run.json')
		lines.push('digests/errors.log')
		lines.push('')
	}

	lines.push('# ── Ephemeral coordination (R2/R6/R10 — per-machine, never committed)')
	lines.push('# Listed explicitly so future maintainers cannot accidentally un-ignore')
	lines.push('# them with a broad `!foo/**` allow rule. These files hold PIDs,')
	lines.push('# hostnames, and liveness counters that are meaningless across machines.')
	lines.push('state.json')
	// active.json was the pre-C34 cross-agent shim; no current apt-tools build
	// writes it. The rule stays as defense in depth against older binaries.
	lines.push('active.json')
	lines.push('events/')
	lines.push('locks/')
	lines.push('team-status/')
	// config.local.json — per-user override file (FRAMEWORK-AUDIT-001). The
	// deny-by-default `*` rule above already ignores it because no allow line
	// exists for it; this explicit deny is defense in depth so a future
	// maintainer cannot accidentally un-ignore it with a broad `!config*.json`
	// allow rule.
	lines.push('config.local.json')
	lines.push('')
	lines.push('# ── Derived markdown indexes (regenerated from JSON) ──────────────')
	lines.push('tasks/INDEX.md')
	lines.push('')

	return lines.join('\n')
}

/**
 * Run the gitignore upgrade on an existing project. Non-destructive.
 *
 * C35: accepts an optional pre-resolved `policy`. When omitted, reads
 * `.aperant/config.json` from the project root and resolves via share-policy
 * — so existing callers that don't pass a policy pick up whatever the user
 * saved via `/apt:setup`. Falls back to DEFAULT_SHARE_POLICY (solo) if
 * config is missing.
 *
 * @param {string} projectRoot
 * @param {{ dryRun?: boolean, policy?: ReturnType<typeof resolveSharePolicy> }} [opts]
 * @returns {{
 *   status: 'ok' | 'noop',
 *   dryRun: boolean,
 *   backupPath: string | null,
 *   gitignorePath: string,
 *   trackedButIgnored: string[],
 *   suggestedUntrack: string | null,
 *   replaced: boolean,
 *   policy: ReturnType<typeof resolveSharePolicy>,
 * }}
 *
 * @internal
 */
export function runGitignoreUpgrade(projectRoot, opts = {}) {
	const dryRun = opts.dryRun === true
	const aperantDir = resolve(projectRoot, '.aperant')
	const gitignorePath = join(aperantDir, '.gitignore')
	const backupPath = join(aperantDir, '.gitignore.pre-c31.backup')

	const policy = opts.policy ?? loadPolicyFromConfig(aperantDir)

	const next = buildAperantGitignoreTemplate(policy)
	const current = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf-8') : ''
	const alreadyUpgraded = current === next

	// Detect files that would become ignored. We check the canonical list
	// (state.json, active.json, tasks/INDEX.md, roadmap/**/ROADMAP.md) against
	// `git ls-files` output — never fails the upgrade on git errors.
	const trackedButIgnored = findTrackedFilesThatWillBeIgnored(projectRoot)
	const suggestedUntrack =
		trackedButIgnored.length > 0
			? `git rm --cached -- ${trackedButIgnored.map((f) => JSON.stringify(f)).join(' ')}`
			: null

	if (alreadyUpgraded) {
		return {
			status: 'noop',
			dryRun,
			backupPath: null,
			gitignorePath,
			trackedButIgnored,
			suggestedUntrack,
			replaced: false,
			policy,
		}
	}

	if (dryRun) {
		return {
			status: 'ok',
			dryRun: true,
			backupPath: null,
			gitignorePath,
			trackedButIgnored,
			suggestedUntrack,
			replaced: false,
			policy,
		}
	}

	if (existsSync(gitignorePath)) {
		copyFileSync(gitignorePath, backupPath)
	}
	writeFileSync(gitignorePath, next, 'utf-8')

	return {
		status: 'ok',
		dryRun: false,
		backupPath: existsSync(backupPath) ? backupPath : null,
		gitignorePath,
		trackedButIgnored,
		suggestedUntrack,
		replaced: true,
		policy,
	}
}

function loadPolicyFromConfig(aperantDir) {
	const configPath = join(aperantDir, 'config.json')
	if (!existsSync(configPath)) return resolveSharePolicy({})
	try {
		const config = JSON.parse(readFileSync(configPath, 'utf-8'))
		return resolveSharePolicy(config)
	} catch {
		return resolveSharePolicy({})
	}
}

function findTrackedFilesThatWillBeIgnored(projectRoot) {
	const candidates = ['.aperant/state.json', '.aperant/active.json', '.aperant/tasks/INDEX.md']
	// roadmap/**/ROADMAP.md — enumerate via git ls-files below.
	const tracked = new Set()
	try {
		const out = execFileSync('git', ['ls-files', '-z', '--', '.aperant/'], {
			cwd: projectRoot,
			encoding: 'utf-8',
		})
		for (const entry of out.split('\0')) {
			if (!entry) continue
			tracked.add(entry)
		}
	} catch {
		// Not a git repo, or no tracked files — return empty list.
		return []
	}

	const hits = []
	for (const c of candidates) {
		if (tracked.has(c)) hits.push(c)
	}
	// Roadmap derived markdown: any tracked `.aperant/roadmap/.../ROADMAP.md`.
	for (const entry of tracked) {
		if (/^\.aperant\/roadmap\/.*ROADMAP\.md$/.test(entry)) hits.push(entry)
	}
	// Ephemeral directories — anything tracked under events/, locks/,
	// team-status/ shouldn't exist, but surface them if it does.
	for (const entry of tracked) {
		if (/^\.aperant\/(events|locks|team-status)\//.test(entry)) hits.push(entry)
	}
	// Workflows: framework-distributed `.md` definitions are durable, but
	// runtime workflow run files (workflows/*.json) and the archive subtree
	// are ephemeral. If older clients tracked them under the broader
	// `!workflows/**` rule, surface them so users can `git rm --cached`.
	for (const entry of tracked) {
		if (/^\.aperant\/workflows\//.test(entry) && !/\.md$/.test(entry)) hits.push(entry)
	}
	return hits.sort()
}
