import { init_paths, resolveSkillsDir } from "../../config/paths.js";
import { parseFrontmatter } from "../../markdown/frontmatter.js";
import { formatScanSummary, scanSkillDirectory } from "./scanner.js";
import { getSkillsLockEntry, recordSkillsHubInstall } from "./hub-lock.js";
import { installSkillFromZip, isValidSkillId } from "./managed-store.js";
import { computeSkillTreeHashSync } from "./hub-hash.js";
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
import { basename, join, normalize, resolve, sep } from "node:path";
import { tmpdir } from "node:os";
import { execFileSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { fetch } from "undici";
//#region src/agent/skills/hub-pull.ts
/**
* CLI / hub: install or update skills from git or archive (zip / tar.gz) into ~/.xopc/skills.
*/
init_paths();
function classifyHubSource(raw) {
	const s = raw.trim();
	const lower = s.toLowerCase();
	try {
		if (s.startsWith("file:")) {
			const pl = fileURLToPath(s).toLowerCase();
			if (pl.endsWith(".zip")) return "archive";
			if (pl.endsWith(".tar.gz") || pl.endsWith(".tgz")) return "archive";
			return "git";
		}
	} catch {}
	if (/^[a-zA-Z]:[\\/]/.test(s) || s.startsWith("/") && !s.startsWith("//")) {
		if (lower.endsWith(".zip")) return "archive";
		if (lower.endsWith(".tar.gz") || lower.endsWith(".tgz")) return "archive";
		return "git";
	}
	try {
		const path = new URL(s).pathname.toLowerCase();
		if (path.endsWith(".zip")) return "archive";
		if (path.endsWith(".tar.gz") || path.endsWith(".tgz")) return "archive";
	} catch {}
	if (lower.endsWith(".zip")) return "archive";
	if (lower.endsWith(".tar.gz") || lower.endsWith(".tgz")) return "archive";
	return "git";
}
function findSkillRoot(repoRoot, subpath) {
	const sub = subpath?.trim() ? normalize(subpath.trim()).replace(/^[/\\]+/, "") : "";
	const base = sub ? resolve(repoRoot, sub) : resolve(repoRoot);
	if (!existsSync(base)) throw new Error(`Path not found in source: ${sub || "."}`);
	if (existsSync(join(base, "SKILL.md"))) return base;
	const entries = readdirSync(base, { withFileTypes: true }).filter((e) => e.isDirectory());
	if (entries.length === 1) {
		const one = join(base, entries[0].name);
		if (existsSync(join(one, "SKILL.md"))) return one;
	}
	throw new Error("Could not locate SKILL.md (expected at root, under --path, or in a single child folder)");
}
function inferSkillIdFromSkillMd(skillRoot) {
	const { frontmatter } = parseFrontmatter(readFileSync(join(skillRoot, "SKILL.md"), "utf-8"));
	const n = String(frontmatter.name || "").trim();
	if (n && isValidSkillId(n)) return n;
	const dirName = basename(skillRoot);
	if (isValidSkillId(dirName)) return dirName;
	return "";
}
async function runScan(dir, strict) {
	const summary = await scanSkillDirectory(dir);
	const label = basename(dir);
	if (summary.critical > 0 && strict) throw new Error(`Security scan reported ${summary.critical} critical finding(s). ${formatScanSummary(summary, label)}`);
	if (summary.critical > 0 || summary.warn > 0) console.warn(formatScanSummary(summary, label));
}
async function copySkillTreeToManaged(skillRoot, ctx) {
	const targetId = ctx.skillId?.trim() || inferSkillIdFromSkillMd(skillRoot);
	if (!targetId) throw new Error("Could not determine skill id: pass --id <name> (must match managed id rules)");
	if (!isValidSkillId(targetId)) throw new Error(`Invalid skill id "${targetId}" (letters, digits, ._-; max 63 chars after first)`);
	const rootDir = resolveSkillsDir();
	mkdirSync(rootDir, { recursive: true });
	const dest = join(rootDir, targetId);
	const destResolved = resolve(dest);
	const rootResolved = resolve(rootDir);
	if (!destResolved.startsWith(rootResolved + sep) && destResolved !== rootResolved) throw new Error("Invalid destination path");
	if (existsSync(dest)) {
		if (!ctx.force) throw new Error(`Skill "${targetId}" already exists. Use --force to replace.`);
		rmSync(dest, {
			recursive: true,
			force: true
		});
	}
	cpSync(skillRoot, dest, { recursive: true });
	if (!existsSync(join(dest, "SKILL.md"))) {
		rmSync(dest, {
			recursive: true,
			force: true
		});
		throw new Error("Installed tree is missing SKILL.md");
	}
	const hash = computeSkillTreeHashSync(dest);
	recordSkillsHubInstall(targetId, {
		kind: ctx.kind,
		source: ctx.source,
		ref: ctx.kind === "git" ? ctx.ref : void 0,
		subpath: ctx.kind === "git" ? ctx.subpath : void 0
	}, hash);
	await runScan(dest, ctx.strictScan);
	return {
		skillId: targetId,
		path: dest,
		contentHash: hash,
		kind: ctx.kind,
		source: ctx.source
	};
}
async function pullSkillFromZipBuffer(buf, sourceLabel, options) {
	const r = installSkillFromZip(buf, {
		skillId: options.skillId,
		overwrite: options.force ?? false
	});
	const hash = computeSkillTreeHashSync(r.path);
	recordSkillsHubInstall(r.skillId, {
		kind: "archive",
		source: sourceLabel
	}, hash);
	await runScan(r.path, options.strictScan);
	return {
		skillId: r.skillId,
		path: r.path,
		contentHash: hash,
		kind: "archive",
		source: sourceLabel
	};
}
async function pullSkillFromTarFile(archivePath, sourceLabel, options) {
	const tmpRoot = mkdtempSync(join(tmpdir(), "xopc-tar-"));
	try {
		execFileSync("tar", [
			"-xzf",
			archivePath,
			"-C",
			tmpRoot
		], {
			stdio: "pipe",
			encoding: "utf-8"
		});
		return await copySkillTreeToManaged(findSkillRoot(tmpRoot), {
			...options,
			kind: "archive",
			source: sourceLabel
		});
	} finally {
		rmSync(tmpRoot, {
			recursive: true,
			force: true
		});
	}
}
async function pullSkillFromLocalPath(absPath, options) {
	const resolved = resolve(absPath);
	if (!existsSync(resolved)) throw new Error(`Not found: ${resolved}`);
	if (!statSync(resolved).isFile()) throw new Error("Expected a path to a .zip or .tar.gz file (directories are not supported here)");
	const lower = resolved.toLowerCase();
	if (lower.endsWith(".zip")) return pullSkillFromZipBuffer(readFileSync(resolved), resolved, options);
	if (lower.endsWith(".tar.gz") || lower.endsWith(".tgz")) return pullSkillFromTarFile(resolved, resolved, options);
	throw new Error("Unsupported file type (expected .zip, .tar.gz, or .tgz)");
}
async function pullSkillFromGit(url, options) {
	const tmpRoot = mkdtempSync(join(tmpdir(), "xopc-hub-"));
	const cloneDir = join(tmpRoot, "clone");
	try {
		const args = [
			"clone",
			"--depth",
			"1"
		];
		if (options.ref?.trim()) args.push("--branch", options.ref.trim());
		args.push(url, cloneDir);
		try {
			execFileSync("git", args, {
				stdio: "pipe",
				encoding: "utf-8"
			});
		} catch {
			if (options.ref?.trim()) execFileSync("git", [
				"clone",
				"--depth",
				"1",
				url,
				cloneDir
			], {
				stdio: "pipe",
				encoding: "utf-8"
			});
			else throw new Error(`git clone failed for ${url}`);
		}
		return await copySkillTreeToManaged(findSkillRoot(cloneDir, options.subpath), {
			...options,
			kind: "git",
			source: url
		});
	} catch (e) {
		const msg = e instanceof Error ? e.message : String(e);
		if (msg.includes("ENOENT") || msg.includes("git clone failed")) throw new Error(`Git operation failed (${msg}). Ensure git is installed and the repository URL is valid.`);
		throw e;
	} finally {
		rmSync(tmpRoot, {
			recursive: true,
			force: true
		});
	}
}
/**
* Install or replace a skill from a git URL, http(s) archive URL, file:// URL, or local archive path.
*/
async function pullSkillFromSource(source, options = {}) {
	const trimmed = source.trim();
	if (!trimmed) throw new Error("Missing source URL or path");
	if (trimmed.startsWith("file:")) return pullSkillFromLocalPath(fileURLToPath(trimmed), options);
	if (!/^[a-zA-Z][a-zA-Z+.-]*:\/\//.test(trimmed) && existsSync(trimmed)) return pullSkillFromLocalPath(resolve(trimmed), options);
	if (classifyHubSource(trimmed) === "archive") {
		const res = await fetch(trimmed);
		if (!res.ok) throw new Error(`HTTP ${res.status} while fetching archive`);
		const buf = Buffer.from(await res.arrayBuffer());
		const lower = trimmed.toLowerCase();
		if (lower.endsWith(".tar.gz") || lower.endsWith(".tgz")) {
			const tmpRoot = mkdtempSync(join(tmpdir(), "xopc-fetch-tar-"));
			const f = join(tmpRoot, "archive.tgz");
			try {
				mkdirSync(tmpRoot, { recursive: true });
				writeFileSync(f, buf);
				return await pullSkillFromTarFile(f, trimmed, options);
			} finally {
				rmSync(tmpRoot, {
					recursive: true,
					force: true
				});
			}
		}
		return pullSkillFromZipBuffer(buf, trimmed, options);
	}
	return pullSkillFromGit(trimmed, options);
}
/** Re-install from the source recorded in ~/.xopc/skills-lock.json for this skill id. */
async function updateSkillFromLock(skillId, options = {}) {
	const e = getSkillsLockEntry(skillId);
	if (!e) throw new Error(`No hub lock entry for "${skillId}". Install with: xopc skills pull <git-or-archive-url>`);
	return pullSkillFromSource(e.source, {
		skillId,
		ref: e.ref,
		subpath: e.subpath,
		force: true,
		strictScan: options.strictScan
	});
}
//#endregion
export { classifyHubSource, findSkillRoot, pullSkillFromSource, updateSkillFromLock };

//# sourceMappingURL=hub-pull.js.map