/**
 * Git 工具集（4 个）
 *
 * git_status, git_diff, git_log, git_commit
 *
 * 使用 execFile 执行 git 命令，防止命令注入。
 * 所有命令在 context.workspacePath 下执行。
 */

import { execFile } from 'child_process';
import { promisify } from 'util';
import { resolve } from 'path';
import { isPathWithinWorkspace } from './file-guard';
import type { IMetonaTool, ToolExecutionContext } from '../../types/metona-tool';
import type { MetonaToolDef } from '../../../harness/types';
import { MetonaToolCategory, MetonaRiskLevel } from '../../../harness/types';

const execFileAsync = promisify(execFile);

/** diff 输出截断阈值（50KB） */
const MAX_DIFF_SIZE = 50 * 1024;

/**
 * 执行 git 命令的统一入口
 *
 * 使用 execFile（非 exec）+ 参数数组，从底层杜绝命令注入。
 * 显式设置 encoding: 'utf-8'，git 默认输出 UTF-8。
 *
 * v0.3.1 修复 WARN-4: 允许调用方覆盖 maxBuffer，默认 1MB；
 * git_diff 传 5MB 以支持超大变更集（避免 maxBuffer exceeded 报错而非截断返回）。
 */
async function runGit(
  args: string[],
  cwd: string,
  timeout = 10_000,
  maxBuffer = 1024 * 1024, // 默认 1MB
): Promise<string> {
  const { stdout } = await execFileAsync('git', args, {
    cwd,
    maxBuffer,
    timeout,
    encoding: 'utf-8',
  });
  return stdout;
}

/** 判断错误是否为 "不是 git 仓库" 或 git 不可用 */
function isNotGitRepoError(error: unknown): boolean {
  if (error instanceof Error) {
    const stderr = (error as Error & { stderr?: string }).stderr ?? '';
    const msg = `${error.message} ${stderr}`;
    return /not a git repository/i.test(msg) || /git: not found|command not found/i.test(msg);
  }
  return false;
}

/** 提取错误信息字符串 */
function extractErrorMessage(error: unknown): string {
  if (error instanceof Error) {
    const stderr = (error as Error & { stderr?: string }).stderr ?? '';
    return stderr ? `${error.message}\n${stderr}` : error.message;
  }
  return String(error);
}

/** 获取当前分支名（失败时返回 'unknown'，不抛错） */
async function getCurrentBranch(cwd: string): Promise<string> {
  try {
    const branch = (await runGit(['rev-parse', '--abbrev-ref', 'HEAD'], cwd)).trim();
    return branch;
  } catch {
    return 'unknown';
  }
}

/** 处理 porcelain v1 重命名格式 "old -> new" → 返回 new */
function extractRenamedFile(file: string): string {
  const idx = file.indexOf(' -> ');
  return idx >= 0 ? file.slice(idx + 4) : file;
}

// ===== 1. git_status =====

export class GitStatusTool implements IMetonaTool {
  readonly definition: MetonaToolDef = {
    name: 'git_status',
    description: 'Show the working tree status. Returns current branch, ahead/behind counts, and lists of staged, unstaged, and untracked files.',
    parameters: {
      type: 'object',
      properties: {
        pathspec: { type: 'string', description: 'Limit to a path scope, e.g. "src/" or "package.json"' },
      },
    },
    category: MetonaToolCategory.FILESYSTEM,
    riskLevel: MetonaRiskLevel.SAFE,
    requiresPermission: false,
    timeoutMs: 15_000,
  };

  async execute(args: Record<string, unknown>, context: ToolExecutionContext): Promise<unknown> {
    const pathspec = args.pathspec as string | undefined;
    const gitArgs = ['status', '--porcelain=v1', '-b'];
    if (pathspec) {
      gitArgs.push('--', pathspec);
    }

    try {
      const output = await runGit(gitArgs, context.workspacePath);
      return this.parseStatus(output);
    } catch (error) {
      if (isNotGitRepoError(error)) {
        return { error: 'Not a git repository or git not available', success: false };
      }
      return { error: extractErrorMessage(error), success: false };
    }
  }

  /**
   * 解析 `git status --porcelain=v1 -b` 输出
   *
   * 分支行格式: `## <branch>...<upstream> [ahead N, behind M]`
   * 文件行格式: `XY <space> <file>`  (X=暂存区状态, Y=工作区状态)
   */
  private parseStatus(output: string): unknown {
    const lines = output.split('\n').filter((l) => l.length > 0);
    const staged: Array<{ status: string; file: string }> = [];
    const unstaged: Array<{ status: string; file: string }> = [];
    const untracked: string[] = [];

    let branch = '';
    let ahead = 0;
    let behind = 0;

    for (const line of lines) {
      // 分支行: ## main...origin/main [ahead 2, behind 1]
      if (line.startsWith('## ')) {
        const header = line.slice(3);
        const bracketIdx = header.indexOf('[');
        const branchPart = bracketIdx >= 0 ? header.slice(0, bracketIdx).trim() : header.trim();
        // branchPart 形如 "main...origin/main" / "main" / "HEAD (no branch)"
        const dotIdx = branchPart.indexOf('...');
        branch = dotIdx >= 0 ? branchPart.slice(0, dotIdx) : branchPart.split(' ')[0];

        if (bracketIdx >= 0) {
          const meta = header.slice(bracketIdx + 1, header.lastIndexOf(']'));
          const aheadMatch = meta.match(/ahead (\d+)/);
          const behindMatch = meta.match(/behind (\d+)/);
          if (aheadMatch) ahead = parseInt(aheadMatch[1], 10);
          if (behindMatch) behind = parseInt(behindMatch[1], 10);
        }
        continue;
      }

      // porcelain v1 文件行: XY <space> <file>
      const x = line[0];
      const y = line[1];
      const file = line.slice(3);

      // 未跟踪: ??
      if (x === '?' && y === '?') {
        untracked.push(file);
        continue;
      }

      // 暂存区变更 (X 非 ' ' 且非 '?')
      if (x !== ' ' && x !== '?') {
        staged.push({ status: x, file: extractRenamedFile(file) });
      }
      // 工作区变更 (Y 非 ' ' 且非 '?')
      if (y !== ' ' && y !== '?') {
        unstaged.push({ status: y, file: extractRenamedFile(file) });
      }
    }

    const clean = staged.length === 0 && unstaged.length === 0 && untracked.length === 0;

    return {
      branch,
      ahead,
      behind,
      staged,
      unstaged,
      untracked,
      clean,
    };
  }
}

// ===== 2. git_diff =====

export class GitDiffTool implements IMetonaTool {
  readonly definition: MetonaToolDef = {
    name: 'git_diff',
    description: 'Show changes between the working tree and the index (or staged changes). Returns unified diff text. Output is truncated at 50KB.',
    parameters: {
      type: 'object',
      properties: {
        cached: { type: 'boolean', description: 'Show only staged (cached) changes (default false)' },
        pathspec: { type: 'string', description: 'Limit to a path scope, e.g. "src/"' },
        contextLines: { type: 'number', description: 'Number of context lines (default 3)' },
      },
    },
    category: MetonaToolCategory.FILESYSTEM,
    riskLevel: MetonaRiskLevel.SAFE,
    requiresPermission: false,
    timeoutMs: 15_000,
  };

  async execute(args: Record<string, unknown>, context: ToolExecutionContext): Promise<unknown> {
    const cached = (args.cached as boolean) ?? false;
    const pathspec = args.pathspec as string | undefined;
    const contextLines = Math.max(0, (args.contextLines as number) ?? 3);

    // #19 修复: 校验 pathspec 在工作空间内，防止读取任意仓库差异
    // 攻击场景：pathspec 传 "../../../other-repo/" 或绝对路径可越权读取工作空间外的 git 差异
    // resolve 后 isPathWithinWorkspace 校验，glob 字符（*?）不影响前缀校验
    if (pathspec) {
      // 审查修复 M18: git magic pathspec（以 : 开头，如 :(glob)**/*.ts）跳过 resolve 校验。
      // resolve 会把 magic 前缀当普通字符拼接到路径，导致 pathspec 被污染；
      // git magic 语法本身不构成路径越权风险（git 自身解析 magic 前缀，不指向工作空间外）。
      if (pathspec.startsWith(':')) {
        // git magic pathspec，跳过 resolve 校验直接放行
      } else {
        const resolved = resolve(context.workspacePath, pathspec);
        if (!isPathWithinWorkspace(resolved, context.workspacePath)) {
          return { error: `Path outside workspace: ${pathspec}`, success: false };
        }
      }
    }

    const gitArgs = ['diff'];
    if (cached) gitArgs.push('--cached');
    gitArgs.push(`--unified=${contextLines}`);
    if (pathspec) gitArgs.push('--', pathspec);

    try {
      // v0.3.1 修复 WARN-4: 使用 5MB maxBuffer 支持超大 diff，避免 maxBuffer exceeded 报错
      // 工具内部仍会截断到 50KB 返回，避免 LLM 上下文溢出
      const diff = await runGit(gitArgs, context.workspacePath, 10_000, 5 * 1024 * 1024);
      const truncated = diff.length > MAX_DIFF_SIZE;
      const truncatedDiff = truncated ? diff.slice(0, MAX_DIFF_SIZE) : diff;
      const filesChanged = this.countFilesChanged(diff);

      return {
        diff: truncatedDiff,
        filesChanged,
        truncated,
      };
    } catch (error) {
      if (isNotGitRepoError(error)) {
        return { error: 'Not a git repository or git not available', success: false };
      }
      return { error: extractErrorMessage(error), success: false };
    }
  }

  /** 统计 diff 中变更的文件数（每变更文件一个 `diff --git` 头） */
  private countFilesChanged(diff: string): number {
    let count = 0;
    for (const line of diff.split('\n')) {
      if (line.startsWith('diff --git ')) count++;
    }
    return count;
  }
}

// ===== 3. git_log =====

export class GitLogTool implements IMetonaTool {
  readonly definition: MetonaToolDef = {
    name: 'git_log',
    description: 'Show commit history. Returns commits with hash, message, and optionally author/date. Supports filtering by author and path.',
    parameters: {
      type: 'object',
      properties: {
        limit: { type: 'number', description: 'Number of entries to return (default 20, max 100)' },
        oneline: { type: 'boolean', description: 'Use compact one-line format (default true)' },
        pathspec: { type: 'string', description: 'Limit to a path scope' },
        author: { type: 'string', description: 'Filter by author name/email' },
      },
    },
    category: MetonaToolCategory.FILESYSTEM,
    riskLevel: MetonaRiskLevel.SAFE,
    requiresPermission: false,
    timeoutMs: 15_000,
  };

  async execute(args: Record<string, unknown>, context: ToolExecutionContext): Promise<unknown> {
    const limit = Math.min(100, Math.max(1, (args.limit as number) ?? 20));
    const oneline = (args.oneline as boolean) ?? true;
    const pathspec = args.pathspec as string | undefined;
    const author = args.author as string | undefined;

    const gitArgs = ['log', `-n${limit}`];
    if (oneline) {
      gitArgs.push('--oneline');
    } else {
      // 用 NULL 字节分隔字段，避免 author/message 含 '|' 导致解析错误
      gitArgs.push('--format=%H%x00%an%x00%ad%x00%s');
    }
    // v0.3.1 修复 WARN-8: 拆分为独立参数，避免特殊字符干扰 git 解析
    if (author) gitArgs.push('--author', author);
    if (pathspec) gitArgs.push('--', pathspec);

    try {
      // 并行获取日志和当前分支名
      const [output, branch] = await Promise.all([
        runGit(gitArgs, context.workspacePath),
        getCurrentBranch(context.workspacePath),
      ]);

      const commits = this.parseLog(output, oneline);
      return { commits, count: commits.length, branch };
    } catch (error) {
      if (isNotGitRepoError(error)) {
        return { error: 'Not a git repository or git not available', success: false };
      }
      return { error: extractErrorMessage(error), success: false };
    }
  }

  private parseLog(
    output: string,
    oneline: boolean,
  ): Array<{ hash: string; author?: string; date?: string; message: string }> {
    const lines = output.split('\n').filter((l) => l.length > 0);
    const commits: Array<{ hash: string; author?: string; date?: string; message: string }> = [];

    for (const line of lines) {
      if (oneline) {
        // <short-hash> <space> <message>
        const spaceIdx = line.indexOf(' ');
        if (spaceIdx < 0) continue;
        commits.push({
          hash: line.slice(0, spaceIdx),
          message: line.slice(spaceIdx + 1),
        });
      } else {
        // <hash>\0<author>\0<date>\0<message>
        const parts = line.split('\0');
        if (parts.length < 4) continue;
        commits.push({
          hash: parts[0],
          author: parts[1],
          date: parts[2],
          message: parts[3],
        });
      }
    }

    return commits;
  }
}

// ===== 4. git_commit =====

export class GitCommitTool implements IMetonaTool {
  readonly definition: MetonaToolDef = {
    name: 'git_commit',
    description: 'Stage files and create a git commit. Supports amending the previous commit. In amend mode, message is optional (omitting it keeps the original commit message). Requires user confirmation.',
    parameters: {
      type: 'object',
      properties: {
        message: { type: 'string', description: 'Commit message. Required for new commits. Optional in amend mode (omitting keeps original message).' },
        files: {
          type: 'array',
          items: { type: 'string', description: 'File path to stage' },
          description: 'Files to stage (default: all changes via git add -A)',
        },
        amend: { type: 'boolean', description: 'Amend the previous commit instead of creating a new one (default false)' },
      },
      // F1-3: message 不再硬性 required — amend 模式下可省略（保留原 message）
      // 校验在 execute 内根据 amend 标志动态进行
    },
    category: MetonaToolCategory.FILESYSTEM,
    riskLevel: MetonaRiskLevel.MEDIUM,
    requiresPermission: true,
    timeoutMs: 30_000,
  };

  async execute(args: Record<string, unknown>, context: ToolExecutionContext): Promise<unknown> {
    const message = args.message as string | undefined;
    const files = (args.files as string[] | undefined) ?? [];
    const amend = (args.amend as boolean) ?? false;

    // F1-3: 非 amend 模式必须有 message；amend 模式可省略（保留原 message）
    if (!amend && (!message || message.trim().length === 0)) {
      return { success: false, error: 'Commit message is required (use amend: true to reuse the previous message)' };
    }
    // amend 模式下若提供 message，需校验非空字符串
    if (amend && message !== undefined && message.trim().length === 0) {
      return { success: false, error: 'Commit message must not be empty (or omit message field to keep original)' };
    }

    try {
      // 1. 暂存文件（指定文件 → git add -- <files>；否则 git add -A）
      // v0.3.1 修复 WARN-1: 校验每个 file 路径在工作空间内
      if (files.length > 0) {
        for (const f of files) {
          const resolved = resolve(context.workspacePath, f);
          if (!isPathWithinWorkspace(resolved, context.workspacePath)) {
            return {
              success: false,
              error: `File path outside workspace: ${f}`,
              commit: '', branch: '', message: '', filesChanged: 0,
            };
          }
        }
        await runGit(['add', '--', ...files], context.workspacePath);
      } else {
        await runGit(['add', '-A'], context.workspacePath);
      }

      // 2. 统计暂存文件数（commit 前取样）
      const stagedOutput = await runGit(['diff', '--cached', '--name-only'], context.workspacePath);
      const filesChanged = stagedOutput.split('\n').filter((l) => l.length > 0).length;

      // 3. 提交（commit 可能触发 hooks，给予更长超时）
      // F1-3: amend 模式下若未提供 message，不加 -m 参数（保留原 message）
      const commitArgs = ['commit'];
      if (amend) commitArgs.push('--amend');
      if (message && message.trim().length > 0) {
        commitArgs.push('-m', message);
      } else if (amend) {
        // amend + 无 message + 无 --no-edit → git 会打开编辑器要求输入
        // 加 --no-edit 明确表示保留原 message（防止 hang 等待编辑器）
        commitArgs.push('--no-edit');
      }
      await runGit(commitArgs, context.workspacePath, 20_000);

      // 4. 获取提交 hash 和当前分支
      const [commit, branch] = await Promise.all([
        runGit(['rev-parse', 'HEAD'], context.workspacePath).then((s) => s.trim()),
        getCurrentBranch(context.workspacePath),
      ]);

      return {
        success: true,
        commit,
        branch,
        // F1-3: amend 模式下若未提供 message，返回值为空字符串（实际 message 保留原值）
        message: message ?? (amend ? '(amended - original message preserved)' : ''),
        filesChanged,
        amended: amend,
      };
    } catch (error) {
      if (isNotGitRepoError(error)) {
        return { error: 'Not a git repository or git not available', success: false };
      }
      return { success: false, error: extractErrorMessage(error) };
    }
  }
}
