# Creates a new git tag when a PR is merged, or on manual dispatch
#
# PR trigger flow:
# - Triggered whenever a PR is merged, if that PR made code changes
# - If version wasn't bumped in PR, increment patch version in Cargo.toml
# - Otherwise (if the PR did bump version) we use the new version from Cargo.toml
# - Creates and pushes a git tag for the new version
# - That git tag then triggers Docker publishing (any tag) and, for minor/major
#   (X.Y.0) tags, the release-drafting workflow
# - Add labels and release comments to referenced issues (if applicable)
# - Finally, shows summary of actions taken and new tag published
#
# Manual dispatch flow:
# - If a version is provided, sets Cargo.toml to that version
# - If no version is provided, increments patch version automatically
# - Creates and pushes a git tag
#
# Note: pushing the tag must trigger downstream workflows (release, docker), so
# checkout uses a PAT (secrets.BOT_TOKEN). With the default GITHUB_TOKEN, GitHub
# suppresses workflow runs triggered by the tag push.

name: 🔖 Tag

on:
  workflow_dispatch:
    inputs:
      version:
        description: 'Version to tag (e.g. 1.7.0). Leave blank to auto-increment patch.'
        required: false
        type: string
  # Btw, this is a safe and intentional trigger
  # It only runs once reviewed PR merged, and secrets are scoped
  pull_request_target: # zizmor: ignore[dangerous-triggers]
    types: [closed]
    branches: [main]

concurrency:
  group: auto-version-and-tag
  cancel-in-progress: false

permissions:
  contents: write
  pull-requests: read
  issues: write

env:
  IS_MANUAL: ${{ github.event_name == 'workflow_dispatch' }}

jobs:
  version-and-tag:
    if: >-
      github.event_name == 'workflow_dispatch'
      || github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - name: 🔢 Validate manual dispatch
        if: env.IS_MANUAL == 'true'
        env:
          INPUT_VERSION: ${{ inputs.version }}
          DISPATCH_REF: ${{ github.ref }}
        run: |
          set -euo pipefail
          if [ "$DISPATCH_REF" != "refs/heads/main" ]; then
            echo "::error::Manual dispatch only allowed from main (got: $DISPATCH_REF)"
            exit 1
          fi
          if [ -n "$INPUT_VERSION" ] && ! printf '%s' "$INPUT_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
            echo "::error::Invalid version '${INPUT_VERSION}'. Must be semver (e.g. 1.7.0)."
            exit 1
          fi

      - name: 📂 Check PR for code changes and version bump
        id: check_pr
        if: env.IS_MANUAL != 'true'
        uses: actions/github-script@v8
        with:
          script: |
            const { owner, repo } = context.repo;
            const pull_number = context.payload.pull_request.number;

            const files = await github.paginate(
              github.rest.pulls.listFiles, { owner, repo, pull_number }
            );
            const codePatterns = [
              /^src\//, /^build\.rs$/, /^Cargo\.lock$/, /^Dockerfile/,
            ];
            const codeChanged = files.some(f =>
              codePatterns.some(p => p.test(f.filename))
            );
            const manifestChanged = files.some(f => f.filename === 'Cargo.toml');

            if (!codeChanged && !manifestChanged) {
              core.info('No code or Cargo.toml changes, skipping');
              core.setOutput('needs_bump', 'false');
              core.setOutput('needs_tag', 'false');
              return;
            }

            const parseVersion = (text) => {
              const m = text.match(/^version\s*=\s*"([^"]+)"/m);
              return m ? m[1] : null;
            };

            let versionBumped = false;
            if (manifestChanged) {
              const mergeSha = context.payload.pull_request.merge_commit_sha;
              const { data: mergeCommit } = await github.rest.git.getCommit({
                owner, repo, commit_sha: mergeSha,
              });
              const parentSha = mergeCommit.parents[0].sha;
              const getVersion = async (ref) => {
                const { data } = await github.rest.repos.getContent({
                  owner, repo, path: 'Cargo.toml', ref,
                });
                return parseVersion(Buffer.from(data.content, 'base64').toString());
              };
              const [prevVersion, mergeVersion] = await Promise.all([
                getVersion(parentSha), getVersion(mergeSha),
              ]);
              versionBumped = prevVersion !== mergeVersion;
              core.info(`Version: ${prevVersion} → ${mergeVersion}`);
            }

            const needsBump = codeChanged && !versionBumped;
            const needsTag = codeChanged || versionBumped;
            core.info(`Needs bump: ${needsBump}, Needs tag: ${needsTag}`);
            core.setOutput('needs_bump', needsBump.toString());
            core.setOutput('needs_tag', needsTag.toString());

      - name: 🔐 Validate release token
        if: env.IS_MANUAL == 'true' || steps.check_pr.outputs.needs_tag == 'true'
        env:
          BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
        run: |
          set -euo pipefail
          if [ -z "$BOT_TOKEN" ]; then
            echo "::error::BOT_TOKEN is required so pushed tags trigger downstream release workflows"
            exit 1
          fi

      - name: 🛎️ Checkout repository
        if: env.IS_MANUAL == 'true' || steps.check_pr.outputs.needs_tag == 'true'
        uses: actions/checkout@v6 # zizmor: ignore[artipacked]
        with:
          ref: ${{ github.event.pull_request.merge_commit_sha || github.ref }}
          token: ${{ secrets.BOT_TOKEN }}

      - name: 👤 Configure git identity
        if: env.IS_MANUAL == 'true' || steps.check_pr.outputs.needs_tag == 'true'
        run: |
          git config user.name "Liss-Bot"
          git config user.email "liss-bot@d0h.co"

      - name: 🔎 Resolve tag version
        id: version
        if: env.IS_MANUAL == 'true' || steps.check_pr.outputs.needs_tag == 'true'
        env:
          INPUT_VERSION: ${{ inputs.version }}
          NEEDS_BUMP: ${{ steps.check_pr.outputs.needs_bump }}
        run: |
          set -euo pipefail
          read_version() { grep -m1 '^version' Cargo.toml | sed -E 's/.*"(.*)".*/\1/'; }
          CUR=$(read_version)
          if [ "$IS_MANUAL" = "true" ] && [ -n "$INPUT_VERSION" ]; then
            VERSION="$INPUT_VERSION"
          elif [ "$IS_MANUAL" = "true" ] || [ "$NEEDS_BUMP" = "true" ]; then
            IFS=. read -r MA MI PA <<< "$CUR"
            VERSION="${MA}.${MI}.$((PA + 1))"
          else
            VERSION="$CUR"
          fi

          git fetch --tags --force
          if git rev-parse "refs/tags/$VERSION" >/dev/null 2>&1; then
            echo "::error::Tag $VERSION already exists"
            echo "version=$VERSION" >> "$GITHUB_OUTPUT"
            exit 1
          fi
          echo "version=$VERSION" >> "$GITHUB_OUTPUT"

      - name: 🔍 Extract referenced issues
        id: issues
        if: env.IS_MANUAL != 'true' && steps.check_pr.outputs.needs_tag == 'true'
        uses: actions/github-script@v8
        with:
          script: |
            const body = context.payload.pull_request.body || '';
            const prNumber = String(context.payload.pull_request.number);
            const matches = body.match(/#(\d+)(?![a-fA-F0-9])/g);
            if (!matches) {
              core.info('No issue references found in PR body');
              core.setOutput('numbers', '');
              return;
            }
            const unique = [...new Set(matches.map(m => m.replace('#', '')))]
              .filter(n => n !== prNumber && parseInt(n, 10) > 0);
            if (unique.length === 0) {
              core.info('No issue references after filtering');
              core.setOutput('numbers', '');
              return;
            }
            core.info(`Found issue references: ${unique.join(', ')}`);
            core.setOutput('numbers', unique.join(','));

      - name: ⬆️ Bump version
        id: bump
        if: >-
          env.IS_MANUAL == 'true'
          || steps.check_pr.outputs.needs_bump == 'true'
        env:
          VERSION: ${{ steps.version.outputs.version }}
        run: |
          set -euo pipefail
          read_version() { grep -m1 '^version' Cargo.toml | sed -E 's/.*"(.*)".*/\1/'; }
          CUR=$(read_version)
          NEW="$VERSION"

          if [ "$NEW" = "$CUR" ]; then
            echo "Cargo.toml already at $NEW, nothing to commit"
            echo "bumped=false" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          # Update the package version (first top-level `version = "..."` only)
          sed -i -E "0,/^version = \".*\"/ s//version = \"${NEW}\"/" Cargo.toml
          # Keep Cargo.lock in sync (version line directly follows the package name)
          if [ -f Cargo.lock ]; then
            sed -i -E "/^name = \"adguardian\"$/{n; s/^version = \".*\"/version = \"${NEW}\"/}" Cargo.lock
          fi

          git add Cargo.toml Cargo.lock
          if git diff --cached --quiet; then
            echo "No changes to commit"
            echo "bumped=false" >> "$GITHUB_OUTPUT"
          else
            git commit -m "Bump version to ${NEW}"
            git push origin HEAD:main
            echo "bumped=true" >> "$GITHUB_OUTPUT"
          fi

      - name: 🏷️ Create and push tag
        id: tag
        if: env.IS_MANUAL == 'true' || steps.check_pr.outputs.needs_tag == 'true'
        env:
          PR_NUMBER: ${{ github.event.pull_request.number || '' }}
          PR_TITLE: ${{ github.event.pull_request.title || '' }}
          PR_AUTHOR: ${{ github.event.pull_request.user.login || github.actor }}
          MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha || github.sha }}
          ISSUES: ${{ steps.issues.outputs.numbers }}
          VERSION: ${{ steps.version.outputs.version }}
        run: |
          set -euo pipefail
          git fetch --tags --force

          {
            printf 'AdGuardian v%s 🚀\n\n' "$VERSION"
            if [ -n "$PR_NUMBER" ]; then
              printf 'PR: #%s - %s\n' "$PR_NUMBER" "$PR_TITLE"
            else
              printf 'Manual release by @%s\n' "$PR_AUTHOR"
            fi
            if [ -n "$ISSUES" ]; then
              printf 'Resolves: %s\n' "$(echo "$ISSUES" | sed 's/,/, #/g; s/^/#/')"
            fi
            printf 'Author: @%s\n' "$PR_AUTHOR"
            printf 'Commit: %s\n' "$MERGE_SHA"
          } > tag-message.txt

          git tag -a "$VERSION" -F tag-message.txt
          git push origin "$VERSION"
          echo "result=created" >> "$GITHUB_OUTPUT"
          echo "version=$VERSION" >> "$GITHUB_OUTPUT"

      - name: 🛩️ Label referenced issues
        id: label
        if: >-
          env.IS_MANUAL != 'true'
          && steps.check_pr.outputs.needs_tag == 'true'
          && steps.issues.outputs.numbers != ''
        continue-on-error: true
        uses: actions/github-script@v8
        env:
          ISSUES: ${{ steps.issues.outputs.numbers }}
        with:
          github-token: ${{ secrets.BOT_TOKEN }}
          script: |
            const { owner, repo } = context.repo;
            const manifest = require('fs').readFileSync('Cargo.toml', 'utf8');
            const version = manifest.match(/^version\s*=\s*"([^"]+)"/m)[1];
            const labelName = `🛩️ Released ${version}`;
            const issues = process.env.ISSUES.split(',').filter(Boolean);

            try {
              await github.rest.issues.createLabel({
                owner, repo,
                name: labelName,
                color: 'EDEDED',
                description: `Included in release v${version}`,
              });
              core.info(`Created label: ${labelName}`);
            } catch (e) {
              if (e.status === 422) {
                core.info(`Label already exists: ${labelName}`);
              } else {
                core.warning(`Failed to create label: ${e.message}`);
              }
            }

            const prNumber = context.payload.pull_request.number;
            const prAuthor = context.payload.pull_request.user?.login;
            const creditAuthor = prAuthor && prAuthor.toLowerCase() !== 'lissy93';
            const marker = `released-${version}`;
            for (const num of issues) {
              const issue_number = parseInt(num, 10);
              try {
                const [{ data: issue }, comments] = await Promise.all([
                  github.rest.issues.get({ owner, repo, issue_number }),
                  github.rest.issues.listComments({
                    owner, repo, issue_number, per_page: 100,
                  }),
                ]);
                const alreadyCommented = comments.data.some(
                  c => c.body?.includes(marker)
                );
                if (!alreadyCommented) {
                  const author = issue.user?.login;
                  const greeting = author ? `Hey @${author},` : 'Hey,';
                  const sixMonthsAgo = new Date();
                  sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
                  const isOld = new Date(issue.created_at) < sixMonthsAgo;
                  const byLine = creditAuthor ? ` by @${prAuthor}` : '';
                  const parts = [
                    greeting,
                    `this has now been implemented${byLine} in #${prNumber},`,
                    `and will be released shortly in ${version} 😇`,
                  ];
                  if (isOld) parts.push(`\n\nWe're sorry this one took so long 😔`);
                  parts.push(`<!-- ${marker} -->`);
                  const body = parts.join(' ');
                  await github.rest.issues.createComment({
                    owner, repo, issue_number, body,
                  });
                }
                await github.rest.issues.addLabels({
                  owner, repo, issue_number, labels: [labelName, '✅ Fixed'],
                });
                core.info(alreadyCommented
                  ? `Already commented on #${num}, labels applied`
                  : `Commented and labeled #${num}`);
              } catch (e) {
                core.warning(`Failed to process #${num}: ${e.message}`);
              }
            }

      - name: 📋 Job summary
        if: always()
        env:
          PR_NUMBER: ${{ github.event.pull_request.number || '' }}
          PR_TITLE: ${{ github.event.pull_request.title || '' }}
          REPO_URL: ${{ github.server_url }}/${{ github.repository }}
          NEEDS_BUMP: ${{ steps.check_pr.outputs.needs_bump }}
          NEEDS_TAG: ${{ steps.check_pr.outputs.needs_tag }}
          ISSUES: ${{ steps.issues.outputs.numbers }}
          BUMPED: ${{ steps.bump.outputs.bumped }}
          TAG_OUTCOME: ${{ steps.tag.outcome }}
          TAG_RESULT: ${{ steps.tag.outputs.result }}
          TAG_VERSION: ${{ steps.tag.outputs.version }}
          LABEL_OUTCOME: ${{ steps.label.outcome }}
        run: |
          set -euo pipefail
          VERSION="${TAG_VERSION:-$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"(.*)".*/\1/' 2>/dev/null || echo "unknown")}"

          {
            echo "## 🔖 Auto Version & Tag"
            echo ""
            echo "| Step | Result |"
            echo "|------|--------|"

            if [ "$IS_MANUAL" = "true" ]; then
              echo "| Trigger | Manual dispatch |"
            elif [ -n "$PR_NUMBER" ]; then
              echo "| PR | [#${PR_NUMBER}](${REPO_URL}/pull/${PR_NUMBER}) — ${PR_TITLE} |"
            fi

            if [ "$BUMPED" = "true" ]; then
              echo "| Version bump | ✅ \`${VERSION}\` |"
            else
              echo "| Version bump | ⏭️ Skipped |"
            fi

            if [ "$TAG_RESULT" = "created" ]; then
              echo "| Tag | ✅ [\`${VERSION}\`](${REPO_URL}/releases/tag/${VERSION}) |"
            elif [ "$TAG_RESULT" = "existed" ]; then
              echo "| Tag | ⏭️ Already exists: \`${VERSION}\` |"
            elif [ "$TAG_OUTCOME" = "failure" ]; then
              echo "| Tag | ❌ Failed |"
            else
              echo "| Tag | ⏭️ Skipped |"
            fi

            if [ -n "$ISSUES" ]; then
              ISSUE_LINKS=$(echo "$ISSUES" | tr ',' '\n' | sed "s|.*|[#&](${REPO_URL}/issues/&)|" | paste -sd ' ' -)
              if [ "$LABEL_OUTCOME" = "success" ]; then
                echo "| Issues labeled | ✅ ${ISSUE_LINKS} |"
              else
                echo "| Issues labeled | ⚠️ ${ISSUE_LINKS} |"
              fi
            fi
          } >> "$GITHUB_STEP_SUMMARY"
