import { promises as fs } from 'fs';
import path from 'path';
import { execFile } from 'child_process';
import { promisify } from 'util';
import { parse as parseShellCommand } from 'shell-quote';
import { parseFrontmatter } from './frontmatter.js';

const execFileAsync = promisify(execFile);

// Configuration
const MAX_INCLUDE_DEPTH = 3;
const BASH_TIMEOUT = 30000; // 30 seconds
const BASH_COMMAND_ALLOWLIST = [
  'echo',
  'ls',
  'pwd',
  'date',
  'whoami',
  'git',
  'npm',
  'node',
  'cat',
  'grep',
  'find',
  'task-master'
];

/**
 * Parse a markdown command file and extract frontmatter and content
 * @param {string} content - Raw markdown content
 * @returns {object} Parsed command with data (frontmatter) and content
 */
export function parseCommand(content) {
  try {
    const parsed = parseFrontmatter(content);
    return {
      data: parsed.data || {},
      content: parsed.content || '',
      raw: content
    };
  } catch (error) {
    throw new Error(`Failed to parse command: ${error.message}`);
  }
}

/**
 * Replace argument placeholders in content
 * @param {string} content - Content with placeholders
 * @param {string|array} args - Arguments to replace (string or array)
 * @returns {string} Content with replaced arguments
 */
export function replaceArguments(content, args) {
  if (!content) return content;

  let result = content;

  // Convert args to array if it's a string
  const argsArray = Array.isArray(args) ? args : (args ? [args] : []);

  // Replace $ARGUMENTS with all arguments joined by space
  const allArgs = argsArray.join(' ');
  result = result.replace(/\$ARGUMENTS/g, allArgs);

  // Replace positional arguments $1-$9
  for (let i = 1; i <= 9; i++) {
    const regex = new RegExp(`\\$${i}`, 'g');
    const value = argsArray[i - 1] || '';
    result = result.replace(regex, value);
  }

  return result;
}

/**
 * Validate file path to prevent directory traversal
 * @param {string} filePath - Path to validate
 * @param {string} basePath - Base directory path
 * @returns {boolean} True if path is safe
 */
export function isPathSafe(filePath, basePath) {
  const resolvedPath = path.resolve(basePath, filePath);
  const resolvedBase = path.resolve(basePath);
  const relative = path.relative(resolvedBase, resolvedPath);
  return (
    relative !== '' &&
    !relative.startsWith('..') &&
    !path.isAbsolute(relative)
  );
}

/**
 * Process file includes in content (@filename syntax)
 * @param {string} content - Content with @filename includes
 * @param {string} basePath - Base directory for resolving file paths
 * @param {number} depth - Current recursion depth
 * @returns {Promise<string>} Content with includes resolved
 */
export async function processFileIncludes(content, basePath, depth = 0) {
  if (!content) return content;

  // Prevent infinite recursion
  if (depth >= MAX_INCLUDE_DEPTH) {
    throw new Error(`Maximum include depth (${MAX_INCLUDE_DEPTH}) exceeded`);
  }

  // Match @filename patterns (at start of line or after whitespace)
  const includePattern = /(?:^|\s)@([^\s]+)/gm;
  const matches = [...content.matchAll(includePattern)];

  if (matches.length === 0) {
    return content;
  }

  let result = content;

  for (const match of matches) {
    const fullMatch = match[0];
    const filename = match[1];

    // Security: prevent directory traversal
    if (!isPathSafe(filename, basePath)) {
      throw new Error(`Invalid file path (directory traversal detected): ${filename}`);
    }

    try {
      const filePath = path.resolve(basePath, filename);
      const fileContent = await fs.readFile(filePath, 'utf-8');

      // Recursively process includes in the included file
      const processedContent = await processFileIncludes(fileContent, basePath, depth + 1);

      // Replace the @filename with the file content
      result = result.replace(fullMatch, fullMatch.startsWith(' ') ? ' ' + processedContent : processedContent);
    } catch (error) {
      if (error.code === 'ENOENT') {
        throw new Error(`File not found: ${filename}`);
      }
      throw error;
    }
  }

  return result;
}

/**
 * Validate that a command and its arguments are safe
 * @param {string} commandString - Command string to validate
 * @returns {{ allowed: boolean, command: string, args: string[], error?: string }} Validation result
 */
export function validateCommand(commandString) {
  const trimmedCommand = commandString.trim();
  if (!trimmedCommand) {
    return { allowed: false, command: '', args: [], error: 'Empty command' };
  }

  // Parse the command using shell-quote to handle quotes properly
  const parsed = parseShellCommand(trimmedCommand);

  // Check for shell operators or control structures
  const hasOperators = parsed.some(token =>
    typeof token === 'object' && token.op
  );

  if (hasOperators) {
    return {
      allowed: false,
      command: '',
      args: [],
      error: 'Shell operators (&&, ||, |, ;, etc.) are not allowed'
    };
  }

  // Extract command and args (all should be strings after validation)
  const tokens = parsed.filter(token => typeof token === 'string');

  if (tokens.length === 0) {
    return { allowed: false, command: '', args: [], error: 'No valid command found' };
  }

  const [command, ...args] = tokens;

  // Extract just the command name (remove path if present)
  const commandName = path.basename(command);

  // Check if command exactly matches allowlist (no prefix matching)
  const isAllowed = BASH_COMMAND_ALLOWLIST.includes(commandName);

  if (!isAllowed) {
    return {
      allowed: false,
      command: commandName,
      args,
      error: `Command '${commandName}' is not in the allowlist`
    };
  }

  // Validate arguments don't contain dangerous metacharacters
  const dangerousPattern = /[;&|`$()<>{}[\]\\]/;
  for (const arg of args) {
    if (dangerousPattern.test(arg)) {
      return {
        allowed: false,
        command: commandName,
        args,
        error: `Argument contains dangerous characters: ${arg}`
      };
    }
  }

  return { allowed: true, command: commandName, args };
}

/**
 * Backward compatibility: Check if command is allowed (deprecated)
 * @deprecated Use validateCommand() instead for better security
 * @param {string} command - Command to validate
 * @returns {boolean} True if command is allowed
 */
export function isBashCommandAllowed(command) {
  const result = validateCommand(command);
  return result.allowed;
}

/**
 * Sanitize bash command output
 * @param {string} output - Raw command output
 * @returns {string} Sanitized output
 */
export function sanitizeOutput(output) {
  if (!output) return '';

  // Remove control characters except \t, \n, \r
  return [...output]
    .filter(ch => {
      const code = ch.charCodeAt(0);
      return code === 9  // \t
          || code === 10 // \n
          || code === 13 // \r
          || (code >= 32 && code !== 127);
    })
    .join('');
}

/**
 * Process bash commands in content (!command syntax)
 * @param {string} content - Content with !command syntax
 * @param {object} options - Options for bash execution
 * @returns {Promise<string>} Content with bash commands executed and replaced
 */
export async function processBashCommands(content, options = {}) {
  if (!content) return content;

  const { cwd = process.cwd(), timeout = BASH_TIMEOUT } = options;

  // Match !command patterns (at start of line or after whitespace)
  const commandPattern = /(?:^|\n)!(.+?)(?=\n|$)/g;
  const matches = [...content.matchAll(commandPattern)];

  if (matches.length === 0) {
    return content;
  }

  let result = content;

  for (const match of matches) {
    const fullMatch = match[0];
    const commandString = match[1].trim();

    // Security: validate command and parse args
    const validation = validateCommand(commandString);

    if (!validation.allowed) {
      throw new Error(`Command not allowed: ${commandString} - ${validation.error}`);
    }

    try {
      // Execute without shell using execFile with parsed args
      const { stdout, stderr } = await execFileAsync(
        validation.command,
        validation.args,
        {
          cwd,
          timeout,
          maxBuffer: 1024 * 1024, // 1MB max output
          shell: false, // IMPORTANT: No shell interpretation
          env: { ...process.env, PATH: process.env.PATH } // Inherit PATH for finding commands
        }
      );

      const output = sanitizeOutput(stdout || stderr || '');

      // Replace the !command with the output
      result = result.replace(fullMatch, fullMatch.startsWith('\n') ? '\n' + output : output);
    } catch (error) {
      if (error.killed) {
        throw new Error(`Command timeout: ${commandString}`);
      }
      throw new Error(`Command failed: ${commandString} - ${error.message}`);
    }
  }

  return result;
}
