name: Anno Apply (PR)

on:
  issues:
    types: [labeled]
  workflow_dispatch:
    inputs:
      issue:
        description: "Issue number to apply"
        required: true

env:
  NODE_OPTIONS: --dns-result-order=ipv4first

defaults:
  run:
    shell: bash

concurrency:
  group: anno-apply-${{ github.event.issue.number || github.event.issue.index || inputs.issue || 'manual' }}
  cancel-in-progress: true

jobs:
  apply-approved:
    runs-on: mac-ci
    container:
      image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm

    steps:
      - name: Tools sanity
        run: |
          set -euo pipefail
          git --version
          node --version
          npm --version

      - name: Derive context (event.json / workflow_dispatch)
        env:
          INPUT_ISSUE: ${{ inputs.issue }}
          FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE || vars.FORGE_BASE_URL }}
        run: |
          set -euo pipefail
          export EVENT_JSON="/var/run/act/workflow/event.json"
          test -f "$EVENT_JSON" || { echo "❌ Missing $EVENT_JSON"; exit 1; }

          node --input-type=module - <<'NODE' > /tmp/anno.env
          import fs from "node:fs";

          const ev = JSON.parse(fs.readFileSync(process.env.EVENT_JSON, "utf8"));
          const repoObj = ev?.repository || {};

          const cloneUrl =
            repoObj?.clone_url ||
            (repoObj?.html_url ? (repoObj.html_url.replace(/\/$/,"") + ".git") : "");

          if (!cloneUrl) throw new Error("No repository clone_url/html_url in event.json");

          let owner =
            repoObj?.owner?.login ||
            repoObj?.owner?.username ||
            (repoObj?.full_name ? repoObj.full_name.split("/")[0] : "");

          let repo =
            repoObj?.name ||
            (repoObj?.full_name ? repoObj.full_name.split("/")[1] : "");

          if (!owner || !repo) {
            const m = cloneUrl.match(/[:/](?<o>[^/]+)\/(?<r>[^/]+?)(?:\.git)?$/);
            if (m?.groups) { owner = owner || m.groups.o; repo = repo || m.groups.r; }
          }
          if (!owner || !repo) throw new Error("Cannot infer owner/repo");

          const defaultBranch = repoObj?.default_branch || "main";

          const issueNumber =
            ev?.issue?.number ||
            ev?.issue?.index ||
            (process.env.INPUT_ISSUE ? Number(process.env.INPUT_ISSUE) : 0);

          if (!issueNumber || !Number.isFinite(Number(issueNumber))) {
            throw new Error("No issue number in event.json or workflow_dispatch input");
          }

          // label name: best-effort (non-bloquant)
          let labelName = "workflow_dispatch";
          const lab = ev?.label;
          if (typeof lab === "string") labelName = lab;
          else if (lab && typeof lab === "object" && typeof lab.name === "string") labelName = lab.name;
          else if (ev?.label?.name) labelName = ev.label.name;

          const u = new URL(cloneUrl);
          const origin = u.origin;

          const apiBase = (process.env.FORGE_API && String(process.env.FORGE_API).trim())
            ? String(process.env.FORGE_API).trim().replace(/\/+$/,"")
            : origin;

          function sh(s){ return JSON.stringify(String(s)); }

          process.stdout.write([
            `CLONE_URL=${sh(cloneUrl)}`,
            `OWNER=${sh(owner)}`,
            `REPO=${sh(repo)}`,
            `DEFAULT_BRANCH=${sh(defaultBranch)}`,
            `ISSUE_NUMBER=${sh(issueNumber)}`,
            `LABEL_NAME=${sh(labelName)}`,
            `API_BASE=${sh(apiBase)}`
          ].join("\n") + "\n");
          NODE

          echo "✅ context:"
          sed -n '1,120p' /tmp/anno.env

      - name: Early gate (label event fast-skip, but tolerant)
        run: |
          set -euo pipefail
          source /tmp/anno.env

          echo "ℹ️ event label = $LABEL_NAME"

          # Fast skip on obvious non-approved label events (avoid noise),
          # BUT do NOT skip if label payload is weird/unknown.
          if [[ "$LABEL_NAME" != "state/approved" && "$LABEL_NAME" != "workflow_dispatch" && "$LABEL_NAME" != "" && "$LABEL_NAME" != "[object Object]" ]]; then
            echo "ℹ️ label=$LABEL_NAME => skip early"
            echo "SKIP=1" >> /tmp/anno.env
            echo "SKIP_REASON=\"label_not_approved_event\"" >> /tmp/anno.env
            exit 0
          fi

          echo "✅ continue to API gating (issue=$ISSUE_NUMBER)"

      - name: Fetch issue + hard gate on labels + Type
        env:
          FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
        run: |
          set -euo pipefail
          source /tmp/anno.env
          [[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }

          test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }

          curl -fsS \
            -H "Authorization: token $FORGE_TOKEN" \
            -H "Accept: application/json" \
            "$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER" \
            -o /tmp/issue.json

          node --input-type=module - <<'NODE' >> /tmp/anno.env
          import fs from "node:fs";

          const issue = JSON.parse(fs.readFileSync("/tmp/issue.json","utf8"));
          const title = String(issue.title || "");
          const body = String(issue.body || "").replace(/\r\n/g, "\n");

          const labels = Array.isArray(issue.labels) ? issue.labels.map(l => String(l.name || "")).filter(Boolean) : [];
          const hasApproved = labels.includes("state/approved");

          function pickLine(key) {
            const re = new RegExp(`^\\s*${key}\\s*:\\s*([^\\n\\r]+)`, "mi");
            const m = body.match(re);
            return m ? m[1].trim() : "";
          }

          const typeRaw = pickLine("Type");
          const type = String(typeRaw || "").trim().toLowerCase();

          const allowed = new Set(["type/media","type/reference","type/comment"]);
          const proposer = new Set(["type/correction","type/fact-check"]);

          const out = [];
          out.push(`ISSUE_TITLE=${JSON.stringify(title)}`);
          out.push(`ISSUE_TYPE=${JSON.stringify(type)}`);

          // HARD gate: must currently have state/approved (avoids depending on event payload)
          if (!hasApproved) {
            out.push(`SKIP=1`);
            out.push(`SKIP_REASON=${JSON.stringify("not_approved_label_present")}`);
            process.stdout.write(out.join("\n") + "\n");
            process.exit(0);
          }

          if (!type) {
            out.push(`SKIP=1`);
            out.push(`SKIP_REASON=${JSON.stringify("missing_type")}`);
          } else if (allowed.has(type)) {
            // proceed
          } else if (proposer.has(type)) {
            out.push(`SKIP=1`);
            out.push(`SKIP_REASON=${JSON.stringify("proposer_type:"+type)}`);
          } else {
            out.push(`SKIP=1`);
            out.push(`SKIP_REASON=${JSON.stringify("unsupported_type:"+type)}`);
          }

          process.stdout.write(out.join("\n") + "\n");
          NODE

          echo "✅ gating result:"
          grep -E '^(ISSUE_TYPE|SKIP|SKIP_REASON)=' /tmp/anno.env || true

      - name: Comment issue if skipped (Proposer / unsupported / missing Type)
        if: ${{ always() }}
        env:
          FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
        run: |
          set -euo pipefail
          source /tmp/anno.env || true

          [[ "${SKIP:-0}" == "1" ]] || exit 0

          # IMPORTANT: do NOT comment for "not_approved_label_present" (avoid spam on other label events)
          if [[ "${SKIP_REASON:-}" == "not_approved_label_present" || "${SKIP_REASON:-}" == "label_not_approved_event" ]]; then
            echo "ℹ️ skip reason=${SKIP_REASON} -> no comment"
            exit 0
          fi

          test -n "${FORGE_TOKEN:-}" || exit 0

          REASON="${SKIP_REASON:-}"
          TYPE="${ISSUE_TYPE:-}"

          if [[ "$REASON" == proposer_type:* ]]; then
            MSG="ℹ️ Ticket #${ISSUE_NUMBER} détecté comme **Proposer** (${TYPE}).\n\n- Ce type est **traité manuellement par les editors**.\n✅ Aucun traitement automatique."
          elif [[ "$REASON" == unsupported_type:* ]]; then
            MSG="ℹ️ Ticket #${ISSUE_NUMBER} ignoré : Type non supporté par le bot (${TYPE}).\n\nTypes supportés : type/media, type/reference, type/comment."
          else
            MSG="ℹ️ Ticket #${ISSUE_NUMBER} ignoré : champ 'Type:' manquant ou illisible.\n\nAjoute : Type: type/media|type/reference|type/comment"
          fi

          PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"

          curl -fsS -X POST \
            -H "Authorization: token $FORGE_TOKEN" \
            -H "Content-Type: application/json" \
            "$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
            --data-binary "$PAYLOAD"

      - name: Checkout default branch
        run: |
          set -euo pipefail
          source /tmp/anno.env
          [[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }

          rm -rf .git
          git init -q
          git remote add origin "$CLONE_URL"
          git fetch --depth 1 origin "$DEFAULT_BRANCH"
          git -c advice.detachedHead=false checkout -q FETCH_HEAD
          git log -1 --oneline

      - name: Install deps
        run: |
          set -euo pipefail
          source /tmp/anno.env
          [[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
          npm ci --no-audit --no-fund

      - name: Check apply script exists
        run: |
          set -euo pipefail
          source /tmp/anno.env
          [[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
          test -f scripts/apply-annotation-ticket.mjs || {
            echo "❌ missing scripts/apply-annotation-ticket.mjs on $DEFAULT_BRANCH"
            ls -la scripts | sed -n '1,200p' || true
            exit 1
          }

      - name: Build dist (needed for --verify)
        run: |
          set -euo pipefail
          source /tmp/anno.env
          [[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }

          npm run build

          test -f dist/para-index.json || {
            echo "❌ missing dist/para-index.json after build"
            ls -la dist | sed -n '1,200p' || true
            exit 1
          }
          echo "✅ dist/para-index.json present"

      - name: Apply ticket on bot branch (strict+verify, commit)
        continue-on-error: true
        env:
          FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
          BOT_GIT_NAME: ${{ secrets.BOT_GIT_NAME }}
          BOT_GIT_EMAIL: ${{ secrets.BOT_GIT_EMAIL }}
        run: |
          set -euo pipefail
          source /tmp/anno.env
          [[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
          test -d .git || { echo "❌ not a git repo (checkout failed)"; echo "APPLY_RC=90" >> /tmp/anno.env; exit 0; }

          test -n "${FORGE_TOKEN:-}" || { echo "❌ Missing secret FORGE_TOKEN"; exit 1; }

          git config user.name  "${BOT_GIT_NAME:-archicratie-bot}"
          git config user.email "${BOT_GIT_EMAIL:-bot@archicratie.local}"

          START_SHA="$(git rev-parse HEAD)"
          TS="$(date -u +%Y%m%d-%H%M%S)"
          BR="bot/anno-${ISSUE_NUMBER}-${TS}"
          echo "BRANCH=$BR" >> /tmp/anno.env
          git checkout -b "$BR"

          export FORGE_API="$API_BASE"
          export GITEA_OWNER="$OWNER"
          export GITEA_REPO="$REPO"

          LOG="/tmp/apply.log"
          set +e
          node scripts/apply-annotation-ticket.mjs "$ISSUE_NUMBER" --strict --verify --commit >"$LOG" 2>&1
          RC=$?
          set -e

          echo "APPLY_RC=$RC" >> /tmp/anno.env

          echo "== apply log (tail) =="
          tail -n 180 "$LOG" || true

          END_SHA="$(git rev-parse HEAD)"

          if [[ "$RC" -ne 0 ]]; then
            echo "NOOP=0" >> /tmp/anno.env
            exit 0
          fi

          if [[ "$START_SHA" == "$END_SHA" ]]; then
            echo "NOOP=1" >> /tmp/anno.env
          else
            echo "NOOP=0" >> /tmp/anno.env
            echo "END_SHA=$END_SHA" >> /tmp/anno.env
          fi

      - name: Comment issue on failure (strict/verify/etc)
        if: ${{ always() }}
        env:
          FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
        run: |
          set -euo pipefail
          source /tmp/anno.env || true
          [[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }

          RC="${APPLY_RC:-0}"
          if [[ "$RC" == "0" ]]; then
            echo "ℹ️ no failure detected"
            exit 0
          fi

          test -n "${FORGE_TOKEN:-}" || exit 0

          if [[ -f /tmp/apply.log ]]; then
            BODY="$(tail -n 160 /tmp/apply.log | sed 's/\r$//')"
          else
            BODY="(no apply log found)"
          fi

          MSG="❌ apply-annotation-ticket a échoué (rc=${RC}).\n\n\`\`\`\n${BODY}\n\`\`\`\n"
          PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"

          curl -fsS -X POST \
            -H "Authorization: token $FORGE_TOKEN" \
            -H "Content-Type: application/json" \
            "$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
            --data-binary "$PAYLOAD"

      - name: Push bot branch
        if: ${{ always() }}
        env:
          FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
        run: |
          set -euo pipefail
          source /tmp/anno.env || true
          [[ "${SKIP:-0}" != "1" ]] || exit 0

          [[ "${APPLY_RC:-0}" == "0" ]] || { echo "ℹ️ apply failed -> skip push"; exit 0; }
          [[ "${NOOP:-0}" == "0" ]] || { echo "ℹ️ no-op -> skip push"; exit 0; }
          test -d .git || { echo "ℹ️ no git repo -> skip push"; exit 0; }

          AUTH_URL="$(node --input-type=module -e '
            const [clone, tok] = process.argv.slice(1);
            const u = new URL(clone);
            u.username = "oauth2";
            u.password = tok;
            console.log(u.toString());
          ' "$CLONE_URL" "$FORGE_TOKEN")"

          git remote set-url origin "$AUTH_URL"
          git push -u origin "$BRANCH"

      - name: Create PR + comment issue
        if: ${{ always() }}
        env:
          FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
        run: |
          set -euo pipefail
          source /tmp/anno.env || true
          [[ "${SKIP:-0}" != "1" ]] || exit 0

          [[ "${APPLY_RC:-0}" == "0" ]] || { echo "ℹ️ apply failed -> skip PR"; exit 0; }
          [[ "${NOOP:-0}" == "0" ]] || { echo "ℹ️ no-op -> skip PR"; exit 0; }

          PR_TITLE="anno: apply ticket #${ISSUE_NUMBER}"
          PR_BODY="PR auto depuis ticket #${ISSUE_NUMBER} (state/approved).\n\n- Branche: ${BRANCH}\n- Commit: ${END_SHA}\n\nMerge si CI OK."

          PR_PAYLOAD="$(node --input-type=module -e '
            const [title, body, base, head] = process.argv.slice(1);
            console.log(JSON.stringify({ title, body, base, head, allow_maintainer_edit: true }));
          ' "$PR_TITLE" "$PR_BODY" "$DEFAULT_BRANCH" "${OWNER}:${BRANCH}")"

          PR_JSON="$(curl -fsS -X POST \
            -H "Authorization: token $FORGE_TOKEN" \
            -H "Content-Type: application/json" \
            "$API_BASE/api/v1/repos/$OWNER/$REPO/pulls" \
            --data-binary "$PR_PAYLOAD")"

          PR_URL="$(node --input-type=module -e '
            const pr = JSON.parse(process.argv[1] || "{}");
            console.log(pr.html_url || pr.url || "");
          ' "$PR_JSON")"

          test -n "$PR_URL" || { echo "❌ PR URL missing. Raw: $PR_JSON"; exit 1; }

          MSG="✅ PR créée pour ticket #${ISSUE_NUMBER} : ${PR_URL}"
          C_PAYLOAD="$(node --input-type=module -e 'console.log(JSON.stringify({body: process.argv[1]||""}))' "$MSG")"

          curl -fsS -X POST \
            -H "Authorization: token $FORGE_TOKEN" \
            -H "Content-Type: application/json" \
            "$API_BASE/api/v1/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/comments" \
            --data-binary "$C_PAYLOAD"

          echo "✅ PR: $PR_URL"

      - name: Finalize (fail job if apply failed)
        if: ${{ always() }}
        run: |
          set -euo pipefail
          source /tmp/anno.env || true

          [[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }

          RC="${APPLY_RC:-0}"
          if [[ "$RC" != "0" ]]; then
            echo "❌ apply failed (rc=$RC)"
            exit "$RC"
          fi
          echo "✅ apply ok"