/**
 * Git Clone Utility
 *
 * Shallow-clones repositories into ~/.codragraph/repos/{name}/.
 * If already cloned, does git pull instead.
 */
import { spawn } from 'child_process';
import path from 'path';
import os from 'os';
import fs from 'fs/promises';
import { isIP } from 'net';
/** Extract the repository name from a git URL (HTTPS or SSH). */
export function extractRepoName(url) {
    const cleaned = url.replace(/\/+$/, '');
    const lastSegment = cleaned.split(/[/:]/).pop() || 'unknown';
    return lastSegment.replace(/\.git$/, '');
}
/** Get the clone target directory for a repo name. */
export function getCloneDir(repoName) {
    return path.join(os.homedir(), '.codragraph', 'repos', repoName);
}
// Cloud metadata hostnames that must never be reachable via user-supplied URLs
const BLOCKED_HOSTNAMES = new Set([
    'localhost',
    'metadata.google.internal',
    'metadata.azure.com',
    'metadata.internal',
]);
/**
 * Validate a git URL to prevent SSRF attacks.
 * Only allows https:// and http:// schemes. Blocks private/internal addresses,
 * IPv6 private ranges, cloud metadata hostnames, and numeric IP encodings.
 */
export function validateGitUrl(url) {
    let parsed;
    try {
        parsed = new URL(url);
    }
    catch {
        throw new Error('Invalid URL');
    }
    if (!['https:', 'http:'].includes(parsed.protocol)) {
        throw new Error('Only https:// and http:// git URLs are allowed');
    }
    const host = parsed.hostname.toLowerCase();
    // Block known dangerous hostnames (cloud metadata services)
    if (BLOCKED_HOSTNAMES.has(host)) {
        throw new Error('Cloning from private/internal addresses is not allowed');
    }
    // Strip IPv6 brackets if present (URL parser behavior varies across Node versions)
    let normalizedHost = host;
    if (host.startsWith('[') && host.endsWith(']')) {
        normalizedHost = host.slice(1, -1);
    }
    // Check if this is an IPv6 address
    // Use manual colon detection as fallback since isIP may return 0 for some
    // normalized IPv6 forms (e.g. ::ffff:7f00:1)
    const isIPv6 = isIP(normalizedHost) === 6 || normalizedHost.includes(':');
    if (isIPv6) {
        assertNotPrivateIPv6(normalizedHost);
        return;
    }
    // Check if this is an IPv4 address (including numeric encodings)
    if (isIP(normalizedHost) === 4) {
        assertNotPrivateIPv4(normalizedHost);
        return;
    }
    // For non-IP hostnames, check for numeric IP tricks
    // Decimal encoding: 2130706433 = 127.0.0.1
    // Hex encoding: 0x7f000001 = 127.0.0.1
    if (/^\d+$/.test(host) || /^0x[0-9a-f]+$/i.test(host)) {
        throw new Error('Cloning from private/internal addresses is not allowed');
    }
    // Standard IPv4 regex checks for dotted notation
    if (/^127\./.test(host) ||
        /^10\./.test(host) ||
        /^172\.(1[6-9]|2\d|3[01])\./.test(host) ||
        /^192\.168\./.test(host) ||
        /^169\.254\./.test(host) ||
        /^0\./.test(host) ||
        host === '0.0.0.0' ||
        /^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(host) ||
        /^198\.1[89]\./.test(host)) {
        throw new Error('Cloning from private/internal addresses is not allowed');
    }
}
function assertNotPrivateIPv6(ip) {
    // Expand common compressed forms for comparison
    const lower = ip.toLowerCase();
    // IPv6 loopback
    if (lower === '::1' || lower === '0:0:0:0:0:0:0:1') {
        throw new Error('Cloning from private/internal addresses is not allowed');
    }
    // Unspecified address
    if (lower === '::' || lower === '0:0:0:0:0:0:0:0') {
        throw new Error('Cloning from private/internal addresses is not allowed');
    }
    // IPv6 Unique Local Address (fc00::/7 = fc and fd prefixes)
    if (lower.startsWith('fc') || lower.startsWith('fd')) {
        throw new Error('Cloning from private/internal addresses is not allowed');
    }
    // IPv6 link-local (fe80::/10)
    if (lower.startsWith('fe80') ||
        lower.startsWith('fe8') ||
        lower.startsWith('fe9') ||
        lower.startsWith('fea') ||
        lower.startsWith('feb')) {
        throw new Error('Cloning from private/internal addresses is not allowed');
    }
    // IPv4-mapped IPv6 (::ffff:x.x.x.x or ::ffff:hex:hex)
    // Node may normalize ::ffff:127.0.0.1 to ::ffff:7f00:1
    if (lower.startsWith('::ffff:')) {
        throw new Error('Cloning from private/internal addresses is not allowed');
    }
    // Also catch the expanded form: 0:0:0:0:0:ffff:
    if (lower.includes(':ffff:')) {
        throw new Error('Cloning from private/internal addresses is not allowed');
    }
}
function assertNotPrivateIPv4(ip) {
    const parts = ip.split('.').map(Number);
    const [a, b] = parts;
    if (a === 127 ||
        a === 10 ||
        (a === 172 && b >= 16 && b <= 31) ||
        (a === 192 && b === 168) ||
        (a === 169 && b === 254) ||
        a === 0 ||
        (a === 100 && b >= 64 && b <= 127) ||
        (a === 198 && (b === 18 || b === 19))) {
        throw new Error('Cloning from private/internal addresses is not allowed');
    }
}
/**
 * Clone or pull a git repository.
 * If targetDir doesn't exist: git clone --depth 1
 * If targetDir exists with .git: git pull --ff-only
 */
export async function cloneOrPull(url, targetDir, onProgress) {
    const exists = await fs.access(path.join(targetDir, '.git')).then(() => true, () => false);
    if (exists) {
        onProgress?.({ phase: 'pulling', message: 'Pulling latest changes...' });
        await runGit(['pull', '--ff-only'], targetDir);
    }
    else {
        validateGitUrl(url);
        await fs.mkdir(path.dirname(targetDir), { recursive: true });
        onProgress?.({ phase: 'cloning', message: `Cloning ${url}...` });
        await runGit(['clone', '--depth', '1', url, targetDir]);
    }
    return targetDir;
}
function runGit(args, cwd) {
    return new Promise((resolve, reject) => {
        const proc = spawn('git', args, {
            cwd,
            stdio: ['ignore', 'pipe', 'pipe'],
            env: {
                ...process.env,
                // Prevent git from prompting for credentials (hangs the process)
                GIT_TERMINAL_PROMPT: '0',
                // Ensure no credential helper tries to open a GUI prompt
                GIT_ASKPASS: process.platform === 'win32' ? 'echo' : '/bin/true',
            },
        });
        let stderr = '';
        proc.stderr.on('data', (chunk) => {
            stderr += chunk;
        });
        proc.on('close', (code) => {
            if (code === 0)
                resolve();
            else {
                // Log full stderr internally but don't expose it to API callers (SSRF mitigation)
                if (stderr.trim())
                    console.error(`git ${args[0]} stderr: ${stderr.trim()}`);
                reject(new Error(`git ${args[0]} failed (exit code ${code})`));
            }
        });
        proc.on('error', (err) => {
            reject(new Error(`Failed to spawn git: ${err.message}`));
        });
    });
}
