import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'

// Merge a PR only when its "PR Gate" check is green. Free tier has no branch
// protection, so this is the enforcement point: agents merge THROUGH this guard
// (a PreToolUse hook blocks raw `gh pr merge`). Fail-closed — a PR with no gate
// run, a pending gate, or a failed gate is refused, never merged.

const GATE_WORKFLOW = 'PR Gate'
const MERGE_METHODS = new Set(['squash', 'merge', 'rebase'])

function defaultExec(command, args) {
  const result = spawnSync(command, args, {
    encoding: 'utf8',
    env: process.env,
    maxBuffer: 50 * 1024 * 1024,
  })
  return {
    status: result.status ?? 1,
    stdout: result.stdout ?? '',
    stderr: result.stderr ?? result.error?.message ?? '',
  }
}

export async function safeMerge(input, dependencies = {}) {
  const { pr, method = 'squash' } = input
  const exec = dependencies.exec ?? defaultExec

  if (!/^\d+$/.test(String(pr ?? ''))) throw new Error('PR number is required (a positive integer)')
  if (!MERGE_METHODS.has(method)) throw new Error(`invalid merge method: ${method} (squash|merge|rebase)`)

  const view = await exec('gh', ['pr', 'view', String(pr), '--json', 'statusCheckRollup,state,headRefName'])
  if (view.status !== 0) throw new Error(`cannot read PR #${pr}: ${(view.stderr || view.stdout || '').trim()}`)

  let parsed
  try {
    parsed = JSON.parse(view.stdout || '{}')
  } catch {
    throw new Error(`cannot parse PR #${pr} check status`)
  }

  if (parsed.state && parsed.state !== 'OPEN') throw new Error(`refusing to merge PR #${pr}: state is ${parsed.state}, not OPEN`)

  const rollup = Array.isArray(parsed.statusCheckRollup) ? parsed.statusCheckRollup : []
  const gate = rollup.filter(check => check?.workflowName === GATE_WORKFLOW)

  if (gate.length === 0) {
    throw new Error(`refusing to merge PR #${pr}: no "${GATE_WORKFLOW}" check has run — push the branch and let the gate complete`)
  }

  const notGreen = gate.filter(check => check?.conclusion !== 'SUCCESS')
  if (notGreen.length > 0) {
    const detail = notGreen.map(c => `${c.name || GATE_WORKFLOW}=${c.conclusion || c.status || 'PENDING'}`).join(', ')
    throw new Error(`refusing to merge PR #${pr}: "${GATE_WORKFLOW}" is not green (${detail})`)
  }

  const merge = await exec('gh', ['pr', 'merge', String(pr), `--${method}`, '--delete-branch'])
  if (merge.status !== 0) {
    // `gh pr merge` performs the merge server-side, then a LOCAL post-merge step
    // (checkout of the base branch). In a worktree-heavy repo the base branch is
    // often held by another git worktree, so that local step fails ("'main' is
    // already used by worktree") even though the merge landed. The source of truth
    // is the PR's actual state, not gh's exit code — verify it before failing.
    const after = await exec('gh', ['pr', 'view', String(pr), '--json', 'state'])
    let afterState
    try {
      afterState = JSON.parse(after.stdout || '{}').state
    } catch {
      afterState = undefined
    }
    if (afterState !== 'MERGED') {
      throw new Error(`gh pr merge failed for #${pr}: ${(merge.stderr || merge.stdout || '').trim()}`)
    }
    // Merge succeeded; gh only failed its local cleanup, which also skips deleting the
    // remote branch that --delete-branch promised. Delete it directly (best-effort).
    if (parsed.headRefName) {
      await exec('gh', ['api', '-X', 'DELETE', `repos/{owner}/{repo}/git/refs/heads/${parsed.headRefName}`])
    }
  }

  return { merged: true, pr: Number(pr), method }
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  const pr = process.argv[2]
  const method = process.argv[3] || 'squash'
  safeMerge({ pr, method })
    .then(result => process.stdout.write(`merged PR #${result.pr} (${result.method})\n`))
    .catch(error => {
      process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
      process.exitCode = 1
    })
}
