/**
 * tools/utils.js
 * Utility functions for Task Master CLI integration
 */

import { spawnSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import { contextManager } from '../core/context-manager.js'; // Import the singleton
import { fileURLToPath } from 'url';
import packageJson from '../../../package.json' with { type: 'json' };
import { getCurrentTag } from '../../../scripts/modules/utils.js';

// Import path utilities to ensure consistent path resolution
import {
	lastFoundProjectRoot,
	PROJECT_MARKERS
} from '../core/utils/path-utils.js';

const __filename = fileURLToPath(import.meta.url);

// Cache for version info to avoid repeated file reads
let cachedVersionInfo = null;

/**
 * Get version information from package.json
 * @returns {Object} Version information
 */
function getVersionInfo() {
	// Return cached version if available
	if (cachedVersionInfo) {
		return cachedVersionInfo;
	}

	// Use the imported packageJson directly
	cachedVersionInfo = {
		version: packageJson.version || 'unknown',
		name: packageJson.name || 'task-master-ai'
	};
	return cachedVersionInfo;
}

/**
 * Get current tag information for MCP responses
 * @param {string} projectRoot - The project root directory
 * @param {Object} log - Logger object
 * @returns {Object} Tag information object
 */
function getTagInfo(projectRoot, log) {
	try {
		if (!projectRoot) {
			log.warn('No project root provided for tag information');
			return { currentTag: 'master', availableTags: ['master'] };
		}

		const currentTag = getCurrentTag(projectRoot);

		// Read available tags from tasks.json
		let availableTags = ['master']; // Default fallback
		try {
			const tasksJsonPath = path.join(
				projectRoot,
				'.taskmaster',
				'tasks',
				'tasks.json'
			);
			if (fs.existsSync(tasksJsonPath)) {
				const tasksData = JSON.parse(fs.readFileSync(tasksJsonPath, 'utf-8'));

				// If it's the new tagged format, extract tag keys
				if (
					tasksData &&
					typeof tasksData === 'object' &&
					!Array.isArray(tasksData.tasks)
				) {
					const tagKeys = Object.keys(tasksData).filter(
						(key) =>
							tasksData[key] &&
							typeof tasksData[key] === 'object' &&
							Array.isArray(tasksData[key].tasks)
					);
					if (tagKeys.length > 0) {
						availableTags = tagKeys;
					}
				}
			}
		} catch (tagError) {
			log.debug(`Could not read available tags: ${tagError.message}`);
		}

		return {
			currentTag: currentTag || 'master',
			availableTags: availableTags
		};
	} catch (error) {
		log.warn(`Error getting tag information: ${error.message}`);
		return { currentTag: 'master', availableTags: ['master'] };
	}
}

/**
 * Get normalized project root path
 * @param {string|undefined} projectRootRaw - Raw project root from arguments
 * @param {Object} log - Logger object
 * @returns {string} - Normalized absolute path to project root
 */
function getProjectRoot(projectRootRaw, log) {
	// PRECEDENCE ORDER:
	// 1. Environment variable override (TASK_MASTER_PROJECT_ROOT)
	// 2. Explicitly provided projectRoot in args
	// 3. Previously found/cached project root
	// 4. Current directory if it has project markers
	// 5. Current directory with warning

	// 1. Check for environment variable override
	if (process.env.TASK_MASTER_PROJECT_ROOT) {
		const envRoot = process.env.TASK_MASTER_PROJECT_ROOT;
		const absolutePath = path.isAbsolute(envRoot)
			? envRoot
			: path.resolve(process.cwd(), envRoot);
		log.info(
			`Using project root from TASK_MASTER_PROJECT_ROOT environment variable: ${absolutePath}`
		);
		return absolutePath;
	}

	// 2. If project root is explicitly provided, use it
	if (projectRootRaw) {
		const absolutePath = path.isAbsolute(projectRootRaw)
			? projectRootRaw
			: path.resolve(process.cwd(), projectRootRaw);

		log.info(`Using explicitly provided project root: ${absolutePath}`);
		return absolutePath;
	}

	// 3. If we have a last found project root from a tasks.json search, use that for consistency
	if (lastFoundProjectRoot) {
		log.info(
			`Using last known project root where tasks.json was found: ${lastFoundProjectRoot}`
		);
		return lastFoundProjectRoot;
	}

	// 4. Check if the current directory has any indicators of being a task-master project
	const currentDir = process.cwd();
	if (
		PROJECT_MARKERS.some((marker) => {
			const markerPath = path.join(currentDir, marker);
			return fs.existsSync(markerPath);
		})
	) {
		log.info(
			`Using current directory as project root (found project markers): ${currentDir}`
		);
		return currentDir;
	}

	// 5. Default to current working directory but warn the user
	log.warn(
		`No task-master project detected in current directory. Using ${currentDir} as project root.`
	);
	log.warn(
		'Consider using --project-root to specify the correct project location or set TASK_MASTER_PROJECT_ROOT environment variable.'
	);
	return currentDir;
}

/**
 * Extracts and normalizes the project root path from the MCP session object.
 * @param {Object} session - The MCP session object.
 * @param {Object} log - The MCP logger object.
 * @returns {string|null} - The normalized absolute project root path or null if not found/invalid.
 */
function getProjectRootFromSession(session, log) {
	try {
		// Add detailed logging of session structure
		log.info(
			`Session object: ${JSON.stringify({
				hasSession: !!session,
				hasRoots: !!session?.roots,
				rootsType: typeof session?.roots,
				isRootsArray: Array.isArray(session?.roots),
				rootsLength: session?.roots?.length,
				firstRoot: session?.roots?.[0],
				hasRootsRoots: !!session?.roots?.roots,
				rootsRootsType: typeof session?.roots?.roots,
				isRootsRootsArray: Array.isArray(session?.roots?.roots),
				rootsRootsLength: session?.roots?.roots?.length,
				firstRootsRoot: session?.roots?.roots?.[0]
			})}`
		);

		let rawRootPath = null;
		let decodedPath = null;
		let finalPath = null;

		// Check primary location
		if (session?.roots?.[0]?.uri) {
			rawRootPath = session.roots[0].uri;
			log.info(`Found raw root URI in session.roots[0].uri: ${rawRootPath}`);
		}
		// Check alternate location
		else if (session?.roots?.roots?.[0]?.uri) {
			rawRootPath = session.roots.roots[0].uri;
			log.info(
				`Found raw root URI in session.roots.roots[0].uri: ${rawRootPath}`
			);
		}

		if (rawRootPath) {
			// Decode URI and strip file:// protocol
			decodedPath = rawRootPath.startsWith('file://')
				? decodeURIComponent(rawRootPath.slice(7))
				: rawRootPath; // Assume non-file URI is already decoded? Or decode anyway? Let's decode.
			if (!rawRootPath.startsWith('file://')) {
				decodedPath = decodeURIComponent(rawRootPath); // Decode even if no file://
			}

			// Handle potential Windows drive prefix after stripping protocol (e.g., /C:/...)
			if (
				decodedPath.startsWith('/') &&
				/[A-Za-z]:/.test(decodedPath.substring(1, 3))
			) {
				decodedPath = decodedPath.substring(1); // Remove leading slash if it's like /C:/...
			}

			log.info(`Decoded path: ${decodedPath}`);

			// Normalize slashes and resolve
			const normalizedSlashes = decodedPath.replace(/\\/g, '/');
			finalPath = path.resolve(normalizedSlashes); // Resolve to absolute path for current OS

			log.info(`Normalized and resolved session path: ${finalPath}`);
			return finalPath;
		}

		// Fallback Logic (remains the same)
		log.warn('No project root URI found in session. Attempting fallbacks...');
		const cwd = process.cwd();

		// Fallback 1: Use server path deduction (Cursor IDE)
		const serverPath = process.argv[1];
		if (serverPath && serverPath.includes('mcp-server')) {
			const mcpServerIndex = serverPath.indexOf('mcp-server');
			if (mcpServerIndex !== -1) {
				const projectRoot = path.dirname(
					serverPath.substring(0, mcpServerIndex)
				); // Go up one level

				if (
					fs.existsSync(path.join(projectRoot, '.cursor')) ||
					fs.existsSync(path.join(projectRoot, 'mcp-server')) ||
					fs.existsSync(path.join(projectRoot, 'package.json'))
				) {
					log.info(
						`Using project root derived from server path: ${projectRoot}`
					);
					return projectRoot; // Already absolute
				}
			}
		}

		// Fallback 2: Use CWD
		log.info(`Using current working directory as ultimate fallback: ${cwd}`);
		return cwd; // Already absolute
	} catch (e) {
		log.error(`Error in getProjectRootFromSession: ${e.message}`);
		// Attempt final fallback to CWD on error
		const cwd = process.cwd();
		log.warn(
			`Returning CWD (${cwd}) due to error during session root processing.`
		);
		return cwd;
	}
}

/**
 * Handle API result with standardized error handling and response formatting
 * @param {Object} result - Result object from API call with success, data, and error properties
 * @param {Object} log - Logger object
 * @param {string} errorPrefix - Prefix for error messages
 * @param {Function} processFunction - Optional function to process successful result data
 * @param {string} [projectRoot] - Optional project root for tag information
 * @returns {Object} - Standardized MCP response object
 */
async function handleApiResult(
	result,
	log,
	errorPrefix = 'API error',
	processFunction = processMCPResponseData,
	projectRoot = null
) {
	// Get version info for every response
	const versionInfo = getVersionInfo();

	// Get tag info if project root is provided
	const tagInfo = projectRoot ? getTagInfo(projectRoot, log) : null;

	if (!result.success) {
		const errorMsg = result.error?.message || `Unknown ${errorPrefix}`;
		log.error(`${errorPrefix}: ${errorMsg}`);
		return createErrorResponse(errorMsg, versionInfo, tagInfo);
	}

	// Process the result data if needed
	const processedData = processFunction
		? processFunction(result.data)
		: result.data;

	log.info('Successfully completed operation');

	// Create the response payload including version info and tag info
	const responsePayload = {
		data: processedData,
		version: versionInfo
	};

	// Add tag information if available
	if (tagInfo) {
		responsePayload.tag = tagInfo;
	}

	return createContentResponse(responsePayload);
}

/**
 * Executes a task-master CLI command synchronously.
 * @param {string} command - The command to execute (e.g., 'add-task')
 * @param {Object} log - Logger instance
 * @param {Array} args - Arguments for the command
 * @param {string|undefined} projectRootRaw - Optional raw project root path (will be normalized internally)
 * @param {Object|null} customEnv - Optional object containing environment variables to pass to the child process
 * @returns {Object} - The result of the command execution
 */
function executeTaskMasterCommand(
	command,
	log,
	args = [],
	projectRootRaw = null,
	customEnv = null // Changed from session to customEnv
) {
	try {
		// Normalize project root internally using the getProjectRoot utility
		const cwd = getProjectRoot(projectRootRaw, log);

		log.info(
			`Executing task-master ${command} with args: ${JSON.stringify(
				args
			)} in directory: ${cwd}`
		);

		// Prepare full arguments array
		const fullArgs = [command, ...args];

		// Common options for spawn
		const spawnOptions = {
			encoding: 'utf8',
			cwd: cwd,
			// Merge process.env with customEnv, giving precedence to customEnv
			env: { ...process.env, ...(customEnv || {}) }
		};

		// Log the environment being passed (optional, for debugging)
		// log.info(`Spawn options env: ${JSON.stringify(spawnOptions.env)}`);

		// Execute the command using the global task-master CLI or local script
		// Try the global CLI first
		let result = spawnSync('task-master', fullArgs, spawnOptions);

		// If global CLI is not available, try fallback to the local script
		if (result.error && result.error.code === 'ENOENT') {
			log.info('Global task-master not found, falling back to local script');
			// Pass the same spawnOptions (including env) to the fallback
			result = spawnSync('node', ['scripts/dev.js', ...fullArgs], spawnOptions);
		}

		if (result.error) {
			throw new Error(`Command execution error: ${result.error.message}`);
		}

		if (result.status !== 0) {
			// Improve error handling by combining stderr and stdout if stderr is empty
			const errorOutput = result.stderr
				? result.stderr.trim()
				: result.stdout
					? result.stdout.trim()
					: 'Unknown error';
			throw new Error(
				`Command failed with exit code ${result.status}: ${errorOutput}`
			);
		}

		return {
			success: true,
			stdout: result.stdout,
			stderr: result.stderr
		};
	} catch (error) {
		log.error(`Error executing task-master command: ${error.message}`);
		return {
			success: false,
			error: error.message
		};
	}
}

/**
 * Checks cache for a result using the provided key. If not found, executes the action function,
 * caches the result upon success, and returns the result.
 *
 * @param {Object} options - Configuration options.
 * @param {string} options.cacheKey - The unique key for caching this operation's result.
 * @param {Function} options.actionFn - The async function to execute if the cache misses.
 *                                      Should return an object like { success: boolean, data?: any, error?: { code: string, message: string } }.
 * @param {Object} options.log - The logger instance.
 * @returns {Promise<Object>} - An object containing the result.
 *                              Format: { success: boolean, data?: any, error?: { code: string, message: string } }
 */
async function getCachedOrExecute({ cacheKey, actionFn, log }) {
	// Check cache first
	const cachedResult = contextManager.getCachedData(cacheKey);

	if (cachedResult !== undefined) {
		log.info(`Cache hit for key: ${cacheKey}`);
		return cachedResult;
	}

	log.info(`Cache miss for key: ${cacheKey}. Executing action function.`);

	// Execute the action function if cache missed
	const result = await actionFn();

	// If the action was successful, cache the result
	if (result.success && result.data !== undefined) {
		log.info(`Action successful. Caching result for key: ${cacheKey}`);
		contextManager.setCachedData(cacheKey, result);
	} else if (!result.success) {
		log.warn(
			`Action failed for cache key ${cacheKey}. Result not cached. Error: ${result.error?.message}`
		);
	} else {
		log.warn(
			`Action for cache key ${cacheKey} succeeded but returned no data. Result not cached.`
		);
	}

	return result;
}

/**
 * Recursively removes specified fields from task objects, whether single or in an array.
 * Handles common data structures returned by task commands.
 * @param {Object|Array} taskOrData - A single task object or a data object containing a 'tasks' array.
 * @param {string[]} fieldsToRemove - An array of field names to remove.
 * @returns {Object|Array} - The processed data with specified fields removed.
 */
function processMCPResponseData(
	taskOrData,
	fieldsToRemove = ['details', 'testStrategy']
) {
	if (!taskOrData) {
		return taskOrData;
	}

	// Helper function to process a single task object
	const processSingleTask = (task) => {
		if (typeof task !== 'object' || task === null) {
			return task;
		}

		const processedTask = { ...task };

		// Remove specified fields from the task
		fieldsToRemove.forEach((field) => {
			delete processedTask[field];
		});

		// Recursively process subtasks if they exist and are an array
		if (processedTask.subtasks && Array.isArray(processedTask.subtasks)) {
			// Use processArrayOfTasks to handle the subtasks array
			processedTask.subtasks = processArrayOfTasks(processedTask.subtasks);
		}

		return processedTask;
	};

	// Helper function to process an array of tasks
	const processArrayOfTasks = (tasks) => {
		return tasks.map(processSingleTask);
	};

	// Check if the input is a data structure containing a 'tasks' array (like from listTasks)
	if (
		typeof taskOrData === 'object' &&
		taskOrData !== null &&
		Array.isArray(taskOrData.tasks)
	) {
		return {
			...taskOrData, // Keep other potential fields like 'stats', 'filter'
			tasks: processArrayOfTasks(taskOrData.tasks)
		};
	}
	// Check if the input is likely a single task object (add more checks if needed)
	else if (
		typeof taskOrData === 'object' &&
		taskOrData !== null &&
		'id' in taskOrData &&
		'title' in taskOrData
	) {
		return processSingleTask(taskOrData);
	}
	// Check if the input is an array of tasks directly (less common but possible)
	else if (Array.isArray(taskOrData)) {
		return processArrayOfTasks(taskOrData);
	}

	// If it doesn't match known task structures, return it as is
	return taskOrData;
}

/**
 * Creates standard content response for tools
 * @param {string|Object} content - Content to include in response
 * @returns {Object} - Content response object in FastMCP format
 */
function createContentResponse(content) {
	// FastMCP requires text type, so we format objects as JSON strings
	return {
		content: [
			{
				type: 'text',
				text:
					typeof content === 'object'
						? // Format JSON nicely with indentation
							JSON.stringify(content, null, 2)
						: // Keep other content types as-is
							String(content)
			}
		]
	};
}

/**
 * Creates error response for tools
 * @param {string} errorMessage - Error message to include in response
 * @param {Object} [versionInfo] - Optional version information object
 * @param {Object} [tagInfo] - Optional tag information object
 * @returns {Object} - Error content response object in FastMCP format
 */
function createErrorResponse(errorMessage, versionInfo, tagInfo) {
	// Provide fallback version info if not provided
	if (!versionInfo) {
		versionInfo = getVersionInfo();
	}

	let responseText = `Error: ${errorMessage}
Version: ${versionInfo.version}
Name: ${versionInfo.name}`;

	// Add tag information if available
	if (tagInfo) {
		responseText += `
Current Tag: ${tagInfo.currentTag}`;
	}

	return {
		content: [
			{
				type: 'text',
				text: responseText
			}
		],
		isError: true
	};
}

/**
 * Creates a logger wrapper object compatible with core function expectations.
 * Adapts the MCP logger to the { info, warn, error, debug, success } structure.
 * @param {Object} log - The MCP logger instance.
 * @returns {Object} - The logger wrapper object.
 */
function createLogWrapper(log) {
	return {
		info: (message, ...args) => log.info(message, ...args),
		warn: (message, ...args) => log.warn(message, ...args),
		error: (message, ...args) => log.error(message, ...args),
		// Handle optional debug method
		debug: (message, ...args) =>
			log.debug ? log.debug(message, ...args) : null,
		// Map success to info as a common fallback
		success: (message, ...args) => log.info(message, ...args)
	};
}

/**
 * Resolves and normalizes a project root path from various formats.
 * Handles URI encoding, Windows paths, and file protocols.
 * @param {string | undefined | null} rawPath - The raw project root path.
 * @param {object} [log] - Optional logger object.
 * @returns {string | null} Normalized absolute path or null if input is invalid/empty.
 */
function normalizeProjectRoot(rawPath, log) {
	if (!rawPath) return null;
	try {
		let pathString = Array.isArray(rawPath) ? rawPath[0] : String(rawPath);
		if (!pathString) return null;

		// 1. Decode URI Encoding
		// Use try-catch for decoding as malformed URIs can throw
		try {
			pathString = decodeURIComponent(pathString);
		} catch (decodeError) {
			if (log)
				log.warn(
					`Could not decode URI component for path "${rawPath}": ${decodeError.message}. Proceeding with raw string.`
				);
			// Proceed with the original string if decoding fails
			pathString = Array.isArray(rawPath) ? rawPath[0] : String(rawPath);
		}

		// 2. Strip file:// prefix (handle 2 or 3 slashes)
		if (pathString.startsWith('file:///')) {
			pathString = pathString.slice(7); // Slice 7 for file:///, may leave leading / on Windows
		} else if (pathString.startsWith('file://')) {
			pathString = pathString.slice(7); // Slice 7 for file://
		}

		// 3. Handle potential Windows leading slash after stripping prefix (e.g., /C:/...)
		// This checks if it starts with / followed by a drive letter C: D: etc.
		if (
			pathString.startsWith('/') &&
			/[A-Za-z]:/.test(pathString.substring(1, 3))
		) {
			pathString = pathString.substring(1); // Remove the leading slash
		}

		// 4. Normalize backslashes to forward slashes
		pathString = pathString.replace(/\\/g, '/');

		// 5. Resolve to absolute path using server's OS convention
		const resolvedPath = path.resolve(pathString);
		return resolvedPath;
	} catch (error) {
		if (log) {
			log.error(
				`Error normalizing project root path "${rawPath}": ${error.message}`
			);
		}
		return null; // Return null on error
	}
}

/**
 * Extracts the raw project root path from the session (without normalization).
 * Used as a fallback within the HOF.
 * @param {Object} session - The MCP session object.
 * @param {Object} log - The MCP logger object.
 * @returns {string|null} The raw path string or null.
 */
function getRawProjectRootFromSession(session, log) {
	try {
		// Check primary location
		if (session?.roots?.[0]?.uri) {
			return session.roots[0].uri;
		}
		// Check alternate location
		else if (session?.roots?.roots?.[0]?.uri) {
			return session.roots.roots[0].uri;
		}
		return null; // Not found in expected session locations
	} catch (e) {
		log.error(`Error accessing session roots: ${e.message}`);
		return null;
	}
}

/**
 * Higher-order function to wrap MCP tool execute methods.
 * Ensures args.projectRoot is present and normalized before execution.
 * Uses TASK_MASTER_PROJECT_ROOT environment variable with proper precedence.
 * @param {Function} executeFn - The original async execute(args, context) function.
 * @returns {Function} The wrapped async execute function.
 */
function withNormalizedProjectRoot(executeFn) {
	return async (args, context) => {
		const { log, session } = context;
		let normalizedRoot = null;
		let rootSource = 'unknown';

		try {
			// PRECEDENCE ORDER:
			// 1. TASK_MASTER_PROJECT_ROOT environment variable (from process.env or session)
			// 2. args.projectRoot (explicitly provided)
			// 3. Session-based project root resolution
			// 4. Current directory fallback

			// 1. Check for TASK_MASTER_PROJECT_ROOT environment variable first
			if (process.env.TASK_MASTER_PROJECT_ROOT) {
				const envRoot = process.env.TASK_MASTER_PROJECT_ROOT;
				normalizedRoot = path.isAbsolute(envRoot)
					? envRoot
					: path.resolve(process.cwd(), envRoot);
				rootSource = 'TASK_MASTER_PROJECT_ROOT environment variable';
				log.info(`Using project root from ${rootSource}: ${normalizedRoot}`);
			}
			// Also check session environment variables for TASK_MASTER_PROJECT_ROOT
			else if (session?.env?.TASK_MASTER_PROJECT_ROOT) {
				const envRoot = session.env.TASK_MASTER_PROJECT_ROOT;
				normalizedRoot = path.isAbsolute(envRoot)
					? envRoot
					: path.resolve(process.cwd(), envRoot);
				rootSource = 'TASK_MASTER_PROJECT_ROOT session environment variable';
				log.info(`Using project root from ${rootSource}: ${normalizedRoot}`);
			}
			// 2. If no environment variable, try args.projectRoot
			else if (args.projectRoot) {
				normalizedRoot = normalizeProjectRoot(args.projectRoot, log);
				rootSource = 'args.projectRoot';
				log.info(`Using project root from ${rootSource}: ${normalizedRoot}`);
			}
			// 3. If no args.projectRoot, try session-based resolution
			else {
				const sessionRoot = getProjectRootFromSession(session, log);
				if (sessionRoot) {
					normalizedRoot = sessionRoot; // getProjectRootFromSession already normalizes
					rootSource = 'session';
					log.info(`Using project root from ${rootSource}: ${normalizedRoot}`);
				}
			}

			if (!normalizedRoot) {
				log.error(
					'Could not determine project root from environment, args, or session.'
				);
				return createErrorResponse(
					'Could not determine project root. Please provide projectRoot argument or ensure TASK_MASTER_PROJECT_ROOT environment variable is set.'
				);
			}

			// Inject the normalized root back into args
			const updatedArgs = { ...args, projectRoot: normalizedRoot };

			// Execute the original function with normalized root in args
			return await executeFn(updatedArgs, context);
		} catch (error) {
			log.error(
				`Error within withNormalizedProjectRoot HOF (Normalized Root: ${normalizedRoot}): ${error.message}`
			);
			// Add stack trace if available and debug enabled
			if (error.stack && log.debug) {
				log.debug(error.stack);
			}
			// Return a generic error or re-throw depending on desired behavior
			return createErrorResponse(`Operation failed: ${error.message}`);
		}
	};
}

/**
 * Checks progress reporting capability and returns the validated function or undefined.
 *
 * STANDARD PATTERN for AI-powered, long-running operations (parse-prd, expand-task, expand-all, analyze):
 *
 * This helper should be used as the first step in any MCP tool that performs long-running
 * AI operations. It validates the availability of progress reporting and provides consistent
 * logging about the capability status.
 *
 * Operations that should use this pattern:
 * - parse-prd: Parsing PRD documents with AI
 * - expand-task: Expanding tasks into subtasks
 * - expand-all: Expanding all tasks in batch
 * - analyze-complexity: Analyzing task complexity
 * - update-task: Updating tasks with AI assistance
 * - add-task: Creating new tasks with AI
 * - Any operation that makes AI service calls
 *
 * @example Basic usage in a tool's execute function:
 * ```javascript
 * import { checkProgressCapability } from './utils.js';
 *
 * async execute(args, context) {
 *   const { log, reportProgress, session } = context;
 *
 *   // Always validate progress capability first
 *   const progressCapability = checkProgressCapability(reportProgress, log);
 *
 *   // Pass to direct function - it handles undefined gracefully
 *   const result = await expandTask(taskId, numSubtasks, {
 *     session,
 *     reportProgress: progressCapability,
 *     mcpLog: log
 *   });
 * }
 * ```
 *
 * @example With progress reporting available:
 * ```javascript
 * // When reportProgress is available, users see real-time updates:
 * // "Starting PRD analysis (Input: 5432 tokens)..."
 * // "Task 1/10 - Implement user authentication"
 * // "Task 2/10 - Create database schema"
 * // "Task Generation Completed | Tokens: 5432/1234"
 * ```
 *
 * @example Without progress reporting (graceful degradation):
 * ```javascript
 * // When reportProgress is not available:
 * // - Operation runs normally without progress updates
 * // - Debug log: "reportProgress not available - operation will run without progress updates"
 * // - User gets final result after completion
 * ```
 *
 * @param {Function|undefined} reportProgress - The reportProgress function from MCP context.
 *                                             Expected signature: async (progress: {progress: number, total: number, message: string}) => void
 * @param {Object} log - Logger instance with debug, info, warn, error methods
 * @returns {Function|undefined} The validated reportProgress function or undefined if not available
 */
function checkProgressCapability(reportProgress, log) {
	// Validate that reportProgress is available for long-running operations
	if (typeof reportProgress !== 'function') {
		log.debug(
			'reportProgress not available - operation will run without progress updates'
		);
		return undefined;
	}

	return reportProgress;
}

// Ensure all functions are exported
export {
	getProjectRoot,
	getProjectRootFromSession,
	getTagInfo,
	handleApiResult,
	executeTaskMasterCommand,
	getCachedOrExecute,
	processMCPResponseData,
	createContentResponse,
	createErrorResponse,
	createLogWrapper,
	normalizeProjectRoot,
	getRawProjectRootFromSession,
	withNormalizedProjectRoot,
	checkProgressCapability
};
