/**
 * Git 快照 API 路由
 *
 * 提供 Git Stash 快照的创建、列表、恢复和删除功能
 */
import { Router } from 'express';
import { spawn } from 'node:child_process';
import { promises as fs } from 'node:fs';
import path from 'node:path';
import { validateToken } from '../middleware/auth.js';
export const gitRouter = Router();
export async function assertGitWorkspacePathAccessible(workspacePath) {
    let stat;
    try {
        stat = await fs.stat(workspacePath);
    }
    catch (error) {
        const err = error;
        if (err.code === 'ENOENT') {
            throw new Error(`workspace path does not exist: ${workspacePath}`);
        }
        throw new Error(`workspace path is not accessible: ${workspacePath}${err.message ? ` (${err.message})` : ''}`);
    }
    if (!stat.isDirectory()) {
        throw new Error(`workspace path is not a directory: ${workspacePath}`);
    }
}
/**
 * 判断 Git diff 汇总行是否应该展示在差异树中。
 * 普通 modified 且文本增删均为 0 通常是 filemode/元数据噪声，避免显示为“无差异内容”。
 */
export function shouldIncludeGitDiffSummaryFile(status, additions, deletions, isBinaryDiff) {
    if (status === 'added' || status === 'deleted' || status === 'renamed') {
        return true;
    }
    return isBinaryDiff || additions > 0 || deletions > 0;
}
/**
 * 执行 git 命令并返回输出
 */
async function execGitCommand(workspacePath, args) {
    return new Promise((resolve) => {
        const child = spawn('git', args, {
            cwd: workspacePath,
            stdio: ['ignore', 'pipe', 'pipe'],
        });
        let stdout = '';
        let stderr = '';
        child.stdout?.on('data', (chunk) => {
            stdout += String(chunk || '');
        });
        child.stderr?.on('data', (chunk) => {
            stderr += String(chunk || '');
        });
        child.on('error', (error) => {
            resolve({ stdout: '', stderr: error.message, exitCode: 1 });
        });
        child.on('close', (code) => {
            resolve({ stdout, stderr, exitCode: code ?? 1 });
        });
    });
}
function normalizeGitPath(targetPath) {
    return String(targetPath || '').replace(/\\/g, '/').replace(/^\.\//, '').trim();
}
async function hasGitMetadataDir(targetPath) {
    const gitDir = path.join(targetPath, '.git');
    try {
        const stat = await fs.stat(gitDir);
        return stat.isDirectory() || stat.isFile();
    }
    catch {
        return false;
    }
}
async function resolveGitRepositoryContext(workspacePath) {
    const normalizedPath = path.resolve(String(workspacePath).trim());
    await assertGitWorkspacePathAccessible(normalizedPath);
    const isGitRepo = await hasGitMetadataDir(normalizedPath);
    if (isGitRepo) {
        return {
            workspacePath: normalizedPath,
            repositoryRootPath: normalizedPath,
            hasRepository: true,
            isGitRepo: true,
            usingParentGitRepo: false,
            relativeWorkspacePath: ''
        };
    }
    const repoRootResult = await execGitCommand(normalizedPath, ['rev-parse', '--show-toplevel']);
    if (repoRootResult.exitCode !== 0) {
        return {
            workspacePath: normalizedPath,
            repositoryRootPath: null,
            hasRepository: false,
            isGitRepo: false,
            usingParentGitRepo: false,
            relativeWorkspacePath: ''
        };
    }
    const repositoryRootPath = path.resolve(repoRootResult.stdout.trim());
    const relativeWorkspacePath = normalizeGitPath(path.relative(repositoryRootPath, normalizedPath));
    return {
        workspacePath: normalizedPath,
        repositoryRootPath,
        hasRepository: true,
        isGitRepo: false,
        usingParentGitRepo: repositoryRootPath !== normalizedPath,
        relativeWorkspacePath
    };
}
function createScopedGitArgs(baseArgs, context) {
    if (!context.relativeWorkspacePath) {
        return [...baseArgs];
    }
    return [
        ...baseArgs,
        `--relative=${context.relativeWorkspacePath}`,
        '--',
        context.relativeWorkspacePath
    ];
}
function resolveRepositoryRelativeFilePath(filePath, context) {
    const normalizedFilePath = normalizeGitPath(filePath);
    if (!context.relativeWorkspacePath) {
        return normalizedFilePath;
    }
    return normalizeGitPath(path.posix.join(context.relativeWorkspacePath, normalizedFilePath));
}
function resolveGitDiffStatus(statusToken) {
    const normalizedToken = String(statusToken || '').trim().toUpperCase();
    if (normalizedToken.startsWith('A')) {
        return 'added';
    }
    if (normalizedToken.startsWith('D')) {
        return 'deleted';
    }
    if (normalizedToken.startsWith('R')) {
        return 'renamed';
    }
    return 'modified';
}
function parseRenameDisplayPath(rawPath) {
    const normalizedPath = normalizeGitPath(rawPath);
    if (!normalizedPath.includes('=>')) {
        return normalizedPath;
    }
    const braceMatch = normalizedPath.match(/^(.*)\{([^{}]+)=>([^{}]+)\}(.*)$/);
    if (braceMatch) {
        const prefix = braceMatch[1];
        const renamedTarget = braceMatch[3];
        const suffix = braceMatch[4];
        return normalizeGitPath(`${prefix}${renamedTarget}${suffix}`);
    }
    const arrowIndex = normalizedPath.lastIndexOf('=>');
    if (arrowIndex >= 0) {
        return normalizeGitPath(normalizedPath.slice(arrowIndex + 2));
    }
    return normalizedPath;
}
/**
 * 检查路径是否为 git 仓库
 * POST /runtime/git/check
 */
gitRouter.post('/git/check', validateToken, async (req, res) => {
    const { workspacePath } = req.body || {};
    if (!workspacePath || !String(workspacePath).trim()) {
        res.status(400).json({ error: 'workspacePath is required' });
        return;
    }
    try {
        const context = await resolveGitRepositoryContext(String(workspacePath).trim());
        res.json({
            ok: true,
            isGitRepo: context.isGitRepo,
            hasRepository: context.hasRepository,
            usingParentGitRepo: context.usingParentGitRepo,
            workspacePath: context.workspacePath,
            repositoryRootPath: context.repositoryRootPath,
            relativeWorkspacePath: context.relativeWorkspacePath
        });
        return;
    }
    catch (error) {
        const err = error;
        res.status(500).json({ error: err.message || 'check git repo failed' });
    }
});
/**
 * 创建 git stash 快照
 * POST /runtime/git/stash/create
 */
gitRouter.post('/git/stash/create', validateToken, async (req, res) => {
    const { workspacePath, message } = req.body || {};
    if (!workspacePath || !String(workspacePath).trim()) {
        res.status(400).json({ error: 'workspacePath is required' });
        return;
    }
    try {
        const normalizedPath = path.resolve(String(workspacePath).trim());
        const defaultMessage = `快照（${new Date().toLocaleString('zh-CN', {
            year: 'numeric',
            month: '2-digit',
            day: '2-digit',
            hour: '2-digit',
            minute: '2-digit'
        })}）`;
        const stashMessage = message && String(message).trim()
            ? `快照: ${String(message).trim()}`
            : defaultMessage;
        const result = await execGitCommand(normalizedPath, ['stash', 'push', '-m', stashMessage]);
        if (result.exitCode !== 0) {
            if (result.stderr.includes('No local changes to save')) {
                res.json({
                    ok: true,
                    stashRef: null,
                    message: '没有本地变更需要保存',
                    noChanges: true,
                    workspacePath: normalizedPath
                });
                return;
            }
            res.status(400).json({ error: result.stderr || 'git stash push failed' });
            return;
        }
        const listResult = await execGitCommand(normalizedPath, ['stash', 'list', '-n', '1']);
        const firstLine = listResult.stdout.split('\n')[0] || '';
        const match = firstLine.match(/^(stash@\{\d+\})/);
        const stashRef = match ? match[1] : 'stash@{0}';
        res.json({
            ok: true,
            stashRef,
            message: stashMessage,
            workspacePath: normalizedPath
        });
    }
    catch (error) {
        const err = error;
        res.status(500).json({ error: err.message || 'create stash failed' });
    }
});
/**
 * 获取 git stash 列表
 * POST /runtime/git/stash/list
 */
gitRouter.post('/git/stash/list', validateToken, async (req, res) => {
    const { workspacePath, limit = 20 } = req.body || {};
    if (!workspacePath || !String(workspacePath).trim()) {
        res.status(400).json({ error: 'workspacePath is required' });
        return;
    }
    try {
        const normalizedPath = path.resolve(String(workspacePath).trim());
        const stashLimit = Math.min(Math.max(1, Number(limit) || 20), 100);
        const result = await execGitCommand(normalizedPath, ['stash', 'list', `-n`, String(stashLimit)]);
        if (result.exitCode !== 0) {
            res.status(400).json({ error: result.stderr || 'git stash list failed' });
            return;
        }
        const stashes = [];
        const lines = result.stdout.split('\n').filter(line => line.trim());
        for (const line of lines) {
            const refMatch = line.match(/^(stash@\{\d+\})/);
            if (!refMatch)
                continue;
            const ref = refMatch[1];
            const restOfLine = line.slice(ref.length + 2);
            let branch = '';
            let message = restOfLine;
            const branchMatch = restOfLine.match(/^(?:On|WIP on)\s+([^:]+):\s*(.*)$/);
            if (branchMatch) {
                branch = branchMatch[1].trim();
                message = branchMatch[2].trim();
            }
            stashes.push({
                ref,
                message,
                branch,
                createdAt: null,
            });
        }
        res.json({
            ok: true,
            stashes,
            count: stashes.length,
            workspacePath: normalizedPath
        });
    }
    catch (error) {
        const err = error;
        res.status(500).json({ error: err.message || 'list stash failed' });
    }
});
/**
 * 恢复 git stash 快照
 * POST /runtime/git/stash/pop
 */
gitRouter.post('/git/stash/pop', validateToken, async (req, res) => {
    const { workspacePath, ref } = req.body || {};
    if (!workspacePath || !String(workspacePath).trim()) {
        res.status(400).json({ error: 'workspacePath is required' });
        return;
    }
    try {
        const normalizedPath = path.resolve(String(workspacePath).trim());
        const stashRef = ref && String(ref).trim() ? String(ref).trim() : 'stash@{0}';
        await execGitCommand(normalizedPath, ['checkout', '--', '.']);
        const result = await execGitCommand(normalizedPath, ['stash', 'pop', stashRef]);
        if (result.exitCode !== 0) {
            if (result.stdout.includes('CONFLICT') || result.stderr.includes('CONFLICT')) {
                res.json({
                    ok: false,
                    hasConflict: true,
                    message: '恢复时发生冲突，请手动解决',
                    details: result.stdout || result.stderr,
                    workspacePath: normalizedPath
                });
                return;
            }
            if (result.stderr.includes('is not a valid reference')) {
                res.status(404).json({ error: `快照 ${stashRef} 不存在` });
                return;
            }
            res.status(400).json({ error: result.stderr || 'git stash pop failed' });
            return;
        }
        res.json({
            ok: true,
            stashRef,
            message: `已恢复 ${stashRef}`,
            workspacePath: normalizedPath
        });
    }
    catch (error) {
        const err = error;
        res.status(500).json({ error: err.message || 'pop stash failed' });
    }
});
/**
 * 删除指定的 git stash
 * POST /runtime/git/stash/drop
 */
gitRouter.post('/git/stash/drop', validateToken, async (req, res) => {
    const { workspacePath, ref } = req.body || {};
    if (!workspacePath || !String(workspacePath).trim()) {
        res.status(400).json({ error: 'workspacePath is required' });
        return;
    }
    try {
        const normalizedPath = path.resolve(String(workspacePath).trim());
        const stashRef = ref && String(ref).trim() ? String(ref).trim() : 'stash@{0}';
        const result = await execGitCommand(normalizedPath, ['stash', 'drop', stashRef]);
        if (result.exitCode !== 0) {
            if (result.stderr.includes('is not a valid reference')) {
                res.status(404).json({ error: `快照 ${stashRef} 不存在` });
                return;
            }
            res.status(400).json({ error: result.stderr || 'git stash drop failed' });
            return;
        }
        res.json({
            ok: true,
            stashRef,
            message: `已删除 ${stashRef}`,
            workspacePath: normalizedPath
        });
    }
    catch (error) {
        const err = error;
        res.status(500).json({ error: err.message || 'drop stash failed' });
    }
});
/**
 * 获取工作区未提交变更概览
 * POST /runtime/git/diff
 */
gitRouter.post('/git/diff', validateToken, async (req, res) => {
    const { workspacePath, repositoryPath } = req.body || {};
    if (!workspacePath || !String(workspacePath).trim()) {
        res.status(400).json({ error: 'workspacePath is required' });
        return;
    }
    try {
        const context = await resolveGitRepositoryContext(String(repositoryPath || workspacePath).trim());
        if (!context.hasRepository || !context.repositoryRootPath) {
            res.json({
                ok: true,
                isGitRepo: context.isGitRepo,
                hasRepository: false,
                usingParentGitRepo: false,
                hasChanges: false,
                files: [],
                workspacePath: context.workspacePath,
                repositoryRootPath: null,
                relativeWorkspacePath: ''
            });
            return;
        }
        const nameStatusResult = await execGitCommand(context.repositoryRootPath, createScopedGitArgs(['diff', '--name-status'], context));
        if (nameStatusResult.exitCode !== 0) {
            res.status(400).json({ error: nameStatusResult.stderr || 'git diff failed' });
            return;
        }
        const result = await execGitCommand(context.repositoryRootPath, createScopedGitArgs(['diff', '--numstat'], context));
        if (result.exitCode !== 0) {
            res.status(400).json({ error: result.stderr || 'git diff failed' });
            return;
        }
        const statusMap = new Map();
        const nameStatusLines = nameStatusResult.stdout.split('\n').filter(line => line.trim());
        for (const line of nameStatusLines) {
            const parts = line.split('\t');
            if (parts.length < 2) {
                continue;
            }
            const status = resolveGitDiffStatus(parts[0]);
            const pathIndex = status === 'renamed' && parts.length >= 3 ? 2 : 1;
            const displayPath = parseRenameDisplayPath(parts[pathIndex]);
            if (!displayPath) {
                continue;
            }
            statusMap.set(displayPath, status);
        }
        const files = [];
        const lines = result.stdout.split('\n').filter(line => line.trim());
        for (const line of lines) {
            // 格式: "12\t5\tsrc/file.java"
            const parts = line.split('\t');
            if (parts.length < 3)
                continue;
            const isBinaryDiff = parts[0] === '-' || parts[1] === '-';
            const additions = parts[0] === '-' ? 0 : parseInt(parts[0], 10) || 0;
            const deletions = parts[1] === '-' ? 0 : parseInt(parts[1], 10) || 0;
            const filePath = parseRenameDisplayPath(parts.slice(2).join('\t').trim());
            const status = statusMap.get(filePath) || 'modified';
            if (!filePath || !shouldIncludeGitDiffSummaryFile(status, additions, deletions, isBinaryDiff)) {
                continue;
            }
            files.push({ path: filePath, status, additions, deletions });
        }
        res.json({
            ok: true,
            isGitRepo: context.isGitRepo,
            hasRepository: true,
            usingParentGitRepo: context.usingParentGitRepo,
            hasChanges: files.length > 0,
            files,
            workspacePath: context.workspacePath,
            repositoryRootPath: context.repositoryRootPath,
            relativeWorkspacePath: context.relativeWorkspacePath
        });
    }
    catch (error) {
        const err = error;
        res.status(500).json({ error: err.message || 'git diff failed' });
    }
});
/**
 * 获取单个文件的详细差异
 * POST /runtime/git/diff-file
 */
gitRouter.post('/git/diff-file', validateToken, async (req, res) => {
    const { workspacePath, filePath } = req.body || {};
    if (!workspacePath || !String(workspacePath).trim()) {
        res.status(400).json({ error: 'workspacePath is required' });
        return;
    }
    if (!filePath || !String(filePath).trim()) {
        res.status(400).json({ error: 'filePath is required' });
        return;
    }
    try {
        // 必须使用 workspacePath 来正确计算 relativeWorkspacePath
        // 如果使用 repositoryPath（仓库根路径），会丢失工作目录信息，导致文件路径拼接错误
        const context = await resolveGitRepositoryContext(String(workspacePath).trim());
        const normalizedFilePath = normalizeGitPath(String(filePath).trim());
        if (!context.hasRepository || !context.repositoryRootPath) {
            res.status(400).json({ error: 'workspace is not inside a git repository' });
            return;
        }
        const repositoryRelativeFilePath = resolveRepositoryRelativeFilePath(normalizedFilePath, context);
        const result = await execGitCommand(context.repositoryRootPath, ['diff', '--', repositoryRelativeFilePath]);
        if (result.exitCode !== 0) {
            res.status(400).json({ error: result.stderr || 'git diff file failed' });
            return;
        }
        res.json({
            ok: true,
            filePath: normalizedFilePath,
            repositoryRelativeFilePath,
            diffContent: result.stdout,
            workspacePath: context.workspacePath,
            repositoryRootPath: context.repositoryRootPath,
            usingParentGitRepo: context.usingParentGitRepo
        });
    }
    catch (error) {
        const err = error;
        res.status(500).json({ error: err.message || 'git diff file failed' });
    }
});
