import fs from "fs";
import os from "os";
import path from "path";
import { spawnSync } from "child_process";

const CLAUTH_NPM_URL = "http://127.0.0.1:52437/v/npm";

function run(cmd, args, opts = {}) {
  const useCmd = process.platform === "win32" && ["npm", "gh"].includes(cmd);
  const executable = useCmd ? "cmd.exe" : cmd;
  const finalArgs = useCmd ? ["/d", "/s", "/c", cmd, ...args] : args;
  const result = spawnSync(executable, finalArgs, {
    encoding: "utf8",
    ...opts,
    env: { ...process.env, ...(opts.env || {}) }
  });
  if (result.error) throw result.error;
  return result;
}

function redactNpmTokenList(text) {
  return String(text || "").replace(/npm_[A-Za-z0-9]+/g, token => `${token.slice(0, 9)}...${token.slice(-4)}`);
}

async function fetchNpmToken() {
  const response = await fetch(CLAUTH_NPM_URL);
  if (!response.ok) throw new Error(`failed to fetch npm token from clauth daemon: HTTP ${response.status}`);
  const token = (await response.text()).trim();
  if (!token) throw new Error("clauth npm token is empty");
  if (!token.startsWith("npm_")) throw new Error("clauth npm token does not look like an npm token");
  return token;
}

function withNpmAuth(token, fn) {
  const npmrc = path.join(os.tmpdir(), `clauth-npm-${process.pid}-${Date.now()}.npmrc`);
  try {
    fs.writeFileSync(npmrc, `//registry.npmjs.org/:_authToken=${token}`, { encoding: "utf8", mode: 0o600 });
    return fn(npmrc);
  } finally {
    try { fs.rmSync(npmrc, { force: true }); } catch {}
  }
}

function runNpmWithToken(token, args) {
  return withNpmAuth(token, npmrc => run("npm", ["--userconfig", npmrc, ...args]));
}

function printResult(result, { redact = true } = {}) {
  const stdout = redact ? redactNpmTokenList(result.stdout) : result.stdout;
  const stderr = redact ? redactNpmTokenList(result.stderr) : result.stderr;
  if (stdout) process.stdout.write(stdout);
  if (stderr) process.stderr.write(stderr);
}

function usage() {
  console.log(`clauth npm <action>

Actions:
  whoami                         Verify the clauth npm token identity
  tokens                         List npm token metadata with token strings redacted
  set-local                      Write the clauth npm token to the user npm config
  sync-github-secret <repo>       Set repo secret NPM_TOKEN from clauth, e.g. LIFEAI/rdc-skills
  rerun <run-id> --repo <repo>    Rerun a failed GitHub Actions workflow
`);
}

export async function runNpm(action = "help", opts = {}) {
  if (action === "help") {
    usage();
    return;
  }

  const token = await fetchNpmToken();

  if (action === "whoami") {
    const result = runNpmWithToken(token, ["whoami", "--registry=https://registry.npmjs.org/"]);
    printResult(result);
    if (result.status !== 0) process.exitCode = result.status;
    return;
  }

  if (action === "tokens") {
    const result = runNpmWithToken(token, ["token", "list", "--json", "--registry=https://registry.npmjs.org/"]);
    printResult(result);
    if (result.status !== 0) process.exitCode = result.status;
    return;
  }

  if (action === "set-local") {
    const result = run("npm", ["config", "set", "//registry.npmjs.org/:_authToken", token, "--location=user"]);
    if (result.status !== 0) {
      printResult(result);
      process.exitCode = result.status;
      return;
    }
    console.log("local npm auth updated from clauth service 'npm'");
    return;
  }

  if (action === "sync-github-secret") {
    const repo = opts.repo || opts.args?.[0];
    if (!repo) throw new Error("repo is required, e.g. clauth npm sync-github-secret LIFEAI/rdc-skills");
    const result = run("gh", ["secret", "set", "NPM_TOKEN", "--repo", repo], { input: token });
    printResult(result);
    if (result.status !== 0) process.exitCode = result.status;
    else console.log(`GitHub secret NPM_TOKEN updated for ${repo}`);
    return;
  }

  if (action === "rerun") {
    const runId = opts.args?.[0];
    const repo = opts.repo;
    if (!runId || !repo) throw new Error("usage: clauth npm rerun <run-id> --repo LIFEAI/rdc-skills");
    const result = run("gh", ["run", "rerun", runId, "--repo", repo, "--failed"]);
    printResult(result);
    if (result.status !== 0) process.exitCode = result.status;
    else console.log(`rerun requested for ${repo} run ${runId}`);
    return;
  }

  usage();
  throw new Error(`unknown clauth npm action: ${action}`);
}

// ── Guarded publish ──────────────────────────────────────────────────────────
// Generic, package-agnostic publish that REFUSES to publish unless the package
// is already committed and pushed to GitHub — so the npm tarball can never be
// built from uncommitted "dev" code that isn't on the repo. Works for standalone
// repos and monorepo subpackages (clean-check is scoped to the package's dir).
export async function runPublish(target, opts = {}) {
  const pkgDir = path.resolve(target || process.cwd());
  const pkgJsonPath = path.join(pkgDir, "package.json");
  if (!fs.existsSync(pkgJsonPath)) throw new Error(`No package.json found at ${pkgDir}`);
  const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
  if (!pkg.name || !pkg.version) throw new Error(`package.json at ${pkgDir} is missing name or version`);
  if (pkg.private === true) throw new Error(`${pkg.name} is marked "private": true — refusing to publish`);
  const access = opts.access || pkg.publishConfig?.access || (pkg.name.startsWith("@") ? "public" : undefined);

  const gitRoot = run("git", ["rev-parse", "--show-toplevel"], { cwd: pkgDir }).stdout?.trim();
  if (!gitRoot) throw new Error(`${pkgDir} is not inside a git repository`);
  const rel = path.relative(gitRoot, pkgDir) || ".";

  // Guard 1 — nothing uncommitted in the package (else we'd pack dev code).
  const dirty = run("git", ["status", "--porcelain", "--", rel], { cwd: gitRoot }).stdout?.trim();
  if (dirty && !opts.allowDirty) {
    throw new Error(`Refusing to publish ${pkg.name}@${pkg.version}: uncommitted changes in ${rel} would be packed but are NOT on GitHub:\n${dirty}\n\nCommit + push first (or pass --allow-dirty to override).`);
  }

  // Guard 2 — HEAD is on a remote (actually pushed to GitHub).
  const head = run("git", ["rev-parse", "HEAD"], { cwd: gitRoot }).stdout?.trim();
  const onRemote = run("git", ["branch", "-r", "--contains", head], { cwd: gitRoot }).stdout?.trim();
  if (!onRemote && !opts.allowUnpushed) {
    throw new Error(`Refusing to publish ${pkg.name}@${pkg.version}: HEAD ${head.slice(0, 9)} is not on any remote branch — push to GitHub first (or pass --allow-unpushed to override).`);
  }

  // Traceability — warn (don't block) if there's no v<version> tag at HEAD.
  const tags = (run("git", ["tag", "--points-at", "HEAD"], { cwd: gitRoot }).stdout || "").split("\n").map((s) => s.trim()).filter(Boolean);
  if (!tags.includes(`v${pkg.version}`)) {
    console.log(`⚠  No git tag v${pkg.version} at HEAD (traceability only). Tags here: ${tags.join(", ") || "none"}`);
  }

  console.log(`${opts.dryRun ? "[dry-run] " : ""}Publishing ${pkg.name}@${pkg.version} from ${rel} @ ${head.slice(0, 9)} (pushed${access ? `, access=${access}` : ""})`);

  const token = await fetchNpmToken();
  const args = ["publish"];
  if (access) args.push("--access", access);
  if (opts.dryRun) args.push("--dry-run");
  const result = withNpmAuth(token, (npmrc) => run("npm", ["--userconfig", npmrc, ...args], { cwd: pkgDir }));
  printResult(result, { redact: true });
  if (result.status !== 0) { process.exitCode = result.status; throw new Error(`npm publish failed for ${pkg.name}`); }
  if (opts.dryRun) { console.log("Dry run complete — nothing published."); return; }

  // Verify on the registry directly (bypasses npm's local cache lag).
  try {
    const reg = await fetch(`https://registry.npmjs.org/${pkg.name}`);
    const meta = await reg.json();
    if (meta.versions?.[pkg.version]) console.log(`✓ Verified ${pkg.name}@${pkg.version} on the registry (latest: ${meta["dist-tags"]?.latest}).`);
    else console.log(`⚠  ${pkg.name}@${pkg.version} not visible on the registry yet (propagation lag) — re-check shortly.`);
  } catch (e) {
    console.log(`(could not verify on registry: ${e.message})`);
  }
}
