/**
 * engine/gh-comment.js — wraps `gh pr comment` / `gh pr review` with a hidden
 * HTML-comment marker so downstream classifiers (engine/github.js) can identify
 * minions-authored PR comments by structure rather than by body shape.
 *
 * Marker format (single line, ASCII):
 *   <!-- minions:agent=<agentId> kind=<kind> wi=<workItemId> -->
 *
 * Followed by `\n\n` and then the caller-provided body. `wi=` is omitted when
 * no workItemId is supplied. The marker is intentionally minimal so it round-
 * trips through GitHub's Markdown rendering and survives quoting.
 *
 * Validation rules (all enforced before any shell call):
 *   - agentId    /^[a-z][a-z0-9-]{0,30}$/  (lowercase, hyphenated, ≤31 chars)
 *   - kind       /^[a-z][a-z0-9-]{0,30}$/  (same shape — categorical tag)
 *   - workItemId /^[A-Z]-[a-z0-9]+$/       (e.g. W-mp3bp0ha000997ab, P-d5a8c9b6)
 *   - no field may contain `--` (would close the HTML comment early)
 *   - no field may contain `=` `<` `>` `\n` `"` `'` `` ` ``  ` `  (whitespace)
 *
 * Idempotency: if the caller body already starts with a `<!-- minions:agent=`
 * marker, the helper returns the body unchanged — never double-prepends, even
 * when the existing marker disagrees with the helper's parameters. The
 * caller's pre-marked body is treated as the source of truth.
 *
 * `gh` invocation: argv form with `--body-file <tmp>` (NOT `--body <inline>`).
 * Avoids platform-specific shell-quoting bugs for bodies that contain quotes,
 * backticks, or `$(…)`. Temp files are cleaned up in `finally`.
 *
 * Per-slug PAT routing (P-bfa2a): each public function resolves a per-slug
 * GitHub token via `engine/gh-token.js#resolveTokenForSlug(repo)` and threads
 * it into the spawned `gh` process via `env.GH_TOKEN`. When the slug has no
 * mapping in `engine.ghAccounts`, we fall back to the ambient `gh` identity
 * (inherited via `process.env`) and emit a one-shot `console.warn` per slug
 * so the dual-account audit can spot stragglers. The pinned "Never run
 * `gh auth switch`" policy means we MUST resolve per-call rather than
 * piggy-backing on the global active account.
 */

const fs = require('fs');
const path = require('path');
const os = require('os');
const { execFileSync: _execFileSync } = require('child_process');
const { resolveTokenForSlug: _defaultResolveTokenForSlug } = require('./gh-token');

// Module-level dedupe set so the "no token mapping for <slug>" warning fires
// at most once per slug per process — keeps the audit signal visible without
// flooding the log on every comment post. Exported via `_clearTokenWarnings`
// for tests.
const _warnedUnresolvedSlugs = new Set();

// ── Validation ───────────────────────────────────────────────────────────────

const AGENT_ID_RE = /^[a-z][a-z0-9-]{0,30}$/;
const KIND_RE = /^[a-z][a-z0-9-]{0,30}$/;
const WORK_ITEM_ID_RE = /^[A-Z]-[a-z0-9]+$/;
const REPO_SLUG_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;

// Marker line (single-line HTML comment). Anchored to start-of-line; capture
// groups: 1 = agentId, 2 = kind, 3 = workItemId (optional, undefined if
// absent). Multiline flag so it matches at the start of a line anywhere in
// the body — required for round-trip detection of the builder's output.
const MINIONS_COMMENT_MARKER_RE =
  /^<!--\s*minions:agent=([^\s]+)\s+kind=([^\s]+)(?:\s+wi=([^\s]+))?\s*-->/m;

// Sample marker constant for tests / fixtures that need a body with a
// canonical-format marker but don't care about the specific agent / kind /
// wi fields. Matches MINIONS_COMMENT_MARKER_RE. Production comments build
// their marker via _buildMarker / buildMinionsCommentBody.
const MINIONS_COMMENT_MARKER = '<!-- minions:agent=minions kind=marker -->';

// Cheaper "is this body already marked?" check that matches only at position 0
// (for idempotency in buildMinionsCommentBody). Kept separate from the
// exported regex so the public regex can be used by downstream classifiers
// that scan the entire body.
const _LEADING_MARKER_RE = /^<!--\s*minions:agent=/;

function _hasNoDoubleDash(value) {
  return !String(value).includes('--');
}

function _validateField(name, value, re) {
  if (typeof value !== 'string' || !re.test(value)) {
    throw new Error(`invalid ${name}: ${JSON.stringify(value)}`);
  }
  if (!_hasNoDoubleDash(value)) {
    throw new Error(`invalid ${name}: must not contain "--" (HTML-comment safety)`);
  }
}

function _validateMarkerInputs({ agentId, kind, workItemId }) {
  _validateField('agentId', agentId, AGENT_ID_RE);
  _validateField('kind', kind, KIND_RE);
  if (workItemId !== undefined && workItemId !== null) {
    _validateField('workItemId', workItemId, WORK_ITEM_ID_RE);
  }
}

function _validateRepo(repo) {
  if (typeof repo !== 'string' || !REPO_SLUG_RE.test(repo)) {
    throw new Error(`invalid repo: ${JSON.stringify(repo)} (expected "owner/name")`);
  }
}

function _validatePrNumber(prNumber) {
  if (typeof prNumber !== 'number' || !Number.isInteger(prNumber) || prNumber <= 0) {
    throw new Error(`invalid prNumber: ${JSON.stringify(prNumber)} (expected positive integer)`);
  }
}

// ── Marker construction / parsing ────────────────────────────────────────────

function _buildMarker({ agentId, kind, workItemId }) {
  _validateMarkerInputs({ agentId, kind, workItemId });
  const wi = (workItemId !== undefined && workItemId !== null) ? ` wi=${workItemId}` : '';
  return `<!-- minions:agent=${agentId} kind=${kind}${wi} -->`;
}

function buildMinionsCommentBody({ agentId, kind, workItemId, body }) {
  // Idempotency: if the body already starts with a minions marker, leave it
  // alone — the caller's marker is authoritative. Validate the inputs anyway
  // so callers don't silently bypass validation by pre-marking their body.
  _validateMarkerInputs({ agentId, kind, workItemId });
  const safeBody = body == null ? '' : String(body);
  if (_LEADING_MARKER_RE.test(safeBody)) return safeBody;
  const marker = _buildMarker({ agentId, kind, workItemId });
  return `${marker}\n\n${safeBody}`;
}

function parseMinionsMarker(body) {
  if (typeof body !== 'string' || body.length === 0) return null;
  const m = body.match(MINIONS_COMMENT_MARKER_RE);
  if (!m) return null;
  return {
    agentId: m[1],
    kind: m[2],
    workItemId: m[3] === undefined ? undefined : m[3],
  };
}

// ── gh invocation ────────────────────────────────────────────────────────────

function _writeTempBodyFile(content) {
  const dir = path.join(os.tmpdir(), 'minions-gh-comment');
  try { fs.mkdirSync(dir, { recursive: true }); } catch {}
  const file = path.join(
    dir,
    `body-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.md`,
  );
  fs.writeFileSync(file, content);
  return file;
}

function _runGh(execFileSync, args, timeoutMs, env) {
  const opts = {
    encoding: 'utf8',
    timeout: timeoutMs,
    windowsHide: true,
  };
  // Only set `env` when we have a token override to thread; leaving the option
  // unset preserves the existing `process.env` inheritance for back-compat with
  // unmapped slugs and tests that don't stub the resolver.
  if (env) opts.env = env;
  return execFileSync('gh', args, opts);
}

/**
 * Resolve the per-slug PAT for `repo` and return an env override suitable for
 * passing to `_runGh`. Returns `undefined` when no mapping exists (caller
 * falls back to the ambient `gh` identity inherited from `process.env`).
 *
 * Emits a one-shot `console.warn` per (slug, reason) so the dual-account audit
 * can spot stragglers without flooding the log on every comment post. Resolver
 * exceptions are swallowed — comment posting must never fail because token
 * resolution did. The pinned "Never run `gh auth switch`" policy survives even
 * when the mapping is missing: we just inherit the ambient identity instead of
 * forcibly mutating it.
 */
function _resolveTokenEnvForRepo(repo, resolveTokenForSlugFn) {
  const resolve = resolveTokenForSlugFn || _defaultResolveTokenForSlug;
  let token = null;
  try {
    token = resolve(repo);
  } catch (e) {
    const key = `error:${repo}`;
    if (!_warnedUnresolvedSlugs.has(key)) {
      _warnedUnresolvedSlugs.add(key);
      console.warn(
        `gh-comment: resolveTokenForSlug threw for "${repo}" (${e?.message || e}) — falling back to ambient gh identity`,
      );
    }
    return undefined;
  }
  if (!token) {
    if (!_warnedUnresolvedSlugs.has(repo)) {
      _warnedUnresolvedSlugs.add(repo);
      console.warn(
        `gh-comment: no token mapping for repo "${repo}" — falling back to ambient gh identity (audit straggler)`,
      );
    }
    return undefined;
  }
  return { ...process.env, GH_TOKEN: token };
}

function postPrComment({
  repo,
  prNumber,
  body,
  agentId,
  kind,
  workItemId,
  timeoutMs = 30000,
  execFileSync = _execFileSync,
  resolveTokenForSlug,
} = {}) {
  _validateRepo(repo);
  _validatePrNumber(prNumber);
  const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, body });
  const file = _writeTempBodyFile(finalBody);
  const env = _resolveTokenEnvForRepo(repo, resolveTokenForSlug);
  try {
    const output = _runGh(
      execFileSync,
      ['pr', 'comment', String(prNumber), '--repo', repo, '--body-file', file],
      timeoutMs,
      env,
    );
    return { output: String(output || '').trim(), bodyFile: file };
  } finally {
    try { fs.unlinkSync(file); } catch {}
  }
}

function postPrReviewComment({
  repo,
  prNumber,
  body,
  agentId,
  kind,
  workItemId,
  timeoutMs = 30000,
  execFileSync = _execFileSync,
  resolveTokenForSlug,
} = {}) {
  _validateRepo(repo);
  _validatePrNumber(prNumber);
  const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, body });
  const file = _writeTempBodyFile(finalBody);
  const env = _resolveTokenEnvForRepo(repo, resolveTokenForSlug);
  try {
    const output = _runGh(
      execFileSync,
      ['pr', 'review', String(prNumber), '--comment', '--repo', repo, '--body-file', file],
      timeoutMs,
      env,
    );
    return { output: String(output || '').trim(), bodyFile: file };
  } finally {
    try { fs.unlinkSync(file); } catch {}
  }
}

const _REVIEW_EVENT_FLAGS = Object.freeze({
  APPROVE: '--approve',
  REQUEST_CHANGES: '--request-changes',
  COMMENT: '--comment',
});

function postPrReview({
  event,
  repo,
  prNumber,
  body,
  agentId,
  kind,
  workItemId,
  timeoutMs = 30000,
  execFileSync = _execFileSync,
  resolveTokenForSlug,
} = {}) {
  const flag = _REVIEW_EVENT_FLAGS[event];
  if (!flag) {
    throw new Error(
      `invalid event: ${JSON.stringify(event)} (expected APPROVE | REQUEST_CHANGES | COMMENT)`,
    );
  }
  _validateRepo(repo);
  _validatePrNumber(prNumber);
  const finalBody = buildMinionsCommentBody({ agentId, kind, workItemId, body });
  const file = _writeTempBodyFile(finalBody);
  const env = _resolveTokenEnvForRepo(repo, resolveTokenForSlug);
  try {
    const output = _runGh(
      execFileSync,
      ['pr', 'review', String(prNumber), flag, '--repo', repo, '--body-file', file],
      timeoutMs,
      env,
    );
    return { output: String(output || '').trim(), bodyFile: file };
  } finally {
    try { fs.unlinkSync(file); } catch {}
  }
}

module.exports = {
  // Builders / parsers (pure functions — usable from anywhere)
  buildMinionsCommentBody,
  parseMinionsMarker,
  MINIONS_COMMENT_MARKER,
  MINIONS_COMMENT_MARKER_RE,
  // Validation regexes (exported for downstream consumers)
  AGENT_ID_RE,
  KIND_RE,
  WORK_ITEM_ID_RE,
  // gh wrappers (argv-form, --body-file)
  postPrComment,
  postPrReviewComment,
  postPrReview,
  // Internal helpers exported for tests / advanced callers
  _buildMarker,
  _writeTempBodyFile,
  _resolveTokenEnvForRepo,
  _clearTokenWarnings: () => _warnedUnresolvedSlugs.clear(),
};
