/**
 * src/web-chat/learn-from-url.ts
 *
 * "Paste a URL → Muassis learns from it" handler. Used by the web-chat
 * preprocessor when a user message looks like a GitHub repo URL or a
 * direct link to a markdown file.
 *
 * Three things we know how to install today:
 *   1. **MCP server repos** (npm-published) — detected by package.json
 *      having a `bin` entry + the README/package referencing MCP.
 *      Adds the package to <dataDir>/mcp.json so the next gateway boot
 *      spawns it and exposes its tools.
 *   2. **Skill markdown files** — direct .md links (raw.githubusercontent
 *      or github.com/blob/.../*.md). Saved to <dataDir>/skills/<file>.md
 *      so loadSkills() picks them up next request.
 *   3. **Skill collections** — repos containing /skills/ or /SKILLS/
 *      directories with .md files. Walks the directory listing and
 *      downloads every .md.
 *
 * No git clone, no local builds — everything via raw HTTP fetches against
 * GitHub's API + raw.githubusercontent. Fast, no deps. If a repo isn't
 * npm-published or has a build step, we tell the user honestly that we
 * can't auto-install it and suggest the manual path.
 */
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
import { createChildLogger } from '../utils/logger.js';
import { getDataDir } from '../utils/data-dir.js';
const log = createChildLogger('web-chat:learn-from-url');
/**
 * Parse a GitHub URL into its parts. Handles all the common shapes:
 *   - https://github.com/owner/repo
 *   - https://github.com/owner/repo/tree/main
 *   - https://github.com/owner/repo/tree/main/some/dir
 *   - https://github.com/owner/repo/blob/main/path/to/file.md
 *   - https://raw.githubusercontent.com/owner/repo/main/path.md
 */
function parseGitHubUrl(url) {
    const trimmed = url.trim().replace(/[?#].*$/, '');
    let m = /^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/(?:tree|blob)\/([^/]+)(?:\/(.*))?)?\/?$/.exec(trimmed);
    if (m) {
        return { owner: m[1], repo: m[2], branch: m[3], path: m[4] };
    }
    m = /^https?:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)\/(.+)$/.exec(trimmed);
    if (m) {
        return { owner: m[1], repo: m[2], branch: m[3], path: m[4] };
    }
    return null;
}
async function fetchText(url) {
    try {
        const r = await fetch(url, { signal: AbortSignal.timeout(15_000) });
        if (!r.ok)
            return null;
        return await r.text();
    }
    catch (err) {
        log.warn({ url, error: err.message }, 'fetch failed');
        return null;
    }
}
async function fetchJson(url) {
    const text = await fetchText(url);
    if (!text)
        return null;
    try {
        return JSON.parse(text);
    }
    catch {
        return null;
    }
}
function rawUrl(ref, filePath, branch) {
    return `https://raw.githubusercontent.com/${ref.owner}/${ref.repo}/${branch ?? ref.branch ?? 'HEAD'}/${filePath}`;
}
/**
 * Probe a repo's package.json. Returns the package name if it looks like
 * an MCP server (has `bin` + name/description mentions MCP).
 *
 * For monorepos (e.g. modelcontextprotocol/servers where each MCP lives
 * in src/<name>/), check the URL's subpath first. If the user pasted
 * .../tree/main/src/sequential-thinking, we look at
 * src/sequential-thinking/package.json.
 */
/**
 * Probe a Python MCP server's pyproject.toml. The MCP ecosystem also has
 * Python servers (e.g. modelcontextprotocol/servers/src/git, ../fetch),
 * which run via `uvx <package>`. We don't parse full TOML — just grep
 * for `name = "..."` in the [project] section, which covers every
 * pyproject.toml we'll encounter.
 */
async function detectPythonMcp(ref) {
    const branches = [ref.branch, 'main', 'master', 'HEAD'].filter(Boolean);
    const subpath = ref.path ? ref.path.replace(/\/+$/, '') : '';
    const probePaths = subpath ? [subpath, ''] : [''];
    for (const sub of probePaths) {
        for (const br of branches) {
            const tomlUrl = sub
                ? `https://raw.githubusercontent.com/${ref.owner}/${ref.repo}/${br}/${sub}/pyproject.toml`
                : `https://raw.githubusercontent.com/${ref.owner}/${ref.repo}/${br}/pyproject.toml`;
            const tomlRaw = await fetchText(tomlUrl);
            if (!tomlRaw)
                continue;
            // Grab `name = "..."` from the [project] section. Prefer the
            // first match — that's the canonical name in PEP 621 metadata.
            const m = /\bname\s*=\s*"([a-zA-Z0-9_.\-]+)"/.exec(tomlRaw);
            if (!m)
                continue;
            const pkgName = m[1];
            const looksLikeMcp = /mcp/i.test(pkgName) ||
                /mcp/i.test(tomlRaw) ||
                /(?:^|\/)(mcp|server-)/i.test(sub);
            if (!looksLikeMcp) {
                log.info({ pkgName, sub }, 'pyproject.toml found but no MCP signals — keeping search');
                continue;
            }
            return { packageName: pkgName };
        }
    }
    return null;
}
async function detectMcpPackage(ref) {
    const branches = [ref.branch, 'main', 'master', 'HEAD'].filter(Boolean);
    // Order of paths to probe:
    //   1. The exact subpath the user pasted (monorepo case)
    //   2. The repo root
    // First match wins.
    const subpath = ref.path ? ref.path.replace(/\/+$/, '') : '';
    const probePaths = subpath ? [subpath, ''] : [''];
    for (const sub of probePaths) {
        for (const br of branches) {
            const pkgUrl = sub
                ? `https://raw.githubusercontent.com/${ref.owner}/${ref.repo}/${br}/${sub}/package.json`
                : `https://raw.githubusercontent.com/${ref.owner}/${ref.repo}/${br}/package.json`;
            const pkgRaw = await fetchText(pkgUrl);
            if (!pkgRaw)
                continue;
            let pkg;
            try {
                pkg = JSON.parse(pkgRaw);
            }
            catch {
                continue;
            }
            if (!pkg.name || !pkg.bin)
                continue;
            // Multiple signals — any one passes is enough.
            // The strongest is the package's own `mcpName` field (now standard
            // in the MCP server packages on npm). Fall back to substring
            // matches against name / description / keywords / dir-path. Match
            // both spellings ("mcp" and "model context protocol" / no spaces).
            const namespaceLooks = /mcp|modelcontextprotocol|model[- _]?context[- _]?protocol/i;
            const looksLikeMcp = typeof pkg.mcpName === 'string' && pkg.mcpName.length > 0 ||
                namespaceLooks.test(pkg.name) ||
                namespaceLooks.test(pkg.description ?? '') ||
                (pkg.keywords ?? []).some(k => namespaceLooks.test(k)) ||
                /(?:^|\/)(mcp|server-)/i.test(sub);
            if (!looksLikeMcp) {
                log.info({ name: pkg.name, sub }, 'package.json found but no MCP signals — keeping search');
                continue;
            }
            const binCommand = typeof pkg.bin === 'string'
                ? pkg.name
                : Object.keys(pkg.bin)[0] ?? pkg.name;
            return { packageName: pkg.name, binCommand };
        }
    }
    return null;
}
function addServerToMcpJson(name, command, args, description) {
    const dataDir = getDataDir();
    const cfgPath = join(dataDir, 'mcp.json');
    let cfg = { mcpServers: {} };
    if (existsSync(cfgPath)) {
        try {
            cfg = JSON.parse(readFileSync(cfgPath, 'utf-8'));
        }
        catch { /* keep blank */ }
    }
    const alreadyHad = !!cfg.mcpServers[name];
    cfg.mcpServers[name] = { command, args, description };
    mkdirSync(dataDir, { recursive: true });
    writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n', { mode: 0o600 });
    return { mcpServersCount: Object.keys(cfg.mcpServers).length, alreadyHad };
}
function saveSkillFile(filename, content) {
    const dir = join(getDataDir(), 'skills');
    mkdirSync(dir, { recursive: true });
    // Sanitize filename — keep alphanumeric / dash / underscore.
    const safe = filename.replace(/[^a-zA-Z0-9_.-]/g, '_').replace(/^_+|_+$/g, '');
    const finalName = safe.endsWith('.md') ? safe : safe + '.md';
    const path = join(dir, finalName);
    writeFileSync(path, content, 'utf-8');
    return path;
}
/** Detect + install from a single GitHub URL. Returns user-facing reply text. */
export async function learnFromGitHubUrl(url) {
    const ref = parseGitHubUrl(url);
    if (!ref) {
        return { ok: false, reply: `هذا الرابط ما يبدو رابط GitHub صالح. الصيغة المتوقعة: https://github.com/<owner>/<repo>` };
    }
    log.info({ ref, url }, 'Learn-from-URL request');
    // ── Case 1: direct .md file (raw or blob) → save as skill ──
    if (ref.path && /\.md$/i.test(ref.path)) {
        const raw = await fetchText(rawUrl(ref, ref.path));
        if (!raw) {
            return { ok: false, reply: `ما قدرت أحمّل الملف من \`${url}\`. تأكد إن الرابط مفتوح للعموم.` };
        }
        const filename = `${ref.repo}-${ref.path.split('/').pop() ?? 'skill.md'}`;
        const saved = saveSkillFile(filename, raw);
        return {
            ok: true,
            reply: `✅ حمّلت المهارة من GitHub وحفظتها على جهازك.\n` +
                `📁 المسار: \`${saved}\`\n` +
                `سأستخدمها تلقائيًا في المحادثة الجاية. (لا حاجة لإعادة تشغيل)`,
        };
    }
    // ── Case 2a: repo URL → detect Node MCP server (package.json) ──
    const mcp = await detectMcpPackage(ref);
    if (mcp) {
        const description = `MCP server from github.com/${ref.owner}/${ref.repo}`;
        // Use a unique server key when multiple subdirs of the same repo
        // get installed (e.g. modelcontextprotocol/servers/{everything,git,…}).
        const serverKey = ref.path
            ? `${ref.repo}-${ref.path.replace(/^src\//, '').replace(/\//g, '-')}`
            : ref.repo;
        const { mcpServersCount, alreadyHad } = addServerToMcpJson(serverKey, 'npx', ['-y', mcp.packageName], description);
        return {
            ok: true,
            reply: `✅ سجّلت خادم MCP من \`${ref.owner}/${ref.repo}${ref.path ? '/' + ref.path : ''}\`.\n` +
                `📦 الحزمة: \`${mcp.packageName}\`\n` +
                `🔧 الأمر: \`npx -y ${mcp.packageName}\`\n` +
                (alreadyHad ? `⚠️ كان مسجّلاً مسبقًا — تم تحديثه.\n` : '') +
                `إجمالي خوادم MCP الآن: ${mcpServersCount}.\n\n` +
                `أعد تشغيل الخدمة (\`muassis start\`) لتفعيل أدوات هذا الخادم.`,
        };
    }
    // ── Case 2b: Python MCP server (pyproject.toml + uvx) ──
    const py = await detectPythonMcp(ref);
    if (py) {
        const description = `Python MCP server from github.com/${ref.owner}/${ref.repo}`;
        const serverKey = ref.path
            ? `${ref.repo}-${ref.path.replace(/^src\//, '').replace(/\//g, '-')}`
            : ref.repo;
        const { mcpServersCount, alreadyHad } = addServerToMcpJson(serverKey, 'uvx', [py.packageName], description);
        return {
            ok: true,
            reply: `✅ سجّلت خادم MCP بايثون من \`${ref.owner}/${ref.repo}${ref.path ? '/' + ref.path : ''}\`.\n` +
                `📦 الحزمة: \`${py.packageName}\`\n` +
                `🔧 الأمر: \`uvx ${py.packageName}\`\n` +
                `⚠️ يتطلّب تثبيت \`uv\` على الجهاز (\`brew install uv\` أو \`pip install uv\`).\n` +
                (alreadyHad ? `كان مسجّلاً مسبقًا — تم تحديثه.\n` : '') +
                `إجمالي خوادم MCP الآن: ${mcpServersCount}.\n\n` +
                `أعد تشغيل الخدمة (\`muassis start\`) لتفعيل أدوات هذا الخادم.`,
        };
    }
    // ── Case 3: repo URL → look for /skills/ or /SKILLS/ directory ──
    // Use the GitHub Trees API to enumerate the repo (one call, full listing).
    const tree = await fetchJson(`https://api.github.com/repos/${ref.owner}/${ref.repo}/git/trees/${ref.branch ?? 'HEAD'}?recursive=1`);
    if (tree?.tree) {
        const skillFiles = tree.tree.filter(f => f.type === 'blob' &&
            /\.md$/i.test(f.path) &&
            /(?:^|\/)(skills|SKILLS)\//.test(f.path));
        if (skillFiles.length > 0) {
            const downloaded = [];
            for (const file of skillFiles.slice(0, 25)) {
                const content = await fetchText(rawUrl(ref, file.path, ref.branch));
                if (content) {
                    const filename = `${ref.repo}-${file.path.split('/').pop()}`;
                    saveSkillFile(filename, content);
                    downloaded.push(file.path);
                }
            }
            if (downloaded.length > 0) {
                return {
                    ok: true,
                    reply: `✅ حمّلت ${downloaded.length} مهارة من \`${ref.owner}/${ref.repo}\`:\n` +
                        downloaded.slice(0, 10).map(p => `• \`${p}\``).join('\n') +
                        (downloaded.length > 10 ? `\n... و${downloaded.length - 10} غيرها` : '') +
                        `\n\nسأستخدمها تلقائيًا في المحادثة الجاية.`,
                };
            }
        }
    }
    return {
        ok: false,
        reply: `حمّلت معلومات المستودع \`${ref.owner}/${ref.repo}\` لكن ما لقيت:\n` +
            `• \`package.json\` يشير إلى MCP server\n` +
            `• مجلد \`skills/\` أو \`SKILLS/\` فيه ملفات \`.md\`\n` +
            `• رابط مباشر لملف \`.md\`\n\n` +
            `إذا كنت تعرف ما هذا المستودع، استخدم: ` +
            `\`muassis mcp add <name> --command 'npx' --args '@org/package'\` ` +
            `(للمنشور كـ npm) أو حمّل ملفات الـ .md يدوياً إلى \`<dataDir>/skills/\`.`,
    };
}
/** Detect a GitHub URL anywhere in the message; return the first one. */
export function extractGitHubUrl(message) {
    const m = /https?:\/\/(?:github\.com|raw\.githubusercontent\.com)\/[^\s]+/i.exec(message.trim());
    return m ? m[0] : null;
}
//# sourceMappingURL=learn-from-url.js.map