const { execSync, spawnSync } = require('child_process');
const fs = require('fs');
const https = require('https');
const os = require('os');
const path = require('path');

function run(cmd, opts = {}) {
  const { dryRun = false } = opts;
  if (dryRun) {
    process.stdout.write(`[dry-run] ${cmd}\n`);
    return '';
  }
  return execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
}

function hasCommand(cmd) {
  try {
    if (process.platform === 'win32') {
      const res = spawnSync('where', [cmd], { stdio: 'ignore' });
      return res.status === 0;
    }
    const res = spawnSync('which', [cmd], { stdio: 'ignore' });
    return res.status === 0;
  } catch (e) {
    return false;
  }
}

function resolveGhExecutable() {
  if (hasCommand('gh')) return 'gh';
  const candidates = [
    'C:\\Program Files\\GitHub CLI\\gh.exe',
    'C:\\Program Files (x86)\\GitHub CLI\\gh.exe',
  ];
  for (const p of candidates) {
    try {
      if (fs.existsSync(p)) return p;
    } catch (e) {
      // ignore
    }
  }
  return null;
}

function resolveClawhubExecutable() {
  // On Windows, Node spawn/spawnSync does not always resolve PATHEXT the same way as shells.
  // Prefer the explicit .cmd shim when available to avoid false "not logged in" detection.
  if (process.platform === 'win32') {
    if (hasCommand('clawhub.cmd')) return 'clawhub.cmd';
    if (hasCommand('clawhub')) return 'clawhub';
  } else {
    if (hasCommand('clawhub')) return 'clawhub';
  }
  // Common npm global bin location on Windows.
  const candidates = [
    'C:\\Users\\Administrator\\AppData\\Roaming\\npm\\clawhub.cmd',
    'C:\\Users\\Administrator\\AppData\\Roaming\\npm\\clawhub.exe',
    'C:\\Users\\Administrator\\AppData\\Roaming\\npm\\clawhub.ps1',
  ];
  for (const p of candidates) {
    try {
      if (fs.existsSync(p)) return p;
    } catch (e) {
      // ignore
    }
  }
  return null;
}

function canUseClawhub() {
  const exe = resolveClawhubExecutable();
  if (!exe) return { ok: false, reason: 'clawhub CLI not found (install: npm i -g clawhub)' };
  return { ok: true, exe };
}

function isClawhubLoggedIn() {
  const exe = resolveClawhubExecutable();
  if (!exe) return false;
  try {
    const res = spawnClawhub(exe, ['whoami'], { stdio: 'ignore' });
    return res.status === 0;
  } catch (e) {
    return false;
  }
}

function spawnClawhub(exe, args, options) {
  // On Windows, directly spawning a .cmd can be flaky; using cmd.exe preserves argument parsing.
  // (Using shell:true can break clap/commander style option parsing for some CLIs.)
  const opts = options || {};
  if (process.platform === 'win32' && typeof exe === 'string') {
    const lower = exe.toLowerCase();
    if (lower.endsWith('.cmd')) {
      return spawnSync('cmd.exe', ['/d', '/s', '/c', exe, ...(args || [])], opts);
    }
  }
  return spawnSync(exe, args || [], opts);
}

function publishToClawhub({ skillDir, slug, name, version, changelog, tags, dryRun }) {
  const ok = canUseClawhub();
  if (!ok.ok) throw new Error(ok.reason);

  // Idempotency: if this version already exists on ClawHub, skip publishing.
  try {
    const inspect = spawnClawhub(ok.exe, ['inspect', slug, '--version', version], { stdio: 'ignore' });
    if (inspect.status === 0) {
      process.stdout.write(`ClawHub already has ${slug}@${version}. Skipping.\n`);
      return;
    }
  } catch (e) {
    // ignore inspect failures; publish will surface errors if needed
  }

  if (!dryRun && !isClawhubLoggedIn()) {
    throw new Error('Not logged in to ClawHub. Run: clawhub login');
  }

  const args = ['publish', skillDir, '--slug', slug, '--name', name, '--version', version];
  if (changelog) args.push('--changelog', changelog);
  if (tags) args.push('--tags', tags);

  if (dryRun) {
    process.stdout.write(`[dry-run] ${ok.exe} ${args.map(a => (/\s/.test(a) ? `"${a}"` : a)).join(' ')}\n`);
    return;
  }

  // Capture output to handle "version already exists" idempotently.
  const res = spawnClawhub(ok.exe, args, { encoding: 'utf8' });
  const out = `${res.stdout || ''}\n${res.stderr || ''}`.trim();

  if (res.status === 0) {
    if (out) process.stdout.write(out + '\n');
    return;
  }

  // Some clawhub deployments do not support reliable "inspect" by slug.
  // Treat "Version already exists" as success to make publishing idempotent.
  if (/version already exists/i.test(out)) {
    process.stdout.write(`ClawHub already has ${slug}@${version}. Skipping.\n`);
    return;
  }

  if (out) process.stderr.write(out + '\n');
  throw new Error(`clawhub publish failed for slug ${slug}`);
}

function requireEnv(name, value) {
  if (!value) {
    throw new Error(`Missing required env var: ${name}`);
  }
}

function ensureClean(dryRun) {
  const status = run('git status --porcelain', { dryRun });
  if (!dryRun && status) {
    throw new Error('Working tree is not clean. Commit or stash before publishing.');
  }
}

function ensureBranch(expected, dryRun) {
  const current = run('git rev-parse --abbrev-ref HEAD', { dryRun }) || expected;
  if (!dryRun && current !== expected) {
    throw new Error(`Current branch is ${current}. Expected ${expected}.`);
  }
}

function ensureRemote(remote, dryRun) {
  try {
    run(`git remote get-url ${remote}`, { dryRun });
  } catch (e) {
    throw new Error(`Remote "${remote}" not found. Add it manually before running this script.`);
  }
}

function ensureTagAvailable(tag, dryRun) {
  if (!tag) return;
  const exists = run(`git tag --list ${tag}`, { dryRun });
  if (!dryRun && exists) {
    throw new Error(`Tag ${tag} already exists.`);
  }
}

function ensureDir(dir, dryRun) {
  if (dryRun) return;
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}

function rmDir(dir, dryRun) {
  if (dryRun) return;
  if (!fs.existsSync(dir)) return;
  fs.rmSync(dir, { recursive: true, force: true });
}

function copyDir(src, dest, dryRun) {
  if (dryRun) return;
  if (!fs.existsSync(src)) throw new Error(`Missing build output dir: ${src}`);
  ensureDir(dest, dryRun);
  const entries = fs.readdirSync(src, { withFileTypes: true });
  for (const ent of entries) {
    const s = path.join(src, ent.name);
    const d = path.join(dest, ent.name);
    if (ent.isDirectory()) copyDir(s, d, dryRun);
    else if (ent.isFile()) {
      ensureDir(path.dirname(d), dryRun);
      fs.copyFileSync(s, d);
    }
  }
}

function createReleaseWithGh({ repo, tag, title, notes, notesFile, dryRun }) {
  if (!repo || !tag) return;
  const ghExe = resolveGhExecutable();
  if (!ghExe) {
    throw new Error('gh CLI not found. Install GitHub CLI or provide a GitHub token for API-based release creation.');
  }
  const args = ['release', 'create', tag, '--repo', repo];
  if (title) args.push('-t', title);
  if (notesFile) args.push('-F', notesFile);
  else if (notes) args.push('-n', notes);
  else args.push('-n', 'Release created by publish script.');

  if (dryRun) {
    process.stdout.write(`[dry-run] ${ghExe} ${args.join(' ')}\n`);
    return;
  }

  const res = spawnSync(ghExe, args, { stdio: 'inherit' });
  if (res.status !== 0) {
    throw new Error('gh release create failed');
  }
}

function canUseGhForRelease() {
  const ghExe = resolveGhExecutable();
  if (!ghExe) return { ok: false, reason: 'gh CLI not found' };
  try {
    // Non-interactive check: returns 0 when authenticated.
    const res = spawnSync(ghExe, ['auth', 'status', '-h', 'github.com'], { stdio: 'ignore' });
    if (res.status === 0) return { ok: true };
    return { ok: false, reason: 'gh not authenticated (run: gh auth login)' };
  } catch (e) {
    return { ok: false, reason: 'failed to check gh auth status' };
  }
}

function getGithubToken() {
  return process.env.GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_PAT || '';
}

function readReleaseNotes(notes, notesFile) {
  if (notesFile) {
    try {
      return fs.readFileSync(notesFile, 'utf8');
    } catch (e) {
      throw new Error(`Failed to read RELEASE_NOTES_FILE: ${notesFile}`);
    }
  }
  if (notes) return String(notes);
  return '';
}

function githubRequestJson({ method, repo, apiPath, token, body, dryRun }) {
  if (dryRun) {
    process.stdout.write(`[dry-run] GitHub API ${method} ${repo} ${apiPath}\n`);
    return Promise.resolve({ status: 200, json: null });
  }

  const data = body ? Buffer.from(JSON.stringify(body)) : null;
  const opts = {
    method,
    hostname: 'api.github.com',
    path: `/repos/${repo}${apiPath}`,
    headers: {
      'User-Agent': 'evolver-publish-script',
      Accept: 'application/vnd.github+json',
      ...(token ? { Authorization: `token ${token}` } : {}),
      ...(data ? { 'Content-Type': 'application/json', 'Content-Length': String(data.length) } : {}),
    },
  };

  return new Promise((resolve, reject) => {
    const req = https.request(opts, res => {
      let raw = '';
      res.setEncoding('utf8');
      res.on('data', chunk => (raw += chunk));
      res.on('end', () => {
        let json = null;
        try {
          json = raw ? JSON.parse(raw) : null;
        } catch (e) {
          json = null;
        }
        resolve({ status: res.statusCode || 0, json, raw });
      });
    });
    req.on('error', reject);
    if (data) req.write(data);
    req.end();
  });
}

async function ensureReleaseWithApi({ repo, tag, title, notes, notesFile, dryRun }) {
  if (!repo || !tag) return;

  const token = getGithubToken();
  if (!dryRun) {
    requireEnv('GITHUB_TOKEN (or GH_TOKEN/GITHUB_PAT)', token);
  }

  // If release already exists, skip.
  const existing = await githubRequestJson({
    method: 'GET',
    repo,
    apiPath: `/releases/tags/${encodeURIComponent(tag)}`,
    token,
    dryRun,
  });

  if (!dryRun && existing.status === 200) {
    process.stdout.write(`Release already exists for tag ${tag}. Skipping.\n`);
    return;
  }

  const bodyText = readReleaseNotes(notes, notesFile) || 'Release created by publish script.';
  const payload = {
    tag_name: tag,
    name: title || tag,
    body: bodyText,
    draft: false,
    prerelease: false,
  };

  const created = await githubRequestJson({
    method: 'POST',
    repo,
    apiPath: '/releases',
    token,
    body: payload,
    dryRun,
  });

  if (!dryRun && (created.status < 200 || created.status >= 300)) {
    const msg = (created.json && created.json.message) || created.raw || 'Unknown error';
    throw new Error(`Failed to create GitHub Release (${created.status}): ${msg}`);
  }

  process.stdout.write(`Created GitHub Release for tag ${tag}\n`);
}

// Collect unique external contributors from private repo commits since the last release.
// Returns an array of "Name <email>" strings suitable for Co-authored-by trailers.
// GitHub counts Co-authored-by toward the Contributors graph.
function getContributorsSinceLastRelease() {
  const EXCLUDED = new Set([
    'evolver-publish@local',
    'evolver@local',
    'openclaw@users.noreply.github.com',
  ]);

  try {
    let baseCommit = '';
    try {
      baseCommit = execSync(
        'git log -n 1 --pretty=%H --grep="chore(release): prepare v"',
        { encoding: 'utf8', cwd: process.cwd(), stdio: ['ignore', 'pipe', 'ignore'] }
      ).trim();
    } catch (_) {}

    const range = baseCommit ? `${baseCommit}..HEAD` : '-30';
    const raw = execSync(
      `git log ${range} --pretty="%aN <%aE>"`,
      { encoding: 'utf8', cwd: process.cwd(), stdio: ['ignore', 'pipe', 'ignore'] }
    ).trim();

    if (!raw) return [];

    const seen = new Set();
    const contributors = [];
    for (const line of raw.split('\n')) {
      const trimmed = line.trim();
      if (!trimmed) continue;
      const emailMatch = trimmed.match(/<([^>]+)>/);
      const email = emailMatch ? emailMatch[1].toLowerCase() : '';
      if (EXCLUDED.has(email)) continue;
      if (seen.has(email)) continue;
      seen.add(email);
      contributors.push(trimmed);
    }
    return contributors;
  } catch (_) {
    return [];
  }
}

function main() {
  const dryRun = String(process.env.DRY_RUN || '').toLowerCase() === 'true';

  const sourceBranch = process.env.SOURCE_BRANCH || 'main';
  const publicRemote = process.env.PUBLIC_REMOTE || 'public';
  const publicBranch = process.env.PUBLIC_BRANCH || 'main';
  const publicRepo = process.env.PUBLIC_REPO || '';
  const outDir = process.env.PUBLIC_OUT_DIR || 'dist-public';
  const useBuildOutput = String(process.env.PUBLIC_USE_BUILD_OUTPUT || 'true').toLowerCase() === 'true';
  const releaseOnly = String(process.env.PUBLIC_RELEASE_ONLY || '').toLowerCase() === 'true';

  const clawhubSkip = String(process.env.CLAWHUB_SKIP || '').toLowerCase() === 'true';
  const clawhubPublish = String(process.env.CLAWHUB_PUBLISH || '').toLowerCase() === 'false' ? false : !clawhubSkip;
  // Workaround for registry redirect/auth issues: default to the www endpoint.
  const clawhubRegistry = process.env.CLAWHUB_REGISTRY || 'https://www.clawhub.ai';

  // If publishing build output, require a repo URL or GH repo slug for cloning.
  if (useBuildOutput) {
    requireEnv('PUBLIC_REPO', publicRepo);
  }

  let releaseTag = process.env.RELEASE_TAG || '';
  let releaseTitle = process.env.RELEASE_TITLE || '';
  const releaseNotes = process.env.RELEASE_NOTES || '';
  const releaseNotesFile = process.env.RELEASE_NOTES_FILE || '';
  const releaseSkip = String(process.env.RELEASE_SKIP || '').toLowerCase() === 'true';
  // Default behavior: create release unless explicitly skipped.
  // Backward compatibility: RELEASE_CREATE=true forces creation.
  // Note: RELEASE_CREATE=false is ignored; use RELEASE_SKIP=true instead.
  const releaseCreate = String(process.env.RELEASE_CREATE || '').toLowerCase() === 'true' ? true : !releaseSkip;
  const releaseUseGh = String(process.env.RELEASE_USE_GH || '').toLowerCase() === 'true';

  // If not provided, infer from build output package.json version.
  if (!releaseTag && useBuildOutput) {
    try {
      const builtPkg = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), outDir, 'package.json'), 'utf8'));
      if (builtPkg && builtPkg.version) releaseTag = `v${builtPkg.version}`;
      if (!releaseTitle && releaseTag) releaseTitle = releaseTag;
    } catch (e) {}
  }

  const releaseVersion = String(releaseTag || '').startsWith('v') ? String(releaseTag).slice(1) : '';

  // Fail fast on missing release prerequisites to avoid half-publishing.
  // Strategy:
  // - If RELEASE_USE_GH=true: require gh + auth
  // - Else: prefer gh+auth; fallback to API token; else fail
  let releaseMode = 'none';
  if (releaseCreate && releaseTag) {
    if (releaseUseGh) {
      const ghOk = canUseGhForRelease();
      if (!dryRun && !ghOk.ok) {
        throw new Error(`Cannot create release via gh: ${ghOk.reason}`);
      }
      releaseMode = 'gh';
    } else {
      const ghOk = canUseGhForRelease();
      if (ghOk.ok) {
        releaseMode = 'gh';
      } else {
        const token = getGithubToken();
        if (!dryRun && !token) {
          throw new Error(
            'Cannot create GitHub Release: neither gh (installed+authenticated) nor GITHUB_TOKEN (or GH_TOKEN/GITHUB_PAT) is available.'
          );
        }
        releaseMode = 'api';
      }
    }
  }

  // In release-only mode we do not push code or tags, only create a GitHub Release for an existing tag.
  if (!releaseOnly) {
    ensureClean(dryRun);
    ensureBranch(sourceBranch, dryRun);
    ensureTagAvailable(releaseTag, dryRun);
  } else {
    requireEnv('RELEASE_TAG', releaseTag);
  }

  if (!releaseOnly) {
    if (!useBuildOutput) {
      ensureRemote(publicRemote, dryRun);
      run(`git push ${publicRemote} ${sourceBranch}:${publicBranch}`, { dryRun });
    } else {
      const tmpBase = path.join(os.tmpdir(), 'evolver-public-publish');
      const tmpRepoDir = path.join(tmpBase, `repo_${Date.now()}`);
      const buildAbs = path.resolve(process.cwd(), outDir);

      rmDir(tmpRepoDir, dryRun);
      ensureDir(tmpRepoDir, dryRun);

      run(`git clone --depth 1 https://github.com/${publicRepo}.git "${tmpRepoDir}"`, { dryRun });
      run(`git -C "${tmpRepoDir}" checkout -B ${publicBranch}`, { dryRun });

      // Replace repo contents with build output (except .git)
      if (!dryRun) {
        const entries = fs.readdirSync(tmpRepoDir, { withFileTypes: true });
        for (const ent of entries) {
          if (ent.name === '.git') continue;
          fs.rmSync(path.join(tmpRepoDir, ent.name), { recursive: true, force: true });
        }
      }
      copyDir(buildAbs, tmpRepoDir, dryRun);

      run(`git -C "${tmpRepoDir}" add -A`, { dryRun });
      const msg = releaseTag ? `Release ${releaseTag}` : `Publish build output`;

      // If build output is identical to current public branch, skip commit/push.
      const pending = run(`git -C "${tmpRepoDir}" status --porcelain`, { dryRun });
      if (!dryRun && !pending) {
        process.stdout.write('Public repo already matches build output. Skipping commit/push.\n');
      } else {
        const contributors = getContributorsSinceLastRelease();
        let commitMsg = msg.replace(/"/g, '\\"');
        if (contributors.length > 0) {
          const trailers = contributors.map(c => `Co-authored-by: ${c}`).join('\n');
          commitMsg += `\n\n${trailers.replace(/"/g, '\\"')}`;
          process.stdout.write(`Including ${contributors.length} contributor(s) in publish commit.\n`);
        }
        run(
          `git -C "${tmpRepoDir}" -c user.name="evolver-publish" -c user.email="evolver-publish@local" commit -m "${commitMsg}"`,
          { dryRun }
        );
        run(`git -C "${tmpRepoDir}" push origin ${publicBranch}`, { dryRun });
      }

      if (releaseTag) {
        const tagMsg = releaseTitle || `Release ${releaseTag}`;
        // If tag already exists in the public repo, do not recreate it.
        try {
          run(`git -C "${tmpRepoDir}" fetch --tags`, { dryRun });
          const exists = run(`git -C "${tmpRepoDir}" tag --list ${releaseTag}`, { dryRun });
          if (!dryRun && exists) {
            process.stdout.write(`Tag ${releaseTag} already exists in public repo. Skipping tag creation.\n`);
          } else {
            run(`git -C "${tmpRepoDir}" tag -a ${releaseTag} -m "${tagMsg.replace(/"/g, '\\"')}"`, { dryRun });
            run(`git -C "${tmpRepoDir}" push origin ${releaseTag}`, { dryRun });
          }
        } catch (e) {
          // If tag operations fail, rethrow to avoid publishing a release without a tag.
          throw e;
        }
      }
    }

    if (releaseTag) {
      if (!useBuildOutput) {
        const msg = releaseTitle || `Release ${releaseTag}`;
        run(`git tag -a ${releaseTag} -m "${msg.replace(/"/g, '\\"')}"`, { dryRun });
        run(`git push ${publicRemote} ${releaseTag}`, { dryRun });
      }
    }
  }

  if (releaseCreate) {
    if (releaseMode === 'gh') {
      createReleaseWithGh({
        repo: publicRepo,
        tag: releaseTag,
        title: releaseTitle,
        notes: releaseNotes,
        notesFile: releaseNotesFile,
        dryRun,
      });
    } else if (releaseMode === 'api') {
      return ensureReleaseWithApi({
        repo: publicRepo,
        tag: releaseTag,
        title: releaseTitle,
        notes: releaseNotes,
        notesFile: releaseNotesFile,
        dryRun,
      });
    }
  }

  // Publish to ClawHub after GitHub release succeeds (default enabled).
  if (clawhubPublish && releaseVersion) {
    process.env.CLAWHUB_REGISTRY = clawhubRegistry;

    const skillDir = useBuildOutput ? path.resolve(process.cwd(), outDir) : process.cwd();
    const changelog = releaseTitle ? `GitHub Release ${releaseTitle}` : `GitHub Release ${releaseTag}`;

    publishToClawhub({
      skillDir,
      slug: 'evolver',
      name: 'Evolver',
      version: releaseVersion,
      changelog,
      tags: 'latest',
      dryRun,
    });

    publishToClawhub({
      skillDir,
      slug: 'capability-evolver',
      name: 'Capability Evolver',
      version: releaseVersion,
      changelog,
      tags: 'latest',
      dryRun,
    });
  }
}

try {
  const maybePromise = main();
  if (maybePromise && typeof maybePromise.then === 'function') {
    maybePromise.catch(e => {
      process.stderr.write(`${e.message}\n`);
      process.exit(1);
    });
  }
} catch (e) {
  process.stderr.write(`${e.message}\n`);
  process.exit(1);
}

