name: Push JSON changes to PocketBase

on:
  issues:
    types:
      - labeled
  workflow_dispatch:
    inputs:
      script_slug:
        description: "Script slug (e.g. my-app)"
        required: true
        type: string

jobs:
  push-json:
    runs-on: self-hosted
    if: >-
      github.event_name == 'workflow_dispatch' ||
      (github.event_name == 'issues' && contains(github.event.issue.labels.*.name, 'Ready For Testing'))
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get JSON file for script
        id: changed
        run: |
          : > changed_app_jsons.txt

          if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
            script_slug="${{ github.event.inputs.script_slug }}"
            if [[ "$script_slug" == "*" || "$script_slug" == "all" ]]; then
              count=0
              for f in json/*.json; do
                [[ -f "$f" ]] || continue
                jq -e '.slug' "$f" >/dev/null 2>&1 || continue
                echo "$f" >> changed_app_jsons.txt
                count=$((count + 1))
              done
              echo "count=${count}" >> "$GITHUB_OUTPUT"
              echo "Queued ${count} JSON files for push."
              exit 0
            fi
            file="json/${script_slug}.json"
            if [[ ! -f "$file" ]]; then
              echo "No JSON file at $file."
              echo "count=0" >> "$GITHUB_OUTPUT"
              exit 0
            fi
            if ! jq -e '.slug' "$file" >/dev/null 2>&1; then
              echo "File $file has no .slug."
              echo "count=0" >> "$GITHUB_OUTPUT"
              exit 0
            fi
            echo "$file" > changed_app_jsons.txt
            echo "count=1" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          # Extract slug from issue title (lowercase, spaces to dashes)
          ISSUE_TITLE="${{ github.event.issue.title }}"
          script_slug=$(echo "$ISSUE_TITLE" | tr '[:upper:]' '[:lower:]' | sed 's/ /-/g')
          file="json/${script_slug}.json"

          if [[ ! -f "$file" ]]; then
            echo "No JSON file at $file for issue '${ISSUE_TITLE}'."
            echo "count=0" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          if ! jq -e '.slug' "$file" >/dev/null 2>&1; then
            echo "File $file has no .slug."
            echo "count=0" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          echo "$file" > changed_app_jsons.txt
          echo "count=1" >> "$GITHUB_OUTPUT"

      - name: Push to PocketBase
        if: steps.changed.outputs.count != '0'
        env:
          POCKETBASE_URL: ${{ secrets.POCKETBASE_URL }}
          POCKETBASE_COLLECTION: ${{ secrets.POCKETBASE_COLLECTION }}
          POCKETBASE_ADMIN_EMAIL: ${{ secrets.POCKETBASE_ADMIN_EMAIL }}
          POCKETBASE_ADMIN_PASSWORD: ${{ secrets.POCKETBASE_ADMIN_PASSWORD }}
        run: |
          node << 'ENDSCRIPT'
          (async function() {
          const fs = require('fs');
          const https = require('https');
          const http = require('http');
          const url = require('url');
          function request(fullUrl, opts, redirectCount) {
            redirectCount = redirectCount || 0;
            return new Promise(function(resolve, reject) {
              const u = url.parse(fullUrl);
              const isHttps = u.protocol === 'https:';
              const body = opts.body;
              const options = {
                hostname: u.hostname,
                port: u.port || (isHttps ? 443 : 80),
                path: u.path,
                method: opts.method || 'GET',
                headers: opts.headers || {}
              };
              if (body) options.headers['Content-Length'] = Buffer.byteLength(body);
              const lib = isHttps ? https : http;
              const req = lib.request(options, function(res) {
                if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
                  if (redirectCount >= 5) return reject(new Error('Too many redirects from ' + fullUrl));
                  const redirectUrl = url.resolve(fullUrl, res.headers.location);
                  res.resume();
                  resolve(request(redirectUrl, opts, redirectCount + 1));
                  return;
                }
                let data = '';
                res.on('data', function(chunk) { data += chunk; });
                res.on('end', function() {
                  resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, statusCode: res.statusCode, body: data });
                });
              });
              req.on('error', reject);
              if (body) req.write(body);
              req.end();
            });
          }
          const raw = process.env.POCKETBASE_URL.replace(/\/$/, '');
          const apiBase = /\/api$/i.test(raw) ? raw : raw + '/api';
          const coll = process.env.POCKETBASE_COLLECTION;
          const files = fs.readFileSync('changed_app_jsons.txt', 'utf8').trim().split(/\s+/).filter(Boolean);
          const authUrl = apiBase + '/collections/users/auth-with-password';
          console.log('Auth URL: ' + authUrl);
          const authBody = JSON.stringify({
            identity: process.env.POCKETBASE_ADMIN_EMAIL,
            password: process.env.POCKETBASE_ADMIN_PASSWORD
          });
          const authRes = await request(authUrl, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: authBody
          });
          if (!authRes.ok) {
            throw new Error('Auth failed. Tried: ' + authUrl + ' - Verify POST to that URL with body {"identity":"...","password":"..."} works. Response: ' + authRes.body);
          }
          const token = JSON.parse(authRes.body).token;
          const recordsUrl = apiBase + '/collections/' + encodeURIComponent(coll) + '/records';
          let categoryIdToName = {};
          try {
            const metadata = JSON.parse(fs.readFileSync('json/metadata.json', 'utf8'));
            (metadata.categories || []).forEach(function(cat) { categoryIdToName[cat.id] = cat.name; });
          } catch (e) { console.warn('Could not load metadata.json:', e.message); }
          let typeValueToId = {};
          let categoryNameToPbId = {};
          try {
            const typesRes = await request(apiBase + '/collections/z_ref_script_types/records?perPage=500', { headers: { 'Authorization': token } });
            if (typesRes.ok) {
              const typesData = JSON.parse(typesRes.body);
              (typesData.items || []).forEach(function(item) {
                if (item.type != null) typeValueToId[item.type] = item.id;
                if (item.name != null) typeValueToId[item.name] = item.id;
                if (item.value != null) typeValueToId[item.value] = item.id;
              });
            }
          } catch (e) { console.warn('Could not fetch z_ref_script_types:', e.message); }
          try {
            const catRes = await request(apiBase + '/collections/script_categories/records?perPage=500', { headers: { 'Authorization': token } });
            if (catRes.ok) {
              const catData = JSON.parse(catRes.body);
              (catData.items || []).forEach(function(item) { if (item.name) categoryNameToPbId[item.name] = item.id; });
            }
          } catch (e) { console.warn('Could not fetch script_categories:', e.message); }

          for (const file of files) {
            if (!fs.existsSync(file)) continue;
            const data = JSON.parse(fs.readFileSync(file, 'utf8'));
            if (!data.slug) { console.log('Skipping', file, '(no slug)'); continue; }
            // execute_in: map type to canonical value (addon runs on both lxc and vm)
            var executeInMap = { ct: 'lxc', lxc: 'lxc', turnkey: 'turnkey', pve: 'pve', vm: 'vm', addon: ['lxc', 'vm'] };
            var executeIn = data.type ? (executeInMap[data.type.toLowerCase()] || null) : null;
            // github: extract owner/repo from full GitHub URL
            var githubField = null;
            if (data.github) {
              var ghMatch = data.github.match(/github\.com\/([^/]+\/[^/?#]+)/);
              if (ghMatch) githubField = ghMatch[1].replace(/\.git$/, '');
            }
            // last_update_commit: last commit touching the actual script files (ct/slug.sh, install/slug-install.sh, vm/slug.sh, etc.)
            var lastCommit = null;
            try {
              var cp = require('child_process');
              var scriptFiles = [];
              // primary script from install_methods[].script (e.g. "ct/teleport.sh", "vm/teleport.sh")
              (data.install_methods || []).forEach(function(im) {
                if (im.script) scriptFiles.push(im.script);
              });
              // derive install script from slug (install/slug-install.sh)
              scriptFiles.push('install/' + data.slug + '-install.sh');
              // filter to only files that actually exist in git
              var existingFiles = scriptFiles.filter(function(f) {
                try { cp.execSync('git ls-files --error-unmatch ' + f, { stdio: 'ignore' }); return true; } catch(e) { return false; }
              });
              if (existingFiles.length > 0) {
                lastCommit = cp.execSync('git log -1 --format=%H -- ' + existingFiles.join(' ')).toString().trim() || null;
              }
            } catch(e) { console.warn('Could not get last commit:', e.message); }
            var payload = {
              name: data.name,
              slug: data.slug,
              script_created: data.date_created || data.script_created,
              script_updated: new Date().toISOString().split('T')[0],
              updateable: data.updateable,
              privileged: data.privileged,
              port: data.interface_port != null ? data.interface_port : data.port,
              documentation: data.documentation,
              website: data.website,
              logo: data.logo,
              description: data.description,
              default_user: (data.default_credentials && data.default_credentials.username) || data.default_user || null,
              default_passwd: (data.default_credentials && data.default_credentials.password) || data.default_passwd || null,
              notes: data.notes || [],
              install_methods: data.install_methods || [],
              is_dev: true
            };
            if (executeIn) payload.execute_in = executeIn;
            if (githubField) payload.github = githubField;
            if (lastCommit) payload.last_update_commit = lastCommit;
            var resolvedType = typeValueToId[data.type];
            if (resolvedType == null && data.type === 'ct') resolvedType = typeValueToId['lxc'];
            if (resolvedType) payload.type = resolvedType;
            var resolvedCats = (data.categories || []).map(function(n) { return categoryNameToPbId[categoryIdToName[n]]; }).filter(Boolean);
            if (resolvedCats.length) payload.categories = resolvedCats;
            if (data.version !== undefined) payload.version = data.version;
            if (data.changelog !== undefined) payload.changelog = data.changelog;
            if (data.screenshots !== undefined) payload.screenshots = data.screenshots;
            const filter = "(slug='" + data.slug + "')";
            const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1', {
              headers: { 'Authorization': token }
            });
            const list = JSON.parse(listRes.body);
            const existingId = list.items && list.items[0] && list.items[0].id;
            if (existingId) {
              console.log('Updating', file, '(slug=' + data.slug + ')');
              const r = await request(recordsUrl + '/' + existingId, {
                method: 'PATCH',
                headers: { 'Authorization': token, 'Content-Type': 'application/json' },
                body: JSON.stringify(payload)
              });
              if (!r.ok) throw new Error('PATCH failed: ' + r.body);
            } else {
              console.log('Creating', file, '(slug=' + data.slug + ')');
              const r = await request(recordsUrl, {
                method: 'POST',
                headers: { 'Authorization': token, 'Content-Type': 'application/json' },
                body: JSON.stringify(payload)
              });
              if (!r.ok) throw new Error('POST failed: ' + r.body);
            }
          }
          console.log('Done.');
          })().catch(e => { console.error(e); process.exit(1); });
          ENDSCRIPT
        shell: bash

      - name: Revalidate Frontend Cache
        if: steps.changed.outputs.count != '0'
        env:
          FRONTEND_URL: ${{ secrets.FRONTEND_URL }}
          REVALIDATE_SECRET: ${{ secrets.REVALIDATE_SECRET }}
        run: |
          if [[ -z "$FRONTEND_URL" || -z "$REVALIDATE_SECRET" ]]; then
            echo "FRONTEND_URL or REVALIDATE_SECRET not set, skipping revalidation."
            exit 0
          fi
          SLUGS=$(cat changed_app_jsons.txt | xargs -I{} basename {} .json | jq -R -s -c 'split("\n") | map(select(length > 0))')
          echo "Revalidating tags: scripts, categories — slugs: $SLUGS"
          curl -sf -X POST "${FRONTEND_URL}/api/revalidate" \
            -H "Authorization: Bearer ${REVALIDATE_SECRET}" \
            -H "Content-Type: application/json" \
            -d "{\"tags\":[\"scripts\",\"categories\"],\"slugs\":${SLUGS}}" \
            && echo "Cache revalidated." \
            || echo "Warning: revalidation failed (non-critical)."
