/**
 * commands/precompact-hook.mjs — PreCompact hook installer + handler (C29).
 *
 * The installer-side merge logic now lives in install/claude-hooks.mjs so
 * SessionStart and any future events can share it. This file owns the
 * PreCompact-specific glue (command string + entry-identity substring) and
 * the runtime PreCompact handler (cmdPrecompactHook).
 */

import { existsSync, readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { mergeClaudeHook } from '../install/claude-hooks.mjs'
import { rawText } from '../util/result.mjs'

/**
 * Install or merge the apt-tools PreCompact hook into
 * .claude/settings.local.json. Returns a short status string for the init
 * output.
 *
 * The hook entry targets Claude Code's PreCompact event. When Claude Code is
 * about to auto-compact (or a user runs /compact), it invokes registered
 * PreCompact hooks; our hook emits task resume instructions that get injected
 * into the compacted summary so the post-compact agent continues execution
 * from build-progress.json instead of dying on a truncated summary.
 *
 * Per-developer, gitignored — hooks belong to the local dev environment
 * because each collaborator may opt in or out.
 */
export function installPrecompactHook(projectDir) {
	return mergeClaudeHook({
		projectDir,
		event: 'PreCompact',
		matcher: '*',
		command: 'node packages/framework/bin/apt-tools.mjs precompact-hook .',
		identitySubstring: 'apt-tools.mjs precompact-hook',
	})
}

/**
 * Install the SessionStart hook that fires the apt-check-update worker.
 * Coexists with PreCompact under settings.hooks.{PreCompact,SessionStart}.
 */
export function installSessionStartHook(projectDir) {
	return mergeClaudeHook({
		projectDir,
		event: 'SessionStart',
		matcher: '*',
		command: 'node packages/framework/hooks/apt-check-update.js .',
		identitySubstring: 'apt-check-update.js',
	})
}

/**
 * Claude Code PreCompact hook handler.
 *
 * Claude Code invokes registered PreCompact hooks right before it auto-compacts
 * or manually compacts a conversation. Each hook receives a JSON object on stdin:
 *
 *   { hook_event_name: "PreCompact", trigger: "auto" | "manual",
 *     custom_instructions: string | null, session_id, cwd, transcript_path, ... }
 *
 * Output protocol — plain stdout only. Claude Code's JSON hook schema whitelist
 * exposes `hookSpecificOutput.additionalContext` for `UserPromptSubmit` and
 * `PostToolUse` but NOT for `PreCompact`; emitting the nested envelope there
 * fails schema validation and Claude Code drops the payload entirely (silent
 * breakage — no task-resume context reaches the post-compact agent). The
 * stable, documented path for PreCompact is plain text on stdout: Claude Code
 * appends non-JSON hook stdout to the summarization prompt verbatim, which is
 * exactly the behavior we need.
 *
 * What we emit:
 *   - Active tasks from .aperant/state.json (task id, spec.md path, plan path,
 *     progress path, current subtask).
 *   - Resume instructions telling the post-compact agent: "re-read these files,
 *     find the next pending subtask in the plan, and continue execution."
 *
 * Output is empty (no-op) when:
 *   - No .aperant directory exists in cwd (not an Aperant project).
 *   - state.json has no active tasks.
 *   - The cwd is not inside an Aperant project root.
 *
 * Return shape (subtask 15): builds plain text and returns it via the
 * `rawText` helper from util/result.mjs (`{ rawText, exitCode }`). The
 * Layer 2 wrap() helper writes it through process.stdout.write and exits 0.
 * No-op paths return `rawText('', 0)`.
 */
export function cmdPrecompactHook(projectDir) {
	const targetDir = resolve(projectDir || process.cwd())
	const aperantDir = join(targetDir, '.aperant')

	// No-op: not an Aperant project.
	if (!existsSync(aperantDir)) {
		return rawText('', 0)
	}

	const statePath = join(aperantDir, 'state.json')
	if (!existsSync(statePath)) {
		return rawText('', 0)
	}

	let state
	try {
		state = JSON.parse(readFileSync(statePath, 'utf-8'))
	} catch {
		return rawText('', 0)
	}

	// Normalize active_task / active_tasks to a map.
	let activeTasks = {}
	if (state.active_tasks && typeof state.active_tasks === 'object') {
		activeTasks = state.active_tasks
	} else if (state.active_task) {
		const oldId = state.active_task.id || 'legacy-task'
		activeTasks = { [oldId]: state.active_task }
	}

	const taskIds = Object.keys(activeTasks)
	if (taskIds.length === 0) {
		return rawText('', 0)
	}

	// Drain stdin if piped — Claude Code pipes the hook payload to the hook.
	// We don't need the content for our logic (cwd + state.json is enough),
	// but leaving stdin unread can cause the parent to block on the pipe buffer.
	try {
		if (!process.stdin.isTTY) {
			readFileSync(0, 'utf-8')
		}
	} catch {
		// Ignore — stdin may not be piped in all invocation paths.
	}

	// Build the resume-instructions block.
	const lines = []
	lines.push('APERANT FRAMEWORK: you are mid-execution on one or more tasks. After compaction,')
	lines.push('your previous in-memory state is gone — the source of truth is on disk.')
	lines.push('')
	lines.push(`Active tasks (${taskIds.length}):`)

	for (const [taskId, task] of Object.entries(activeTasks)) {
		// Resolve task_dir deterministically via getTaskDir() — the task object
		// does not store its own directory path; it's always derived from id.
		// Use project-relative path so the post-compact agent can Read it
		// regardless of cwd changes.
		const taskDir = join('.aperant', 'tasks', taskId)
		// Real schema from cmdTask create: description, lifecycle_phase,
		// progress.{subtasks_total, subtasks_completed, current_subtask}.
		// Title/status are fallbacks for legacy or hand-edited state.
		const description = task.description || task.title || taskId
		const phase = task.lifecycle_phase || task.phase || task.status || 'active'
		const progress = task.progress || {}
		const total = progress.subtasks_total ?? 0
		const completed = progress.subtasks_completed ?? 0
		const currentSubtask = progress.current_subtask || task.current_subtask || 'unknown'

		lines.push('')
		lines.push(`  • ${taskId} — ${description}`)
		lines.push(`    phase: ${phase}  (subtasks: ${completed}/${total})`)
		lines.push(`    task_dir: ${taskDir}`)
		lines.push(`    spec:     ${taskDir}/spec.md`)
		lines.push(`    plan:     ${taskDir}/implementation_plan.json`)
		lines.push(`    progress: ${taskDir}/build-progress.json`)
		lines.push(`    last known subtask: ${currentSubtask}`)
	}

	lines.push('')
	lines.push('TO RESUME (do this immediately after compaction, before anything else):')
	lines.push('  1. Re-read implementation_plan.json AND build-progress.json for each active task.')
	lines.push(
		'  2. Find the first subtask with status="pending" whose dependencies are "completed".',
	)
	lines.push('  3. Continue executing from that subtask. Do NOT restart from the beginning.')
	lines.push('  4. Do NOT ask the human to re-issue the task — checkpoint state is authoritative.')
	lines.push('  5. If all subtasks are completed, run /apt:verify.')
	lines.push('')
	lines.push('This block is injected by the apt-tools PreCompact hook. It is how the Aperant')
	lines.push('Framework survives auto-compact without bailing out to the human.')

	// Plain text, not JSON — see function docstring for rationale.
	return rawText(lines.join('\n') + '\n', 0)
}
