name: PR Gate

on:
  pull_request_target:
    types: [opened, closed, reopened]

concurrency:
  group: pr-gate-${{ github.event.pull_request.number }}
  cancel-in-progress: false

jobs:
  check-contributor:
    if: github.repository == 'herdrdev/herdr'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      issues: write
      pull-requests: write
    steps:
      - name: Check pull request intake policy
        uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
        with:
          github-token: ${{ secrets.KANGAL_GITHUB_TOKEN }}
          script: |
            const KANGAL_USER_ID = 285672167;
            const CI_ONLY_PR_AUTHOR_IDS = new Set([
              49699333, // dependabot[bot]
              41898282, // github-actions[bot]
            ]);
            const REVIEW_LABEL = 'ai-review';
            const COMMENT_MARKER = '<!-- herdr:pr-gate -->';

            const pullNumber = context.payload.pull_request.number;
            const defaultBranch = context.payload.repository.default_branch;

            const { data: pr } = await github.rest.pulls.get({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: pullNumber,
            });
            const prAuthor = pr.user.login;

            async function getPermission(username) {
              try {
                const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  username,
                });
                return data.permission;
              } catch {
                return null;
              }
            }

            async function getTextFile(path) {
              const { data } = await github.rest.repos.getContent({
                owner: context.repo.owner,
                repo: context.repo.repo,
                path,
                ref: defaultBranch,
              });
              if (!('content' in data) || typeof data.content !== 'string') {
                throw new Error(`Expected file content for ${path}`);
              }
              return Buffer.from(data.content, 'base64').toString('utf8');
            }

            function parseUserList(content) {
              return new Set(content
                .split('\n')
                .map(line => line.trim().toLowerCase())
                .filter(line => line && !line.startsWith('#')));
            }

            const [maintainersContent, approvedContributorsContent] = await Promise.all([
              getTextFile('.github/MAINTAINERS'),
              getTextFile('.github/APPROVED_CONTRIBUTORS'),
            ]);
            const maintainers = parseUserList(maintainersContent);
            const approvedContributors = parseUserList(approvedContributorsContent);

            async function isVerifiedMaintainer(username) {
              if (!username || !maintainers.has(username.toLowerCase())) return false;
              return ['admin', 'maintain', 'write'].includes(await getPermission(username));
            }

            async function currentLabels() {
              const labels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: pullNumber,
                per_page: 100,
              });
              return new Set(labels.map(label => label.name));
            }

            async function hasVerifiedRecovery() {
              const events = await github.paginate(github.rest.issues.listEventsForTimeline, {
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: pullNumber,
                per_page: 100,
              });
              const latestStateEvent = events.findLast(event =>
                ['closed', 'reopened'].includes(event.event));
              return latestStateEvent?.event === 'reopened' &&
                await isVerifiedMaintainer(latestStateEvent.actor?.login);
            }

            async function addReviewLabel() {
              if ((await currentLabels()).has(REVIEW_LABEL)) return;
              await github.rest.issues.addLabels({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: pullNumber,
                labels: [REVIEW_LABEL],
              });
            }

            async function removeReviewLabel() {
              if (!(await currentLabels()).has(REVIEW_LABEL)) return;
              try {
                await github.request('DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{name}', {
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  issue_number: pullNumber,
                  name: REVIEW_LABEL,
                });
              } catch (error) {
                if (error.status !== 404) throw error;
              }
            }

            async function upsertGateComment(message) {
              const comments = await github.paginate(github.rest.issues.listComments, {
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: pullNumber,
                per_page: 100,
              });
              const existing = comments.find(comment =>
                comment.user?.id === KANGAL_USER_ID && comment.body?.includes(COMMENT_MARKER));
              const body = `${COMMENT_MARKER}\n${message}`;
              if (existing?.body === body) return;
              if (existing) {
                await github.rest.issues.updateComment({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  comment_id: existing.id,
                  body,
                });
                return;
              }
              await github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: pullNumber,
                body,
              });
            }

            async function closePullRequest(reason) {
              if (await hasVerifiedRecovery()) {
                core.info(`PR #${pullNumber} was recovered by a verified maintainer; leaving it open`);
                await addReviewLabel();
                return;
              }
              await removeReviewLabel();
              const message = [
                `Hi @${prAuthor}, thanks for your interest in contributing.`,
                '',
                'Herdr does not accept unsolicited implementation pull requests from contributors who are not listed in `.github/APPROVED_CONTRIBUTORS`.',
                '',
                reason,
                '',
                'If you encountered a reproducible bug, report the observed behavior through the bug issue template. A report does not reserve the work or authorize a pull request; accepted fixes are normally implemented by Herdr’s maintainer-controlled agents.',
                '',
                'Feature requests, behavior changes, and other proposals belong in GitHub Discussions. Do not open an issue merely to justify an implementation that was already written.',
                '',
                'If a maintainer explicitly wants this implementation, they can reopen the pull request. Reopening by anyone else will be closed again automatically.',
                '',
                `See https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md for the contribution policy.`,
              ].join('\n');
              await upsertGateComment(message);
              if (await hasVerifiedRecovery()) {
                core.info(`PR #${pullNumber} was recovered while the gate was running; leaving it open`);
                await addReviewLabel();
                return;
              }
              await github.rest.pulls.update({
                owner: context.repo.owner,
                repo: context.repo.repo,
                pull_number: pullNumber,
                state: 'closed',
              });
            }

            if (pr.state === 'closed') {
              await removeReviewLabel();
              return;
            }

            if (CI_ONLY_PR_AUTHOR_IDS.has(pr.user.id)) {
              core.info(`Leaving CI-only bot PR open without automated AI review: ${prAuthor}`);
              await removeReviewLabel();
              return;
            }

            if (await isVerifiedMaintainer(prAuthor)) {
              core.info(`${prAuthor} is a verified maintainer`);
              await addReviewLabel();
              return;
            }

            if (approvedContributors.has(prAuthor.toLowerCase())) {
              core.info(`${prAuthor} is in the approved contributors list`);
              await addReviewLabel();
              return;
            }

            await closePullRequest('The pull request author is not an approved contributor.');
