/**
 * 开发工具集（3 个）
 *
 * lint_code, run_tests, project_info
 *
 * 用于代码检查、测试运行和项目结构分析。
 * 使用 execFile 执行命令，防止命令注入。
 */

import { execFile } from 'child_process';
import { promisify } from 'util';
import { readFile, readdir } from 'fs/promises';
import { existsSync } from 'fs';
import { join } from 'path';
import type { IMetonaTool, ToolExecutionContext } from '../../types/metona-tool';
import type { MetonaToolDef } from '../../../harness/types';
import { MetonaToolCategory, MetonaRiskLevel } from '../../../harness/types';

const execFileAsync = promisify(execFile);

/** 输出最大长度（50KB） */
const MAX_OUTPUT_LENGTH = 50 * 1024;

/** Windows 平台检测 */
const isWindows = process.platform === 'win32';

/** ESLint 配置文件列表 */
const ESLINT_CONFIG_FILES = [
  '.eslintrc',
  '.eslintrc.js',
  '.eslintrc.json',
  '.eslintrc.yml',
  '.eslintrc.yaml',
  '.eslintrc.cjs',
  'eslint.config.js',
  'eslint.config.mjs',
  'eslint.config.cjs',
];

/** 检测工作空间是否存在 ESLint 配置 */
function hasEslintConfig(workspacePath: string): boolean {
  return ESLINT_CONFIG_FILES.some((f) => existsSync(join(workspacePath, f)));
}

/** 读取并解析 package.json */
async function readPackageJson(workspacePath: string): Promise<Record<string, unknown> | null> {
  try {
    const content = await readFile(join(workspacePath, 'package.json'), 'utf-8');
    return JSON.parse(content) as Record<string, unknown>;
  } catch {
    return null;
  }
}

/** 截断输出到 50KB */
function truncateOutput(output: string): { output: string; truncated: boolean } {
  if (output.length > MAX_OUTPUT_LENGTH) {
    return { output: output.slice(0, MAX_OUTPUT_LENGTH), truncated: true };
  }
  return { output, truncated: false };
}

/** 从 execFile 错误对象中提取 stdout/stderr 字符串 */
function extractOutput(err: { stdout?: string | Buffer; stderr?: string | Buffer }): { stdout: string; stderr: string } {
  return {
    stdout: typeof err.stdout === 'string' ? err.stdout : '',
    stderr: typeof err.stderr === 'string' ? err.stderr : '',
  };
}

// ===== 1. lint_code =====

export class LintCodeTool implements IMetonaTool {
  readonly definition: MetonaToolDef = {
    name: 'lint_code',
    description:
      'Run code linting: TypeScript type checking (tsc) or ESLint code style checking. Auto-detects project configuration when type is "auto".',
    parameters: {
      type: 'object',
      properties: {
        type: {
          type: 'string',
          description:
            'Check type: "tsc" (TypeScript type check), "eslint" (ESLint), or "auto" (auto-detect, prioritizes tsc then eslint). Defaults to "tsc".',
          enum: ['tsc', 'eslint', 'auto'],
        },
      },
    },
    category: MetonaToolCategory.CODE_EXECUTION,
    riskLevel: MetonaRiskLevel.SAFE,
    requiresPermission: false,
    timeoutMs: 60_000,
  };

  async execute(args: Record<string, unknown>, context: ToolExecutionContext): Promise<unknown> {
    const type = (args.type as string) ?? 'tsc';

    try {
      let actualType: 'tsc' | 'eslint';

      if (type === 'auto') {
        // 自动检测：优先 tsc，然后 eslint
        if (existsSync(join(context.workspacePath, 'tsconfig.json'))) {
          actualType = 'tsc';
        } else if (hasEslintConfig(context.workspacePath)) {
          actualType = 'eslint';
        } else {
          return {
            type: 'none',
            success: true,
            errorCount: 0,
            warningCount: 0,
            output: 'No TypeScript or ESLint configuration found in workspace.',
            truncated: false,
          };
        }
      } else if (type === 'tsc') {
        if (!existsSync(join(context.workspacePath, 'tsconfig.json'))) {
          return {
            type: 'tsc',
            success: true,
            errorCount: 0,
            warningCount: 0,
            output: 'No tsconfig.json found, skipping TypeScript type check.',
            truncated: false,
          };
        }
        actualType = 'tsc';
      } else {
        // eslint
        if (!hasEslintConfig(context.workspacePath)) {
          return {
            type: 'eslint',
            success: true,
            errorCount: 0,
            warningCount: 0,
            output: 'No ESLint configuration found, skipping ESLint check.',
            truncated: false,
          };
        }
        actualType = 'eslint';
      }

      let stdout = '';
      let stderr = '';
      let exitCode = 0;

      try {
        if (actualType === 'tsc') {
          const result = await execFileAsync('npx', ['tsc', '--noEmit'], {
            cwd: context.workspacePath,
            maxBuffer: 5 * 1024 * 1024,
            timeout: 60_000,
            shell: isWindows,
            encoding: 'utf-8',  // v0.3.1 修复 WARN-7: 显式设置编码
          });
          stdout = result.stdout;
          stderr = result.stderr;
        } else {
          const result = await execFileAsync('npx', ['eslint', '--ext', '.ts,.tsx', '.'], {
            cwd: context.workspacePath,
            maxBuffer: 5 * 1024 * 1024,
            timeout: 60_000,
            shell: isWindows,
            encoding: 'utf-8',  // v0.3.1 修复 WARN-7: 显式设置编码
          });
          stdout = result.stdout;
          stderr = result.stderr;
        }
      } catch (error) {
        // lint 命令退出码非 0 是正常情况（代码有错误时），需要捕获输出用于解析
        const err = error as { stdout?: string | Buffer; stderr?: string | Buffer; code?: number };
        const extracted = extractOutput(err);
        stdout = extracted.stdout;
        stderr = extracted.stderr;
        exitCode = err.code ?? 1;
      }

      const output = (stdout + (stderr ? '\n' + stderr : '')).trim();
      const { errorCount, warningCount } = this.parseCounts(output, actualType);
      const { output: finalOutput, truncated } = truncateOutput(output);

      return {
        type: actualType,
        success: exitCode === 0,
        errorCount,
        warningCount,
        output: finalOutput,
        truncated,
      };
    } catch (error) {
      const errMsg = error instanceof Error ? error.message : String(error);
      return { error: errMsg, success: false };
    }
  }

  /** 解析 lint 输出中的错误和警告数量 */
  private parseCounts(output: string, type: 'tsc' | 'eslint'): { errorCount: number; warningCount: number } {
    if (type === 'tsc') {
      // tsc 输出格式: "file.ts(line,col): error TS1234: message"
      const errorMatches = output.match(/error TS\d+:/g);
      return { errorCount: errorMatches?.length ?? 0, warningCount: 0 };
    }
    // eslint 输出末尾汇总: "✖ X problems (Y errors, Z warnings)"
    const match = output.match(/(\d+)\s+problems?\s*\((\d+)\s+errors?,\s*(\d+)\s+warnings?\)/);
    if (match) {
      return {
        errorCount: parseInt(match[2], 10),
        warningCount: parseInt(match[3], 10),
      };
    }
    return { errorCount: 0, warningCount: 0 };
  }
}

// ===== 2. run_tests =====

export class RunTestsTool implements IMetonaTool {
  readonly definition: MetonaToolDef = {
    name: 'run_tests',
    description:
      'Run the project test suite. Auto-detects the test command from package.json scripts, or uses a custom command. Supports test file filtering.',
    parameters: {
      type: 'object',
      properties: {
        filter: {
          type: 'string',
          description: 'Test file filter pattern (e.g., "auth" or "utils/*"). Passed to the test runner.',
        },
        watch: {
          type: 'boolean',
          description: 'Watch mode (not applicable in this context, ignored).',
        },
      },
    },
    category: MetonaToolCategory.CODE_EXECUTION,
    riskLevel: MetonaRiskLevel.LOW,
    requiresPermission: false,
    timeoutMs: 120_000,
  };

  async execute(args: Record<string, unknown>, context: ToolExecutionContext): Promise<unknown> {
    const filter = args.filter as string | undefined;
    // v0.3.1 修复 WARN-2: 移除 customCommand — LLM 输出不可信，shell:true 有注入风险
    // watch 参数不适用，忽略

    // v0.3.1 修复 WARN-1: filter 字符白名单校验，防止 Windows shell:true 下的 cmd 元字符注入
    // 允许字母/数字/连字符/下划线/斜杠/星号/点（常见测试文件路径模式）
    const FILTER_ALLOWED_RE = /^[A-Za-z0-9_\-/. *]+$/;
    if (filter && !FILTER_ALLOWED_RE.test(filter)) {
      return {
        command: 'npm test',
        success: false,
        passed: 0,
        failed: 0,
        duration: '0s',
        output: `Invalid filter: contains disallowed characters. Only letters, digits, '-', '_', '/', '.', '*', ' ' are allowed.`,
        truncated: false,
      };
    }

    try {
      let actualCommand: string;
      let execCmd: string;
      let execArgs: string[];

      // 检测 package.json 的 scripts.test
      const pkg = await readPackageJson(context.workspacePath);
      const scripts = (pkg?.scripts as Record<string, string> | undefined) ?? {};
      const testScript = scripts.test;

      if (!testScript || testScript === 'no test specified') {
        return {
          command: 'npm test',
          success: false,
          passed: 0,
          failed: 0,
          duration: '0s',
          output: 'No test script found in package.json.',
          truncated: false,
        };
      }

      actualCommand = filter ? `npm test -- ${filter}` : 'npm test';
      execCmd = 'npm';
      execArgs = filter ? ['test', '--', filter] : ['test'];

      let stdout = '';
      let stderr = '';
      let exitCode = 0;

      try {
        const result = await execFileAsync(execCmd, execArgs, {
          cwd: context.workspacePath,
          maxBuffer: 5 * 1024 * 1024,
          timeout: 120_000,
          shell: isWindows,
          encoding: 'utf-8',  // v0.3.1 修复 WARN-7: 显式设置编码
        });
        stdout = result.stdout;
        stderr = result.stderr;
      } catch (error) {
        // 测试失败（exitCode !== 0）是正常情况，需要捕获输出解析结果
        const err = error as { stdout?: string | Buffer; stderr?: string | Buffer; code?: number };
        const extracted = extractOutput(err);
        stdout = extracted.stdout;
        stderr = extracted.stderr;
        exitCode = err.code ?? 1;
      }

      const output = (stdout + (stderr ? '\n' + stderr : '')).trim();
      const { passed, failed, duration } = this.parseTestResults(output);
      const { output: finalOutput, truncated } = truncateOutput(output);

      return {
        command: actualCommand,
        success: exitCode === 0,
        passed,
        failed,
        duration,
        output: finalOutput,
        truncated,
      };
    } catch (error) {
      const errMsg = error instanceof Error ? error.message : String(error);
      return { error: errMsg, success: false };
    }
  }

  /** 从测试输出中解析通过/失败数量和耗时（支持 jest/vitest/mocha 格式） */
  private parseTestResults(output: string): { passed: number; failed: number; duration: string } {
    let passed = 0;
    let failed = 0;
    let duration = '0s';

    // jest/vitest 格式: "Tests: 5 passed, 2 failed, 7 total"
    const jestMatch = output.match(/(\d+)\s+passed(?:,\s*(\d+)\s+failed)?/);
    if (jestMatch) {
      passed = parseInt(jestMatch[1], 10);
      failed = jestMatch[2] ? parseInt(jestMatch[2], 10) : 0;
    }

    // mocha 格式: "X passing (Ys)" / "X failing"
    const mochaPass = output.match(/(\d+)\s+passing/);
    const mochaFail = output.match(/(\d+)\s+failing/);
    if (mochaPass) passed = parseInt(mochaPass[1], 10);
    if (mochaFail) failed = parseInt(mochaFail[1], 10);

    // 耗时解析: "Time: 3.5 s" / "Duration: 120ms" / "(3.5s)"
    const timeMatch = output.match(/(?:Time|Duration|耗时)[:\s]+([\d.]+\s*(?:ms|s|m|h|seconds?|minutes?|hours?)?)/i);
    if (timeMatch) {
      duration = timeMatch[1];
    } else {
      const parenTime = output.match(/\(([\d.]+\s*(?:ms|s|m|h))\)/);
      if (parenTime) duration = parenTime[1];
    }

    return { passed, failed, duration };
  }
}

// ===== 3. project_info =====

export class ProjectInfoTool implements IMetonaTool {
  readonly definition: MetonaToolDef = {
    name: 'project_info',
    description:
      'Analyze project structure and dependencies. Returns package.json overview, full dependency list, npm scripts, or directory structure (2 levels deep).',
    parameters: {
      type: 'object',
      properties: {
        detail: {
          type: 'string',
          description:
            'Detail level: "overview" (project summary), "dependencies" (full dependency list), "scripts" (npm scripts), or "structure" (directory tree, 2 levels). Defaults to "overview".',
          enum: ['overview', 'dependencies', 'scripts', 'structure'],
        },
      },
    },
    category: MetonaToolCategory.FILESYSTEM,
    riskLevel: MetonaRiskLevel.SAFE,
    requiresPermission: false,
    timeoutMs: 10_000,
  };

  async execute(args: Record<string, unknown>, context: ToolExecutionContext): Promise<unknown> {
    const detail = (args.detail as string) ?? 'overview';

    try {
      switch (detail) {
        case 'dependencies':
          return await this.getDependencies(context.workspacePath);
        case 'scripts':
          return await this.getScripts(context.workspacePath);
        case 'structure':
          return await this.getStructure(context.workspacePath);
        default:
          return await this.getOverview(context.workspacePath);
      }
    } catch (error) {
      const errMsg = error instanceof Error ? error.message : String(error);
      return { error: errMsg, success: false };
    }
  }

  private async getOverview(workspacePath: string): Promise<unknown> {
    const pkg = await readPackageJson(workspacePath);
    if (!pkg) {
      return { error: 'package.json not found', success: false };
    }

    const deps = (pkg.dependencies as Record<string, string> | undefined) ?? {};
    const devDeps = (pkg.devDependencies as Record<string, string> | undefined) ?? {};
    const scripts = (pkg.scripts as Record<string, string> | undefined) ?? {};

    return {
      name: (pkg.name as string) ?? 'unknown',
      version: (pkg.version as string) ?? '0.0.0',
      description: (pkg.description as string) ?? '',
      type: pkg.type === 'module' ? 'module' : 'commonjs',
      mainEntry: (pkg.module as string) ?? (pkg.main as string) ?? 'index.js',
      dependencyCount: Object.keys(deps).length,
      devDependencyCount: Object.keys(devDeps).length,
      scriptCount: Object.keys(scripts).length,
      hasTypeScript: existsSync(join(workspacePath, 'tsconfig.json')),
      hasEslint: hasEslintConfig(workspacePath),
    };
  }

  private async getDependencies(workspacePath: string): Promise<unknown> {
    const pkg = await readPackageJson(workspacePath);
    if (!pkg) {
      return { error: 'package.json not found', success: false };
    }

    const deps = (pkg.dependencies as Record<string, string> | undefined) ?? {};
    const devDeps = (pkg.devDependencies as Record<string, string> | undefined) ?? {};

    const dependencies = Object.entries(deps).map(([name, version]) => ({ name, version }));
    const devDependencies = Object.entries(devDeps).map(([name, version]) => ({ name, version }));

    return {
      dependencies,
      devDependencies,
      total: dependencies.length + devDependencies.length,
    };
  }

  private async getScripts(workspacePath: string): Promise<unknown> {
    const pkg = await readPackageJson(workspacePath);
    if (!pkg) {
      return { error: 'package.json not found', success: false };
    }

    const scripts = (pkg.scripts as Record<string, string> | undefined) ?? {};
    const scriptList = Object.entries(scripts).map(([name, command]) => ({ name, command }));

    return {
      scripts: scriptList,
      count: scriptList.length,
    };
  }

  private async getStructure(workspacePath: string): Promise<unknown> {
    const directories = await this.listDirectory(workspacePath, 0, 2);
    return {
      root: workspacePath,
      directories,
    };
  }

  /** 递归列出目录（最多 maxDepth 层），跳过 node_modules/.git/dist */
  private async listDirectory(
    dirPath: string,
    currentDepth: number,
    maxDepth: number,
  ): Promise<Array<{ name: string; type: 'dir' | 'file'; children?: Array<{ name: string; type: string }> }>> {
    const results: Array<{ name: string; type: 'dir' | 'file'; children?: Array<{ name: string; type: string }> }> = [];

    try {
      const entries = await readdir(dirPath, { withFileTypes: true });

      for (const entry of entries) {
        if (this.shouldSkip(entry.name)) continue;

        if (entry.isDirectory()) {
          const item: { name: string; type: 'dir' | 'file'; children?: Array<{ name: string; type: string }> } = {
            name: entry.name,
            type: 'dir',
          };
          if (currentDepth < maxDepth - 1) {
            item.children = await this.listChildren(join(dirPath, entry.name));
          }
          results.push(item);
        } else {
          results.push({ name: entry.name, type: 'file' });
        }
      }
    } catch {
      // 目录读取失败，跳过
    }

    return results;
  }

  /** 列出子目录内容（仅 1 层，不再递归） */
  private async listChildren(dirPath: string): Promise<Array<{ name: string; type: string }>> {
    const results: Array<{ name: string; type: string }> = [];

    try {
      const entries = await readdir(dirPath, { withFileTypes: true });

      for (const entry of entries) {
        if (this.shouldSkip(entry.name)) continue;
        results.push({ name: entry.name, type: entry.isDirectory() ? 'dir' : 'file' });
      }
    } catch {
      // 目录读取失败，跳过
    }

    return results;
  }

  /** 判断目录/文件是否应跳过（node_modules/.git/dist 不递归） */
  private shouldSkip(name: string): boolean {
    return ['node_modules', '.git', 'dist'].includes(name);
  }
}
