/**
 * RepoMap 工具
 * 生成代码仓库的上下文地图，帮助 AI 理解代码结构
 */

import * as path from 'path';
import * as fs from 'fs/promises';
import type { ToolResult } from '../../types/index.js';
import type { ToolWithMetadata } from '../types.js';
import { loadDescription } from '../load_description.js';
import { getPermissionManager } from '../../permission/index.js';
import { createRepoMap, type RepoMapConfig } from '../../repomap/index.js';

// 缓存 RepoMap 实例
const repoMapCache = new Map<string, ReturnType<typeof createRepoMap>>();

/**
 * 获取或创建 RepoMap 实例
 */
function getRepoMap(root: string, config?: Partial<RepoMapConfig>) {
  const key = root;
  if (!repoMapCache.has(key)) {
    repoMapCache.set(key, createRepoMap(root, config));
  }
  return repoMapCache.get(key)!;
}

/**
 * 支持的文件扩展名
 */
const SUPPORTED_EXTENSIONS = new Set([
  '.ts',
  '.tsx',
  '.js',
  '.jsx',
  '.mjs',
  '.cjs',
  '.py',
]);

/**
 * 要排除的目录
 */
const EXCLUDED_DIRS = new Set([
  'node_modules',
  'dist',
  'build',
  '.git',
  '.next',
  '__pycache__',
  '.pytest_cache',
  'coverage',
  '.nyc_output',
]);

/**
 * 要排除的文件模式
 */
function shouldExcludeFile(filename: string): boolean {
  return (
    filename.includes('.test.') ||
    filename.includes('.spec.') ||
    filename.endsWith('.d.ts')
  );
}

/**
 * 递归获取目录中的文件
 */
async function getFilesRecursive(
  directory: string,
  maxDepth = 10
): Promise<string[]> {
  const files: string[] = [];

  async function walk(dir: string, depth: number): Promise<void> {
    if (depth > maxDepth) return;

    try {
      const entries = await fs.readdir(dir, { withFileTypes: true });

      for (const entry of entries) {
        const fullPath = path.join(dir, entry.name);

        if (entry.isDirectory()) {
          if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
            await walk(fullPath, depth + 1);
          }
        } else if (entry.isFile()) {
          const ext = path.extname(entry.name).toLowerCase();
          if (SUPPORTED_EXTENSIONS.has(ext) && !shouldExcludeFile(entry.name)) {
            files.push(fullPath);
          }
        }
      }
    } catch {
      // 忽略无法访问的目录
    }
  }

  await walk(directory, 0);
  return files;
}

export const repoMapTool: ToolWithMetadata = {
  name: 'repo_map',
  description: loadDescription('repo_map'),
  metadata: {
    name: 'repo_map',
    category: 'core',
    description: '生成代码仓库上下文地图，帮助理解代码结构',
    keywords: [
      'repomap',
      'repo',
      'map',
      'context',
      'code',
      'structure',
      'ast',
      'symbol',
      'definition',
      '代码地图',
      '仓库结构',
      '代码结构',
      '符号',
      '定义',
    ],
    deferLoading: true,
  },
  parameters: {
    directory: {
      type: 'string',
      description: '要分析的目录路径（默认为当前工作目录）',
      required: false,
    },
    chat_files: {
      type: 'string',
      description:
        '当前对话中涉及的文件列表，逗号分隔（这些文件会被排除在地图外）',
      required: false,
    },
    mentioned_files: {
      type: 'string',
      description: '对话中提到的文件名，逗号分隔（用于提高相关性）',
      required: false,
    },
    mentioned_identifiers: {
      type: 'string',
      description: '对话中提到的标识符/符号名，逗号分隔（用于提高相关性）',
      required: false,
    },
    max_tokens: {
      type: 'number',
      description: '最大输出 token 数（默认 1024）',
      required: false,
    },
  },
  execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
    const cwd = process.cwd();
    const directory = (params.directory as string) || cwd;
    const absolutePath = path.isAbsolute(directory)
      ? directory
      : path.join(cwd, directory);

    // 解析逗号分隔的字符串为数组
    const chatFilesStr = (params.chat_files as string) || '';
    const chatFiles = chatFilesStr
      ? chatFilesStr.split(',').map((s) => s.trim())
      : [];

    const mentionedFilesStr = (params.mentioned_files as string) || '';
    const mentionedFiles = new Set(
      mentionedFilesStr ? mentionedFilesStr.split(',').map((s) => s.trim()) : []
    );

    const mentionedIdentsStr = (params.mentioned_identifiers as string) || '';
    const mentionedIdents = new Set(
      mentionedIdentsStr ? mentionedIdentsStr.split(',').map((s) => s.trim()) : []
    );

    const maxTokens = (params.max_tokens as number) || 1024;

    // 权限检查
    const permissionManager = getPermissionManager();
    const permResult = await permissionManager.checkFilePermission({
      operation: 'search',
      path: absolutePath,
      workdir: cwd,
    });

    if (!permResult.allowed) {
      if (permResult.needsConfirmation) {
        return {
          success: false,
          output: '',
          error: `需要用户确认: 分析目录 ${absolutePath}\n原因: ${permResult.reason || '需要权限确认'}`,
        };
      }
      return {
        success: false,
        output: '',
        error: `权限被拒绝: ${permResult.reason || '不允许分析此目录'}`,
      };
    }

    try {
      // 检查目录是否存在
      const stat = await fs.stat(absolutePath);
      if (!stat.isDirectory()) {
        return {
          success: false,
          output: '',
          error: `${absolutePath} 不是一个目录`,
        };
      }

      // 获取 RepoMap 实例
      const repoMap = getRepoMap(absolutePath, {
        mapTokens: maxTokens,
      });

      // 获取文件列表
      const allFiles = await getFilesRecursive(absolutePath);

      // 分离聊天文件和其他文件
      const chatFilesAbs = chatFiles.map((f) =>
        path.isAbsolute(f) ? f : path.join(absolutePath, f)
      );
      const otherFiles = allFiles.filter((f) => !chatFilesAbs.includes(f));

      // 生成 repo map
      const mapContent = await repoMap.getRepoMap(
        chatFilesAbs,
        otherFiles,
        mentionedFiles,
        mentionedIdents
      );

      if (!mapContent || mapContent.trim() === '') {
        return {
          success: true,
          output:
            '未找到相关的代码符号。可能是因为目录中没有支持的代码文件，或者文件过少。',
        };
      }

      const header = `# Repository Map\n# Directory: ${absolutePath}\n# Files analyzed: ${allFiles.length}\n\n`;

      return {
        success: true,
        output: header + mapContent,
      };
    } catch (error) {
      return {
        success: false,
        output: '',
        error: error instanceof Error ? error.message : String(error),
      };
    }
  },
};
