import { createHash } from "node:crypto";
import { constants } from "node:fs";
import {
  access,
  mkdir,
  readFile,
  readdir,
  writeFile,
} from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, "..", "..");
const sourceRoot = path.join(repoRoot, "bkit_skills");
const outputRoot = path.join(repoRoot, "bkit_agent_skills");
const args = process.argv.slice(2);
const checkOnly = args.includes("--check");
const versionArgument = args.find((arg) => arg.startsWith("--version="));
const sourceCommitArgument = args.find((arg) =>
  arg.startsWith("--source-commit="),
);
const unknownArgs = args.filter(
  (arg) =>
    arg !== "--check" &&
    !arg.startsWith("--version=") &&
    !arg.startsWith("--source-commit="),
);

if (unknownArgs.length > 0) {
  throw new Error(`Unknown argument(s): ${unknownArgs.join(", ")}`);
}

const providers = [
  {
    id: "openai-codex",
    vendor: "OpenAI",
    product: "Codex",
    skillRoot: ".agents/skills",
    includeOpenaiMetadata: true,
    documentation: "https://learn.chatgpt.com/docs/build-skills",
  },
  {
    id: "anthropic-claude-code",
    vendor: "Anthropic",
    product: "Claude Code",
    skillRoot: ".claude/skills",
    documentation: "https://code.claude.com/docs/en/skills",
  },
  {
    id: "github-copilot",
    vendor: "GitHub",
    product: "GitHub Copilot",
    skillRoot: ".github/skills",
    documentation:
      "https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/customize-cloud-agent/add-skills",
  },
  {
    id: "google-gemini-cli",
    vendor: "Google",
    product: "Gemini CLI",
    skillRoot: "skills",
    documentation: "https://geminicli.com/docs/cli/using-agent-skills/",
  },
  {
    id: "cursor",
    vendor: "Cursor",
    product: "Cursor",
    skillRoot: ".cursor/skills",
    documentation: "https://cursor.com/docs/skills",
  },
  {
    id: "cline",
    vendor: "Cline",
    product: "Cline",
    skillRoot: ".cline/skills",
    documentation: "https://docs.cline.bot/customization/skills",
  },
  {
    id: "windsurf",
    vendor: "Cognition",
    product: "Windsurf Cascade",
    skillRoot: ".windsurf/skills",
    documentation: "https://docs.windsurf.com/windsurf/cascade/skills",
  },
  {
    id: "kilo-code",
    vendor: "Kilo",
    product: "Kilo Code",
    skillRoot: ".kilo/skills",
    documentation: "https://kilo.ai/docs/customize/skills",
  },
  {
    id: "opencode",
    vendor: "OpenCode",
    product: "OpenCode",
    skillRoot: ".opencode/skills",
    documentation: "https://opencode.ai/docs/skills",
  },
  {
    id: "agent-skills-standard",
    vendor: "Agent Skills",
    product: "Open Agent Skills Standard",
    skillRoot: ".agents/skills",
    documentation: "https://agentskills.io/specification",
  },
];

const issues = [];
let updatedFiles = 0;
const versionPattern = /^\d{4}_\d{2}_\d{2}__\d{2}_\d{2}_\d{2}$/;

async function exists(target) {
  try {
    await access(target, constants.F_OK);
    return true;
  } catch {
    return false;
  }
}

async function resolveSourceCommit() {
  const requested = sourceCommitArgument?.slice("--source-commit=".length);
  if (requested) {
    if (!/^[0-9a-f]{40}$/i.test(requested)) {
      throw new Error("--source-commit must be a 40-character Git commit hash");
    }
    return requested.toLowerCase();
  }

  const manifestPath = path.join(outputRoot, "provider-manifest.json");
  if (await exists(manifestPath)) {
    const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
    const existing = manifest.sourceCommit ?? manifest.version;
    if (/^[0-9a-f]{40}$/i.test(existing ?? "")) {
      return existing.toLowerCase();
    }
  }

  throw new Error(
    "Missing skill source version; pass --source-commit=<40-character Git commit hash>",
  );
}

function formatVersionTimestamp(date = new Date()) {
  const pad = (value) => String(value).padStart(2, "0");
  return [
    `${date.getFullYear()}_${pad(date.getMonth() + 1)}_${pad(date.getDate())}`,
    `${pad(date.getHours())}_${pad(date.getMinutes())}_${pad(date.getSeconds())}`,
  ].join("__");
}

async function resolveVersion(digest, sourceCommit) {
  const requested = versionArgument?.slice("--version=".length);
  if (requested) {
    if (!versionPattern.test(requested)) {
      throw new Error(
        "--version must use the format YYYY_MM_DD__HH_mm_ss",
      );
    }
    return requested;
  }

  const manifestPath = path.join(outputRoot, "provider-manifest.json");
  if (await exists(manifestPath)) {
    const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
    const existing = manifest.version;
    if (checkOnly && versionPattern.test(existing ?? "")) {
      return existing;
    }
    if (
      versionPattern.test(existing ?? "") &&
      manifest.contentDigest === digest &&
      manifest.sourceCommit === sourceCommit
    ) {
      return existing;
    }
  }

  if (checkOnly) {
    throw new Error(
      "Missing human-readable version; run synchronization with --version=YYYY_MM_DD__HH_mm_ss",
    );
  }

  return formatVersionTimestamp();
}

function parseFrontmatter(content, sourcePath) {
  const block = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
  if (!block) {
    throw new Error(`Missing YAML frontmatter: ${sourcePath}`);
  }

  const nameMatch = block[1].match(
    /^name:\s*(?:"([^"]+)"|'([^']+)'|([^\r\n#]+))\s*$/m,
  );
  const descriptionMatch = block[1].match(/^description:\s*(.+)$/m);
  if (!nameMatch || !descriptionMatch) {
    throw new Error(`Missing name or description: ${sourcePath}`);
  }

  return {
    name: (nameMatch[1] ?? nameMatch[2] ?? nameMatch[3]).trim(),
    description: descriptionMatch[1].trim(),
  };
}

async function listFiles(root, prefix = "") {
  const entries = await readdir(root, { withFileTypes: true });
  const result = [];

  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
    const relativePath = path.join(prefix, entry.name);
    const absolutePath = path.join(root, entry.name);
    if (entry.isDirectory()) {
      result.push(...(await listFiles(absolutePath, relativePath)));
    } else if (entry.isFile()) {
      result.push(relativePath);
    }
  }

  return result;
}

async function readSourceSkills() {
  const entries = await readdir(sourceRoot, { withFileTypes: true });
  const skills = [];

  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
    if (!entry.isDirectory()) continue;

    const skillDir = path.join(sourceRoot, entry.name);
    const skillPath = path.join(skillDir, "SKILL.md");
    if (!(await exists(skillPath))) continue;

    if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(entry.name)) {
      throw new Error(`Invalid skill directory name: ${entry.name}`);
    }

    const content = await readFile(skillPath, "utf8");
    const metadata = parseFrontmatter(content, skillPath);
    if (metadata.name !== entry.name) {
      throw new Error(
        `Skill name mismatch: folder=${entry.name}, frontmatter=${metadata.name}`,
      );
    }

    skills.push({
      name: entry.name,
      dir: skillDir,
      content,
      files: await listFiles(skillDir),
    });
  }

  if (skills.length === 0) {
    throw new Error(`No skills found in ${sourceRoot}`);
  }

  return skills;
}

async function syncBuffer(destination, expected) {
  let actual = null;
  if (await exists(destination)) {
    actual = await readFile(destination);
  }

  if (actual?.equals(expected)) return;

  if (checkOnly) {
    issues.push(`Missing or stale: ${path.relative(repoRoot, destination)}`);
    return;
  }

  await mkdir(path.dirname(destination), { recursive: true });
  await writeFile(destination, expected);
  updatedFiles += 1;
}

async function syncJson(destination, value) {
  await syncBuffer(destination, Buffer.from(`${JSON.stringify(value, null, 2)}\n`));
}

async function collectDigest(skills) {
  const hash = createHash("sha256");
  for (const skill of skills) {
    hash.update(skill.name);
    hash.update("\0");
    for (const relativePath of skill.files) {
      if (relativePath.split(path.sep)[0] === "agents") continue;
      hash.update(relativePath.split(path.sep).join("/"));
      hash.update("\0");
      hash.update(await readFile(path.join(skill.dir, relativePath)));
      hash.update("\0");
    }
  }
  return `sha256:${hash.digest("hex")}`;
}

async function syncProvider(provider, skills, digest) {
  const providerRoot = path.join(outputRoot, provider.id);
  const targetSkillsRoot = path.join(providerRoot, provider.skillRoot);

  for (const skill of skills) {
    const targetSkillDir = path.join(targetSkillsRoot, skill.name);
    for (const relativePath of skill.files) {
      const topDirectory = relativePath.split(path.sep)[0];
      if (topDirectory === "agents" && !provider.includeOpenaiMetadata) continue;

      const source = path.join(skill.dir, relativePath);
      const destination = path.join(targetSkillDir, relativePath);
      await syncBuffer(destination, await readFile(source));
    }
  }

  if (await exists(targetSkillsRoot)) {
    const targetEntries = await readdir(targetSkillsRoot, { withFileTypes: true });
    const expectedNames = new Set(skills.map((skill) => skill.name));
    for (const entry of targetEntries) {
      if (entry.isDirectory() && !expectedNames.has(entry.name)) {
        issues.push(
          `Stale skill directory: ${path.relative(repoRoot, path.join(targetSkillsRoot, entry.name))}`,
        );
      }
    }
  }

  await syncJson(path.join(providerRoot, "provider.json"), {
    schemaVersion: 1,
    id: provider.id,
    vendor: provider.vendor,
    product: provider.product,
    format: "agent-skills",
    version,
    sourceCommit,
    nativeSkillRoot: provider.skillRoot,
    source: "../../bkit_skills",
    skillCount: skills.length,
    contentDigest: digest,
    documentation: provider.documentation,
  });

  if (provider.id === "google-gemini-cli") {
    await syncJson(path.join(providerRoot, "gemini-extension.json"), {
      name: "google-gemini-cli",
      version: "1.0.0",
      description: "BKIT Accounting workflows for Gemini CLI",
    });
  }
}

async function validateProvider(provider, skills) {
  const targetSkillsRoot = path.join(outputRoot, provider.id, provider.skillRoot);
  for (const skill of skills) {
    const targetSkillDir = path.join(targetSkillsRoot, skill.name);
    const targetSkillPath = path.join(targetSkillDir, "SKILL.md");
    if (!(await exists(targetSkillPath))) {
      issues.push(`Missing SKILL.md: ${path.relative(repoRoot, targetSkillPath)}`);
      continue;
    }

    const targetContent = await readFile(targetSkillPath, "utf8");
    const targetMetadata = parseFrontmatter(targetContent, targetSkillPath);
    if (targetMetadata.name !== skill.name) {
      issues.push(`Name mismatch: ${path.relative(repoRoot, targetSkillPath)}`);
    }
    if (targetContent !== skill.content) {
      issues.push(`Content mismatch: ${path.relative(repoRoot, targetSkillPath)}`);
    }

    const openaiMetadata = path.join(targetSkillDir, "agents", "openai.yaml");
    if (provider.includeOpenaiMetadata && !(await exists(openaiMetadata))) {
      issues.push(
        `Missing OpenAI metadata: ${path.relative(repoRoot, openaiMetadata)}`,
      );
    }
  }
}

const skills = await readSourceSkills();
const digest = await collectDigest(skills);
const sourceCommit = await resolveSourceCommit();
const version = await resolveVersion(digest, sourceCommit);

for (const provider of providers) {
  await syncProvider(provider, skills, digest);
}

await syncJson(path.join(outputRoot, "provider-manifest.json"), {
  schemaVersion: 1,
  generatedBy: "scripts/sync-provider-skills.mjs",
  version,
  sourceCommit,
  source: "../bkit_skills",
  skillCount: skills.length,
  contentDigest: digest,
  providers: providers.map((provider) => ({
    id: provider.id,
    vendor: provider.vendor,
    product: provider.product,
    nativeSkillRoot: provider.skillRoot,
    includeOpenaiMetadata: Boolean(provider.includeOpenaiMetadata),
    documentation: provider.documentation,
  })),
});

for (const provider of providers) {
  await validateProvider(provider, skills);
}

if (issues.length > 0) {
  for (const issue of [...new Set(issues)].sort()) {
    console.error(`ERROR ${issue}`);
  }
  process.exitCode = 1;
} else {
  const action = checkOnly ? "Validated" : "Synchronized";
  console.log(
    `${action} ${skills.length} skills across ${providers.length} provider packs (${skills.length * providers.length} SKILL.md files).`,
  );
  if (!checkOnly) {
    console.log(`Updated files: ${updatedFiles}`);
  }
  console.log(`Content digest: ${digest}`);
}
