import { spawnSync } from 'child_process';
import { readdirSync, statSync } from 'fs';
import { homedir } from 'os';
import { basename, dirname, join } from 'path';
import { fuzzyFilter } from './fuzzy.js';
import { extractInlineSkillAutocompleteToken } from './skill-autocomplete.js';
const PATH_DELIMITERS = new Set([' ', '\t', '"', "'", '=']);
function findLastDelimiter(text) {
  for (let i = text.length - 1; i >= 0; i -= 1) {
    if (PATH_DELIMITERS.has(text[i] ?? '')) {
      return i;
    }
  }
  return -1;
}
function findUnclosedQuoteStart(text) {
  let inQuotes = false;
  let quoteStart = -1;
  for (let i = 0; i < text.length; i += 1) {
    if (text[i] === '"') {
      inQuotes = !inQuotes;
      if (inQuotes) {
        quoteStart = i;
      }
    }
  }
  return inQuotes ? quoteStart : null;
}
function isTokenStart(text, index) {
  return index === 0 || PATH_DELIMITERS.has(text[index - 1] ?? '');
}
function extractQuotedPrefix(text) {
  const quoteStart = findUnclosedQuoteStart(text);
  if (quoteStart === null) {
    return null;
  }
  if (quoteStart > 0 && text[quoteStart - 1] === '@') {
    if (!isTokenStart(text, quoteStart - 1)) {
      return null;
    }
    return text.slice(quoteStart - 1);
  }
  if (!isTokenStart(text, quoteStart)) {
    return null;
  }
  return text.slice(quoteStart);
}
function parsePathPrefix(prefix) {
  if (prefix.startsWith('@"')) {
    return { rawPrefix: prefix.slice(2), isAtPrefix: true, isQuotedPrefix: true };
  }
  if (prefix.startsWith('"')) {
    return { rawPrefix: prefix.slice(1), isAtPrefix: false, isQuotedPrefix: true };
  }
  if (prefix.startsWith('@')) {
    return { rawPrefix: prefix.slice(1), isAtPrefix: true, isQuotedPrefix: false };
  }
  return { rawPrefix: prefix, isAtPrefix: false, isQuotedPrefix: false };
}
function buildCompletionValue(path, options) {
  const needsQuotes = options.isQuotedPrefix || path.includes(' ');
  const prefix = options.isAtPrefix ? '@' : '';
  if (!needsQuotes) {
    return `${prefix}${path}`;
  }
  const openQuote = `${prefix}"`;
  const closeQuote = '"';
  return `${openQuote}${path}${closeQuote}`;
}
// Use fd to walk directory tree (fast, respects .gitignore)
function walkDirectoryWithFd(baseDir, fdPath, query, maxResults) {
  const args = [
    '--base-directory',
    baseDir,
    '--max-results',
    String(maxResults),
    '--type',
    'f',
    '--type',
    'd',
    '--full-path',
    '--hidden',
    '--exclude',
    '.git',
    '--exclude',
    '.git/*',
    '--exclude',
    '.git/**'
  ];
  // Add query as pattern if provided
  if (query) {
    args.push(query);
  }
  const result = spawnSync(fdPath, args, {
    encoding: 'utf-8',
    stdio: ['pipe', 'pipe', 'pipe'],
    maxBuffer: 10 * 1024 * 1024
  });
  if (result.status !== 0 || !result.stdout) {
    return [];
  }
  const lines = result.stdout.trim().split('\n').filter(Boolean);
  const results = [];
  for (const line of lines) {
    const normalizedPath = line.endsWith('/') ? line.slice(0, -1) : line;
    if (
      normalizedPath === '.git' ||
      normalizedPath.startsWith('.git/') ||
      normalizedPath.includes('/.git/')
    ) {
      continue;
    }
    // fd outputs directories with trailing /
    const isDirectory = line.endsWith('/');
    results.push({
      path: line,
      isDirectory
    });
  }
  return results;
}
// Combined provider that handles both slash commands and file paths
export class CombinedAutocompleteProvider {
  commands;
  basePath;
  fdPath;
  constructor(commands = [], basePath = process.cwd(), fdPath = null) {
    this.commands = commands;
    this.basePath = basePath;
    this.fdPath = fdPath;
  }
  getSuggestions(lines, cursorLine, cursorCol) {
    const currentLine = lines[cursorLine] || '';
    const textBeforeCursor = currentLine.slice(0, cursorCol);
    // Check for @ file reference (fuzzy search) - must be after a delimiter or at start
    const atPrefix = this.extractAtPrefix(textBeforeCursor);
    if (atPrefix) {
      const { rawPrefix, isQuotedPrefix } = parsePathPrefix(atPrefix);
      const suggestions = this.getFuzzyFileSuggestions(rawPrefix, {
        isQuotedPrefix: isQuotedPrefix
      });
      if (suggestions.length === 0) return null;
      return {
        items: suggestions,
        prefix: atPrefix
      };
    }
    // Check for slash commands
    if (textBeforeCursor.startsWith('/')) {
      const spaceIndex = textBeforeCursor.indexOf(' ');
      if (spaceIndex === -1) {
        // No space yet - complete command names with fuzzy matching
        const prefix = textBeforeCursor.slice(1); // Remove the "/"
        const commandItems = this.commands.map((cmd) => ({
          name: 'name' in cmd ? cmd.name : cmd.value,
          label: 'name' in cmd ? cmd.name : cmd.label,
          description: cmd.description
        }));
        const filtered = fuzzyFilter(commandItems, prefix, (item) => item.name).map((item) => ({
          value: item.name,
          label: item.label,
          ...(item.description && { description: item.description })
        }));
        if (filtered.length === 0) return null;
        return {
          items: filtered,
          prefix: textBeforeCursor
        };
      } else {
        // Space found - complete command arguments
        const commandName = textBeforeCursor.slice(1, spaceIndex); // Command without "/"
        const argumentText = textBeforeCursor.slice(spaceIndex + 1); // Text after space
        const command = this.commands.find((cmd) => {
          const name = 'name' in cmd ? cmd.name : cmd.value;
          return name === commandName;
        });
        if (!command || !('getArgumentCompletions' in command) || !command.getArgumentCompletions) {
          return null; // No argument completion for this command
        }
        const argumentSuggestions = command.getArgumentCompletions(argumentText);
        if (!argumentSuggestions || argumentSuggestions.length === 0) {
          return null;
        }
        return {
          items: argumentSuggestions,
          prefix: argumentText
        };
      }
    }
    const inlineSkillToken = !textBeforeCursor.startsWith('/')
      ? extractInlineSkillAutocompleteToken(textBeforeCursor)
      : null;
    if (inlineSkillToken) {
      const rawQuery = inlineSkillToken.query;
      const commandItems = this.commands
        .map((cmd) => ({
          name: 'name' in cmd ? cmd.name : cmd.value,
          label: 'name' in cmd ? cmd.name : cmd.label,
          description: cmd.description
        }))
        .filter((item) => item.name.startsWith('skill:'))
        .map((item) => ({
          ...item,
          matchName: item.name.slice('skill:'.length)
        }));
      const filtered = fuzzyFilter(commandItems, rawQuery, (item) => item.matchName).map(
        (item) => ({
          value: item.name,
          label: item.label,
          ...(item.description && { description: item.description })
        })
      );
      if (filtered.length === 0) return null;
      return {
        items: filtered,
        prefix: inlineSkillToken.token
      };
    }
    // Check for file paths - triggered by Tab or if we detect a path pattern
    const pathMatch = this.extractPathPrefix(textBeforeCursor, false);
    if (pathMatch !== null) {
      const suggestions = this.getFileSuggestions(pathMatch);
      if (suggestions.length === 0) return null;
      // Check if we have an exact match that is a directory
      // In that case, we might want to return suggestions for the directory content instead
      // But only if the prefix ends with /
      if (
        suggestions.length === 1 &&
        suggestions[0]?.value === pathMatch &&
        !pathMatch.endsWith('/')
      ) {
        // Exact match found (e.g. user typed "src" and "src/" is the only match)
        // We still return it so user can select it and add /
        return {
          items: suggestions,
          prefix: pathMatch
        };
      }
      return {
        items: suggestions,
        prefix: pathMatch
      };
    }
    return null;
  }
  applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
    const currentLine = lines[cursorLine] || '';
    const beforePrefix = currentLine.slice(0, cursorCol - prefix.length);
    const afterCursor = currentLine.slice(cursorCol);
    const isQuotedPrefix = prefix.startsWith('"') || prefix.startsWith('@"');
    const hasLeadingQuoteAfterCursor = afterCursor.startsWith('"');
    const hasTrailingQuoteInItem = item.value.endsWith('"');
    const adjustedAfterCursor =
      isQuotedPrefix && hasTrailingQuoteInItem && hasLeadingQuoteAfterCursor
        ? afterCursor.slice(1)
        : afterCursor;
    // Check if we're completing a slash command (prefix starts with "/" but NOT a file path)
    // Slash commands are at the start of the line and don't contain path separators after the first /
    const isSlashCommand =
      prefix.startsWith('/') &&
      !prefix.slice(1).includes('/') &&
      (beforePrefix.trim() === '' || item.value.startsWith('skill:'));
    if (isSlashCommand) {
      // This is a command name completion
      const newLine = `${beforePrefix}/${item.value} ${adjustedAfterCursor}`;
      const newLines = [...lines];
      newLines[cursorLine] = newLine;
      return {
        lines: newLines,
        cursorLine,
        cursorCol: beforePrefix.length + item.value.length + 2 // +2 for "/" and space
      };
    }
    // Check if we're completing a file attachment (prefix starts with "@")
    if (prefix.startsWith('@')) {
      // This is a file attachment completion
      // Don't add space after directories so user can continue autocompleting
      const isDirectory = item.label.endsWith('/');
      const suffix = isDirectory ? '' : ' ';
      const newLine = `${beforePrefix + item.value}${suffix}${adjustedAfterCursor}`;
      const newLines = [...lines];
      newLines[cursorLine] = newLine;
      const hasTrailingQuote = item.value.endsWith('"');
      const cursorOffset =
        isDirectory && hasTrailingQuote ? item.value.length - 1 : item.value.length;
      return {
        lines: newLines,
        cursorLine,
        cursorCol: beforePrefix.length + cursorOffset + suffix.length
      };
    }
    // Check if we're in a slash command context (beforePrefix contains "/command ")
    const textBeforeCursor = currentLine.slice(0, cursorCol);
    if (textBeforeCursor.includes('/') && textBeforeCursor.includes(' ')) {
      // This is likely a command argument completion
      const newLine = beforePrefix + item.value + adjustedAfterCursor;
      const newLines = [...lines];
      newLines[cursorLine] = newLine;
      const isDirectory = item.label.endsWith('/');
      const hasTrailingQuote = item.value.endsWith('"');
      const cursorOffset =
        isDirectory && hasTrailingQuote ? item.value.length - 1 : item.value.length;
      return {
        lines: newLines,
        cursorLine,
        cursorCol: beforePrefix.length + cursorOffset
      };
    }
    // For file paths, complete the path
    const newLine = beforePrefix + item.value + adjustedAfterCursor;
    const newLines = [...lines];
    newLines[cursorLine] = newLine;
    const isDirectory = item.label.endsWith('/');
    const hasTrailingQuote = item.value.endsWith('"');
    const cursorOffset =
      isDirectory && hasTrailingQuote ? item.value.length - 1 : item.value.length;
    return {
      lines: newLines,
      cursorLine,
      cursorCol: beforePrefix.length + cursorOffset
    };
  }
  // Extract @ prefix for fuzzy file suggestions
  extractAtPrefix(text) {
    const quotedPrefix = extractQuotedPrefix(text);
    if (quotedPrefix?.startsWith('@"')) {
      return quotedPrefix;
    }
    const lastDelimiterIndex = findLastDelimiter(text);
    const tokenStart = lastDelimiterIndex === -1 ? 0 : lastDelimiterIndex + 1;
    if (text[tokenStart] === '@') {
      return text.slice(tokenStart);
    }
    return null;
  }
  // Extract a path-like prefix from the text before cursor
  extractPathPrefix(text, forceExtract = false) {
    const quotedPrefix = extractQuotedPrefix(text);
    if (quotedPrefix) {
      return quotedPrefix;
    }
    const lastDelimiterIndex = findLastDelimiter(text);
    const pathPrefix = lastDelimiterIndex === -1 ? text : text.slice(lastDelimiterIndex + 1);
    // For forced extraction (Tab key), always return something
    if (forceExtract) {
      return pathPrefix;
    }
    // For natural triggers, return if it looks like a path, ends with /, starts with ~/, .
    // Only return empty string if the text looks like it's starting a path context
    if (pathPrefix.includes('/') || pathPrefix.startsWith('.') || pathPrefix.startsWith('~/')) {
      return pathPrefix;
    }
    // Return empty string only after a space (not for completely empty text)
    // Empty text should not trigger file suggestions - that's for forced Tab completion
    if (pathPrefix === '' && text.endsWith(' ')) {
      return pathPrefix;
    }
    return null;
  }
  // Expand home directory (~/) to actual home path
  expandHomePath(path) {
    if (path.startsWith('~/')) {
      const expandedPath = join(homedir(), path.slice(2));
      // Preserve trailing slash if original path had one
      return path.endsWith('/') && !expandedPath.endsWith('/') ? `${expandedPath}/` : expandedPath;
    } else if (path === '~') {
      return homedir();
    }
    return path;
  }
  resolveScopedFuzzyQuery(rawQuery) {
    const slashIndex = rawQuery.lastIndexOf('/');
    if (slashIndex === -1) {
      return null;
    }
    const displayBase = rawQuery.slice(0, slashIndex + 1);
    const query = rawQuery.slice(slashIndex + 1);
    let baseDir;
    if (displayBase.startsWith('~/')) {
      baseDir = this.expandHomePath(displayBase);
    } else if (displayBase.startsWith('/')) {
      baseDir = displayBase;
    } else {
      baseDir = join(this.basePath, displayBase);
    }
    try {
      if (!statSync(baseDir).isDirectory()) {
        return null;
      }
    } catch {
      return null;
    }
    return { baseDir, query, displayBase };
  }
  scopedPathForDisplay(displayBase, relativePath) {
    if (displayBase === '/') {
      return `/${relativePath}`;
    }
    return `${displayBase}${relativePath}`;
  }
  // Get file/directory suggestions for a given path prefix
  getFileSuggestions(prefix) {
    try {
      let searchDir;
      let searchPrefix;
      const { rawPrefix, isAtPrefix, isQuotedPrefix } = parsePathPrefix(prefix);
      let expandedPrefix = rawPrefix;
      // Handle home directory expansion
      if (expandedPrefix.startsWith('~')) {
        expandedPrefix = this.expandHomePath(expandedPrefix);
      }
      const isRootPrefix =
        rawPrefix === '' ||
        rawPrefix === './' ||
        rawPrefix === '../' ||
        rawPrefix === '~' ||
        rawPrefix === '~/' ||
        rawPrefix === '/' ||
        (isAtPrefix && rawPrefix === '');
      if (isRootPrefix) {
        // Complete from specified position
        if (rawPrefix.startsWith('~') || expandedPrefix.startsWith('/')) {
          searchDir = expandedPrefix;
        } else {
          searchDir = join(this.basePath, expandedPrefix);
        }
        searchPrefix = '';
      } else if (rawPrefix.endsWith('/')) {
        // If prefix ends with /, show contents of that directory
        if (rawPrefix.startsWith('~') || expandedPrefix.startsWith('/')) {
          searchDir = expandedPrefix;
        } else {
          searchDir = join(this.basePath, expandedPrefix);
        }
        searchPrefix = '';
      } else {
        // Split into directory and file prefix
        const dir = dirname(expandedPrefix);
        const file = basename(expandedPrefix);
        if (rawPrefix.startsWith('~') || expandedPrefix.startsWith('/')) {
          searchDir = dir;
        } else {
          searchDir = join(this.basePath, dir);
        }
        searchPrefix = file;
      }
      const entries = readdirSync(searchDir, { withFileTypes: true });
      const suggestions = [];
      for (const entry of entries) {
        if (!entry.name.toLowerCase().startsWith(searchPrefix.toLowerCase())) {
          continue;
        }
        // Check if entry is a directory (or a symlink pointing to a directory)
        let isDirectory = entry.isDirectory();
        if (!isDirectory && entry.isSymbolicLink()) {
          try {
            const fullPath = join(searchDir, entry.name);
            isDirectory = statSync(fullPath).isDirectory();
          } catch {
            // Broken symlink or permission error - treat as file
          }
        }
        let relativePath;
        const name = entry.name;
        const displayPrefix = rawPrefix;
        if (displayPrefix.endsWith('/')) {
          // If prefix ends with /, append entry to the prefix
          relativePath = displayPrefix + name;
        } else if (displayPrefix.includes('/')) {
          // Preserve ~/ format for home directory paths
          if (displayPrefix.startsWith('~/')) {
            const homeRelativeDir = displayPrefix.slice(2); // Remove ~/
            const dir = dirname(homeRelativeDir);
            relativePath = `~/${dir === '.' ? name : join(dir, name)}`;
          } else if (displayPrefix.startsWith('/')) {
            // Absolute path - construct properly
            const dir = dirname(displayPrefix);
            if (dir === '/') {
              relativePath = `/${name}`;
            } else {
              relativePath = `${dir}/${name}`;
            }
          } else {
            relativePath = join(dirname(displayPrefix), name);
          }
        } else {
          // For standalone entries, preserve ~/ if original prefix was ~/
          if (displayPrefix.startsWith('~')) {
            relativePath = `~/${name}`;
          } else {
            relativePath = name;
          }
        }
        const pathValue = isDirectory ? `${relativePath}/` : relativePath;
        const value = buildCompletionValue(pathValue, {
          isDirectory,
          isAtPrefix,
          isQuotedPrefix
        });
        suggestions.push({
          value,
          label: name + (isDirectory ? '/' : '')
        });
      }
      // Sort directories first, then alphabetically
      suggestions.sort((a, b) => {
        const aIsDir = a.value.endsWith('/');
        const bIsDir = b.value.endsWith('/');
        if (aIsDir && !bIsDir) return -1;
        if (!aIsDir && bIsDir) return 1;
        return a.label.localeCompare(b.label);
      });
      return suggestions;
    } catch (_e) {
      // Directory doesn't exist or not accessible
      return [];
    }
  }
  // Score an entry against the query (higher = better match)
  // isDirectory adds bonus to prioritize folders
  scoreEntry(filePath, query, isDirectory) {
    const fileName = basename(filePath);
    const lowerFileName = fileName.toLowerCase();
    const lowerQuery = query.toLowerCase();
    let score = 0;
    // Exact filename match (highest)
    if (lowerFileName === lowerQuery) score = 100;
    // Filename starts with query
    else if (lowerFileName.startsWith(lowerQuery)) score = 80;
    // Substring match in filename
    else if (lowerFileName.includes(lowerQuery)) score = 50;
    // Substring match in full path
    else if (filePath.toLowerCase().includes(lowerQuery)) score = 30;
    // Directories get a bonus to appear first
    if (isDirectory && score > 0) score += 10;
    return score;
  }
  // Fuzzy file search using fd (fast, respects .gitignore)
  getFuzzyFileSuggestions(query, options) {
    if (!this.fdPath) {
      // fd not available, return empty results
      return [];
    }
    try {
      const scopedQuery = this.resolveScopedFuzzyQuery(query);
      const fdBaseDir = scopedQuery?.baseDir ?? this.basePath;
      const fdQuery = scopedQuery?.query ?? query;
      const entries = walkDirectoryWithFd(fdBaseDir, this.fdPath, fdQuery, 100);
      // Score entries
      const scoredEntries = entries
        .map((entry) => ({
          ...entry,
          score: fdQuery ? this.scoreEntry(entry.path, fdQuery, entry.isDirectory) : 1
        }))
        .filter((entry) => entry.score > 0);
      // Sort by score (descending) and take top 20
      scoredEntries.sort((a, b) => b.score - a.score);
      const topEntries = scoredEntries.slice(0, 20);
      // Build suggestions
      const suggestions = [];
      for (const { path: entryPath, isDirectory } of topEntries) {
        // fd already includes trailing / for directories
        const pathWithoutSlash = isDirectory ? entryPath.slice(0, -1) : entryPath;
        const displayPath = scopedQuery
          ? this.scopedPathForDisplay(scopedQuery.displayBase, pathWithoutSlash)
          : pathWithoutSlash;
        const entryName = basename(pathWithoutSlash);
        const completionPath = isDirectory ? `${displayPath}/` : displayPath;
        const value = buildCompletionValue(completionPath, {
          isDirectory,
          isAtPrefix: true,
          isQuotedPrefix: options.isQuotedPrefix
        });
        suggestions.push({
          value,
          label: entryName + (isDirectory ? '/' : ''),
          description: displayPath
        });
      }
      return suggestions;
    } catch {
      return [];
    }
  }
  // Force file completion (called on Tab key) - always returns suggestions
  getForceFileSuggestions(lines, cursorLine, cursorCol) {
    const currentLine = lines[cursorLine] || '';
    const textBeforeCursor = currentLine.slice(0, cursorCol);
    // Don't trigger if we're typing a slash command at the start of the line
    if (textBeforeCursor.trim().startsWith('/') && !textBeforeCursor.trim().includes(' ')) {
      return null;
    }
    // Force extract path prefix - this will always return something
    const pathMatch = this.extractPathPrefix(textBeforeCursor, true);
    if (pathMatch !== null) {
      const suggestions = this.getFileSuggestions(pathMatch);
      if (suggestions.length === 0) return null;
      return {
        items: suggestions,
        prefix: pathMatch
      };
    }
    return null;
  }
  // Check if we should trigger file completion (called on Tab key)
  shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
    const currentLine = lines[cursorLine] || '';
    const textBeforeCursor = currentLine.slice(0, cursorCol);
    // Don't trigger if we're typing a slash command at the start of the line
    if (textBeforeCursor.trim().startsWith('/') && !textBeforeCursor.trim().includes(' ')) {
      return false;
    }
    return true;
  }
}
//# sourceMappingURL=autocomplete.js.map
