/**
 * Core task non-CRUD operations - business logic extracted from task-engine.ts.
 *
 * These are pure business logic functions that throw on failure and return
 * data directly (no EngineResult wrapper). The engine layer wraps these
 * in try/catch to produce EngineResult.
 *
 * Covers: next, blockers, tree, deps, relates, relatesAdd, analyze, restore,
 * unarchive, reorder, reparent, promote, reopen, complexityEstimate, depends,
 * stats, export, history, lint, batchValidate, import
 *
 * @task T4790
 * @epic T4654
 */
import { TASK_STATUSES } from '@cleocode/contracts';
import { getAccessor } from '../store/data-accessor.js';
import { getDataPath, readJsonFile as storeReadJsonFile } from '../store/file-utils.js';
import { canCancel } from './cancel-ops.js';
import { detectCircularDeps, getBlockedTasks, getLeafBlockers, getReadyTasks, getTransitiveBlockers, validateDependencies, } from './dependency-check.js';
import { depsReady } from './deps-ready.js';
// ============================================================================
// Helpers
// ============================================================================
const PRIORITY_SCORE = {
    critical: 100,
    high: 75,
    medium: 50,
    low: 25,
};
async function loadAllTasks(projectRoot) {
    const accessor = await getAccessor(projectRoot);
    const { tasks } = await accessor.queryTasks({});
    return tasks;
}
function buildTreeNode(task, childrenMap) {
    const children = (childrenMap.get(task.id) ?? []).map((child) => buildTreeNode(child, childrenMap));
    return {
        id: task.id,
        title: task.title,
        status: task.status,
        type: task.type,
        children,
    };
}
function buildUpstreamTree(taskId, taskMap, visited = new Set()) {
    const task = taskMap.get(taskId);
    if (!task?.depends?.length)
        return [];
    const nodes = [];
    for (const depId of task.depends) {
        if (visited.has(depId))
            continue;
        visited.add(depId);
        const dep = taskMap.get(depId);
        if (!dep)
            continue;
        nodes.push({
            id: dep.id,
            title: dep.title,
            status: dep.status,
            type: dep.type,
            children: buildUpstreamTree(depId, taskMap, visited),
        });
    }
    return nodes;
}
function countNodes(nodes) {
    let count = nodes.length;
    for (const node of nodes) {
        count += countNodes(node.children);
    }
    return count;
}
function measureDependencyDepth(taskId, taskMap, visited = new Set()) {
    if (visited.has(taskId))
        return 0;
    visited.add(taskId);
    const task = taskMap.get(taskId);
    if (!task || !task.depends || task.depends.length === 0)
        return 0;
    let maxDepth = 0;
    for (const depId of task.depends) {
        const depth = 1 + measureDependencyDepth(depId, taskMap, visited);
        if (depth > maxDepth)
            maxDepth = depth;
    }
    return maxDepth;
}
function getHierarchyLimits(projectRoot) {
    const configPath = getDataPath(projectRoot, 'config.json');
    const config = storeReadJsonFile(configPath);
    let maxDepth = 3;
    let maxSiblings = 0;
    if (config) {
        const hierarchy = config.hierarchy;
        if (hierarchy) {
            if (typeof hierarchy.maxDepth === 'number')
                maxDepth = hierarchy.maxDepth;
            if (typeof hierarchy.maxSiblings === 'number')
                maxSiblings = hierarchy.maxSiblings;
        }
    }
    return { maxDepth, maxSiblings };
}
// ============================================================================
// taskNext
// ============================================================================
/**
 * Suggest next task to work on based on priority, phase, age, and deps.
 * @task T4790
 */
export async function coreTaskNext(projectRoot, params) {
    const accessor = await getAccessor(projectRoot);
    const allTasks = await loadAllTasks(projectRoot);
    const taskMap = new Map(allTasks.map((t) => [t.id, t]));
    const projectMeta = await accessor.getMetaValue('project_meta');
    const currentPhase = projectMeta?.currentPhase ?? null;
    const candidates = allTasks.filter((t) => t.status === 'pending' && depsReady(t.depends, taskMap));
    if (candidates.length === 0) {
        return { suggestions: [], totalCandidates: 0 };
    }
    const scored = candidates
        .map((task) => {
        const reasons = [];
        let score = 0;
        score += PRIORITY_SCORE[task.priority] ?? 50;
        reasons.push(`priority: ${task.priority} (+${PRIORITY_SCORE[task.priority] ?? 50})`);
        if (currentPhase && task.phase === currentPhase) {
            score += 20;
            reasons.push(`phase alignment: ${currentPhase} (+20)`);
        }
        if (depsReady(task.depends, taskMap)) {
            score += 10;
            reasons.push('all dependencies satisfied (+10)');
        }
        if (task.createdAt) {
            const ageMs = Date.now() - new Date(task.createdAt).getTime();
            const ageDays = ageMs / (1000 * 60 * 60 * 24);
            if (ageDays > 7) {
                const ageBonus = Math.min(15, Math.floor(ageDays / 7));
                score += ageBonus;
                reasons.push(`age: ${Math.floor(ageDays)} days (+${ageBonus})`);
            }
        }
        return { task, score, reasons };
    })
        .sort((a, b) => b.score - a.score);
    // Brain pattern scoring (best-effort)
    try {
        const { searchPatterns } = await import('../memory/patterns.js');
        const [successPatterns, failurePatterns] = await Promise.all([
            searchPatterns(projectRoot, { type: 'success', limit: 20 }),
            searchPatterns(projectRoot, { type: 'failure', limit: 20 }),
        ]);
        if (successPatterns.length > 0 || failurePatterns.length > 0) {
            for (const item of scored) {
                const titleLower = item.task.title.toLowerCase();
                const labels = (item.task.labels ?? []).map((l) => l.toLowerCase());
                const matchText = [titleLower, ...labels].join(' ');
                for (const sp of successPatterns) {
                    if (matchText.includes(sp.pattern.toLowerCase())) {
                        item.score += 10;
                        item.reasons.push(`brain: success pattern match "${sp.pattern}" (+10)`);
                        break;
                    }
                }
                for (const fp of failurePatterns) {
                    if (matchText.includes(fp.pattern.toLowerCase())) {
                        item.score -= 5;
                        item.reasons.push(`brain: failure pattern match "${fp.pattern}" (-5)`);
                        break;
                    }
                }
            }
            scored.sort((a, b) => b.score - a.score);
        }
    }
    catch {
        // Brain pattern scoring is best-effort
    }
    const count = Math.min(params?.count || 1, scored.length);
    const explain = params?.explain ?? false;
    const suggestions = scored.slice(0, count).map(({ task, score, reasons }) => ({
        id: task.id,
        title: task.title,
        priority: task.priority,
        phase: task.phase ?? null,
        score,
        ...(explain && { reasons }),
    }));
    return { suggestions, totalCandidates: candidates.length };
}
// ============================================================================
// taskBlockers
// ============================================================================
/**
 * Show blocked tasks and analyze blocking chains.
 * @task T4790
 */
export async function coreTaskBlockers(projectRoot, params) {
    const allTasks = await loadAllTasks(projectRoot);
    const taskMap = new Map(allTasks.map((t) => [t.id, t]));
    const analyze = params?.analyze ?? false;
    const effectiveLimit = params?.limit ?? 20;
    const blockedTasks = allTasks.filter((t) => t.status === 'blocked');
    const depBlockedTasks = allTasks.filter((t) => t.status === 'pending' &&
        t.depends &&
        t.depends.length > 0 &&
        t.depends.some((depId) => {
            const dep = taskMap.get(depId);
            return dep && dep.status !== 'done' && dep.status !== 'cancelled';
        }));
    const tasksAsTask = allTasks;
    const blockerInfos = [
        ...blockedTasks.map((t) => ({
            id: t.id,
            title: t.title,
            status: t.status,
            depends: t.depends,
            blockingChain: analyze ? getTransitiveBlockers(t.id, tasksAsTask) : [],
        })),
        ...depBlockedTasks
            .filter((t) => !blockedTasks.some((bt) => bt.id === t.id))
            .map((t) => ({
            id: t.id,
            title: t.title,
            status: t.status,
            depends: t.depends,
            blockingChain: analyze ? getTransitiveBlockers(t.id, tasksAsTask) : [],
        })),
    ];
    const total = blockerInfos.length;
    const pagedBlockerInfos = blockerInfos.slice(0, effectiveLimit);
    const blockerCounts = new Map();
    for (const info of pagedBlockerInfos) {
        for (const depId of info.blockingChain) {
            blockerCounts.set(depId, (blockerCounts.get(depId) ?? 0) + 1);
        }
    }
    const criticalBlockers = [...blockerCounts.entries()]
        .sort((a, b) => b[1] - a[1])
        .slice(0, 5)
        .map(([id, count]) => {
        const task = taskMap.get(id);
        return { id, title: task?.title ?? 'Unknown', blocksCount: count };
    });
    return {
        blockedTasks: pagedBlockerInfos,
        criticalBlockers,
        summary: total === 0 ? 'No blocked tasks found' : `${total} blocked task(s)`,
        total,
        limit: effectiveLimit,
    };
}
// ============================================================================
// taskTree
// ============================================================================
/**
 * Build hierarchy tree.
 * @task T4790
 */
export async function coreTaskTree(projectRoot, taskId) {
    const allTasks = await loadAllTasks(projectRoot);
    if (taskId) {
        const task = allTasks.find((t) => t.id === taskId);
        if (!task) {
            throw new Error(`Task '${taskId}' not found`);
        }
    }
    const childrenMap = new Map();
    for (const task of allTasks) {
        const parentKey = task.parentId ?? '__root__';
        if (!childrenMap.has(parentKey)) {
            childrenMap.set(parentKey, []);
        }
        childrenMap.get(parentKey).push(task);
    }
    let roots;
    if (taskId) {
        roots = [allTasks.find((t) => t.id === taskId)];
    }
    else {
        roots = childrenMap.get('__root__') ?? [];
    }
    const tree = roots.map((root) => buildTreeNode(root, childrenMap));
    return { tree, totalNodes: countNodes(tree) };
}
// ============================================================================
// taskDeps
// ============================================================================
/**
 * Show dependencies for a task.
 * @task T4790
 */
export async function coreTaskDeps(projectRoot, taskId) {
    const allTasks = await loadAllTasks(projectRoot);
    const task = allTasks.find((t) => t.id === taskId);
    if (!task) {
        throw new Error(`Task '${taskId}' not found`);
    }
    const taskMap = new Map(allTasks.map((t) => [t.id, t]));
    const completedIds = new Set(allTasks.filter((t) => t.status === 'done' || t.status === 'cancelled').map((t) => t.id));
    const dependsOn = (task.depends ?? [])
        .map((depId) => {
        const dep = taskMap.get(depId);
        return dep ? { id: dep.id, title: dep.title, status: dep.status } : null;
    })
        .filter((d) => d !== null);
    const dependedOnBy = allTasks
        .filter((t) => t.depends?.includes(taskId))
        .map((t) => ({ id: t.id, title: t.title, status: t.status }));
    const unresolvedDeps = (task.depends ?? []).filter((depId) => !completedIds.has(depId));
    return {
        taskId,
        dependsOn,
        dependedOnBy,
        unresolvedDeps,
        allDepsReady: unresolvedDeps.length === 0,
    };
}
// ============================================================================
// taskRelates
// ============================================================================
/**
 * Show task relations.
 * @task T4790
 */
export async function coreTaskRelates(projectRoot, taskId) {
    const allTasks = await loadAllTasks(projectRoot);
    const task = allTasks.find((t) => t.id === taskId);
    if (!task) {
        throw new Error(`Task '${taskId}' not found`);
    }
    const relations = task.relates ?? [];
    return { taskId, relations, count: relations.length };
}
// ============================================================================
// taskRelatesAdd
// ============================================================================
/**
 * Add a relation between two tasks.
 * @task T4790
 */
export async function coreTaskRelatesAdd(projectRoot, taskId, relatedId, type, reason) {
    const accessor = await getAccessor(projectRoot);
    const fromTask = await accessor.loadSingleTask(taskId);
    if (!fromTask) {
        throw new Error(`Task '${taskId}' not found`);
    }
    const toExists = await accessor.taskExists(relatedId);
    if (!toExists) {
        throw new Error(`Task '${relatedId}' not found`);
    }
    if (!fromTask.relates) {
        fromTask.relates = [];
    }
    fromTask.relates.push({
        taskId: relatedId,
        type,
        reason: reason || undefined,
    });
    fromTask.updatedAt = new Date().toISOString();
    await accessor.upsertSingleTask(fromTask);
    // Persist to task_relations table (T5168)
    await accessor.addRelation(taskId, relatedId, type, reason);
    return { from: taskId, to: relatedId, type, reason, added: true };
}
// ============================================================================
// taskAnalyze
// ============================================================================
/**
 * Analyze tasks for priority and leverage.
 * @task T4790
 */
export async function coreTaskAnalyze(projectRoot, taskId, params) {
    const allTasks = await loadAllTasks(projectRoot);
    const effectiveTierLimit = params?.tierLimit ?? 10;
    const tasks = taskId
        ? allTasks.filter((t) => t.id === taskId || t.parentId === taskId)
        : allTasks;
    const blocksMap = {};
    for (const task of tasks) {
        if (task.depends) {
            for (const dep of task.depends) {
                if (!blocksMap[dep])
                    blocksMap[dep] = [];
                blocksMap[dep].push(task.id);
            }
        }
    }
    const leverageMap = {};
    for (const task of tasks) {
        leverageMap[task.id] = (blocksMap[task.id] ?? []).length;
    }
    const actionable = tasks.filter((t) => t.status === 'pending' || t.status === 'active');
    const blocked = tasks.filter((t) => t.status === 'blocked');
    const bottlenecks = tasks
        .filter((t) => (blocksMap[t.id]?.length ?? 0) > 0 && t.status !== 'done')
        .map((t) => ({ id: t.id, title: t.title, blocksCount: blocksMap[t.id].length }))
        .sort((a, b) => b.blocksCount - a.blocksCount)
        .slice(0, 5);
    const scored = actionable.map((t) => ({
        id: t.id,
        title: t.title,
        leverage: leverageMap[t.id] ?? 0,
        priority: t.priority,
    }));
    scored.sort((a, b) => {
        const priorityWeight = { critical: 100, high: 50, medium: 20, low: 5 };
        const aScore = (priorityWeight[a.priority ?? 'medium'] ?? 20) + a.leverage * 10;
        const bScore = (priorityWeight[b.priority ?? 'medium'] ?? 20) + b.leverage * 10;
        return bScore - aScore;
    });
    const critical = scored.filter((t) => t.priority === 'critical');
    const high = scored.filter((t) => t.priority === 'high');
    const normal = scored.filter((t) => t.priority !== 'critical' && t.priority !== 'high');
    const recommended = scored.length > 0
        ? {
            id: scored[0].id,
            title: scored[0].title,
            leverage: scored[0].leverage,
            reason: 'Highest combined priority and leverage score',
        }
        : null;
    const totalLeverage = Object.values(leverageMap).reduce((s, v) => s + v, 0);
    const avgLeverage = tasks.length > 0 ? Math.round((totalLeverage / tasks.length) * 100) / 100 : 0;
    return {
        recommended,
        bottlenecks,
        tiers: {
            critical: critical
                .slice(0, effectiveTierLimit)
                .map(({ id, title, leverage }) => ({ id, title, leverage })),
            high: high
                .slice(0, effectiveTierLimit)
                .map(({ id, title, leverage }) => ({ id, title, leverage })),
            normal: normal
                .slice(0, effectiveTierLimit)
                .map(({ id, title, leverage }) => ({ id, title, leverage })),
        },
        metrics: {
            totalTasks: tasks.length,
            actionable: actionable.length,
            blocked: blocked.length,
            avgLeverage,
        },
        tierLimit: effectiveTierLimit,
    };
}
// ============================================================================
// taskRestore
// ============================================================================
/**
 * Restore a cancelled task back to pending.
 * @task T4790
 */
export async function coreTaskRestore(projectRoot, taskId, params) {
    const accessor = await getAccessor(projectRoot);
    const task = await accessor.loadSingleTask(taskId);
    if (!task) {
        throw new Error(`Task '${taskId}' not found`);
    }
    if (task.status !== 'cancelled') {
        throw new Error(`Task '${taskId}' is not cancelled (status: ${task.status}). Only cancelled tasks can be restored.`);
    }
    const tasksToRestore = [task];
    if (params?.cascade) {
        const findCancelledChildren = async (parentId) => {
            const children = await accessor.getChildren(parentId);
            const cancelledChildren = children.filter((t) => t.status === 'cancelled');
            for (const child of cancelledChildren) {
                tasksToRestore.push(child);
                await findCancelledChildren(child.id);
            }
        };
        await findCancelledChildren(taskId);
    }
    const now = new Date().toISOString();
    const restored = [];
    for (const t of tasksToRestore) {
        t.status = 'pending';
        t.cancelledAt = undefined;
        t.cancellationReason = undefined;
        t.updatedAt = now;
        if (!t.notes)
            t.notes = [];
        t.notes.push(`[${now}] Restored from cancelled${params?.notes ? ': ' + params.notes : ''}`);
        restored.push(t.id);
    }
    for (const t of tasksToRestore) {
        await accessor.upsertSingleTask(t);
    }
    return { task: taskId, restored, count: restored.length };
}
// ============================================================================
// taskCancel
// ============================================================================
/**
 * Cancel a task (sets status to 'cancelled', a soft terminal state).
 * Use restore to reverse. Use delete for permanent removal.
 * @task T4529
 */
export async function coreTaskCancel(projectRoot, taskId, params) {
    const accessor = await getAccessor(projectRoot);
    const task = await accessor.loadSingleTask(taskId);
    if (!task) {
        throw new Error(`Task ${taskId} not found`);
    }
    const check = canCancel(task);
    if (!check.allowed) {
        throw new Error(check.reason);
    }
    const cancelledAt = new Date().toISOString();
    task.status = 'cancelled';
    task.cancelledAt = cancelledAt;
    task.cancellationReason = params?.reason ?? undefined;
    task.updatedAt = cancelledAt;
    await accessor.upsertSingleTask(task);
    return { task: taskId, cancelled: true, reason: params?.reason, cancelledAt };
}
// ============================================================================
// taskUnarchive
// ============================================================================
/**
 * Move an archived task back to active tasks.
 * @task T4790
 */
export async function coreTaskUnarchive(projectRoot, taskId, params) {
    const accessor = await getAccessor(projectRoot);
    // Check if task already exists in active tasks
    const existingTask = await accessor.taskExists(taskId);
    if (existingTask) {
        throw new Error(`Task '${taskId}' already exists in active tasks`);
    }
    const archive = await accessor.loadArchive();
    if (!archive || !archive.archivedTasks) {
        throw new Error('No archive file found');
    }
    const taskIndex = archive.archivedTasks.findIndex((t) => t.id === taskId);
    if (taskIndex === -1) {
        throw new Error(`Task '${taskId}' not found in archive`);
    }
    const task = archive.archivedTasks[taskIndex];
    // Remove archive metadata if present on the raw record
    if ('_archive' in task) {
        Reflect.deleteProperty(task, '_archive');
    }
    if (!params?.preserveStatus) {
        const rawStatus = params?.status || 'pending';
        if (!TASK_STATUSES.includes(rawStatus)) {
            throw new Error(`Invalid status: ${rawStatus}`);
        }
        // rawStatus is validated above as a member of TASK_STATUSES
        const targetStatus = rawStatus;
        if (targetStatus !== 'done') {
            task.completedAt = undefined;
        }
    }
    task.updatedAt = new Date().toISOString();
    // Fine-grained: upsert the restored task (now active)
    await accessor.upsertSingleTask(task);
    return { task: taskId, unarchived: true, title: task.title, status: task.status };
}
// ============================================================================
// taskReorder
// ============================================================================
/**
 * Change task position within its sibling group.
 * @task T4790
 */
export async function coreTaskReorder(projectRoot, taskId, position) {
    const accessor = await getAccessor(projectRoot);
    const task = await accessor.loadSingleTask(taskId);
    if (!task) {
        throw new Error(`Task '${taskId}' not found`);
    }
    // Get siblings: tasks with same parentId
    const parentFilter = task.parentId ? { parentId: task.parentId } : {};
    const { tasks: siblingCandidates } = await accessor.queryTasks(parentFilter);
    // For root-level tasks (no parentId), filter to only those without a parentId
    const allSiblings = task.parentId
        ? siblingCandidates.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
        : siblingCandidates
            .filter((t) => !t.parentId)
            .sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
    const currentIndex = allSiblings.findIndex((t) => t.id === taskId);
    const newIndex = Math.max(0, Math.min(position - 1, allSiblings.length - 1));
    allSiblings.splice(currentIndex, 1);
    allSiblings.splice(newIndex, 0, task);
    // Use bulk SQL for position updates (T025) — updateTaskFields is lighter than upsertSingleTask
    const now = new Date().toISOString();
    for (let i = 0; i < allSiblings.length; i++) {
        const sibling = allSiblings[i];
        const newPos = i + 1;
        const newVersion = (sibling.positionVersion ?? 0) + 1;
        // Only update if position actually changed
        if (sibling.position !== newPos || sibling.id === taskId) {
            await accessor.updateTaskFields(sibling.id, {
                position: newPos,
                positionVersion: newVersion,
                updatedAt: now,
            });
        }
        sibling.position = newPos;
        sibling.positionVersion = newVersion;
        sibling.updatedAt = now;
    }
    return {
        task: taskId,
        reordered: true,
        newPosition: newIndex + 1,
        totalSiblings: allSiblings.length,
    };
}
// ============================================================================
// taskReparent
// ============================================================================
/**
 * Move task under a different parent.
 * @task T4790
 */
export async function coreTaskReparent(projectRoot, taskId, newParentId) {
    const accessor = await getAccessor(projectRoot);
    const task = await accessor.loadSingleTask(taskId);
    if (!task) {
        throw new Error(`Task '${taskId}' not found`);
    }
    const effectiveParentId = newParentId || null;
    if (!effectiveParentId) {
        const oldParent = task.parentId ?? null;
        task.parentId = null;
        if (task.type === 'subtask')
            task.type = 'task';
        task.updatedAt = new Date().toISOString();
        await accessor.upsertSingleTask(task);
        return { task: taskId, reparented: true, oldParent, newParent: null, newType: task.type };
    }
    const newParent = await accessor.loadSingleTask(effectiveParentId);
    if (!newParent) {
        throw new Error(`Parent task '${effectiveParentId}' not found`);
    }
    if (newParent.type === 'subtask') {
        throw new Error(`Cannot parent under subtask '${effectiveParentId}'`);
    }
    // Check circular reference using subtree
    const subtree = await accessor.getSubtree(taskId);
    if (subtree.some((t) => t.id === effectiveParentId)) {
        throw new Error(`Moving '${taskId}' under '${effectiveParentId}' would create circular reference`);
    }
    // Check depth limit using ancestor chain
    const ancestors = await accessor.getAncestorChain(effectiveParentId);
    const parentDepth = ancestors.length;
    const reparentLimits = getHierarchyLimits(projectRoot);
    if (parentDepth + 1 >= reparentLimits.maxDepth) {
        throw new Error(`Move would exceed max depth of ${reparentLimits.maxDepth}`);
    }
    // Check sibling limit (0 = unlimited)
    const children = await accessor.getChildren(effectiveParentId);
    const siblingCount = children.filter((t) => t.id !== taskId).length;
    if (reparentLimits.maxSiblings > 0 && siblingCount >= reparentLimits.maxSiblings) {
        throw new Error(`Cannot add child to ${effectiveParentId}: max siblings (${reparentLimits.maxSiblings}) exceeded`);
    }
    const oldParent = task.parentId ?? null;
    task.parentId = effectiveParentId;
    const newDepth = parentDepth + 1;
    if (newDepth === 1)
        task.type = 'task';
    else if (newDepth >= 2)
        task.type = 'subtask';
    task.updatedAt = new Date().toISOString();
    await accessor.upsertSingleTask(task);
    return {
        task: taskId,
        reparented: true,
        oldParent,
        newParent: effectiveParentId,
        newType: task.type,
    };
}
// ============================================================================
// taskPromote
// ============================================================================
/**
 * Promote a subtask to task or task to root.
 * @task T4790
 */
export async function coreTaskPromote(projectRoot, taskId) {
    const accessor = await getAccessor(projectRoot);
    const task = await accessor.loadSingleTask(taskId);
    if (!task) {
        throw new Error(`Task '${taskId}' not found`);
    }
    if (!task.parentId) {
        return { task: taskId, promoted: false, previousParent: null, typeChanged: false };
    }
    const oldParent = task.parentId;
    task.parentId = null;
    task.updatedAt = new Date().toISOString();
    let typeChanged = false;
    if (task.type === 'subtask') {
        task.type = 'task';
        typeChanged = true;
    }
    await accessor.upsertSingleTask(task);
    return { task: taskId, promoted: true, previousParent: oldParent, typeChanged };
}
// ============================================================================
// taskReopen
// ============================================================================
/**
 * Reopen a completed task.
 * @task T4790
 */
export async function coreTaskReopen(projectRoot, taskId, params) {
    const accessor = await getAccessor(projectRoot);
    const task = await accessor.loadSingleTask(taskId);
    if (!task) {
        throw new Error(`Task '${taskId}' not found`);
    }
    if (task.status !== 'done') {
        throw new Error(`Task '${taskId}' is not completed (status: ${task.status}). Only done tasks can be reopened.`);
    }
    const targetStatus = params?.status || 'pending';
    if (targetStatus !== 'pending' && targetStatus !== 'active') {
        throw new Error(`Invalid target status: ${targetStatus}. Must be 'pending' or 'active'.`);
    }
    const previousStatus = task.status;
    task.status = targetStatus;
    task.completedAt = undefined;
    task.updatedAt = new Date().toISOString();
    if (!task.notes)
        task.notes = [];
    const reason = params?.reason;
    task.notes.push(`[${task.updatedAt}] Reopened from ${previousStatus}${reason ? ': ' + reason : ''}`);
    await accessor.upsertSingleTask(task);
    return { task: taskId, reopened: true, previousStatus, newStatus: targetStatus };
}
// ============================================================================
// taskComplexityEstimate
// ============================================================================
/**
 * Deterministic complexity scoring from task metadata.
 * @task T4790
 */
export async function coreTaskComplexityEstimate(projectRoot, params) {
    const allTasks = await loadAllTasks(projectRoot);
    const task = allTasks.find((t) => t.id === params.taskId);
    if (!task) {
        throw new Error(`Task '${params.taskId}' not found`);
    }
    const factors = [];
    let score = 0;
    const descLen = (task.description || '').length;
    let descScore;
    let descLabel;
    if (descLen < 100) {
        descScore = 1;
        descLabel = 'short';
    }
    else if (descLen < 500) {
        descScore = 2;
        descLabel = 'medium';
    }
    else {
        descScore = 3;
        descLabel = 'long';
    }
    score += descScore;
    factors.push({
        name: 'descriptionLength',
        value: descScore,
        detail: `${descLabel} (${descLen} chars)`,
    });
    const acceptanceCount = task.acceptance?.length ?? 0;
    const acceptanceScore = Math.min(acceptanceCount, 3);
    score += acceptanceScore;
    factors.push({
        name: 'acceptanceCriteria',
        value: acceptanceScore,
        detail: `${acceptanceCount} criteria`,
    });
    const taskMap = new Map(allTasks.map((t) => [t.id, t]));
    const dependencyDepth = measureDependencyDepth(params.taskId, taskMap);
    const depthScore = Math.min(dependencyDepth, 3);
    score += depthScore;
    factors.push({ name: 'dependencyDepth', value: depthScore, detail: `depth ${dependencyDepth}` });
    const subtaskCount = allTasks.filter((t) => t.parentId === params.taskId).length;
    const subtaskScore = Math.min(subtaskCount, 3);
    score += subtaskScore;
    factors.push({ name: 'subtaskCount', value: subtaskScore, detail: `${subtaskCount} subtasks` });
    const fileCount = task.files?.length ?? 0;
    const fileScore = Math.min(fileCount, 3);
    score += fileScore;
    factors.push({ name: 'fileReferences', value: fileScore, detail: `${fileCount} files` });
    let size;
    if (score <= 3)
        size = 'small';
    else if (score <= 7)
        size = 'medium';
    else
        size = 'large';
    return { size, score, factors, dependencyDepth, subtaskCount, fileCount };
}
// ============================================================================
// taskDepsOverview
// ============================================================================
/**
 * Overview of all dependencies across the project.
 * @task T5157
 */
export async function coreTaskDepsOverview(projectRoot) {
    const allTasks = await loadAllTasks(projectRoot);
    const tasksAsTask = allTasks;
    const tasksWithDeps = allTasks.filter((t) => t.depends && t.depends.length > 0);
    const blocked = getBlockedTasks(tasksAsTask);
    const ready = getReadyTasks(tasksAsTask);
    const validation = validateDependencies(tasksAsTask);
    return {
        totalTasks: allTasks.length,
        tasksWithDeps: tasksWithDeps.length,
        blockedTasks: blocked.map((t) => ({
            id: t.id,
            title: t.title,
            status: t.status,
            unblockedBy: (t.depends ?? []).filter((depId) => {
                const dep = allTasks.find((x) => x.id === depId);
                return dep && dep.status !== 'done' && dep.status !== 'cancelled';
            }),
        })),
        readyTasks: ready
            .filter((t) => t.status !== 'done' && t.status !== 'cancelled')
            .map((t) => ({
            id: t.id,
            title: t.title,
            status: t.status,
        })),
        validation: {
            valid: validation.valid,
            errorCount: validation.errors.length,
            warningCount: validation.warnings.length,
        },
    };
}
// ============================================================================
// taskDepsCycles
// ============================================================================
/**
 * Detect circular dependencies across the project.
 * @task T5157
 */
export async function coreTaskDepsCycles(projectRoot) {
    const allTasks = await loadAllTasks(projectRoot);
    const tasksAsTask = allTasks;
    const taskMap = new Map(allTasks.map((t) => [t.id, t]));
    const visited = new Set();
    const cycles = [];
    for (const task of allTasks) {
        if (visited.has(task.id))
            continue;
        if (!task.depends?.length)
            continue;
        const cycle = detectCircularDeps(task.id, tasksAsTask);
        if (cycle.length > 0) {
            cycles.push({
                path: cycle,
                // Deduplicate: detectCircularDeps returns [A,B,C,A] where
                // last element closes the cycle. Use Set for robustness.
                tasks: [...new Set(cycle)].map((id) => {
                    const t = taskMap.get(id);
                    return { id, title: t?.title ?? 'unknown' };
                }),
            });
            for (const id of cycle) {
                visited.add(id);
            }
        }
    }
    return { hasCycles: cycles.length > 0, cycles };
}
// ============================================================================
// taskDepends
// ============================================================================
/**
 * List dependencies for a task in a given direction.
 * @task T4790
 */
export async function coreTaskDepends(projectRoot, taskId, direction = 'both', options) {
    const allTasks = await loadAllTasks(projectRoot);
    const task = allTasks.find((t) => t.id === taskId);
    if (!task) {
        throw new Error(`Task '${taskId}' not found`);
    }
    const taskMap = new Map(allTasks.map((t) => [t.id, t]));
    const upstream = [];
    if (direction === 'upstream' || direction === 'both') {
        for (const depId of task.depends ?? []) {
            const dep = taskMap.get(depId);
            if (dep) {
                upstream.push({ id: dep.id, title: dep.title, status: dep.status });
            }
        }
    }
    const downstream = [];
    if (direction === 'downstream' || direction === 'both') {
        for (const t of allTasks) {
            if (t.depends?.includes(taskId)) {
                downstream.push({ id: t.id, title: t.title, status: t.status });
            }
        }
    }
    // Transitive dependency hints
    const tasksAsTask = allTasks;
    const transitiveIds = getTransitiveBlockers(taskId, tasksAsTask);
    const unresolvedChain = transitiveIds.length;
    const leafIds = getLeafBlockers(taskId, tasksAsTask);
    const leafBlockers = leafIds.map((id) => {
        const t = taskMap.get(id);
        return { id: t.id, title: t.title, status: t.status };
    });
    const allDepsReady = unresolvedChain === 0;
    const hint = unresolvedChain > 0
        ? `Run 'ct deps show ${taskId} --tree' for full dependency graph`
        : undefined;
    // Optional upstream tree
    let upstreamTree;
    if (options?.tree) {
        upstreamTree = buildUpstreamTree(taskId, taskMap);
    }
    return {
        taskId,
        direction,
        upstream,
        downstream,
        unresolvedChain,
        leafBlockers,
        allDepsReady,
        ...(hint && { hint }),
        ...(upstreamTree && { upstreamTree }),
    };
}
// ============================================================================
// taskStats
// ============================================================================
/**
 * Compute task statistics.
 * @task T4790
 */
export async function coreTaskStats(projectRoot, epicId) {
    const allTasks = await loadAllTasks(projectRoot);
    let tasks = allTasks;
    if (epicId) {
        const epicIds = new Set();
        epicIds.add(epicId);
        const collectChildren = (parentId) => {
            for (const t of allTasks) {
                if (t.parentId === parentId && !epicIds.has(t.id)) {
                    epicIds.add(t.id);
                    collectChildren(t.id);
                }
            }
        };
        collectChildren(epicId);
        tasks = allTasks.filter((t) => epicIds.has(t.id));
    }
    const byStatus = {};
    const byPriority = {};
    const byType = {};
    for (const task of tasks) {
        byStatus[task.status] = (byStatus[task.status] ?? 0) + 1;
        byPriority[task.priority] = (byPriority[task.priority] ?? 0) + 1;
        const taskType = task.type ?? 'task';
        byType[taskType] = (byType[taskType] ?? 0) + 1;
    }
    return {
        total: tasks.length,
        pending: byStatus['pending'] ?? 0,
        active: byStatus['active'] ?? 0,
        blocked: byStatus['blocked'] ?? 0,
        done: byStatus['done'] ?? 0,
        cancelled: byStatus['cancelled'] ?? 0,
        byPriority,
        byType,
    };
}
// ============================================================================
// taskExport
// ============================================================================
/**
 * Export tasks as JSON or CSV.
 * @task T4790
 */
export async function coreTaskExport(projectRoot, params) {
    const allTasks = await loadAllTasks(projectRoot);
    let tasks = allTasks;
    if (params?.status) {
        tasks = tasks.filter((t) => t.status === params.status);
    }
    if (params?.parent) {
        const parentIds = new Set();
        parentIds.add(params.parent);
        const collectChildren = (parentId) => {
            for (const t of allTasks) {
                if (t.parentId === parentId && !parentIds.has(t.id)) {
                    parentIds.add(t.id);
                    collectChildren(t.id);
                }
            }
        };
        collectChildren(params.parent);
        tasks = tasks.filter((t) => parentIds.has(t.id));
    }
    if (params?.format === 'csv') {
        const headers = ['id', 'title', 'status', 'priority', 'type', 'parentId', 'createdAt'];
        const rows = tasks.map((t) => [
            t.id,
            `"${(t.title || '').replace(/"/g, '""')}"`,
            t.status,
            t.priority,
            t.type ?? 'task',
            t.parentId ?? '',
            t.createdAt,
        ].join(','));
        const csv = [headers.join(','), ...rows].join('\n');
        return { format: 'csv', content: csv, taskCount: tasks.length };
    }
    return { format: 'json', tasks, taskCount: tasks.length };
}
// ============================================================================
// taskHistory
// ============================================================================
/**
 * Get task history from the log file.
 * @task T4790
 */
export async function coreTaskHistory(projectRoot, taskId, limit) {
    try {
        const { getDb } = await import('../store/sqlite.js');
        const { auditLog } = await import('../store/tasks-schema.js');
        const { sql } = await import('drizzle-orm');
        const db = await getDb(projectRoot);
        const maxRows = limit && limit > 0 ? limit : 100;
        const rows = await db.all(sql `SELECT * FROM ${auditLog}
          WHERE ${auditLog.taskId} = ${taskId}
          ORDER BY ${auditLog.timestamp} DESC
          LIMIT ${maxRows}`);
        return rows.map((row) => ({
            id: row.id,
            timestamp: row.timestamp,
            operation: row.operation ?? row.action,
            action: row.action,
            taskId: row.task_id,
            actor: row.actor,
            details: row.details_json ? JSON.parse(row.details_json) : {},
            before: row.before_json ? JSON.parse(row.before_json) : undefined,
            after: row.after_json ? JSON.parse(row.after_json) : undefined,
            domain: row.domain,
            sessionId: row.session_id,
            requestId: row.request_id,
            durationMs: row.duration_ms,
            success: row.success === null ? undefined : row.success === 1,
            source: row.source,
            gateway: row.gateway,
            error: row.error_message,
        }));
    }
    catch {
        return [];
    }
}
// ============================================================================
// taskLint
// ============================================================================
/**
 * Lint tasks for common issues.
 * @task T4790
 */
export async function coreTaskLint(projectRoot, taskId) {
    const allTasks = await loadAllTasks(projectRoot);
    const tasks = taskId ? allTasks.filter((t) => t.id === taskId) : allTasks;
    if (taskId && tasks.length === 0) {
        throw new Error(`Task '${taskId}' not found`);
    }
    const issues = [];
    const allDescriptions = new Set();
    const allIds = new Set();
    for (const task of allTasks) {
        if (allIds.has(task.id)) {
            issues.push({
                taskId: task.id,
                severity: 'error',
                rule: 'unique-id',
                message: `Duplicate task ID: ${task.id}`,
            });
        }
        allIds.add(task.id);
        if (taskId && task.id !== taskId) {
            if (task.description)
                allDescriptions.add(task.description.toLowerCase());
            continue;
        }
        if (!task.title || task.title.trim().length === 0) {
            issues.push({
                taskId: task.id,
                severity: 'error',
                rule: 'title-required',
                message: 'Task is missing a title',
            });
        }
        if (!task.description || task.description.trim().length === 0) {
            issues.push({
                taskId: task.id,
                severity: 'warning',
                rule: 'description-required',
                message: 'Task is missing a description',
            });
        }
        if (task.title && task.description && task.title.trim() === task.description.trim()) {
            issues.push({
                taskId: task.id,
                severity: 'warning',
                rule: 'title-description-different',
                message: 'Title and description should not be identical',
            });
        }
        if (task.description) {
            const descLower = task.description.toLowerCase();
            if (allDescriptions.has(descLower)) {
                issues.push({
                    taskId: task.id,
                    severity: 'warning',
                    rule: 'unique-description',
                    message: 'Duplicate task description found',
                });
            }
            allDescriptions.add(descLower);
        }
        if (!TASK_STATUSES.includes(task.status)) {
            issues.push({
                taskId: task.id,
                severity: 'error',
                rule: 'valid-status',
                message: `Invalid status: ${task.status}`,
            });
        }
        const now = new Date();
        if (task.createdAt && new Date(task.createdAt) > now) {
            issues.push({
                taskId: task.id,
                severity: 'warning',
                rule: 'no-future-timestamps',
                message: 'createdAt is in the future',
            });
        }
        if (task.parentId && !allTasks.some((t) => t.id === task.parentId)) {
            issues.push({
                taskId: task.id,
                severity: 'error',
                rule: 'valid-parent',
                message: `Parent task '${task.parentId}' does not exist`,
            });
        }
        for (const depId of task.depends ?? []) {
            if (!allTasks.some((t) => t.id === depId)) {
                issues.push({
                    taskId: task.id,
                    severity: 'warning',
                    rule: 'valid-dependency',
                    message: `Dependency '${depId}' does not exist`,
                });
            }
        }
    }
    return issues;
}
// ============================================================================
// taskBatchValidate
// ============================================================================
/**
 * Validate multiple tasks at once.
 * @task T4790
 */
export async function coreTaskBatchValidate(projectRoot, taskIds, checkMode = 'full') {
    const allTasks = await loadAllTasks(projectRoot);
    const results = {};
    let totalErrors = 0;
    let totalWarnings = 0;
    for (const id of taskIds) {
        const task = allTasks.find((t) => t.id === id);
        if (!task) {
            results[id] = [{ severity: 'error', rule: 'exists', message: `Task '${id}' not found` }];
            totalErrors++;
            continue;
        }
        const taskIssues = [];
        if (!task.title || task.title.trim().length === 0) {
            taskIssues.push({ severity: 'error', rule: 'title-required', message: 'Missing title' });
        }
        if (!task.description || task.description.trim().length === 0) {
            taskIssues.push({
                severity: 'warning',
                rule: 'description-required',
                message: 'Missing description',
            });
        }
        if (!TASK_STATUSES.includes(task.status)) {
            taskIssues.push({
                severity: 'error',
                rule: 'valid-status',
                message: `Invalid status: ${task.status}`,
            });
        }
        if (checkMode === 'full') {
            if (task.title && task.description && task.title.trim() === task.description.trim()) {
                taskIssues.push({
                    severity: 'warning',
                    rule: 'title-description-different',
                    message: 'Title equals description',
                });
            }
            if (task.parentId && !allTasks.some((t) => t.id === task.parentId)) {
                taskIssues.push({
                    severity: 'error',
                    rule: 'valid-parent',
                    message: `Parent '${task.parentId}' not found`,
                });
            }
            for (const depId of task.depends ?? []) {
                if (!allTasks.some((t) => t.id === depId)) {
                    taskIssues.push({
                        severity: 'warning',
                        rule: 'valid-dependency',
                        message: `Dependency '${depId}' not found`,
                    });
                }
            }
            const now = new Date();
            if (task.createdAt && new Date(task.createdAt) > now) {
                taskIssues.push({
                    severity: 'warning',
                    rule: 'no-future-timestamps',
                    message: 'createdAt in future',
                });
            }
        }
        results[id] = taskIssues;
        totalErrors += taskIssues.filter((i) => i.severity === 'error').length;
        totalWarnings += taskIssues.filter((i) => i.severity === 'warning').length;
    }
    const invalidTasks = Object.values(results).filter((issues) => issues.some((i) => i.severity === 'error')).length;
    return {
        results,
        summary: {
            totalTasks: taskIds.length,
            validTasks: taskIds.length - invalidTasks,
            invalidTasks,
            totalIssues: totalErrors + totalWarnings,
            errors: totalErrors,
            warnings: totalWarnings,
        },
    };
}
// ============================================================================
// taskImport
// ============================================================================
/**
 * Import tasks from a JSON source string.
 * @task T4790
 */
export async function coreTaskImport(projectRoot, source, overwrite) {
    const accessor = await getAccessor(projectRoot);
    // Load all existing task IDs using queryTasks (bulk operation needs full ID set)
    const { tasks: existingTasks } = await accessor.queryTasks({});
    let importData;
    try {
        importData = JSON.parse(source);
    }
    catch {
        throw new Error('Invalid JSON in import source');
    }
    let importTasks = [];
    if (Array.isArray(importData)) {
        importTasks = importData;
    }
    else if (typeof importData === 'object' && importData !== null) {
        const data = importData;
        if (Array.isArray(data.tasks)) {
            importTasks = data.tasks;
        }
    }
    if (importTasks.length === 0) {
        return { imported: 0, skipped: 0, errors: ['No tasks found in import source'] };
    }
    const existingIds = new Set(existingTasks.map((t) => t.id));
    const allIds = new Set(existingTasks.map((t) => t.id));
    const errors = [];
    let imported = 0;
    let skipped = 0;
    const remapTable = {};
    let nextIdNum = 0;
    for (const t of existingTasks) {
        const num = parseInt(t.id.replace('T', ''), 10);
        if (!Number.isNaN(num) && num > nextIdNum)
            nextIdNum = num;
    }
    for (const importTask of importTasks) {
        if (!importTask.id || !importTask.title) {
            errors.push(`Skipped task with missing id or title`);
            skipped++;
            continue;
        }
        if (existingIds.has(importTask.id) && !overwrite) {
            skipped++;
            continue;
        }
        let newId = importTask.id;
        if (allIds.has(importTask.id) && !overwrite) {
            nextIdNum++;
            newId = `T${String(nextIdNum).padStart(3, '0')}`;
            remapTable[importTask.id] = newId;
        }
        const now = new Date().toISOString();
        const newTask = {
            ...importTask,
            id: newId,
            createdAt: importTask.createdAt || now,
            updatedAt: now,
        };
        // Use targeted upsert per task instead of bulk saveTaskFile
        await accessor.upsertSingleTask(newTask);
        allIds.add(newId);
        imported++;
    }
    return {
        imported,
        skipped,
        errors,
        ...(Object.keys(remapTable).length > 0 ? { remapTable } : {}),
    };
}
//# sourceMappingURL=task-ops.js.map