#!/usr/bin/env node
// crowdcode.js — MiniLlama Terminal CLI (Linux + Windows)
// Chat + Dateioperationen via llama.cpp API
// Aufruf: node crowdcode.js [endpoint] [model]

// ─── Konfiguration ────────────────────────────────────────────────────────────
const API = process.argv[2] || 'http://127.0.0.1:8080/v1';
const MODEL = process.argv[3] || '';

// ─── Terminal-Farben (ANSI, funktioniert auf Win/Lin) ─────────────────────────
const C = {
    reset: '\x1b[0m',
    bold: '\x1b[1m',
    dim: '\x1b[2m',
    green: '\x1b[32m',
    yellow: '\x1b[33m',
    blue: '\x1b[34m',
    magenta: '\x1b[35m',
    cyan: '\x1b[36m',
    red: '\x1b[31m',
    grey: '\x1b[90m',
};

// ─── async/await readline ─────────────────────────────────────────────────────
const readline = require('readline');
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

const WORKSPACE = process.argv[4] ? path.resolve(process.argv[4]) : process.cwd();

const SYSTEM_PROMPT = `You are CrowdCode, a concise coding assistant.
Context: @file reads a file, @dir/ lists a directory, @dir/** shows the tree, @?term searches in files.
When writing or rewriting a file, ALWAYS output FILE: path/to/file on its own line BEFORE the fenced code block with the complete file content.
To create a directory: DIR: path/to/dir on its own line.
For targeted edits: EDIT: path/to/file followed by ---OLD, old text, ---NEW, new text, --- on separate lines.
To run shell commands (only when user explicitly asks): output a fenced \`\`\`bash code block.
No explanations unless the user asks a question.
NEVER output touch, mkdir, echo, cat commands — use FILE: and DIR: instead.`;

function askSingleLine(prompt) {
    const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
    return new Promise((resolve) => {
        rl.question(prompt, (answer) => { rl.close(); resolve(answer.trim()); });
    });
}

async function askInput() {
    const line = await askSingleLine(`${C.cyan}›${C.reset} `);
    if (line === ':m') {
        // Multiline-Modus
        return askMultiline();
    }
    return line;
}

function askMultiline() {
    const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
    return new Promise((resolve) => {
        const lines = [];
        console.log(`${C.dim}  (Multiline — ${C.reset}:send${C.dim} zum Senden, ${C.reset}:cancel${C.dim} zum Abbrechen)${C.reset}`);
        rl.on('line', (line) => {
            const trimmed = line.trim();
            if (trimmed === ':send' || trimmed === ':s') { rl.close(); return; }
            if (trimmed === ':cancel' || trimmed === ':c') { rl.close(); resolve(null); return; }
            lines.push(line);
        });
        rl.on('close', () => resolve(lines.join('\n').replace(/\n+$/, '')));
    });
}

// ─── Modelle auflisten ─────────────────────────────────────────────────────────
async function fetchModels() {
    try {
        const res = await fetch(`${API}/models`);
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const data = await res.json();
        return data.data || data.models || [];
    } catch { return []; }
}

// ─── Chat Completion (Streaming) ───────────────────────────────────────────────
async function chatComplete(messages, onToken) {
    const model = MODEL || (await fetchModels())[0]?.id || 'unknown';
    const res = await fetch(`${API}/chat/completions`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer local' },
        body: JSON.stringify({ model, messages, stream: true, temperature: 0.2, top_p: 0.9 }),
    });
    if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    let fullText = '';
    while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        const chunk = decoder.decode(value, { stream: true });
        for (const line of chunk.split('\n')) {
            const trimmed = line.trim();
            if (!trimmed.startsWith('data:')) continue;
            const data = trimmed.slice(5).trim();
            if (data === '[DONE]') continue;
            try {
                const parsed = JSON.parse(data);
                // reasoning_content für DeepSeek-artige Modelle
                const reasoning = parsed.choices?.[0]?.delta?.reasoning_content || '';
                const token = parsed.choices?.[0]?.delta?.content || '';
                if (reasoning) {
                    fullText += reasoning;
                    onToken(reasoning, true);
                }
                if (token) {
                    fullText += token;
                    onToken(token, false);
                }
            } catch { /* skip malformed JSON */ }
        }
    }
    return fullText;
}

// ─── @mention Auflösung ───────────────────────────────────────────────────────
async function resolveMentions(text) {
    const mentionRegex = /@(\S+)/g;
    let result = text;
    for (const match of text.matchAll(mentionRegex)) {
        const ref = match[1];
        let replacement = '';
        try {
            const targetPath = path.resolve(WORKSPACE, ref);
            // @dir/** — tree
            if (ref.endsWith('/**')) {
                const dirPath = ref.slice(0, -3);
                replacement = await buildTree(path.resolve(WORKSPACE, dirPath), '');
            }
            // @dir/ — listing
            else if (ref.endsWith('/')) {
                const dirPath = ref.slice(0, -1);
                const entries = fs.readdirSync(path.resolve(WORKSPACE, dirPath), { withFileTypes: true });
                entries.sort((a, b) => {
                    if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
                    return a.name.localeCompare(b.name);
                });
                replacement = entries.map(e => e.isDirectory() ? e.name + '/' : e.name).join('\n');
            }
            // @?term — grep
            else if (ref.startsWith('?')) {
                const term = ref.slice(1);
                const isGlob = term.includes('*');
                replacement = isGlob ? await globSearch(term) : await grepSearch(term);
            }
            // @file.ext — file read
            else if (fs.existsSync(targetPath) && fs.statSync(targetPath).isFile()) {
                const content = fs.readFileSync(targetPath, 'utf8');
                const ext = ref.split('.').pop() || '';
                replacement = `\n[Dateiinhalt: ${ref}]\n\`\`\`${ext}\n${content}\n\`\`\`\n`;
            }
        } catch { replacement = `(${ref}: nicht gefunden) ⚠️`; }
        result = result.replace(match[0], replacement);
    }
    return result;
}

async function buildTree(dirPath, prefix) {
    const entries = fs.readdirSync(dirPath, { withFileTypes: true });
    entries.sort((a, b) => {
        if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
        return a.name.localeCompare(b.name);
    });
    const lines = [];
    for (let i = 0; i < entries.length; i++) {
        const e = entries[i];
        const isLast = i === entries.length - 1;
        const conn = isLast ? '└── ' : '├── ';
        lines.push(`${prefix}${conn}${e.name}${e.isDirectory() ? '/' : ''}`);
        if (e.isDirectory()) {
            const childPrefix = prefix + (isLast ? '    ' : '│   ');
            const sub = await buildTree(path.join(dirPath, e.name), childPrefix);
            lines.push(...sub);
        }
    }
    return lines.join('\n');
}

async function grepSearch(term) {
    const results = [];
    const maxFiles = 20;
    const maxPerFile = 5;
    let filesWithMatches = 0;
    const allFiles = walkSync(WORKSPACE).filter(f => !f.includes('node_modules') && !f.includes('.git'));
    for (const file of allFiles) {
        if (filesWithMatches >= maxFiles) break;
        try {
            const content = fs.readFileSync(file, 'utf8');
            const fileResults = [];
            const lines = content.split('\n');
            for (let i = 0; i < lines.length; i++) {
                if (lines[i].toLowerCase().includes(term.toLowerCase())) {
                    const relPath = path.relative(WORKSPACE, file);
                    fileResults.push(`  ${relPath}:${i + 1}: ${lines[i].trim()}`);
                    if (fileResults.length >= maxPerFile) break;
                }
            }
            if (fileResults.length > 0) { results.push(...fileResults); filesWithMatches++; }
        } catch { /* skip */ }
    }
    return results.length > 0 ? results.join('\n') : `(keine Treffer für "${term}")`;
}

async function globSearch(pattern) {
    const allFiles = walkSync(WORKSPACE).filter(f => !f.includes('node_modules') && !f.includes('.git'));
    const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$', 'i');
    const matches = allFiles.map(f => path.relative(WORKSPACE, f)).filter(f => regex.test(f));
    return matches.length > 0 ? matches.join('\n') : `(keine Dateien für "${pattern}")`;
}

function walkSync(dir) {
    const files = [];
    try {
        for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
            const fullPath = path.join(dir, entry.name);
            if (entry.isDirectory()) {
                if (entry.name !== 'node_modules' && entry.name !== '.git') files.push(...walkSync(fullPath));
            } else { files.push(fullPath); }
        }
    } catch { /* permission denied, skip */ }
    return files;
}

// ─── Aktionen aus LLM-Response ausführen ─────────────────────────────────────
function executeActions(response) {
    const actions = [];

    // FILE: pattern
    const fileRegex = /FILE:\s*(.+)\n```(?:\w+)?\n([\s\S]+?)```/g;
    let m;
    while ((m = fileRegex.exec(response)) !== null) {
        actions.push({ type: 'file', path: m[1].trim(), content: m[2] });
    }

    // DIR: pattern
    const dirRegex = /^DIR:\s*(.+)$/gm;
    while ((m = dirRegex.exec(response)) !== null) {
        actions.push({ type: 'dir', path: m[1].trim() });
    }

    // EDIT: pattern
    const editRegex = /EDIT:\s*(.+?)\n---OLD\n([\s\S]*?)\n---NEW\n([\s\S]*?)\n---/gm;
    while ((m = editRegex.exec(response)) !== null) {
        actions.push({ type: 'edit', path: m[1].trim(), oldText: m[2], newText: m[3] });
    }

    // Bash pattern
    const bashRegex = /```bash\n([\s\S]+?)```/g;
    while ((m = bashRegex.exec(response)) !== null) {
        actions.push({ type: 'bash', command: m[1].trim() });
    }

    return actions;
}

async function applyActions(actions) {
    for (const a of actions) {
        switch (a.type) {
            case 'file': {
                const fullPath = path.resolve(WORKSPACE, a.path);
                const parentDir = path.dirname(fullPath);
                fs.mkdirSync(parentDir, { recursive: true });
                fs.writeFileSync(fullPath, a.content, 'utf8');
                console.log(`  ${C.green}✓${C.reset} Datei gespeichert: ${path.relative(WORKSPACE, fullPath)}`);
                break;
            }
            case 'dir': {
                const fullPath = path.resolve(WORKSPACE, a.path);
                fs.mkdirSync(fullPath, { recursive: true });
                console.log(`  ${C.yellow}✓${C.reset} Verzeichnis erstellt: ${path.relative(WORKSPACE, fullPath)}`);
                break;
            }
            case 'edit': {
                const fullPath = path.resolve(WORKSPACE, a.path);
                if (fs.existsSync(fullPath)) {
                    let content = fs.readFileSync(fullPath, 'utf8');
                    if (content.includes(a.oldText)) {
                        content = content.replace(a.oldText, a.newText);
                        fs.writeFileSync(fullPath, content, 'utf8');
                        console.log(`  ${C.magenta}✓${C.reset} Edit angewendet: ${path.relative(WORKSPACE, fullPath)}`);
                    } else {
                        console.log(`  ${C.red}✗${C.reset} Edit fehlgeschlagen: "${a.oldText}" nicht gefunden in ${path.relative(WORKSPACE, fullPath)}`);
                    }
                } else {
                    console.log(`  ${C.red}✗${C.reset} Datei nicht gefunden: ${path.relative(WORKSPACE, fullPath)}`);
                }
                break;
            }
            case 'bash': {
                console.log(`  ${C.cyan}⚡${C.reset} Bash-Befehl erkannt:`);
                console.log(`    ${C.grey}${a.command}${C.reset}`);
                const confirm = await askSingleLine(`  ${C.yellow}Ausführen?${C.reset} (j/N) `);
                if (confirm.toLowerCase() === 'j' || confirm.toLowerCase() === 'ja') {
                    try {
                        const out = execSync(a.command, { cwd: WORKSPACE, timeout: 30000, encoding: 'utf8', maxBuffer: 1024 * 512 });
                        if (out) console.log(`  ${C.grey}${out.split('\n').map(l => '  ' + l).join('\n')}${C.reset}`);
                        console.log(`  ${C.green}✓${C.reset} Bash beendet (exit 0)`);
                    } catch (e) {
                        console.log(`  ${C.red}✗${C.reset} Bash-Fehler: ${e.stderr || e.message}`);
                    }
                } else {
                    console.log(`  ${C.dim}  (übersprungen)${C.reset}`);
                }
                break;
            }
        }
    }
}

// ─── Modelle aus dem MiniLlama/models-Ordner anzeigen ─────────────────────────
function getLocalModels() {
    const modelsDir = path.join(__dirname, '..', 'models');
    if (!fs.existsSync(modelsDir)) return [];
    return fs.readdirSync(modelsDir)
        .filter(f => f.endsWith('.gguf'))
        .sort()
        .map((name, i) => {
            const fullPath = path.join(modelsDir, name);
            try {
                const size = (fs.statSync(fullPath).size / 1024 / 1024 / 1024).toFixed(1);
                return { index: i + 1, name, path: fullPath, size };
            } catch { return { index: i + 1, name, path: fullPath, size: '?' }; }
        });
}

function showLocalModels(models) {
    if (models.length === 0) return;
    console.log(`\n${C.bold}📦 Verfügbare Modelle:${C.reset}`);
    models.forEach(m => {
        console.log(`  ${C.cyan}${m.index})${C.reset} ${m.name} ${C.grey}(${m.size} GB)${C.reset}`);
    });
}

// ─── Server starten ───────────────────────────────────────────────────────────
function findLlamaServer() {
    // Pfade für llama-server Binary suchen
    const candidates = [
        path.join(__dirname, '..', 'bin', 'linux', 'llama-server'),
        path.join(__dirname, '..', 'bin', 'win', 'llama-server.exe'),
    ];
    for (const p of candidates) {
        if (fs.existsSync(p)) return p;
    }
    return null;
}

async function startServerWithModel(localModels) {
    showLocalModels(localModels);
    if (localModels.length === 0) {
        console.log(`\n${C.red}❌ Keine .gguf-Modelle im MiniLlama/models/-Ordner gefunden.${C.reset}`);
        process.exit(1);
    }

    // User wählt Modell per Ziffer
    let modelPath = null;
    while (!modelPath) {
        const input = await askSingleLine(`\n${C.yellow}🔌 Server starten mit Modell Nr.${C.reset} [1-${localModels.length}] ${C.dim}(Enter=3=kleinstes)${C.reset}: `);
        const choice = input ? parseInt(input) : 3;
        if (!isNaN(choice) && choice >= 1 && choice <= localModels.length) {
            modelPath = localModels[choice - 1];
        } else {
            console.log(`  ${C.red}Bitte eine Zahl zwischen 1 und ${localModels.length}${C.reset}`);
        }
    }

    const serverBin = findLlamaServer();
    if (!serverBin) {
        console.log(`\n${C.red}❌ Kein llama-server Binary gefunden.${C.reset}`);
        console.log(`  ${C.dim}Erwartet in bin/linux/llama-server oder bin/win/llama-server.exe${C.reset}`);
        process.exit(1);
    }

    console.log(`\n${C.dim}🚀 Starte Server mit ${modelPath.name}...${C.reset}\n`);

    const { spawn } = require('child_process');
    const MINILLAMA_DIR = path.join(__dirname, '..');

    return new Promise((resolve, reject) => {
        const server = spawn(serverBin, [
            '-m', modelPath.path,
            '--host', '127.0.0.1',
            '--port', '8080',
            '--ctx-size', '4096',
            '--n-gpu-layers', '0',
        ], {
            cwd: MINILLAMA_DIR,
            stdio: ['ignore', 'pipe', 'pipe'],
        });

        let started = false;

        server.stdout.on('data', (data) => {
            const text = data.toString();
            process.stdout.write(text); // Live-Logs zeigen
            if (!started && (text.includes('model loaded') || text.includes('listening on') || text.includes('start'))) {
                started = true;
                // Kurz warten bis Server wirklich bereit
                setTimeout(() => resolve(server), 500);
            }
        });

        server.stderr.on('data', (data) => {
            const text = data.toString();
            process.stdout.write(text); // Live-Logs zeigen
            if (!started && (text.includes('model loaded') || text.includes('listening on') || text.includes('start')) && !text.includes('error')) {
                started = true;
                setTimeout(() => resolve(server), 500);
            }
        });

        server.on('error', (err) => {
            reject(new Error(`Server-Start fehlgeschlagen: ${err.message}`));
        });

        server.on('exit', (code) => {
            if (!started) reject(new Error(`Server beendet mit Code ${code}`));
        });

        // Timeout: Wenn Server nicht innerhalb von 120s startet (große Modelle vom Stick)
        setTimeout(() => {
            if (!started) reject(new Error('Timeout: Server nicht innerhalb von 120s gestartet. Möglicherweise zu großes Modell oder USB-Stick zu langsam.'));
        }, 120000);
    });
}

// ─── Hauptprogramm ────────────────────────────────────────────────────────────
async function main() {
    console.clear ? console.clear() : null;
    console.log(`\n${C.bold}${C.cyan}╔══════════════════════════════════════════╗${C.reset}`);
    console.log(`${C.bold}${C.cyan}║  🦙 CrowdCode CLI  —  ${C.reset}miniLLM Chat     ${C.bold}${C.cyan}║${C.reset}`);
    console.log(`${C.bold}${C.cyan}╚══════════════════════════════════════════╝${C.reset}`);
    console.log(`${C.grey}   API: ${API}   |   Workspace: ${WORKSPACE}${C.reset}\n`);

    // Verfügbare Modelle anzeigen
    const localModels = getLocalModels();

    // Lokale Modelle IMMER anzeigen (auch wenn Server schon läuft)
    if (localModels.length > 0) {
        console.log(`${C.bold}📦 Verfügbare lokale Modelle:${C.reset}`);
        localModels.forEach(m => {
            console.log(`  ${C.cyan}${m.index})${C.reset} ${m.name} ${C.grey}(${m.size} GB)${C.reset}`);
        });
    }

    // API-Verbindung testen → falls Server schon läuft, direkt loslegen
    let serverProcess = null;
    let connected = false;
    console.log(`\n${C.dim}🔌 Teste Verbindung zu ${API}...${C.reset}`);
    try {
        const models = await fetchModels();
        if (models.length > 0) {
            const modelName = MODEL || models[0]?.id || models[0]?.name || models[0]?.model || '(unbekannt)';
            console.log(`  ${C.green}✅${C.reset} Server läuft bereits. Modell: ${C.bold}${modelName}${C.reset}`);
            connected = true;
        } else {
            console.log(`  ${C.yellow}⚠️${C.reset} Server antwortet, aber kein Modell geladen.`);
            console.log(`  ${C.dim}  → Starte Server neu mit einem lokalen Modell.${C.reset}`);
        }
    } catch {
        console.log(`  ${C.red}❌${C.reset} Kein Server an ${API}`);
        console.log(`  ${C.dim}  → Starte Server automatisch mit lokalem Modell.${C.reset}`);
    }

    // Server starten, falls nicht verbunden
    if (!connected) {
        if (localModels.length === 0) {
            console.log(`\n${C.red}❌ Keine .gguf-Modelle gefunden.${C.reset}`);
            console.log(`  ${C.dim}Lege Modelle in models/ ab oder starte llama-server manuell.${C.reset}`);
            process.exit(1);
        }

        try {
            serverProcess = await startServerWithModel(localModels);
            connected = true;
            console.log(`\n${C.green}✅ Server bereit.${C.reset}`);
        } catch (err) {
            console.log(`\n${C.red}❌ Server-Start fehlgeschlagen: ${err.message}${C.reset}`);
            process.exit(1);
        }
    }

    // ─── Chat-Loop ──────────────────────────────────────────────────────────
    const history = [{ role: 'system', content: SYSTEM_PROMPT }];

    console.log(`\n${C.green}💬 Chat bereit.${C.reset} ${C.dim}Tippe deine Nachricht, :q zum Beenden, :h für Hilfe.${C.reset}`);

    while (true) {
        const input = await askInput();
        if (input === null) continue; // :c (abbrechen)
        const trimmed = input.trim();
        if (trimmed === ':q' || trimmed === ':quit' || trimmed === ':exit') break;
        if (trimmed === ':h' || trimmed === ':help') {
            console.log(`\n${C.bold}Hilfe:${C.reset}
  :h, :help    Diese Hilfe
  :q, :quit    Beenden
  :m           Multiline-Eingabe (für Code, :send zum Senden)
  :clear       Chat-Verlauf löschen

  ${C.cyan}@datei.py${C.reset}     Dateiinhalt lesen
  ${C.cyan}@verz/${C.reset}       Verzeichnis auflisten
  ${C.cyan}@verz/**${C.reset}     Rekursiver Dateibaum
  ${C.cyan}@?begriff${C.reset}    Grep-Suche
  ${C.cyan}@?*.ts${C.reset}       Glob-Suche

  Das LLM kann: FILE:, DIR:, EDIT:, \`\`\`bash ausgeben
`);
            continue;
        }
        if (trimmed === ':clear' || trimmed === ':reset') {
            history.length = 1; // nur System-Prompt behalten
            console.log(`  ${C.dim}Chat-Verlauf gelöscht.${C.reset}`);
            continue;
        }
        if (!trimmed) continue;

        // @mentions auflösen
        const enhanced = await resolveMentions(trimmed);
        history.push({ role: 'user', content: enhanced });

        // Streaming-Antwort
        let fullResponse = '';
        let thinkingDone = false;
        process.stdout.write(`\n${C.cyan}──${C.reset}`);
        try {
            await chatComplete(history, (token, isReasoning) => {
                if (isReasoning) {
                    if (!thinkingDone) {
                        process.stdout.write(`\n${C.dim}🤔 `);
                        thinkingDone = true;
                    }
                    process.stdout.write(`${C.grey}${token}${C.reset}`);
                } else {
                    if (thinkingDone) {
                        process.stdout.write(`${C.reset}\n${C.cyan}──${C.reset}`);
                        thinkingDone = false;
                    }
                    process.stdout.write(token);
                }
            });
        } catch (err) {
            process.stdout.write(`\n${C.red}❌ Fehler: ${err.message}${C.reset}`);
            console.log(`\n  ${C.dim}Versuche erneute Verbindung...${C.reset}`);
            history.pop(); // fehlgeschlagene User-Nachricht entfernen
            continue;
        }
        process.stdout.write('\n');
        history.push({ role: 'assistant', content: fullResponse });

        // Aktionen ausführen
        const actions = executeActions(fullResponse);
        if (actions.length > 0) {
            console.log(`\n${C.bold}Aktionen:${C.reset}`);
            await applyActions(actions);
        }
        console.log(''); // Leerzeile vor nächstem Prompt
    }

    console.log(`\n${C.cyan}Tschüss!${C.reset}\n`);
    process.exit(0);
}

main().catch((err) => {
    console.error(`${C.red}❌ Fatal:${C.reset}`, err.message);
    process.exit(1);
});
