name: PR Triage

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

permissions:
  contents: read
  pull-requests: write

jobs:
  label:
    name: Label PR
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - name: Triage PR
        uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
        with:
          script: |
            const pr = context.payload.pull_request;
            const labels = new Set();

            // --- Bot detection ---
            const botLogins = ['dependabot[bot]', 'renovate[bot]', 'emdashbot[bot]'];
            if (botLogins.includes(pr.user.login)) {
              labels.add('bot');
            }

            // --- Size labels ---
            const { data: files } = await github.rest.pulls.listFiles({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: pr.number,
              per_page: 100,
            });

            const linesChanged = files.reduce((sum, f) => sum + f.additions + f.deletions, 0);
            const sizeLabels = ['size/XS', 'size/S', 'size/M', 'size/L', 'size/XL'];
            let sizeLabel;
            if (linesChanged < 10) sizeLabel = 'size/XS';
            else if (linesChanged < 50) sizeLabel = 'size/S';
            else if (linesChanged < 200) sizeLabel = 'size/M';
            else if (linesChanged < 500) sizeLabel = 'size/L';
            else sizeLabel = 'size/XL';
            labels.add(sizeLabel);

            // --- Area labels ---
            const areaMap = {
              'area/core': (f) => f.startsWith('packages/core/'),
              'area/admin': (f) => f.startsWith('packages/admin/'),
              'area/plugins': (f) => f.startsWith('packages/plugins/'),
              'area/docs': (f) => f.startsWith('docs/'),
              'area/templates': (f) => f.startsWith('templates/'),
              'area/ci': (f) => f.startsWith('.github/'),
              'area/auth': (f) => f.startsWith('packages/auth/'),
              'area/cloudflare': (f) => f.startsWith('packages/cloudflare/'),
            };

            for (const file of files) {
              for (const [label, matcher] of Object.entries(areaMap)) {
                if (matcher(file.filename)) {
                  labels.add(label);
                }
              }
            }

            // --- Merge conflict detection ---
            // mergeable is available on the full PR object (may need a separate fetch
            // since the webhook payload sometimes has mergeable=null while computing)
            try {
              const { data: fullPr } = await github.rest.pulls.get({
                owner: context.repo.owner,
                repo: context.repo.repo,
                pull_number: pr.number,
              });
              if (fullPr.mergeable === false) {
                labels.add('needs-rebase');
              }
            } catch {
              // Ignore -- mergeable state may not be computed yet
            }

            // CLA labels are managed by the CLA workflow (cla.yml), not here.

            // --- Ensure all labels exist, then apply ---
            const labelColors = {
              'bot': 'ededed',
              'size/XS': '3cbf00',
              'size/S': '5dba3f',
              'size/M': 'fbca04',
              'size/L': 'ee9b00',
              'size/XL': 'd93f0b',
              'area/core': '0052cc',
              'area/admin': '7057ff',
              'area/plugins': '008672',
              'area/docs': '0075ca',
              'area/templates': 'bfdadc',
              'area/ci': '000000',
              'area/auth': 'd4c5f9',
              'area/cloudflare': 'f9a825',
              'needs-rebase': 'e11d48',
            };

            // Get existing labels on the repo
            const existingLabels = new Set();
            for await (const response of github.paginate.iterator(
              github.rest.issues.listLabelsForRepo,
              { owner: context.repo.owner, repo: context.repo.repo, per_page: 100 }
            )) {
              for (const label of response.data) {
                existingLabels.add(label.name);
              }
            }

            // Create any missing labels
            for (const label of labels) {
              if (!existingLabels.has(label) && labelColors[label]) {
                await github.rest.issues.createLabel({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  name: label,
                  color: labelColors[label],
                });
              }
            }

            // Get current labels on the PR to remove stale ones
            const currentLabels = new Set(pr.labels.map(l => l.name));

            // Helper: remove a label, ignoring 404 if it's already gone
            async function safeRemoveLabel(name) {
              try {
                await github.rest.issues.removeLabel({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  issue_number: pr.number,
                  name,
                });
              } catch (e) {
                if (e.status !== 404) throw e;
              }
            }

            // Remove stale size labels (only one size label at a time)
            for (const sl of sizeLabels) {
              if (sl !== sizeLabel && currentLabels.has(sl)) {
                await safeRemoveLabel(sl);
              }
            }

            // Remove needs-rebase if PR is now mergeable
            if (!labels.has('needs-rebase') && currentLabels.has('needs-rebase')) {
              await safeRemoveLabel('needs-rebase');
            }

            // Add new labels
            const toAdd = [...labels].filter(l => !currentLabels.has(l));
            if (toAdd.length > 0) {
              await github.rest.issues.addLabels({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: pr.number,
                labels: toAdd,
              });
            }

            core.info(`Applied labels: ${[...labels].join(', ')}`);

            // --- Scope guard: comment on oversized PRs ---
            // Only on open (not synchronize) to avoid spamming on every push
            if (context.payload.action === 'opened') {
              const warnings = [];

              if (linesChanged > 500) {
                warnings.push(`This PR changes **${linesChanged.toLocaleString()} lines** across **${files.length} files**. Large PRs are harder to review and more likely to be closed without review.`);
              } else if (files.length > 20) {
                warnings.push(`This PR touches **${files.length} files**. PRs with a broad scope are harder to review. Please confirm the scope hasn't drifted beyond the intended change.`);
              }

              // Check for cross-area changes (touching multiple packages)
              const areas = Object.keys(areaMap).filter(a => labels.has(a));
              if (areas.length > 3) {
                warnings.push(`This PR spans ${areas.length} different areas (${areas.join(', ')}). Consider breaking it into smaller, focused PRs.`);
              }

              if (warnings.length > 0) {
                const marker = '<!-- scope-guard -->';
                const body = [
                  marker,
                  '## Scope check',
                  '',
                  ...warnings,
                  '',
                  'If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.',
                  '',
                  'See [CONTRIBUTING.md](https://github.com/emdash-cms/emdash/blob/main/CONTRIBUTING.md) for contribution guidelines.',
                ].join('\n');

                await github.rest.issues.createComment({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  issue_number: pr.number,
                  body,
                });
              }
            }
