/**
 * commands/seed.mjs — seeds CLI (C12).
 *
 * Seeds are the long-running thematic tier ABOVE milestones. See spec at
 * docs/frameworks/spec-gaps.md#c12 (and the C12 task's spec.md).
 *
 * Subcommands (subtask 2):
 *   init      — create empty seeds.json
 *   list      — list every seed + milestone_count + task_count
 *   get       — full view of one seed; --include-tasks walks state.json
 *   create    — append a new seed
 *   close     — set status=completed (D-10: does NOT cascade to milestones)
 *   archive   — set status=archived
 *
 * Subcommands added in later subtasks:
 *   assign    — subtask 3 (two-sided atomic write)
 *   unassign  — subtask 3
 *   validate  — subtask 4
 */
import { existsSync, readFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { getGitIdentity } from '../git/identity.mjs';
import { loadRoadmapForScope, saveRoadmapForScope } from '../roadmap/io.mjs';
import { listRoadmapScopes } from '../roadmap/paths.mjs';
import { resolveMilestoneScope } from '../roadmap/scope-lookup.mjs';
import { emptySeedsFile, findSeed, findSeedForMilestone, loadSeeds, loadSeedsOrEmpty, saveSeeds, seedsJsonPath, writeSeedsUnlocked, } from '../seeds/io.mjs';
import { validateSeeds } from '../seeds/validate.mjs';
import { updateTeamMember } from '../team/team.mjs';
import { parseFlags } from '../util/args.mjs';
import { withFileLock } from '../util/fs-lock.mjs';
import { err, exitWith, ok } from '../util/result.mjs';
/**
 * Run validateSeeds against the current on-disk state.
 * Returns { ok, drift } — same shape as validateSeeds().
 */
function runValidation(targetDir) {
    const seedsFile = loadSeedsOrEmpty(targetDir);
    const scopes = listRoadmapScopes(targetDir);
    const roadmapsByScope = new Map();
    for (const scope of scopes) {
        roadmapsByScope.set(scope, loadRoadmapForScope(targetDir, scope));
    }
    return validateSeeds(seedsFile, roadmapsByScope);
}
/**
 * Count how many tasks (active + completed) reference a given
 * (scope, milestone_id) pair in state.json. Best-effort: missing state.json
 * returns 0, malformed returns 0. Used by `seed list` + `seed get`.
 *
 * @param {string} targetDir
 * @param {Array<{scope: string, milestone_id: string}>} refs
 * @returns {{active: number, completed: number, total: number, tasks: Array<{id: string, source: 'active'|'completed', scope: string, milestone_id: string}>}}
 */
function gatherSeedTasks(targetDir, refs) {
    const out = { active: 0, completed: 0, total: 0, tasks: [] };
    const statePath = join(targetDir, '.aperant', 'state.json');
    if (!existsSync(statePath))
        return out;
    let state;
    try {
        state = JSON.parse(readFileSync(statePath, 'utf-8'));
    }
    catch {
        return out;
    }
    const refSet = new Set((refs || []).map((r) => `${r.scope || 'project'}\u0000${r.milestone_id}`));
    for (const [id, t] of Object.entries(state.active_tasks || {})) {
        if (!t || !t.milestone_id)
            continue;
        const key = `${t.scope || 'project'}\u0000${t.milestone_id}`;
        if (refSet.has(key)) {
            out.active++;
            out.tasks.push({
                id,
                source: 'active',
                scope: t.scope || 'project',
                milestone_id: t.milestone_id,
            });
        }
    }
    for (const t of state.completed_tasks || []) {
        if (!t || !t.id || !t.milestone_id)
            continue;
        const key = `${t.scope || 'project'}\u0000${t.milestone_id}`;
        if (refSet.has(key)) {
            out.completed++;
            out.tasks.push({
                id: t.id,
                source: 'completed',
                scope: t.scope || 'project',
                milestone_id: t.milestone_id,
            });
        }
    }
    out.total = out.active + out.completed;
    return out;
}
export function cmdSeed(subcommand, projectDir, extraArgs) {
    if (!subcommand) {
        return err('Usage: apt-tools seed <init|list|get|create|close|archive|assign|unassign|validate> <project-dir>');
    }
    if (!projectDir) {
        return err(`Usage: apt-tools seed ${subcommand} <project-dir>`);
    }
    const targetDir = resolve(projectDir);
    updateTeamMember(targetDir);
    const flags = parseFlags(extraArgs || []);
    switch (subcommand) {
        case 'init': {
            const path = seedsJsonPath(targetDir);
            if (existsSync(path) && !flags.has('force')) {
                return ok({
                    status: 'ok',
                    command: 'seed-init',
                    path,
                    note: 'already exists (use --force to reset)',
                });
            }
            saveSeeds(targetDir, emptySeedsFile());
            return ok({ status: 'ok', command: 'seed-init', path });
        }
        case 'list': {
            const seedsFile = loadSeedsOrEmpty(targetDir);
            const statusFilter = flags.get('status') || null;
            const seeds = (seedsFile.seeds || []).filter((s) => {
                if (!statusFilter)
                    return true;
                return (s.status || 'active') === statusFilter;
            });
            const items = seeds.map((s) => {
                const refs = s.milestones || [];
                const tasks = gatherSeedTasks(targetDir, refs);
                return {
                    id: s.id,
                    title: s.title,
                    goal: s.goal || '',
                    status: s.status || 'active',
                    milestone_count: refs.length,
                    task_count: tasks.total,
                    task_active: tasks.active,
                    task_completed: tasks.completed,
                    created_at: s.created_at || null,
                    closed_at: s.closed_at || null,
                    notes_url: s.notes_url || null,
                };
            });
            return ok({ status: 'ok', command: 'seed-list', seeds: items });
        }
        case 'get': {
            const id = flags.get('id');
            if (!id)
                return err('Required: --id <seed-id>');
            const seedsFile = loadSeeds(targetDir);
            if (!seedsFile)
                return err('No seeds.json found. Run: apt-tools seed init');
            const seed = findSeed(seedsFile, id);
            if (!seed)
                return err(`Seed "${id}" not found`);
            // Resolve each milestone ref to its on-disk record (status, phases
            // count). Scopes not initialized are skipped silently.
            const scopes = listRoadmapScopes(targetDir);
            const roadmapsByScope = new Map();
            for (const scope of scopes) {
                roadmapsByScope.set(scope, loadRoadmapForScope(targetDir, scope));
            }
            const milestones = (seed.milestones || []).map((ref) => {
                const rm = roadmapsByScope.get(ref.scope);
                const m = rm && Array.isArray(rm.milestones)
                    ? rm.milestones.find((x) => x && x.id === ref.milestone_id)
                    : null;
                return {
                    scope: ref.scope,
                    milestone_id: ref.milestone_id,
                    title: m ? m.title : null,
                    goal: m ? m.goal || '' : null,
                    seed_id_on_disk: m ? (m.seed_id ?? null) : null,
                    phase_count: m && Array.isArray(m.phases) ? m.phases.length : 0,
                    found: !!m,
                };
            });
            const reply = {
                status: 'ok',
                command: 'seed-get',
                seed: {
                    id: seed.id,
                    title: seed.title,
                    goal: seed.goal || '',
                    status: seed.status || 'active',
                    created_at: seed.created_at || null,
                    closed_at: seed.closed_at || null,
                    notes_url: seed.notes_url || null,
                    milestones,
                },
            };
            if (flags.has('include-tasks')) {
                const tasks = gatherSeedTasks(targetDir, seed.milestones || []);
                reply.tasks = tasks.tasks;
                reply.task_counts = {
                    active: tasks.active,
                    completed: tasks.completed,
                    total: tasks.total,
                };
            }
            return ok(reply);
        }
        case 'create': {
            const id = flags.get('id');
            const title = flags.get('title');
            if (!id || !title)
                return err('Required: --id <id> --title <title>');
            const goal = flags.get('goal') || '';
            const notesUrl = flags.get('notes-url') || null;
            const seedsFile = loadSeedsOrEmpty(targetDir);
            if ((seedsFile.seeds || []).some((s) => s.id === id)) {
                return err(`Seed "${id}" already exists`);
            }
            const now = new Date().toISOString();
            seedsFile.seeds = seedsFile.seeds || [];
            seedsFile.seeds.push({
                id,
                title,
                goal,
                status: 'active',
                milestones: [],
                created_at: now,
                closed_at: null,
                ...(notesUrl ? { notes_url: notesUrl } : {}),
            });
            saveSeeds(targetDir, seedsFile);
            return ok({ status: 'ok', command: 'seed-create', id, title });
        }
        case 'close': {
            const id = flags.get('id');
            if (!id)
                return err('Required: --id <seed-id>');
            const seedsFile = loadSeeds(targetDir);
            if (!seedsFile)
                return err('No seeds.json found. Run: apt-tools seed init');
            const seed = findSeed(seedsFile, id);
            if (!seed)
                return err(`Seed "${id}" not found`);
            // D-10: closing a seed is a labelling decision. We do NOT cascade
            // to any milestone's status — milestones keep their own lifecycle.
            seed.status = 'completed';
            seed.closed_at = new Date().toISOString();
            saveSeeds(targetDir, seedsFile);
            return ok({
                status: 'ok',
                command: 'seed-close',
                id,
                seed_status: seed.status,
                closed_at: seed.closed_at,
            });
        }
        case 'archive': {
            const id = flags.get('id');
            if (!id)
                return err('Required: --id <seed-id>');
            const seedsFile = loadSeeds(targetDir);
            if (!seedsFile)
                return err('No seeds.json found. Run: apt-tools seed init');
            const seed = findSeed(seedsFile, id);
            if (!seed)
                return err(`Seed "${id}" not found`);
            seed.status = 'archived';
            if (!seed.closed_at)
                seed.closed_at = new Date().toISOString();
            saveSeeds(targetDir, seedsFile);
            return ok({ status: 'ok', command: 'seed-archive', id, seed_status: seed.status });
        }
        case 'validate': {
            // AC6 — exit 1 on drift, 0 on clean. Report the drift taxonomy so
            // operators can fix by hand or re-run `seed assign`/`unassign`.
            const result = runValidation(targetDir);
            if (!result.ok) {
                return exitWith({
                    status: 'error',
                    command: 'seed-validate',
                    drift_count: result.drift.length,
                    drift: result.drift,
                }, 1);
            }
            return ok({ status: 'ok', command: 'seed-validate', drift_count: 0, drift: [] });
        }
        case 'assign':
        case 'unassign': {
            // Two-sided atomic write across seeds.json + {scope}/milestones.json.
            //
            // Lock ordering (CRITICAL — mirrors commands/roadmap.mjs:327-368):
            //   outer lock: seeds.json  (taken first, always)
            //   inner lock: milestones.json (taken by saveRoadmapForScope)
            //
            // Every caller that mutates both files MUST follow the same order
            // (seeds → milestones) to avoid deadlock with concurrent workflows.
            const id = flags.get('id');
            const milestoneId = flags.get('milestone');
            if (!id || !milestoneId)
                return err('Required: --id <seed-id> --milestone <milestone-id>');
            // Resolve target scope: explicit --scope wins; otherwise cross-scope
            // lookup via shared helper. Ambiguity fails with standard message.
            let scope = flags.get('scope');
            if (scope) {
                scope = String(scope).trim();
            }
            else {
                const { scope: resolved, error: resolveError } = resolveMilestoneScope(targetDir, milestoneId);
                if (resolveError === 'not-found') {
                    return err(`Milestone "${milestoneId}" not found in any scope (looked in: ${listRoadmapScopes(targetDir).join(', ') || '∅'})`);
                }
                if (resolveError && resolveError.startsWith('ambiguous')) {
                    const scopes = resolveError.slice('ambiguous:'.length).trim();
                    return err(`Milestone "${milestoneId}" exists in multiple scopes (${scopes}); pass --scope`);
                }
                scope = resolved;
            }
            const force = (extraArgs || []).includes('--force');
            const reason = flags.get('reason') || null;
            if (force && !reason) {
                return exitWith({
                    status: 'error',
                    command: `seed-${subcommand}`,
                    reason: '--reason required when --force',
                }, 1);
            }
            const seedsPath = seedsJsonPath(targetDir);
            let reply = null;
            let errorEnvelope = null;
            // OUTER LOCK — seeds.json. Held across the entire two-sided write.
            withFileLock(seedsPath, () => {
                // Re-load under the lock so we observe the latest state.
                const seedsFile = loadSeeds(targetDir) || emptySeedsFile();
                const seed = findSeed(seedsFile, id);
                if (!seed) {
                    errorEnvelope = {
                        status: 'error',
                        command: `seed-${subcommand}`,
                        error: `Seed "${id}" not found`,
                    };
                    return;
                }
                const roadmap = loadRoadmapForScope(targetDir, scope);
                if (!roadmap || !Array.isArray(roadmap.milestones)) {
                    errorEnvelope = {
                        status: 'error',
                        command: `seed-${subcommand}`,
                        error: `No roadmap for scope "${scope}". Run: apt-tools roadmap init --scope ${scope}`,
                    };
                    return;
                }
                const milestone = roadmap.milestones.find((m) => m && m.id === milestoneId);
                if (!milestone) {
                    errorEnvelope = {
                        status: 'error',
                        command: `seed-${subcommand}`,
                        error: `Milestone "${milestoneId}" not found in scope "${scope}"`,
                    };
                    return;
                }
                seed.milestones = seed.milestones || [];
                const now = new Date().toISOString();
                const identity = getGitIdentity(targetDir);
                const actor = identity?.name || 'unknown';
                if (subcommand === 'assign') {
                    // Conflict check: a milestone can be in AT MOST one seed.
                    const existingSeed = findSeedForMilestone(seedsFile, scope, milestoneId);
                    if (existingSeed && existingSeed.id !== id) {
                        if (!force) {
                            errorEnvelope = {
                                status: 'error',
                                command: 'seed-assign',
                                reason: 'already-assigned',
                                current_seed_id: existingSeed.id,
                                message: `Milestone "${milestoneId}" is already assigned to seed "${existingSeed.id}". Use --force --reason to override.`,
                            };
                            return;
                        }
                        // Force-override: remove from the old seed and history-log both sides.
                        existingSeed.milestones = (existingSeed.milestones || []).filter((r) => !(r && r.scope === scope && r.milestone_id === milestoneId));
                        if (!Array.isArray(existingSeed.history))
                            existingSeed.history = [];
                        existingSeed.history.push({
                            kind: 'force-unassign',
                            scope,
                            milestone_id: milestoneId,
                            to_seed: id,
                            at: now,
                            actor,
                            reason,
                        });
                    }
                    // Also guard against same-seed-already-has-it — a second
                    // assign from the same seed is a no-op but worth reporting.
                    const already = (seed.milestones || []).some((r) => r && r.scope === scope && r.milestone_id === milestoneId);
                    if (!already) {
                        seed.milestones.push({ scope, milestone_id: milestoneId });
                    }
                    // Write seed_id on the milestone.
                    milestone.seed_id = id;
                    // Audit trail on the receiving seed when --force overrode.
                    if (force) {
                        if (!Array.isArray(seed.history))
                            seed.history = [];
                        seed.history.push({
                            kind: 'force-assign',
                            scope,
                            milestone_id: milestoneId,
                            from_seed: existingSeed?.id || null,
                            at: now,
                            actor,
                            reason,
                        });
                    }
                    reply = {
                        status: 'ok',
                        command: 'seed-assign',
                        id,
                        scope,
                        milestone_id: milestoneId,
                        forced: force ? true : undefined,
                        from_seed: existingSeed && existingSeed.id !== id ? existingSeed.id : null,
                    };
                }
                else {
                    // unassign
                    seed.milestones = (seed.milestones || []).filter((r) => !(r && r.scope === scope && r.milestone_id === milestoneId));
                    // Only clear milestone.seed_id if it pointed at THIS seed.
                    if (milestone.seed_id === id) {
                        delete milestone.seed_id;
                    }
                    reply = {
                        status: 'ok',
                        command: 'seed-unassign',
                        id,
                        scope,
                        milestone_id: milestoneId,
                    };
                }
                // ── Two-sided write ──────────────────────────────────────
                // 1. Write seeds.json FIRST (we hold its lock — use unlocked
                //    writer so we don't re-enter the lock).
                writeSeedsUnlocked(targetDir, seedsFile);
                // 2. Write milestones.json SECOND. saveRoadmapForScope takes
                //    its OWN inner lock on milestones.json — this is the
                //    lock-ordering path (seeds lock held, milestones lock
                //    acquired inside it) that prevents deadlock with any
                //    caller that only needs milestones.json.
                saveRoadmapForScope(targetDir, scope, roadmap);
            }, { projectRoot: targetDir });
            if (errorEnvelope) {
                return exitWith(errorEnvelope, 1);
            }
            // Post-check: run validate AFTER the two-sided write lands. Drift
            // here means a hand-edit elsewhere in seeds.json or milestones.json
            // left the two files out of sync — the mutation still succeeded,
            // but operators should see the warning and fix the stale entry.
            const postCheck = runValidation(targetDir);
            if (!postCheck.ok) {
                reply.drift_warning = {
                    drift_count: postCheck.drift.length,
                    drift: postCheck.drift,
                };
            }
            return ok(reply);
        }
        default:
            return err(`Unknown seed subcommand: ${subcommand}. Expected: init, list, get, create, close, archive, assign, unassign, validate`);
    }
}
//# sourceMappingURL=seed.mjs.map