/**
 * release-notes/draft.mjs — towncrier-style per-PR fragment writer.
 *
 * Writes `<output_dir>/_unreleased/PR-<n>.md` for a single PR. Idempotent:
 * if the target file already exists with byte-identical content, the call
 * is a no-op and the envelope reports `written: false`. The same input
 * always produces the same bytes on disk — that is the load-bearing
 * invariant the news-fragment pattern depends on (zero merge conflicts
 * on the unreleased file, AC2).
 *
 * Deterministic I/O only. No LLM calls. No prompt templates. The host
 * LLM authors the note text and hands it in via --note.
 */

import { execFileSync } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'

import { atomicWriteText } from '../util/atomic-write.mjs'
import { resolveOutputDir } from './output-dir.mjs'

/**
 * Build the canonical fragment body. The shape is intentionally simple +
 * stable so the compile step can parse it back without YAML / front-matter
 * gymnastics. Date is the calling day in UTC, not a wall-clock timestamp —
 * idempotency demands the same inputs produce the same bytes regardless of
 * when the day they're called within.
 *
 * @param {{
 *   pr: number,
 *   note: string,
 *   persona: string | null,
 *   date: string,
 *   commits: string[],
 * }} input
 * @returns {string}
 */
export function renderFragment(input) {
	const personaLine = input.persona ? `persona: ${input.persona}` : 'persona: (none)'
	const commitsLine =
		input.commits.length === 0 ? 'commits: (none)' : `commits: ${input.commits.join(', ')}`
	return [
		`# PR-${input.pr}`,
		'',
		`date: ${input.date}`,
		`pr: ${input.pr}`,
		personaLine,
		commitsLine,
		'',
		'## Release note',
		'',
		input.note.trim(),
		'',
	].join('\n')
}

/**
 * Resolve the commits referencing a PR by number. Best-effort — if git is
 * unavailable or no matching commits are found, returns an empty array.
 * Caller can pass commits explicitly via --commits to bypass git inspection.
 *
 * @param {string} projectDir
 * @param {number} pr
 * @returns {string[]}
 */
function discoverCommitsForPr(projectDir, pr) {
	try {
		const out = execFileSync(
			'git',
			['-C', projectDir, 'log', '--no-merges', `--grep=#${pr}\\b`, '--format=%H'],
			{ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] },
		)
		return out
			.split('\n')
			.map((s) => s.trim())
			.filter((s) => s.length > 0)
			.map((sha) => sha.slice(0, 12))
	} catch {
		return []
	}
}

/**
 * @typedef {Object} DraftInput
 * @property {string} projectDir
 * @property {number} pr
 * @property {string} note
 * @property {string | null} persona
 * @property {string | null} outputDir   // CLI flag override (relative to projectDir or absolute).
 * @property {string[] | null} commits   // explicit commit list; null → autodetect via git log.
 * @property {string | null} date        // ISO yyyy-mm-dd; null → today (UTC).
 */

/**
 * @typedef {Object} DraftResult
 * @property {boolean} written
 * @property {string} path
 * @property {string} reason     // 'created' | 'no-change' | 'updated'
 */

/**
 * Write the PR fragment idempotently. Returns the result envelope.
 *
 * @param {DraftInput} input
 * @returns {DraftResult}
 */
export function writeDraft(input) {
	const outputDir = resolveOutputDir(input.projectDir, input.outputDir)
	const unreleasedDir = join(outputDir, '_unreleased')
	const target = join(unreleasedDir, `PR-${input.pr}.md`)

	const date = input.date ?? new Date().toISOString().slice(0, 10)
	const commits = input.commits ?? discoverCommitsForPr(input.projectDir, input.pr)

	const body = renderFragment({
		pr: input.pr,
		note: input.note,
		persona: input.persona,
		date,
		commits,
	})

	if (existsSync(target)) {
		const existing = readFileSync(target, 'utf-8')
		if (existing === body) {
			return { written: false, path: target, reason: 'no-change' }
		}
		atomicWriteText(target, body)
		return { written: true, path: target, reason: 'updated' }
	}
	atomicWriteText(target, body)
	return { written: true, path: target, reason: 'created' }
}
