# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# This file is part of a Moko Consulting project.
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# FILE INFORMATION
# DEFGROUP: GitHub.Workflow
# INGROUP: MokoStandards.Deploy
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /templates/workflows/shared/deploy-demo.yml
# VERSION: 04.05.13
# BRIEF: SFTP deployment workflow for demo server — synced to all governed repos
# NOTE: Synced via bulk-repo-sync to .github/workflows/deploy-demo.yml in all governed repos.
#       Port is resolved in order: DEMO_FTP_PORT variable → :port suffix in DEMO_FTP_HOST → 22.

name: Deploy to Demo Server (SFTP)

# Deploys the contents of the src/ directory to the demo server via SFTP.
# Triggers on push/merge to main — deploys the production-ready build to the demo server.
#
# Required org-level variables:  DEMO_FTP_HOST, DEMO_FTP_PATH, DEMO_FTP_USERNAME
# Optional org-level variable:   DEMO_FTP_PORT (auto-detected from host or defaults to 22)
# Optional org/repo variable:    DEMO_FTP_SUFFIX — when set, appended to DEMO_FTP_PATH to form the
#                                 full remote destination: DEMO_FTP_PATH/DEMO_FTP_SUFFIX
# Ignore rules:                  Place a .ftpignore file in the repository root. Each non-empty,
#                                 non-comment line is a glob pattern tested against the relative path
#                                 of each file (e.g. "subdir/file.txt"). The .gitignore is NOT used.
# Required org-level secret:     DEMO_FTP_KEY (preferred) or DEMO_FTP_PASSWORD
#
# Access control: only users with admin or maintain role on the repository may deploy.

on:
  push:
    branches:
      - main
      - master
    paths:
      - 'src/**'
      - 'htdocs/**'
  pull_request:
    types: [opened, synchronize, reopened, closed]
    branches:
      - main
      - master
    paths:
      - 'src/**'
      - 'htdocs/**'
  workflow_dispatch:
    inputs:
      clear_remote:
        description: 'Delete all files inside the remote destination folder before uploading'
        required: false
        default: false
        type: boolean

permissions:
  contents: read
  pull-requests: write

env:
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
  check-permission:
    name: Verify Deployment Permission
    runs-on: ubuntu-latest
    steps:
      - name: Check actor permission
        env:
          # Prefer the org-scoped GH_TOKEN secret (needed for the org membership
          # fallback). Falls back to the built-in github.token so the collaborator
          # endpoint still works even if GH_TOKEN is not configured.
          GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
        run: |
          ACTOR="${{ github.actor }}"
          REPO="${{ github.repository }}"
          ORG="${{ github.repository_owner }}"

          METHOD=""
          AUTHORIZED="false"

          # Hardcoded authorized users — always allowed to deploy
          AUTHORIZED_USERS="jmiller-moko github-actions[bot]"
          for user in $AUTHORIZED_USERS; do
            if [ "$ACTOR" = "$user" ]; then
              AUTHORIZED="true"
              METHOD="hardcoded allowlist"
              PERMISSION="admin"
              break
            fi
          done

          # For other actors, check repo/org permissions via API
          if [ "$AUTHORIZED" != "true" ]; then
            PERMISSION=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" \
              --jq '.permission' 2>/dev/null)
            METHOD="repo collaborator API"

            if [ -z "$PERMISSION" ]; then
              ORG_ROLE=$(gh api "orgs/${ORG}/memberships/${ACTOR}" \
                --jq '.role' 2>/dev/null)
              METHOD="org membership API"
              if [ "$ORG_ROLE" = "owner" ]; then
                PERMISSION="admin"
              else
                PERMISSION="none"
              fi
            fi

            case "$PERMISSION" in
              admin|maintain) AUTHORIZED="true" ;;
            esac
          fi

          # Write detailed summary
          {
            echo "## 🔐 Deploy Authorization"
            echo ""
            echo "| Field | Value |"
            echo "|-------|-------|"
            echo "| **Actor** | \`${ACTOR}\` |"
            echo "| **Repository** | \`${REPO}\` |"
            echo "| **Permission** | \`${PERMISSION}\` |"
            echo "| **Method** | ${METHOD} |"
            echo "| **Authorized** | ${AUTHORIZED} |"
            echo "| **Trigger** | \`${{ github.event_name }}\` |"
            echo "| **Branch** | \`${{ github.ref_name }}\` |"
            echo ""
          } >> "$GITHUB_STEP_SUMMARY"

          if [ "$AUTHORIZED" = "true" ]; then
            echo "✅ ${ACTOR} authorized to deploy (${METHOD})" >> "$GITHUB_STEP_SUMMARY"
          else
            echo "❌ ${ACTOR} is NOT authorized to deploy." >> "$GITHUB_STEP_SUMMARY"
            echo "" >> "$GITHUB_STEP_SUMMARY"
            echo "Deployment requires one of:" >> "$GITHUB_STEP_SUMMARY"
            echo "- Being in the hardcoded allowlist" >> "$GITHUB_STEP_SUMMARY"
            echo "- Having \`admin\` or \`maintain\` role on the repository" >> "$GITHUB_STEP_SUMMARY"
            exit 1
          fi

  deploy:
    name: SFTP Deploy → Demo
    runs-on: ubuntu-latest
    needs: [check-permission]
    if: >-
      !startsWith(github.head_ref || github.ref_name, 'chore/') &&
      (github.event_name == 'workflow_dispatch' ||
      github.event_name == 'push' ||
      (github.event_name == 'pull_request' &&
        (github.event.action == 'opened' ||
         github.event.action == 'synchronize' ||
         github.event.action == 'reopened' ||
         github.event.pull_request.merged == true)))

    steps:
      - name: Checkout repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

      - name: Resolve source directory
        id: source
        run: |
          # Resolve source directory: src/ preferred, htdocs/ as fallback
          if [ -d "src" ]; then
            SRC="src"
          elif [ -d "htdocs" ]; then
            SRC="htdocs"
          else
            echo "⚠️ No src/ or htdocs/ directory found — skipping deployment"
            echo "skip=true" >> "$GITHUB_OUTPUT"
            exit 0
          fi
          COUNT=$(find "$SRC" -type f | wc -l)
          echo "✅ Source: ${SRC}/ (${COUNT} file(s))"
          echo "skip=false" >> "$GITHUB_OUTPUT"
          echo "dir=${SRC}"  >> "$GITHUB_OUTPUT"

      - name: Preview files to deploy
        if: steps.source.outputs.skip == 'false'
        env:
          SOURCE_DIR: ${{ steps.source.outputs.dir }}
        run: |
          # ── Convert a ftpignore-style glob line to an ERE pattern ──────────────
          ftpignore_to_regex() {
            local line="$1"
            local anchored=false
            # Strip inline comments and whitespace
            line=$(printf '%s' "$line" | sed 's/[[:space:]]*#.*$//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
            [ -z "$line" ] && return
            # Skip negation patterns (not supported)
            [[ "$line" == !* ]] && return
            # Trailing slash = directory marker; strip it
            line="${line%/}"
            # Leading slash = anchored to root; strip it
            if [[ "$line" == /* ]]; then
              anchored=true
              line="${line#/}"
            fi
            # Escape ERE special chars, then restore glob semantics
            local regex
            regex=$(printf '%s' "$line" \
              | sed 's/[.+^${}()|[\\]/\\&/g' \
              | sed 's/\\\*\\\*/\x01/g' \
              | sed 's/\\\*/[^\/]*/g' \
              | sed 's/\x01/.*/g' \
              | sed 's/\\\?/[^\/]/g')
            if $anchored; then
              printf '^%s(/|$)' "$regex"
            else
              printf '(^|/)%s(/|$)' "$regex"
            fi
          }

          # ── Read .ftpignore (ftpignore-style globs) ─────────────────────────
          IGNORE_PATTERNS=()
          IGNORE_SOURCES=()
          if [ -f ".ftpignore" ]; then
            while IFS= read -r line; do
              [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue
              regex=$(ftpignore_to_regex "$line")
              [ -n "$regex" ] && IGNORE_PATTERNS+=("$regex") && IGNORE_SOURCES+=("$line")
            done < ".ftpignore"
          fi

          # ── Walk src/ and classify every file ────────────────────────────────
          WILL_UPLOAD=()
          IGNORED_FILES=()
          while IFS= read -r -d '' file; do
            rel="${file#${SOURCE_DIR}/}"
            SKIP=false
            for i in "${!IGNORE_PATTERNS[@]}"; do
              if echo "$rel" | grep -qE "${IGNORE_PATTERNS[$i]}" 2>/dev/null; then
                IGNORED_FILES+=("$rel | .ftpignore \`${IGNORE_SOURCES[$i]}\`")
                SKIP=true; break
              fi
            done
            $SKIP && continue
            WILL_UPLOAD+=("$rel")
          done < <(find "$SOURCE_DIR" -type f -print0 | sort -z)

          UPLOAD_COUNT="${#WILL_UPLOAD[@]}"
          IGNORE_COUNT="${#IGNORED_FILES[@]}"

          echo "ℹ️  ${UPLOAD_COUNT} file(s) will be uploaded, ${IGNORE_COUNT} ignored"

          # ── Write deployment preview to step summary ──────────────────────────
          {
            echo "## 📋 Deployment Preview"
            echo ""
            echo "| Field | Value |"
            echo "|---|---|"
            echo "| Source | \`${SOURCE_DIR}/\` |"
            echo "| Files to upload | **${UPLOAD_COUNT}** |"
            echo "| Files ignored | **${IGNORE_COUNT}** |"
            echo ""
            if [ "${UPLOAD_COUNT}" -gt 0 ]; then
              echo "### 📂 Files that will be uploaded"
              echo '```'
              printf '%s\n' "${WILL_UPLOAD[@]}"
              echo '```'
              echo ""
            fi
            if [ "${IGNORE_COUNT}" -gt 0 ]; then
              echo "### ⏭️ Files excluded"
              echo "| File | Reason |"
              echo "|---|---|"
              for entry in "${IGNORED_FILES[@]}"; do
                f="${entry% | *}"; r="${entry##* | }"
                echo "| \`${f}\` | ${r} |"
              done
              echo ""
            fi
          } >> "$GITHUB_STEP_SUMMARY"

      - name: Resolve SFTP host and port
        if: steps.source.outputs.skip == 'false'
        id: conn
        env:
          HOST_RAW: ${{ vars.DEMO_FTP_HOST }}
          PORT_VAR: ${{ vars.DEMO_FTP_PORT }}
        run: |
          HOST="$HOST_RAW"
          PORT="$PORT_VAR"

          if [ -z "$HOST" ]; then
            echo "⏭️ DEMO_FTP_HOST not configured — skipping demo deployment."
            echo "skip=true" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          # Priority 1 — explicit DEMO_FTP_PORT variable
          if [ -n "$PORT" ]; then
            echo "ℹ️  Using explicit DEMO_FTP_PORT=${PORT}"

          # Priority 2 — port embedded in DEMO_FTP_HOST  (host:port)
          elif [[ "$HOST" == *:* ]]; then
            PORT="${HOST##*:}"
            HOST="${HOST%:*}"
            echo "ℹ️  Extracted port ${PORT} from DEMO_FTP_HOST"

          # Priority 3 — SFTP default
          else
            PORT="22"
            echo "ℹ️  No port specified — defaulting to SFTP port 22"
          fi

          echo "host=${HOST}" >> "$GITHUB_OUTPUT"
          echo "port=${PORT}" >> "$GITHUB_OUTPUT"
          echo "SFTP target: ${HOST}:${PORT}"

      - name: Build remote path
        if: steps.source.outputs.skip == 'false' && steps.conn.outputs.skip != 'true'
        id: remote
        env:
          DEMO_FTP_PATH: ${{ vars.DEMO_FTP_PATH }}
          DEMO_FTP_SUFFIX:  ${{ vars.DEMO_FTP_SUFFIX }}
        run: |
          BASE="$DEMO_FTP_PATH"

          if [ -z "$BASE" ]; then
            echo "⏭️ DEMO_FTP_PATH not configured — skipping demo deployment."
            echo "skip=true" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          # DEMO_FTP_SUFFIX is required — it identifies the remote subdirectory for this repo.
          # Without it we cannot safely determine the deployment target.
          if [ -z "$DEMO_FTP_SUFFIX" ]; then
            echo "⏭️  DEMO_FTP_SUFFIX variable is not set — skipping deployment."
            echo "   Set DEMO_FTP_SUFFIX as a repo or org variable to enable deploy-demo."
            echo "skip=true" >> "$GITHUB_OUTPUT"
            echo "path=" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          REMOTE="${BASE%/}/${DEMO_FTP_SUFFIX#/}"

          # ── Platform-specific path safety guards ──────────────────────────────
          PLATFORM=""
          MOKO_FILE=".github/.mokostandards"; [ ! -f "$MOKO_FILE" ] && MOKO_FILE=".mokostandards"; if [ -f "$MOKO_FILE" ]; then
            PLATFORM=$(grep -E '^platform:' "$MOKO_FILE" | sed 's/.*:[[:space:]]*//' | tr -d '"')
          fi

          if [ "$PLATFORM" = "crm-module" ]; then
            # Dolibarr modules must deploy under htdocs/custom/ — guard against
            # accidentally overwriting server root or unrelated directories.
            if [[ "$REMOTE" != *custom* ]]; then
              echo "❌ Safety check failed: Dolibarr (crm-module) remote path must contain 'custom'."
              echo "   Current path: ${REMOTE}"
              echo "   Set DEMO_FTP_SUFFIX to the module's htdocs/custom/ subdirectory."
              exit 1
            fi
          fi

          if [ "$PLATFORM" = "waas-component" ]; then
            # Joomla extensions may only deploy to the server's tmp/ directory.
            if [[ "$REMOTE" != *tmp* ]]; then
              echo "❌ Safety check failed: Joomla (waas-component) remote path must contain 'tmp'."
              echo "   Current path: ${REMOTE}"
              echo "   Set DEMO_FTP_SUFFIX to a path under the server tmp/ directory."
              exit 1
            fi
          fi

          echo "ℹ️  Remote path: ${REMOTE}"
          echo "path=${REMOTE}" >> "$GITHUB_OUTPUT"

      - name: Detect SFTP authentication method
        if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
        id: auth
        env:
          HAS_KEY:      ${{ secrets.DEMO_FTP_KEY }}
          HAS_PASSWORD: ${{ secrets.DEMO_FTP_PASSWORD }}
        run: |
          if [ -n "$HAS_KEY" ] && [ -n "$HAS_PASSWORD" ]; then
            # Both set: key auth with password as passphrase; falls back to password-only if key fails
            echo "method=key" >> "$GITHUB_OUTPUT"
            echo "use_passphrase=true" >> "$GITHUB_OUTPUT"
            echo "has_password=true" >> "$GITHUB_OUTPUT"
            echo "ℹ️  Primary: SSH key + passphrase (DEMO_FTP_KEY / DEMO_FTP_PASSWORD)"
            echo "ℹ️  Fallback: password-only auth if key authentication fails"
          elif [ -n "$HAS_KEY" ]; then
            # Key only: no passphrase, no password fallback
            echo "method=key" >> "$GITHUB_OUTPUT"
            echo "use_passphrase=false" >> "$GITHUB_OUTPUT"
            echo "has_password=false" >> "$GITHUB_OUTPUT"
            echo "ℹ️  Using SSH key authentication (DEMO_FTP_KEY, no passphrase, no fallback)"
          elif [ -n "$HAS_PASSWORD" ]; then
            # Password only: direct SFTP password auth
            echo "method=password" >> "$GITHUB_OUTPUT"
            echo "use_passphrase=false" >> "$GITHUB_OUTPUT"
            echo "has_password=true" >> "$GITHUB_OUTPUT"
            echo "ℹ️  Using password authentication (DEMO_FTP_PASSWORD)"
          else
            echo "❌ No SFTP credentials configured."
            echo "   Set DEMO_FTP_KEY (preferred) or DEMO_FTP_PASSWORD as an org-level secret."
            exit 1
          fi

      - name: Setup PHP
        if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
        uses: shivammathur/setup-php@fcafdd6392932010c2bd5094439b8e33be2a8a09 # v2.37.0
        with:
          php-version: '8.1'
          tools: composer

      - name: Setup MokoStandards deploy tools
        if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
        env:
          GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
          COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || github.token }}"}}'
        run: |
          git clone --depth 1 --branch version/04 --quiet \
            "https://x-access-token:${GH_TOKEN}@github.com/mokoconsulting-tech/MokoStandards.git" \
            /tmp/mokostandards
          cd /tmp/mokostandards
          composer install --no-dev --no-interaction --quiet

      - name: Clear remote destination folder (manual only)
        if: >-
          steps.source.outputs.skip == 'false' &&
          steps.remote.outputs.skip != 'true' &&
          inputs.clear_remote == true
        env:
          SFTP_HOST:      ${{ steps.conn.outputs.host }}
          SFTP_PORT:      ${{ steps.conn.outputs.port }}
          SFTP_USER:      ${{ vars.DEMO_FTP_USERNAME }}
          SFTP_KEY:       ${{ secrets.DEMO_FTP_KEY }}
          SFTP_PASSWORD:  ${{ secrets.DEMO_FTP_PASSWORD }}
          AUTH_METHOD:    ${{ steps.auth.outputs.method }}
          USE_PASSPHRASE: ${{ steps.auth.outputs.use_passphrase }}
          HAS_PASSWORD:   ${{ steps.auth.outputs.has_password }}
          REMOTE_PATH:    ${{ steps.remote.outputs.path }}
        run: |
          cat > /tmp/moko_clear.php << 'PHPEOF'
          <?php
          declare(strict_types=1);
          require '/tmp/mokostandards/vendor/autoload.php';

          use phpseclib3\Net\SFTP;
          use phpseclib3\Crypt\PublicKeyLoader;

          $host          = (string) getenv('SFTP_HOST');
          $port          = (int)    getenv('SFTP_PORT');
          $username      = (string) getenv('SFTP_USER');
          $authMethod    = (string) getenv('AUTH_METHOD');
          $usePassphrase = getenv('USE_PASSPHRASE') === 'true';
          $hasPassword   = getenv('HAS_PASSWORD')   === 'true';
          $remotePath    = rtrim((string) getenv('REMOTE_PATH'), '/');

          echo "⚠️  Clearing remote folder: {$remotePath}\n";

          $sftp = new SFTP($host, $port);

          // ── Authentication ──────────────────────────────────────────────
          if ($authMethod === 'key') {
              $keyData    = (string) getenv('SFTP_KEY');
              $passphrase = $usePassphrase ? (string) getenv('SFTP_PASSWORD') : false;
              $password   = $hasPassword   ? (string) getenv('SFTP_PASSWORD') : '';
              $key = PublicKeyLoader::load($keyData, $passphrase);
              if (!$sftp->login($username, $key)) {
                  if ($password !== '') {
                      echo "⚠️  Key auth failed — falling back to password\n";
                      if (!$sftp->login($username, $password)) {
                          fwrite(STDERR, "❌ Both key and password authentication failed\n");
                          exit(1);
                      }
                      echo "✅ Connected via password authentication (key fallback)\n";
                  } else {
                      fwrite(STDERR, "❌ Key authentication failed and no password fallback is available\n");
                      exit(1);
                  }
              } else {
                  echo "✅ Connected via SSH key authentication\n";
              }
          } else {
              if (!$sftp->login($username, (string) getenv('SFTP_PASSWORD'))) {
                  fwrite(STDERR, "❌ Password authentication failed\n");
                  exit(1);
              }
              echo "✅ Connected via password authentication\n";
          }

          // ── Recursive delete ────────────────────────────────────────────
          function rmrf(SFTP $sftp, string $path): void
          {
              $entries = $sftp->nlist($path);
              if ($entries === false) {
                  return; // path does not exist — nothing to clear
              }
              foreach ($entries as $name) {
                  if ($name === '.' || $name === '..') {
                      continue;
                  }
                  $entry = "{$path}/{$name}";
                  if ($sftp->is_dir($entry)) {
                      rmrf($sftp, $entry);
                      $sftp->rmdir($entry);
                      echo "  🗑️  Removed dir:  {$entry}\n";
                  } else {
                      $sftp->delete($entry);
                      echo "  🗑️  Removed file: {$entry}\n";
                  }
              }
          }

          // ── Create remote directory tree ────────────────────────────────
          function sftpMakedirs(SFTP $sftp, string $path): void
          {
              $parts = array_values(array_filter(explode('/', $path), fn(string $p) => $p !== ''));
              $current = str_starts_with($path, '/') ? '' : '';
              foreach ($parts as $part) {
                  $current .= '/' . $part;
                  $sftp->mkdir($current); // silently returns false if already exists
              }
          }

          rmrf($sftp, $remotePath);
          sftpMakedirs($sftp, $remotePath);
          echo "✅ Remote folder ready: {$remotePath}\n";
          PHPEOF
          php /tmp/moko_clear.php

      - name: Deploy via SFTP
        if: steps.source.outputs.skip == 'false' && steps.remote.outputs.skip != 'true'
        env:
          SFTP_HOST:       ${{ steps.conn.outputs.host }}
          SFTP_PORT:       ${{ steps.conn.outputs.port }}
          SFTP_USER:       ${{ vars.DEMO_FTP_USERNAME }}
          SFTP_KEY:        ${{ secrets.DEMO_FTP_KEY }}
          SFTP_PASSWORD:   ${{ secrets.DEMO_FTP_PASSWORD }}
          AUTH_METHOD:     ${{ steps.auth.outputs.method }}
          USE_PASSPHRASE:  ${{ steps.auth.outputs.use_passphrase }}
          REMOTE_PATH:     ${{ steps.remote.outputs.path }}
          SOURCE_DIR:      ${{ steps.source.outputs.dir }}
        run: |
          # ── Write SSH key to temp file (key auth only) ────────────────────────
          if [ "$AUTH_METHOD" = "key" ]; then
            printf '%s' "$SFTP_KEY" > /tmp/deploy_key
            chmod 600 /tmp/deploy_key
          fi

          # ── Generate sftp-config.json safely via jq ───────────────────────────
          if [ "$AUTH_METHOD" = "key" ]; then
            jq -n \
              --arg  host "$SFTP_HOST" \
              --argjson port "${SFTP_PORT:-22}" \
              --arg  user "$SFTP_USER" \
              --arg  path "$REMOTE_PATH" \
              --arg  key  "/tmp/deploy_key" \
              '{host:$host, port:$port, user:$user, remote_path:$path, ssh_key_file:$key}' \
              > /tmp/sftp-config.json
          else
            jq -n \
              --arg  host "$SFTP_HOST" \
              --argjson port "${SFTP_PORT:-22}" \
              --arg  user "$SFTP_USER" \
              --arg  path "$REMOTE_PATH" \
              --arg  pass "$SFTP_PASSWORD" \
              '{host:$host, port:$port, user:$user, remote_path:$path, password:$pass}' \
              > /tmp/sftp-config.json
          fi

          # ── Write update files (demo = stable) ─────────────────────────────
          PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true)
          VERSION=$(php /tmp/mokostandards/api/cli/version_read.php --path . 2>/dev/null || echo "unknown")
          REPO="${{ github.repository }}"

          if [ "$PLATFORM" = "crm-module" ]; then
            printf '%s' "$VERSION" > update.txt
          fi

          if [ "$PLATFORM" = "waas-component" ]; then
            MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)
            if [ -n "$MANIFEST" ]; then
              EXT_NAME=$(grep -oP '<name>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || echo "${{ github.event.repository.name }}")
              EXT_TYPE=$(grep -oP '<extension[^>]+type="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "component")
              EXT_ELEMENT=$(grep -oP '<element>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1 || basename "$MANIFEST" .xml)
              EXT_CLIENT=$(grep -oP '<extension[^>]+client="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "")
              EXT_FOLDER=$(grep -oP '<extension[^>]+group="\K[^"]+' "$MANIFEST" 2>/dev/null || echo "")
              TARGET_PLATFORM=$(grep -oP '<targetplatform[^/]*/' "$MANIFEST" 2>/dev/null | head -1 || true)
              [ -n "$TARGET_PLATFORM" ] && TARGET_PLATFORM="${TARGET_PLATFORM}>"
              [ -z "$TARGET_PLATFORM" ] && TARGET_PLATFORM=$(printf '<targetplatform name="joomla" version="5.*" %s>' "/")

              CLIENT_TAG=""
              if [ -n "$EXT_CLIENT" ]; then CLIENT_TAG="<client>${EXT_CLIENT}</client>"; elif [ "$EXT_TYPE" = "module" ] || [ "$EXT_TYPE" = "plugin" ]; then CLIENT_TAG="<client>site</client>"; fi
              FOLDER_TAG=""
              if [ -n "$EXT_FOLDER" ] && [ "$EXT_TYPE" = "plugin" ]; then FOLDER_TAG="<folder>${EXT_FOLDER}</folder>"; fi

              DOWNLOAD_URL="https://github.com/${REPO}/releases/download/v${VERSION}/${EXT_ELEMENT}-${VERSION}.zip"
              {
                printf '%s\n' '<?xml version="1.0" encoding="utf-8"?>'
                printf '%s\n' '<updates>'
                printf '%s\n' '  <update>'
                printf '%s\n' "    <name>${EXT_NAME}</name>"
                printf '%s\n' "    <description>${EXT_NAME} update</description>"
                printf '%s\n' "    <element>${EXT_ELEMENT}</element>"
                printf '%s\n' "    <type>${EXT_TYPE}</type>"
                printf '%s\n' "    <version>${VERSION}</version>"
                [ -n "$CLIENT_TAG" ] && printf '%s\n' "    ${CLIENT_TAG}"
                [ -n "$FOLDER_TAG" ] && printf '%s\n' "    ${FOLDER_TAG}"
                printf '%s\n' '    <tags>'
                printf '%s\n' '      <tag>stable</tag>'
                printf '%s\n' '    </tags>'
                printf '%s\n' "    <infourl title=\"${EXT_NAME}\">https://github.com/${REPO}</infourl>"
                printf '%s\n' '    <downloads>'
                printf '%s\n' "      <downloadurl type=\"full\" format=\"zip\">${DOWNLOAD_URL}</downloadurl>"
                printf '%s\n' '    </downloads>'
                printf '%s\n' "    ${TARGET_PLATFORM}"
                printf '%s\n' '    <maintainer>Moko Consulting</maintainer>'
                printf '%s\n' '    <maintainerurl>https://mokoconsulting.tech</maintainerurl>'
                printf '%s\n' '  </update>'
                printf '%s\n' '</updates>'
              } > updates.xml
            fi
          fi

          # ── Run deploy-sftp.php from MokoStandards ────────────────────────────
          DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
          if [ "$USE_PASSPHRASE" = "true" ]; then
            DEPLOY_ARGS+=(--key-passphrase "$SFTP_PASSWORD")
          fi

          PLATFORM=$(php /tmp/mokostandards/api/cli/platform_detect.php --path . 2>/dev/null || true)
          if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards/api/deploy/deploy-joomla.php" ]; then
            php /tmp/mokostandards/api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
          else
            php /tmp/mokostandards/api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
          fi
          # Remove temp files that should never be left behind
          rm -f /tmp/deploy_key /tmp/sftp-config.json

      - name: Create or update failure issue
        if: failure() && steps.remote.outputs.skip != 'true' && steps.conn.outputs.skip != 'true'
        env:
          GH_TOKEN: ${{ secrets.GH_TOKEN || github.token }}
        run: |
          REPO="${{ github.repository }}"
          RUN_URL="${{ github.server_url }}/${REPO}/actions/runs/${{ github.run_id }}"
          ACTOR="${{ github.actor }}"
          BRANCH="${{ github.ref_name }}"
          EVENT="${{ github.event_name }}"
          NOW=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
          LABEL="deploy-failure"

          TITLE="fix: Demo deployment failed — ${REPO}"
          BODY="## Demo Deployment Failed

          A deployment to the demo server failed and requires attention.

          | Field | Value |
          |-------|-------|
          | **Repository** | \`${REPO}\` |
          | **Branch** | \`${BRANCH}\` |
          | **Trigger** | ${EVENT} |
          | **Actor** | @${ACTOR} |
          | **Failed at** | ${NOW} |
          | **Run** | [View workflow run](${RUN_URL}) |

          ### Next steps
          1. Review the [workflow run log](${RUN_URL}) for the specific error.
          2. Fix the underlying issue (credentials, SFTP connectivity, permissions).
          3. Re-trigger the deployment via **Actions → Deploy to Demo Server → Run workflow**.

          ---
          *Auto-created by deploy-demo.yml — close this issue once the deployment is resolved.*"

          # Ensure the label exists (idempotent — no-op if already present)
          gh label create "$LABEL" \
            --repo "$REPO" \
            --color "CC0000" \
            --description "Automated deploy failure tracking" \
            --force 2>/dev/null || true

          # Look for an existing open deploy-failure issue
          EXISTING=$(gh api "repos/${REPO}/issues?labels=${LABEL}&state=all&per_page=1&sort=created&direction=desc" \
            --jq '.[0].number' 2>/dev/null)

          if [ -n "$EXISTING" ] && [ "$EXISTING" != "null" ]; then
            gh api "repos/${REPO}/issues/${EXISTING}" \
              -X PATCH \
              -f title="$TITLE" \
              -f body="$BODY" \
              -f state="open" \
              --silent
            echo "📋 Failure issue #${EXISTING} updated/reopened: ${REPO}" >> "$GITHUB_STEP_SUMMARY"
          else
            gh issue create \
              --repo "$REPO" \
              --title "$TITLE" \
              --body "$BODY" \
              --label "$LABEL" \
              --assignee "jmiller-moko" \
              | tee -a "$GITHUB_STEP_SUMMARY"
          fi

      - name: Deployment summary
        if: always()
        run: |
          if [ "${{ steps.source.outputs.skip }}" == "true" ]; then
            echo "### ⏭️ Deployment Skipped" >> "$GITHUB_STEP_SUMMARY"
            echo "" >> "$GITHUB_STEP_SUMMARY"
            echo "No \`src/\` directory found in this repository." >> "$GITHUB_STEP_SUMMARY"
          elif [ "${{ job.status }}" == "success" ]; then
            echo "" >> "$GITHUB_STEP_SUMMARY"
            echo "### ✅ Demo Deployment Successful" >> "$GITHUB_STEP_SUMMARY"
            echo "" >> "$GITHUB_STEP_SUMMARY"
            echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY"
            echo "|-------|-------|" >> "$GITHUB_STEP_SUMMARY"
            echo "| Host | \`${{ steps.conn.outputs.host }}:${{ steps.conn.outputs.port }}\` |" >> "$GITHUB_STEP_SUMMARY"
            echo "| Remote path | \`${{ steps.remote.outputs.path }}\` |" >> "$GITHUB_STEP_SUMMARY"
            echo "| Source | \`src/\` |" >> "$GITHUB_STEP_SUMMARY"
            echo "| Trigger | ${{ github.event_name }} |" >> "$GITHUB_STEP_SUMMARY"
            echo "| Auth | ${{ steps.auth.outputs.method }} |" >> "$GITHUB_STEP_SUMMARY"
            echo "| Clear remote | ${{ inputs.clear_remote || 'false' }} |" >> "$GITHUB_STEP_SUMMARY"
          else
            echo "### ❌ Demo Deployment Failed" >> "$GITHUB_STEP_SUMMARY"
            echo "" >> "$GITHUB_STEP_SUMMARY"
            echo "Check the job log above for error details." >> "$GITHUB_STEP_SUMMARY"
          fi
