name: Proposer Apply (PR)

on:
  issues:
    types: [labeled]
  workflow_dispatch:
    inputs:
      issue:
        description: "Issue number to apply (Proposer: correction/fact-check)"
        required: true

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

defaults:
  run:
    shell: bash

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

jobs:
  apply-proposer:
    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 }}
        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/proposer.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");
          }

          const labelName =
            ev?.label?.name ||
            ev?.label ||
            "workflow_dispatch";

          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/proposer.env

      - name: Gate on label state/approved
        run: |
          set -euo pipefail
          source /tmp/proposer.env

          if [[ "$LABEL_NAME" != "state/approved" && "$LABEL_NAME" != "workflow_dispatch" ]]; then
            echo "ℹ️ label=$LABEL_NAME => skip"
            echo "SKIP=1" >> /tmp/proposer.env
            exit 0
          fi
          echo "✅ proceed (issue=$ISSUE_NUMBER)"

      - name: Fetch issue + API-hard gate on (state/approved present + proposer type)
        env:
          FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
        run: |
          set -euo pipefail
          source /tmp/proposer.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/proposer.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) : [];

          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 hasApproved = labels.includes("state/approved");
          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)}`);
          out.push(`HAS_APPROVED=${hasApproved ? "1":"0"}`);

          if (!hasApproved) {
            out.push(`SKIP=1`);
            out.push(`SKIP_REASON=${JSON.stringify("approved_not_present")}`);
          } else if (!type) {
            out.push(`SKIP=1`);
            out.push(`SKIP_REASON=${JSON.stringify("missing_type")}`);
          } else if (!proposer.has(type)) {
            out.push(`SKIP=1`);
            out.push(`SKIP_REASON=${JSON.stringify("not_proposer:"+type)}`);
          }
          process.stdout.write(out.join("\n") + "\n");
          NODE

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

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

          [[ "${SKIP:-0}" == "1" ]] || exit 0
          [[ "$LABEL_NAME" == "state/approved" || "$LABEL_NAME" == "workflow_dispatch" ]] || exit 0

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

          if [[ "$REASON" == "approved_not_present" ]]; then
            MSG="ℹ️ Proposer Apply: skip — le label **state/approved** n'est pas présent sur le ticket au moment du run (gate API-hard)."
          elif [[ "$REASON" == "missing_type" ]]; then
            MSG="ℹ️ Proposer Apply: skip — champ **Type:** manquant/illisible. Attendu: type/correction ou type/fact-check."
          else
            MSG="ℹ️ Proposer Apply: skip — Type non-Proposer (${TYPE}). (Ce workflow ne traite que correction/fact-check.)"
          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" || true

      - name: Checkout default branch
        run: |
          set -euo pipefail
          source /tmp/proposer.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
          echo "✅ workspace:"
          ls -la | sed -n '1,120p'

      - name: Detect app dir (repo-root vs ./site)
        run: |
          set -euo pipefail
          source /tmp/proposer.env
          [[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }

          APP_DIR="."
          if [[ -d "site" && -f "site/package.json" ]]; then
            APP_DIR="site"
          fi

          echo "APP_DIR=$APP_DIR" >> /tmp/proposer.env
          echo "✅ APP_DIR=$APP_DIR"
          ls -la "$APP_DIR" | sed -n '1,120p'
          test -f "$APP_DIR/package.json" || { echo "❌ package.json missing in APP_DIR=$APP_DIR"; exit 1; }
          test -d "$APP_DIR/scripts" || { echo "❌ scripts/ missing in APP_DIR=$APP_DIR"; exit 1; }

      - name: NPM harden (reduce flakiness)
        run: |
          set -euo pipefail
          source /tmp/proposer.env
          [[ "${SKIP:-0}" != "1" ]] || exit 0
          cd "$APP_DIR"
          npm config set fetch-retries 5
          npm config set fetch-retry-mintimeout 20000
          npm config set fetch-retry-maxtimeout 120000
          npm config set registry https://registry.npmjs.org

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

      - name: Build dist baseline (APP_DIR)
        run: |
          set -euo pipefail
          source /tmp/proposer.env
          [[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }
          cd "$APP_DIR"
          npm run build

      - name: Apply ticket (alias + commit) on bot branch
        continue-on-error: true
        env:
          FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
          BOT_GIT_NAME: ${{ secrets.BOT_GIT_NAME }}
          BOT_GIT_EMAIL: ${{ secrets.BOT_GIT_EMAIL }}
          FORGE_API: ${{ vars.FORGE_API || vars.FORGE_BASE }}
        run: |
          set -euo pipefail
          source /tmp/proposer.env
          [[ "${SKIP:-0}" != "1" ]] || { echo "ℹ️ skipped"; exit 0; }

          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/proposer-${ISSUE_NUMBER}-${TS}"
          echo "BRANCH=$BR" >> /tmp/proposer.env
          git checkout -b "$BR"

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

          LOG="/tmp/proposer-apply.log"
          set +e
          (cd "$APP_DIR" && node scripts/apply-ticket.mjs "$ISSUE_NUMBER" --alias --commit) >"$LOG" 2>&1
          RC=$?
          set -e

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

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

          END_SHA="$(git rev-parse HEAD)"
          if [[ "$RC" -ne 0 ]]; then
            echo "NOOP=0" >> /tmp/proposer.env
            exit 0
          fi

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

      - name: Push bot branch
        if: ${{ always() }}
        env:
          FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
        run: |
          set -euo pipefail
          source /tmp/proposer.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; }
          [[ -n "${BRANCH:-}" ]] || { echo "ℹ️ BRANCH unset -> 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/proposer.env || true
          [[ "${SKIP:-0}" != "1" ]] || exit 0

          [[ "${APPLY_RC:-0}" == "0" ]] || exit 0
          [[ "${NOOP:-0}" == "0" ]] || exit 0
          [[ -n "${BRANCH:-}" ]] || { echo "ℹ️ BRANCH unset -> skip PR"; exit 0; }

          PR_TITLE="proposer: apply ticket #${ISSUE_NUMBER}"
          PR_BODY="PR auto depuis ticket #${ISSUE_NUMBER} (state/approved).\n\n- Branche: ${BRANCH}\n- Commit: ${END_SHA:-unknown}\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 Proposer 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"

      - name: Finalize (fail job if apply failed)
        if: ${{ always() }}
        run: |
          set -euo pipefail
          source /tmp/proposer.env || true
          [[ "${SKIP:-0}" != "1" ]] || exit 0

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