/**
 * Task creation logic.
 * @task T4460
 * @epic T4454
 */
import { randomBytes } from 'node:crypto';
// setMetaValue now called via tx.setMetaValue inside transaction (T023)
import { ExitCode, TASK_STATUSES } from '@cleocode/contracts';
import { getRawConfigValue, loadConfig } from '../config.js';
import { CleoError } from '../errors.js';
import { allocateNextTaskId } from '../sequence/index.js';
import { requireActiveSession } from '../sessions/session-enforcement.js';
import { createAcceptanceEnforcement } from './enforcement.js';
import { findEpicAncestor, getLifecycleMode, validateChildStageCeiling, validateEpicCreation, } from './epic-enforcement.js';
import { resolveHierarchyPolicy } from './hierarchy-policy.js';
import { resolveDefaultPipelineStage, validatePipelineStage } from './pipeline-stage.js';
/**
 * Build the default verification metadata applied to every new task.
 * Gates are initialized to false (not yet passed). `passed` starts false
 * because no gates have been verified yet.
 * @task T061
 */
export function buildDefaultVerification(initializedAt) {
    return {
        passed: false,
        round: 1,
        gates: {
            implemented: false,
            testsPassed: false,
            qaPassed: false,
        },
        lastAgent: null,
        lastUpdated: null,
        failureLog: [],
        initializedAt,
    };
}
/**
 * Validate a task title.
 * @task T4460
 */
export function validateTitle(title) {
    if (!title || title.trim().length === 0) {
        throw new CleoError(ExitCode.INVALID_INPUT, 'Task title is required');
    }
    if (title.length > 200) {
        throw new CleoError(ExitCode.VALIDATION_ERROR, 'Task title must be 200 characters or less');
    }
}
/**
 * Validate task status.
 * @task T4460
 */
export function validateStatus(status) {
    if (!TASK_STATUSES.includes(status)) {
        throw new CleoError(ExitCode.VALIDATION_ERROR, `Invalid status: ${status} (must be ${TASK_STATUSES.join('|')})`);
    }
}
/**
 * Mapping from numeric priority (1-9) to string priority names.
 * 1-2 = critical, 3-4 = high, 5-6 = medium, 7-9 = low.
 * @task T4572
 */
const NUMERIC_PRIORITY_MAP = {
    1: 'critical',
    2: 'critical',
    3: 'high',
    4: 'high',
    5: 'medium',
    6: 'medium',
    7: 'low',
    8: 'low',
    9: 'low',
};
/** Valid string priority values. */
export const VALID_PRIORITIES = [
    'critical',
    'high',
    'medium',
    'low',
];
/**
 * Normalize priority to canonical string format.
 * Accepts both string names ("critical","high","medium","low") and numeric (1-9).
 * Returns the canonical string format per todo.schema.json.
 * @task T4572
 */
export function normalizePriority(priority) {
    // Handle numeric input
    if (typeof priority === 'number') {
        const mapped = NUMERIC_PRIORITY_MAP[priority];
        if (!mapped) {
            throw new CleoError(ExitCode.VALIDATION_ERROR, `Invalid numeric priority: ${priority} (must be 1-9)`);
        }
        return mapped;
    }
    // Handle string input - check for numeric string first
    const asNumber = Number(priority);
    if (!Number.isNaN(asNumber) && Number.isInteger(asNumber) && asNumber >= 1 && asNumber <= 9) {
        return NUMERIC_PRIORITY_MAP[asNumber];
    }
    // Canonical string validation
    const lower = priority.toLowerCase().trim();
    if (VALID_PRIORITIES.includes(lower)) {
        return lower;
    }
    throw new CleoError(ExitCode.VALIDATION_ERROR, `Invalid priority: ${priority} (must be ${VALID_PRIORITIES.join('|')} or numeric 1-9)`);
}
/**
 * Validate task priority.
 * @task T4460
 * @task T4572
 */
export function validatePriority(priority) {
    normalizePriority(priority);
}
/**
 * Validate task type.
 * @task T4460
 */
export function validateTaskType(type) {
    const valid = ['epic', 'task', 'subtask'];
    if (!valid.includes(type)) {
        throw new CleoError(ExitCode.VALIDATION_ERROR, `Invalid task type: ${type} (must be ${valid.join('|')})`);
    }
}
/**
 * Validate task size.
 * @task T4460
 */
export function validateSize(size) {
    const valid = ['small', 'medium', 'large'];
    if (!valid.includes(size)) {
        throw new CleoError(ExitCode.VALIDATION_ERROR, `Invalid size: ${size} (must be ${valid.join('|')})`);
    }
}
/**
 * Validate label format.
 * @task T4460
 */
export function validateLabels(labels) {
    for (const label of labels) {
        const trimmed = label.trim();
        if (!/^[a-z][a-z0-9.-]*$/.test(trimmed)) {
            throw new CleoError(ExitCode.VALIDATION_ERROR, `Invalid label format: '${trimmed}' (must be lowercase alphanumeric with hyphens/periods)`);
        }
    }
}
/**
 * Validate phase slug format.
 * @task T4460
 */
export function validatePhaseFormat(phase) {
    if (!/^[a-z][a-z0-9-]*$/.test(phase)) {
        throw new CleoError(ExitCode.VALIDATION_ERROR, `Invalid phase format: ${phase} (must be lowercase alphanumeric with hyphens)`);
    }
}
/**
 * Validate dependency IDs exist.
 * @task T4460
 */
export function validateDepends(depends, tasks) {
    const existingIds = new Set(tasks.map((t) => t.id));
    for (const depId of depends) {
        const trimmed = depId.trim();
        if (!/^T\d{3,}$/.test(trimmed)) {
            throw new CleoError(ExitCode.VALIDATION_ERROR, `Invalid dependency ID format: '${trimmed}' (must be T### format)`);
        }
        if (!existingIds.has(trimmed)) {
            throw new CleoError(ExitCode.NOT_FOUND, `Dependency task not found: ${trimmed}`);
        }
    }
}
/**
 * Validate parent hierarchy constraints.
 * @task T4460
 */
export function validateParent(parentId, tasks, maxDepth = 3, maxSiblings = 0) {
    // Check parent exists
    const parent = tasks.find((t) => t.id === parentId);
    if (!parent) {
        throw new CleoError(ExitCode.PARENT_NOT_FOUND, `Parent task not found: ${parentId}`, {
            fix: `Use 'cleo show ${parentId}' to check or create as standalone task`,
        });
    }
    // Check parent type allows children
    if (parent.type === 'subtask') {
        throw new CleoError(ExitCode.INVALID_PARENT_TYPE, `Cannot add child to ${parentId}: subtasks cannot have children`, { fix: `Create as standalone task or add to parent's parent instead` });
    }
    // Check depth
    const depth = getTaskDepth(parentId, tasks);
    if (depth >= maxDepth) {
        throw new CleoError(ExitCode.DEPTH_EXCEEDED, `Cannot add child to ${parentId}: max hierarchy depth (${maxDepth}) would be exceeded`);
    }
    // Check sibling count
    if (maxSiblings > 0) {
        const siblingCount = tasks.filter((t) => t.parentId === parentId).length;
        if (siblingCount >= maxSiblings) {
            throw new CleoError(ExitCode.SIBLING_LIMIT, `Cannot add child to ${parentId}: max siblings (${maxSiblings}) exceeded`, { fix: `Create as standalone task or increase hierarchy.maxSiblings in config` });
        }
    }
}
/**
 * Get the depth of a task in the hierarchy.
 * @task T4460
 */
export function getTaskDepth(taskId, tasks) {
    let depth = 0;
    let currentId = taskId;
    const visited = new Set();
    while (currentId) {
        if (visited.has(currentId))
            break; // circular reference guard
        visited.add(currentId);
        const task = tasks.find((t) => t.id === currentId);
        if (!task?.parentId)
            break;
        depth++;
        currentId = task.parentId;
    }
    return depth;
}
/**
 * Infer task type from parent context.
 * @task T4460
 */
export function inferTaskType(parentId, tasks) {
    if (!parentId)
        return 'task';
    const parent = tasks.find((t) => t.id === parentId);
    if (!parent)
        return 'task';
    if (parent.type === 'epic')
        return 'task';
    return 'subtask';
}
/**
 * Get the next position for a task within a parent scope.
 * @task T4460
 */
export function getNextPosition(parentId, tasks) {
    const siblings = tasks.filter((t) => parentId ? t.parentId === parentId : !t.parentId || t.parentId === null);
    let maxPos = 0;
    for (const s of siblings) {
        if (s.position !== undefined && s.position !== null && s.position > maxPos) {
            maxPos = s.position;
        }
    }
    return maxPos + 1;
}
/**
 * Log an operation to the audit log.
 * @task T4460
 */
export async function logOperation(operation, taskId, details, accessor) {
    const logId = `log-${Math.floor(Date.now() / 1000)}-${randomBytes(3).toString('hex')}`;
    const entry = {
        id: logId,
        timestamp: new Date().toISOString(),
        action: operation,
        taskId,
        actor: 'system',
        details,
        before: null,
        after: details,
    };
    try {
        if (accessor) {
            await accessor.appendLog(entry);
        }
    }
    catch {
        // Log failure is non-fatal
    }
}
/**
 * Check for recent duplicate task.
 * @task T4460
 */
export function findRecentDuplicate(title, phase, tasks, windowSeconds = 60) {
    const now = Date.now();
    const cutoff = now - windowSeconds * 1000;
    for (const task of tasks) {
        if (task.title !== title)
            continue;
        if (phase) {
            if (task.phase !== phase)
                continue;
        }
        else {
            if (task.phase && task.phase !== '')
                continue;
        }
        const created = new Date(task.createdAt).getTime();
        if (created > cutoff)
            return task;
    }
    return null;
}
/**
 * Add a new task to the todo file.
 * @task T4460
 */
export async function addTask(options, cwd, accessor) {
    // Validate title
    validateTitle(options.title);
    // Anti-hallucination: title and description must be different (T5698)
    if (options.description &&
        options.title.trim().toLowerCase() === options.description.trim().toLowerCase()) {
        throw new CleoError(ExitCode.VALIDATION_ERROR, 'Title and description must be different (anti-hallucination rule)');
    }
    await requireActiveSession('tasks.add', cwd);
    // Orphan prevention (T101): non-epic tasks must have a parent in strict mode.
    // Epics are root containers and are exempt. Only enforced in strict lifecycle mode.
    const parentId = options.parentId ?? null;
    if (!parentId && options.type !== 'epic') {
        const lifecycleMode = await getLifecycleMode(cwd);
        if (lifecycleMode === 'strict') {
            throw new CleoError(ExitCode.VALIDATION_ERROR, 'Tasks must have a parent (epic or task) in strict mode. Use --parent <epicId> or set lifecycle.mode to "advisory".', {
                fix: 'cleo add "Task title" --parent T### --acceptance "AC1|AC2|AC3"',
            });
        }
    }
    // Always use accessor (SQLite canonical storage per ADR-006)
    const dataAccessor = accessor ?? (await (await import('../store/data-accessor.js')).getAccessor(cwd));
    // Resolve defaults
    const status = options.status ?? 'pending';
    const priority = normalizePriority(options.priority ?? 'medium');
    const size = options.size ?? 'medium';
    let taskType = options.type;
    // Validate inputs
    validateStatus(status);
    // priority is already normalized above
    validateSize(size);
    if (options.labels?.length)
        validateLabels(options.labels);
    // Enforce Acceptance Criteria (general rule: min 3 for all task types)
    const enforcement = await createAcceptanceEnforcement(cwd);
    const acValidation = enforcement.validateCreation({
        acceptance: options.acceptance,
        priority: priority,
    });
    if (!acValidation.valid) {
        throw new CleoError(acValidation.exitCode ?? ExitCode.VALIDATION_ERROR, acValidation.error, {
            fix: acValidation.fix,
        });
    }
    // Epic-specific creation enforcement (T062): min 5 AC + non-empty description.
    // This runs after general AC enforcement so epics face both the general check
    // (min 3 from enforcement config) AND the stricter epic check (min 5).
    if (options.type === 'epic') {
        await validateEpicCreation({ acceptance: options.acceptance, description: options.description }, cwd);
    }
    // Validate dependency IDs exist using targeted queries
    if (options.depends?.length) {
        for (const depId of options.depends) {
            const trimmed = depId.trim();
            if (!/^T\d{3,}$/.test(trimmed)) {
                throw new CleoError(ExitCode.VALIDATION_ERROR, `Invalid dependency ID format: '${trimmed}' (must be T### format)`);
            }
            const exists = await dataAccessor.taskExists(trimmed);
            if (!exists) {
                throw new CleoError(ExitCode.NOT_FOUND, `Dependency task not found: ${trimmed}`);
            }
        }
    }
    // Phase validation using targeted metadata queries
    let phase = options.phase;
    const projectMeta = await dataAccessor.getMetaValue('project_meta');
    let updatedProjectMeta = null;
    if (phase) {
        validatePhaseFormat(phase);
        // Check if phase exists in project
        const phases = projectMeta?.phases ?? {};
        if (!phases[phase]) {
            if (!options.addPhase) {
                const validPhases = Object.keys(phases).join(', ');
                throw new CleoError(ExitCode.NOT_FOUND, `Phase '${phase}' not found. Valid phases: ${validPhases || 'none'}. Use --add-phase to create new.`);
            }
            // Create phase
            const order = Object.keys(phases).length + 1;
            const name = phase
                .split('-')
                .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
                .join(' ');
            updatedProjectMeta = projectMeta ?? { name: '', phases: {} };
            if (!updatedProjectMeta.phases)
                updatedProjectMeta.phases = {};
            updatedProjectMeta.phases[phase] = { order, name, status: 'pending' };
        }
    }
    else {
        // Inherit from project.currentPhase
        if (projectMeta?.currentPhase) {
            phase = projectMeta.currentPhase;
        }
    }
    // Parent hierarchy validation using targeted queries
    if (parentId) {
        if (!/^T\d{3,}$/.test(parentId)) {
            throw new CleoError(ExitCode.INVALID_INPUT, `Invalid parent ID format: ${parentId}`);
        }
        // Validate parent exists
        const parentTask = await dataAccessor.loadSingleTask(parentId);
        if (!parentTask) {
            throw new CleoError(ExitCode.PARENT_NOT_FOUND, `Parent task ${parentId} not found`);
        }
        // Read hierarchy limits from config via policy module
        const config = await loadConfig(cwd);
        const policy = resolveHierarchyPolicy(config);
        // Depth check using ancestor chain
        const ancestors = await dataAccessor.getAncestorChain(parentId);
        const parentDepth = ancestors.length;
        if (parentDepth + 1 >= policy.maxDepth) {
            throw new CleoError(ExitCode.DEPTH_EXCEEDED, `Maximum nesting depth ${policy.maxDepth} would be exceeded`);
        }
        // Sibling limit (0 = unlimited)
        if (policy.maxSiblings > 0) {
            const children = await dataAccessor.getChildren(parentId);
            const counted = policy.countDoneInLimit
                ? children.length
                : children.filter((t) => t.status !== 'done').length;
            if (counted >= policy.maxSiblings) {
                throw new CleoError(ExitCode.SIBLING_LIMIT, `Parent ${parentId} already has ${counted} children (limit: ${policy.maxSiblings})`);
            }
        }
        // Active siblings cap
        const activeCount = await dataAccessor.countActiveChildren(parentId);
        if (policy.maxActiveSiblings > 0 && activeCount >= policy.maxActiveSiblings) {
            throw new CleoError(ExitCode.SIBLING_LIMIT, `Parent ${parentId} already has ${activeCount} active children (maxActiveSiblings=${policy.maxActiveSiblings})`);
        }
        // Validate type constraints
        if (taskType === 'epic') {
            throw new CleoError(ExitCode.VALIDATION_ERROR, 'Epic tasks cannot have a parent - they must be root-level', { fix: 'Remove --parent flag or change --type to task|subtask' });
        }
    }
    if (taskType === 'subtask' && !parentId) {
        throw new CleoError(ExitCode.VALIDATION_ERROR, 'Subtask tasks require a parent - specify with --parent', { fix: 'Add --parent T### flag or change --type to task|epic' });
    }
    if (taskType) {
        validateTaskType(taskType);
    }
    else {
        // Infer type from parent using targeted query
        if (!parentId) {
            taskType = 'task';
        }
        else {
            const parent = await dataAccessor.loadSingleTask(parentId);
            if (!parent) {
                taskType = 'task';
            }
            else if (parent.type === 'epic') {
                taskType = 'task';
            }
            else {
                taskType = 'subtask';
            }
        }
    }
    // Validate explicit pipelineStage if provided (T060)
    if (options.pipelineStage) {
        validatePipelineStage(options.pipelineStage);
    }
    // Duplicate detection using targeted query
    const { tasks: candidateDupes } = await dataAccessor.queryTasks({
        search: options.title,
        limit: 50,
    });
    const duplicate = findRecentDuplicate(options.title, phase, candidateDupes);
    if (duplicate) {
        return { task: duplicate, duplicate: true };
    }
    // Dry run: build a preview task without allocating a sequence ID or writing to the DB.
    // Must be checked before allocateNextTaskId to avoid advancing the counter on no-op runs.
    if (options.dryRun) {
        const previewNow = new Date().toISOString();
        // Resolve pipeline stage for the preview without any DB writes
        let previewParentForStage = null;
        if (parentId) {
            const previewParentTask = await dataAccessor.loadSingleTask(parentId);
            previewParentForStage = previewParentTask
                ? { pipelineStage: previewParentTask.pipelineStage, type: previewParentTask.type }
                : null;
        }
        const previewPipelineStage = resolveDefaultPipelineStage({
            explicitStage: options.pipelineStage,
            taskType: taskType ?? null,
            parentTask: previewParentForStage,
        });
        const previewPosition = options.position !== undefined
            ? options.position
            : await dataAccessor.getNextPosition(parentId);
        const previewTask = {
            id: 'T???',
            title: options.title,
            description: options.description,
            status,
            priority,
            type: taskType,
            parentId: parentId || null,
            position: previewPosition,
            positionVersion: 0,
            size,
            pipelineStage: previewPipelineStage,
            createdAt: previewNow,
            updatedAt: previewNow,
        };
        if (phase)
            previewTask.phase = phase;
        if (options.labels?.length)
            previewTask.labels = options.labels.map((l) => l.trim());
        if (options.files?.length)
            previewTask.files = options.files.map((f) => f.trim());
        if (options.acceptance?.length)
            previewTask.acceptance = options.acceptance.map((a) => a.trim());
        if (options.depends?.length)
            previewTask.depends = options.depends.map((d) => d.trim());
        if (options.notes) {
            const previewNote = `${new Date()
                .toISOString()
                .replace('T', ' ')
                .replace(/\.\d+Z$/, ' UTC')}: ${options.notes}`;
            previewTask.notes = [previewNote];
        }
        if (status === 'blocked' && options.description) {
            previewTask.blockedBy = options.description;
        }
        if (status === 'done') {
            previewTask.completedAt = previewNow;
        }
        if (taskType !== 'epic') {
            const verificationEnabledRaw = await getRawConfigValue('verification.enabled', cwd);
            if (verificationEnabledRaw === true) {
                previewTask.verification = buildDefaultVerification(previewNow);
            }
        }
        return { task: previewTask, dryRun: true };
    }
    const taskId = await allocateNextTaskId(cwd);
    const now = new Date().toISOString();
    // Resolve pipeline stage: explicit > parent inheritance > type default (T060)
    let resolvedParentForStage = null;
    if (parentId) {
        // Re-use the already-validated parent task (loaded above)
        const parentForStage = await dataAccessor.loadSingleTask(parentId);
        resolvedParentForStage = parentForStage
            ? { pipelineStage: parentForStage.pipelineStage, type: parentForStage.type }
            : null;
    }
    const resolvedPipelineStage = resolveDefaultPipelineStage({
        explicitStage: options.pipelineStage,
        taskType: taskType ?? null,
        parentTask: resolvedParentForStage,
    });
    // Child stage ceiling check (T062): child stage must not exceed parent epic's stage.
    // If the direct parent is an epic, check against it; otherwise walk ancestors.
    if (parentId && taskType !== 'epic') {
        let epicToCheck = null;
        if (resolvedParentForStage?.type === 'epic') {
            // Direct parent is an epic — load its full record to pass to validateChildStageCeiling
            epicToCheck = await dataAccessor.loadSingleTask(parentId);
        }
        else {
            // Walk up from the parent to find the nearest epic ancestor
            epicToCheck = await findEpicAncestor(parentId, dataAccessor);
        }
        if (epicToCheck) {
            await validateChildStageCeiling({ childStage: resolvedPipelineStage, epicId: epicToCheck.id }, dataAccessor, cwd);
        }
    }
    // Compute next position using SQL-level allocation (race-safe, T024)
    let position;
    if (options.position !== undefined) {
        position = options.position;
    }
    else {
        position = await dataAccessor.getNextPosition(parentId);
    }
    // Build task object
    const task = {
        id: taskId,
        title: options.title,
        description: options.description,
        status,
        priority,
        type: taskType,
        parentId: parentId || null,
        position,
        positionVersion: 0,
        size,
        createdAt: now,
        updatedAt: now,
    };
    // Assign pipeline stage (always set — auto-assigned if not explicit) (T060)
    task.pipelineStage = resolvedPipelineStage;
    // Add optional fields
    if (phase)
        task.phase = phase;
    if (options.labels?.length)
        task.labels = options.labels.map((l) => l.trim());
    if (options.files?.length)
        task.files = options.files.map((f) => f.trim());
    if (options.acceptance?.length)
        task.acceptance = options.acceptance.map((a) => a.trim());
    if (options.depends?.length)
        task.depends = options.depends.map((d) => d.trim());
    if (options.notes) {
        const timestampedNote = `${new Date()
            .toISOString()
            .replace('T', ' ')
            .replace(/\.\d+Z$/, ' UTC')}: ${options.notes}`;
        task.notes = [timestampedNote];
    }
    if (status === 'blocked' && options.description) {
        task.blockedBy = options.description;
    }
    if (status === 'done') {
        task.completedAt = now;
    }
    // Auto-initialize verification metadata on task creation (T061).
    // Only for non-epic tasks when verification is enabled in config.
    // Epics are containers and do not go through verification gates themselves.
    if (taskType !== 'epic') {
        const verificationEnabledRaw = await getRawConfigValue('verification.enabled', cwd);
        if (verificationEnabledRaw === true) {
            task.verification = buildDefaultVerification(now);
        }
    }
    // Wrap all writes in a transaction for TOCTOU safety (T023)
    await dataAccessor.transaction(async (tx) => {
        // Position shuffling via bulk SQL update (T025)
        if (options.position !== undefined) {
            await dataAccessor.shiftPositions(parentId, options.position, 1);
        }
        // Insert the new task
        await tx.upsertSingleTask(task);
        // Update project metadata if a new phase was created
        if (options.addPhase && phase && updatedProjectMeta?.phases?.[phase]) {
            await tx.setMetaValue('project_meta', updatedProjectMeta);
        }
        // Audit log
        await tx.appendLog({
            id: `log-${Math.floor(Date.now() / 1000)}-${randomBytes(3).toString('hex')}`,
            timestamp: new Date().toISOString(),
            action: 'task_created',
            taskId,
            actor: 'system',
            details: { title: options.title, status, priority },
            before: null,
            after: { title: options.title, status, priority },
        });
    });
    return { task };
}
//# sourceMappingURL=add.js.map