#!/usr/bin/env node
// Migrate legacy sealseek skills from `<workspace>/{active_skills,customized_skills,skills}/`
// into sealclaw's managed skills directory at `<stateDir>/skills/`, deduped by
// SKILL.md frontmatter `name` and idempotent via a manifest at
// `<stateDir>/skills/.legacy-skills-import-manifest.json`. Skills marked
// `source: "builtin"` in either `<stateDir>/skill_pool/skill.json` (the pool
// manifest — authoritative) or `<workspace>/skill.json` are skipped.

import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";

function atomicWriteJson(filePath, value) {
  const tmpPath = `${filePath}.tmp`;
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
  fs.writeFileSync(tmpPath, JSON.stringify(value, null, 2));
  fs.renameSync(tmpPath, filePath);
}

export function loadManifest(manifestPath) {
  if (!fs.existsSync(manifestPath)) return { version: 1, imported: {} };
  let raw;
  try {
    raw = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
  } catch {
    raw = null;
  }
  if (
    raw &&
    typeof raw === "object" &&
    raw.version === 1 &&
    raw.imported &&
    typeof raw.imported === "object"
  ) {
    return raw;
  }
  const err = new Error(`legacy-skills-import-manifest at ${manifestPath} is corrupt`);
  err.code = "LEGACY_SKILLS_MANIFEST_CORRUPT";
  throw err;
}

export function recordImport({ manifestPath, name, record }) {
  const current = loadManifest(manifestPath);
  current.imported[name] = record;
  atomicWriteJson(manifestPath, current);
}

const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;

export function parseFrontmatterName(text) {
  if (typeof text !== "string") return null;
  const m = FRONTMATTER_RE.exec(text);
  if (!m) return null;
  for (const line of m[1].split(/\r?\n/)) {
    if (/^\s/.test(line)) continue;
    const km = /^name:\s*(.*)$/.exec(line);
    if (!km) continue;
    let val = km[1].trim();
    if (!val) return null;
    if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
      val = val.slice(1, -1);
    }
    return val || null;
  }
  return null;
}

export function readSkillEntry({ rootDir, dirName }) {
  const sourceDir = path.join(rootDir, dirName);
  const skillMd = path.join(sourceDir, "SKILL.md");
  let stat;
  try {
    stat = fs.statSync(skillMd);
  } catch {
    return null;
  }
  if (!stat.isFile()) return null;
  let text;
  try {
    text = fs.readFileSync(skillMd, "utf8");
  } catch {
    return null;
  }
  const name = parseFrontmatterName(text);
  if (!name) return null;
  let dirStat;
  try {
    dirStat = fs.statSync(sourceDir);
  } catch {
    return null;
  }
  return {
    name,
    sourceDir,
    sourceMtimeMs: dirStat.mtimeMs,
  };
}

// Order is priority-ascending: later sources overwrite earlier ones when the
// same skill name appears in multiple places. `skills/` holds the canonical
// English-named layout and wins over the legacy Chinese-named directories.
const LEGACY_SOURCES = ["active_skills", "customized_skills", "skills"];

export function loadBuiltinSkillNames({ workspaceDir, stateDir }) {
  // The pool manifest at `<sealseekHome>/skill_pool/skill.json` is the authoritative
  // source for builtin vs customized classification; the workspace manifest is
  // a fallback (older layouts may classify there). Look for the pool both under
  // stateDir and as a sibling of <workspaceDir>/../ (the legacy sealseek home),
  // since the caller's stateDir may point at sealclaw's home, not sealseek's.
  const out = new Set();
  const manifestPaths = [];
  if (stateDir) manifestPaths.push(path.join(stateDir, "skill_pool", "skill.json"));
  if (workspaceDir) {
    // <workspaceDir> is `<sealseekHome>/workspaces/<name>`; pool lives at
    // `<sealseekHome>/skill_pool/skill.json` — two levels up + skill_pool.
    manifestPaths.push(path.join(workspaceDir, "..", "..", "skill_pool", "skill.json"));
    manifestPaths.push(path.join(workspaceDir, "skill.json"));
  }
  for (const p of manifestPaths) {
    let raw;
    try {
      raw = JSON.parse(fs.readFileSync(p, "utf8"));
    } catch {
      continue;
    }
    if (!raw || typeof raw !== "object" || !raw.skills || typeof raw.skills !== "object") continue;
    for (const [name, value] of Object.entries(raw.skills)) {
      if (value && typeof value === "object" && value.source === "builtin") {
        out.add(name);
      }
    }
  }
  return out;
}

const SEALCLAW_SKILL_SOURCES = [
  ["skills"],
  ["workspace", "skills"],
  ["plugin-skills"],
  [".agents", "skills"],
  ["workspace", ".agents", "skills"],
];

export function collectExistingSealclawSkillNames({ stateDir }) {
  const names = new Set();
  for (const segments of SEALCLAW_SKILL_SOURCES) {
    const dir = path.join(stateDir, ...segments);
    let children;
    try {
      children = fs.readdirSync(dir, { withFileTypes: true });
    } catch {
      continue;
    }
    for (const child of children) {
      if (!child.isDirectory() && !child.isSymbolicLink()) continue;
      const skillMd = path.join(dir, child.name, "SKILL.md");
      let text;
      try {
        text = fs.readFileSync(skillMd, "utf8");
      } catch {
        continue;
      }
      const name = parseFrontmatterName(text);
      if (name) names.add(name);
    }
  }
  return names;
}

export function scanLegacySkills({ workspaceDir, stateDir }) {
  // skill.json keys are directory names, not always SKILL.md frontmatter names
  // (e.g., `dingtalk_channel/` has frontmatter `name: dingtalk_channel_connect`),
  // so filter by directory name and frontmatter name.
  const builtin = loadBuiltinSkillNames({ workspaceDir, stateDir });
  const out = new Map();
  for (const source of LEGACY_SOURCES) {
    const sourceDir = path.join(workspaceDir, source);
    if (!fs.existsSync(sourceDir)) continue;
    let children;
    try {
      children = fs.readdirSync(sourceDir, { withFileTypes: true });
    } catch {
      continue;
    }
    const dirs = children
      .filter((c) => c.isDirectory())
      .map((c) => c.name)
      .sort();
    for (const dirName of dirs) {
      if (builtin.has(dirName)) continue;
      const entry = readSkillEntry({ rootDir: sourceDir, dirName });
      if (!entry) continue;
      if (builtin.has(entry.name)) continue;
      out.set(entry.name, entry);
    }
  }
  return out;
}

export function copySkillDirectory({ src, dest, force = false }) {
  if (fs.existsSync(dest) && !force) {
    const err = new Error(`destination already exists: ${dest}`);
    err.code = "SKILL_DEST_EXISTS";
    throw err;
  }
  const tmpDir = `${dest}.tmp-${crypto.randomBytes(4).toString("hex")}`;
  try {
    fs.mkdirSync(path.dirname(dest), { recursive: true });
    fs.cpSync(src, tmpDir, { recursive: true });
    if (fs.existsSync(dest)) {
      fs.rmSync(dest, { recursive: true, force: true });
    }
    fs.renameSync(tmpDir, dest);
  } catch (err) {
    try {
      fs.rmSync(tmpDir, { recursive: true, force: true });
    } catch {}
    throw err;
  }
}

function parseArgs(argv) {
  const out = {
    workspace: null,
    stateDir: null,
    dryRun: false,
    force: false,
    verbose: false,
  };
  for (let i = 0; i < argv.length; i++) {
    const a = argv[i];
    switch (a) {
      case "--workspace":
        out.workspace = argv[++i];
        break;
      case "--state-dir":
        out.stateDir = argv[++i];
        break;
      case "--dry-run":
        out.dryRun = true;
        break;
      case "--force":
        out.force = true;
        break;
      case "--verbose":
        out.verbose = true;
        break;
      default:
        throw new Error(`unknown arg: ${a}`);
    }
  }
  return out;
}

function resolveStateDir(env, override) {
  if (override) return override;
  const explicit = env.OPENCLAW_STATE_DIR;
  if (explicit) return explicit;
  const home = env.HOME || env.USERPROFILE || os.homedir();
  return path.join(home, ".sealseek");
}

export async function runCli({ argv, env }) {
  let args;
  try {
    args = parseArgs(argv);
  } catch (err) {
    console.error(err.message);
    return 1;
  }
  const stateDir = resolveStateDir(env, args.stateDir);
  const workspaceDir = args.workspace ?? path.join(stateDir, "workspaces", "default");
  const skillsDir = path.join(stateDir, "workspace", "skills");
  const manifestPath = path.join(skillsDir, ".legacy-skills-import-manifest.json");

  if (!fs.existsSync(workspaceDir)) {
    console.error(`workspace not found: ${workspaceDir}`);
    return 1;
  }
  const sourceDirs = LEGACY_SOURCES.map((s) => path.join(workspaceDir, s));
  if (!sourceDirs.some((d) => fs.existsSync(d))) {
    console.error(
      `no legacy skill source (${LEGACY_SOURCES.join("/, ")}/) found under ${workspaceDir}`,
    );
    return 1;
  }

  let manifest;
  try {
    manifest = loadManifest(manifestPath);
  } catch (err) {
    if (err.code !== "LEGACY_SKILLS_MANIFEST_CORRUPT" || !args.force) {
      console.error(
        err.message +
          (err.code === "LEGACY_SKILLS_MANIFEST_CORRUPT"
            ? " (rerun with --force to overwrite)"
            : ""),
      );
      return 1;
    }
    console.error(`${err.message} — proceeding because --force is set`);
    manifest = { version: 1, imported: {} };
    if (!args.dryRun) {
      atomicWriteJson(manifestPath, manifest);
    }
  }

  const legacy = scanLegacySkills({ workspaceDir, stateDir });
  const existing = collectExistingSealclawSkillNames({ stateDir });

  let imported = 0;
  let skipped = 0;
  let conflict = 0;
  let failed = 0;
  const newManifestRecords = [];

  for (const [name, entry] of legacy) {
    const prior = manifest.imported[name];
    const manifestUnchanged =
      prior && prior.sourceDir === entry.sourceDir && prior.sourceMtimeMs === entry.sourceMtimeMs;
    if (!args.force && manifestUnchanged) {
      if (args.verbose) console.log(`skip ${name} (manifest unchanged)`);
      skipped++;
      continue;
    }
    const isReimport = Boolean(prior);
    if (!isReimport && existing.has(name)) {
      console.warn(`skip ${name}: already in sealclaw`);
      conflict++;
      continue;
    }

    const destDir = path.join(skillsDir, name);
    if (args.dryRun) {
      if (args.verbose) console.log(`plan ${name} -> ${destDir}`);
      imported++;
      continue;
    }

    try {
      copySkillDirectory({
        src: entry.sourceDir,
        dest: destDir,
        force: isReimport || args.force,
      });
    } catch (err) {
      console.error(`fail ${name}: ${err.message}`);
      failed++;
      continue;
    }

    newManifestRecords.push({
      name,
      record: {
        sourceDir: entry.sourceDir,
        sourceMtimeMs: entry.sourceMtimeMs,
        destDir,
        importedAt: Date.now(),
      },
    });
    imported++;
    if (args.verbose) console.log(`import ${name} -> ${destDir}`);
  }

  if (!args.dryRun && newManifestRecords.length > 0) {
    for (const r of newManifestRecords) {
      recordImport({ manifestPath, name: r.name, record: r.record });
    }
  }

  console.log(
    `legacy=${legacy.size} imported=${imported} skipped=${skipped} conflict=${conflict} failed=${failed} dry=${args.dryRun ? "true" : "false"}`,
  );
  return 0;
}

const invokedDirect = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
if (invokedDirect) {
  runCli({ argv: process.argv.slice(2), env: process.env })
    .then((code) => process.exit(code ?? 0))
    .catch((err) => {
      console.error(err?.stack ?? String(err));
      process.exit(1);
    });
}
