name: pr-standards

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

jobs:
  check-standards:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - name: Check PR standards
        uses: actions/github-script@v7
        with:
          script: |
            const pr = context.payload.pull_request;
            const login = pr.user.login;

            // Skip PRs older than Feb 18, 2026 at 6PM EST (Feb 19, 2026 00:00 UTC)
            const cutoff = new Date('2026-02-19T00:00:00Z');
            const prCreated = new Date(pr.created_at);
            if (prCreated < cutoff) {
              console.log(`Skipping: PR #${pr.number} was created before cutoff (${prCreated.toISOString()})`);
              return;
            }

            // Check if author is a team member or bot
            if (login === 'opencode-agent[bot]') return;
            const { data: file } = await github.rest.repos.getContent({
              owner: context.repo.owner,
              repo: context.repo.repo,
              path: '.github/TEAM_MEMBERS',
              ref: 'dev'
            });
            const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean);
            if (members.includes(login)) {
              console.log(`Skipping: ${login} is a team member`);
              return;
            }

            const title = pr.title;

            async function addLabel(label) {
              await github.rest.issues.addLabels({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: pr.number,
                labels: [label]
              });
            }

            async function removeLabel(label) {
              try {
                await github.rest.issues.removeLabel({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  issue_number: pr.number,
                  name: label
                });
              } catch (e) {
                // Label wasn't present, ignore
              }
            }

            async function comment(marker, body) {
              const markerText = `<!-- pr-standards:${marker} -->`;
              const { data: comments } = await github.rest.issues.listComments({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: pr.number
              });
              
              const existing = comments.find(c => c.body.includes(markerText));
              if (existing) return;
              
              await github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: pr.number,
                body: markerText + '\n' + body
              });
            }

            // Step 1: Check title format
            // Matches: feat:, feat(scope):, feat (scope):, etc.
            const titlePattern = /^(feat|fix|docs|chore|refactor|test)\s*(\([a-zA-Z0-9-]+\))?\s*:/;
            const hasValidTitle = titlePattern.test(title);

            if (!hasValidTitle) {
              await addLabel('needs:title');
              await comment('title', `Hey! Your PR title \`${title}\` doesn't follow conventional commit format.

            Please update it to start with one of:
            - \`feat:\` or \`feat(scope):\` new feature
            - \`fix:\` or \`fix(scope):\` bug fix
            - \`docs:\` or \`docs(scope):\` documentation changes
            - \`chore:\` or \`chore(scope):\` maintenance tasks
            - \`refactor:\` or \`refactor(scope):\` code refactoring
            - \`test:\` or \`test(scope):\` adding or updating tests

            Where \`scope\` is the package name (e.g., \`app\`, \`desktop\`, \`opencode\`).

            See [CONTRIBUTING.md](../blob/dev/CONTRIBUTING.md#pr-titles) for details.`);
              return;
            }

            await removeLabel('needs:title');

            // Step 2: Check for linked issue (skip for docs/refactor/feat PRs)
            const skipIssueCheck = /^(docs|refactor|feat)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title);
            if (skipIssueCheck) {
              await removeLabel('needs:issue');
              console.log('Skipping issue check for docs/refactor/feat PR');
              return;
            }
            const query = `
              query($owner: String!, $repo: String!, $number: Int!) {
                repository(owner: $owner, name: $repo) {
                  pullRequest(number: $number) {
                    closingIssuesReferences(first: 1) {
                      totalCount
                    }
                  }
                }
              }
            `;

            const result = await github.graphql(query, {
              owner: context.repo.owner,
              repo: context.repo.repo,
              number: pr.number
            });

            const linkedIssues = result.repository.pullRequest.closingIssuesReferences.totalCount;

            if (linkedIssues === 0) {
              await addLabel('needs:issue');
              await comment('issue', `Thanks for your contribution!

            This PR doesn't have a linked issue. All PRs must reference an existing issue.

            Please:
            1. Open an issue describing the bug/feature (if one doesn't exist)
            2. Add \`Fixes #<number>\` or \`Closes #<number>\` to this PR description

            See [CONTRIBUTING.md](../blob/dev/CONTRIBUTING.md#issue-first-policy) for details.`);
              return;
            }

            await removeLabel('needs:issue');
            console.log('PR meets all standards');

  check-compliance:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - name: Check PR template compliance
        uses: actions/github-script@v7
        with:
          script: |
            const pr = context.payload.pull_request;
            const login = pr.user.login;

            // Skip PRs older than Feb 18, 2026 at 6PM EST (Feb 19, 2026 00:00 UTC)
            const cutoff = new Date('2026-02-19T00:00:00Z');
            const prCreated = new Date(pr.created_at);
            if (prCreated < cutoff) {
              console.log(`Skipping: PR #${pr.number} was created before cutoff (${prCreated.toISOString()})`);
              return;
            }

            // Check if author is a team member or bot
            if (login === 'opencode-agent[bot]') return;
            const { data: file } = await github.rest.repos.getContent({
              owner: context.repo.owner,
              repo: context.repo.repo,
              path: '.github/TEAM_MEMBERS',
              ref: 'dev'
            });
            const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean);
            if (members.includes(login)) {
              console.log(`Skipping: ${login} is a team member`);
              return;
            }

            const body = pr.body || '';
            const title = pr.title;
            const isDocsRefactorOrFeat = /^(docs|refactor|feat)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title);

            const issues = [];

            // Check: template sections exist
            const hasWhatSection = /### What does this PR do\?/.test(body);
            const hasTypeSection = /### Type of change/.test(body);
            const hasVerifySection = /### How did you verify your code works\?/.test(body);
            const hasChecklistSection = /### Checklist/.test(body);
            const hasIssueSection = /### Issue for this PR/.test(body);

            if (!hasWhatSection || !hasTypeSection || !hasVerifySection || !hasChecklistSection || !hasIssueSection) {
              issues.push('PR description is missing required template sections. Please use the [PR template](../blob/dev/.github/pull_request_template.md).');
            }

            // Check: "What does this PR do?" has real content (not just placeholder text)
            if (hasWhatSection) {
              const whatMatch = body.match(/### What does this PR do\?\s*\n([\s\S]*?)(?=###|$)/);
              const whatContent = whatMatch ? whatMatch[1].trim() : '';
              const placeholder = 'Please provide a description of the issue';
              const onlyPlaceholder = whatContent.includes(placeholder) && whatContent.replace(placeholder, '').replace(/[*\s]/g, '').length < 20;
              if (!whatContent || onlyPlaceholder) {
                issues.push('"What does this PR do?" section is empty or only contains placeholder text. Please describe your changes.');
              }
            }

            // Check: at least one "Type of change" checkbox is checked
            if (hasTypeSection) {
              const typeMatch = body.match(/### Type of change\s*\n([\s\S]*?)(?=###|$)/);
              const typeContent = typeMatch ? typeMatch[1] : '';
              const hasCheckedBox = /- \[x\]/i.test(typeContent);
              if (!hasCheckedBox) {
                issues.push('No "Type of change" checkbox is checked. Please select at least one.');
              }
            }

            // Check: issue reference (skip for docs/refactor/feat)
            if (!isDocsRefactorOrFeat && hasIssueSection) {
              const issueMatch = body.match(/### Issue for this PR\s*\n([\s\S]*?)(?=###|$)/);
              const issueContent = issueMatch ? issueMatch[1].trim() : '';
              const hasIssueRef = /(closes|fixes|resolves)\s+#\d+/i.test(issueContent) || /#\d+/.test(issueContent);
              if (!hasIssueRef) {
                issues.push('No issue referenced. Please add `Closes #<number>` linking to the relevant issue.');
              }
            }

            // Check: "How did you verify" has content
            if (hasVerifySection) {
              const verifyMatch = body.match(/### How did you verify your code works\?\s*\n([\s\S]*?)(?=###|$)/);
              const verifyContent = verifyMatch ? verifyMatch[1].trim() : '';
              if (!verifyContent) {
                issues.push('"How did you verify your code works?" section is empty. Please explain how you tested.');
              }
            }

            // Check: checklist boxes are checked
            if (hasChecklistSection) {
              const checklistMatch = body.match(/### Checklist\s*\n([\s\S]*?)(?=###|$)/);
              const checklistContent = checklistMatch ? checklistMatch[1] : '';
              const unchecked = (checklistContent.match(/- \[ \]/g) || []).length;
              const checked = (checklistContent.match(/- \[x\]/gi) || []).length;
              if (checked < 2) {
                issues.push('Not all checklist items are checked. Please confirm you have tested locally and have not included unrelated changes.');
              }
            }

            // Helper functions
            async function addLabel(label) {
              await github.rest.issues.addLabels({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: pr.number,
                labels: [label]
              });
            }

            async function removeLabel(label) {
              try {
                await github.rest.issues.removeLabel({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  issue_number: pr.number,
                  name: label
                });
              } catch (e) {}
            }

            const hasComplianceLabel = pr.labels.some(l => l.name === 'needs:compliance');

            if (issues.length > 0) {
              // Non-compliant
              if (!hasComplianceLabel) {
                await addLabel('needs:compliance');
              }

              const marker = '<!-- issue-compliance -->';
              const { data: comments } = await github.rest.issues.listComments({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: pr.number
              });
              const existing = comments.find(c => c.body.includes(marker));

              const body_text = `${marker}
            This PR doesn't fully meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) and [PR template](../blob/dev/.github/pull_request_template.md).

            **What needs to be fixed:**
            ${issues.map(i => `- ${i}`).join('\n')}

            Please edit this PR description to address the above within **2 hours**, or it will be automatically closed.

            If you believe this was flagged incorrectly, please let a maintainer know.`;

              if (existing) {
                await github.rest.issues.updateComment({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  comment_id: existing.id,
                  body: body_text
                });
              } else {
                await github.rest.issues.createComment({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  issue_number: pr.number,
                  body: body_text
                });
              }

              console.log(`PR #${pr.number} is non-compliant: ${issues.join(', ')}`);
            } else if (hasComplianceLabel) {
              // Was non-compliant, now fixed
              await removeLabel('needs:compliance');

              const { data: comments } = await github.rest.issues.listComments({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: pr.number
              });
              const marker = '<!-- issue-compliance -->';
              const existing = comments.find(c => c.body.includes(marker));
              if (existing) {
                await github.rest.issues.deleteComment({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  comment_id: existing.id
                });
              }

              await github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: pr.number,
                body: 'Thanks for updating your PR! It now meets our contributing guidelines. :+1:'
              });

              console.log(`PR #${pr.number} is now compliant, label removed`);
            } else {
              console.log(`PR #${pr.number} is compliant`);
            }
