/**
 * seeds/io.mjs — seeds.json read/write primitives (C12).
 *
 * Seeds are the long-running thematic tier ABOVE milestones. Unlike the
 * per-scope roadmap (`.aperant/roadmap/{scope}/milestones.json`), seeds are
 * cross-scope by design — a single "Close parity gaps" initiative can span
 * `framework` and `project` work. They therefore live in a single top-level
 * file:
 *
 *   .aperant/roadmap/seeds.json
 *
 * Spec: docs/frameworks/spec-gaps.md#c12 — section 4a of the task spec.
 *
 * Shape:
 *   {
 *     version: 1,
 *     updated_at: ISO-8601,
 *     seeds: [
 *       {
 *         id: string,               // unique, kebab-case recommended
 *         title: string,
 *         goal?: string,
 *         status: 'active' | 'dormant' | 'completed' | 'archived',
 *         milestones: [             // cross-scope refs
 *           { scope: string, milestone_id: string },
 *           ...
 *         ],
 *         created_at: ISO-8601,
 *         closed_at: ISO-8601 | null,
 *         notes_url?: string,
 *         history?: Array<{...}>    // optional audit trail (--force --reason)
 *       },
 *       ...
 *     ]
 *   }
 */

import { existsSync, mkdirSync, readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { atomicWriteJson } from '../util/atomic-write.mjs'
import { withFileLock } from '../util/fs-lock.mjs'

/**
 * Absolute path to seeds.json for a project root.
 */
export function seedsJsonPath(targetDir) {
	return join(targetDir, '.aperant', 'roadmap', 'seeds.json')
}

/**
 * Return an empty seeds file with the current schema version.
 */
export function emptySeedsFile() {
	return {
		version: 1,
		updated_at: new Date().toISOString(),
		seeds: [],
	}
}

/**
 * Load `seeds.json`. Returns `null` when the file does not exist so callers can
 * distinguish "zero seeds declared" (file absent — AC9: no warning, no
 * migration required) from "seeds declared but empty array".
 */
export function loadSeeds(targetDir) {
	const path = seedsJsonPath(targetDir)
	if (!existsSync(path)) return null
	try {
		return JSON.parse(readFileSync(path, 'utf-8'))
	} catch {
		// Malformed seeds.json — surface as null so callers can either fail or
		// treat as absent. The CLI layer gives a clearer error than the parser.
		return null
	}
}

/**
 * Load seeds, filling in the empty file shape when absent. Used by read paths
 * that want an always-valid object to iterate over.
 */
export function loadSeedsOrEmpty(targetDir) {
	return loadSeeds(targetDir) || emptySeedsFile()
}

/**
 * Atomic save. Stamps `updated_at`, acquires an exclusive lock on seeds.json,
 * writes via atomicWriteJson (tmp+fsync+rename).
 *
 * NOTE: callers that need to coordinate a two-sided write with
 * milestones.json (e.g. `seed assign` / `seed unassign` / delete-seed) must
 * take their own outer `withFileLock(seedsJsonPath)` and call
 * `writeSeedsUnlocked` from inside it so the outer lock spans both sides.
 * Mirrors `commands/roadmap.mjs:327-368` — seeds.json lock is always the
 * OUTER lock; `saveRoadmapForScope` takes a milestones.json lock inside.
 */
export function saveSeeds(targetDir, seedsFile) {
	const path = seedsJsonPath(targetDir)
	const stamped = { ...seedsFile, version: seedsFile.version || 1 }
	stamped.updated_at = new Date().toISOString()
	stamped.seeds = stamped.seeds || []
	mkdirSync(dirname(path), { recursive: true })
	withFileLock(
		path,
		() => {
			atomicWriteJson(path, stamped)
		},
		{ projectRoot: targetDir },
	)
	return stamped
}

/**
 * Write seeds file WITHOUT taking its lock. Callers MUST already hold the
 * seeds.json lock (see `saveSeeds` comment). Used by the two-sided atomic
 * write path in `seed assign` / `seed unassign`.
 */
export function writeSeedsUnlocked(targetDir, seedsFile) {
	const path = seedsJsonPath(targetDir)
	const stamped = { ...seedsFile, version: seedsFile.version || 1 }
	stamped.updated_at = new Date().toISOString()
	stamped.seeds = stamped.seeds || []
	mkdirSync(dirname(path), { recursive: true })
	atomicWriteJson(path, stamped)
	return stamped
}

/**
 * Find a seed by id. Returns `undefined` when not found.
 */
export function findSeed(seedsFile, seedId) {
	if (!seedsFile || !Array.isArray(seedsFile.seeds)) return undefined
	return seedsFile.seeds.find((s) => s.id === seedId)
}

/**
 * Find the seed (if any) that owns a given (scope, milestone_id) ref.
 * Returns the seed record or `undefined`.
 */
export function findSeedForMilestone(seedsFile, scope, milestoneId) {
	if (!seedsFile || !Array.isArray(seedsFile.seeds)) return undefined
	for (const seed of seedsFile.seeds) {
		for (const ref of seed.milestones || []) {
			if (ref && ref.scope === scope && ref.milestone_id === milestoneId) {
				return seed
			}
		}
	}
	return undefined
}
