name: Free Bambu A1 Slicer (3D-Masterskaya)

on:
  workflow_dispatch:
    inputs:
      model:
        description: "Модель из каталога мастерской или custom"
        type: choice
        required: true
        options:
          - LED_Clip_10mm_MID_5mm
          - LED_Clip_10mm_LOW_3p5mm
          - LED_Clip_10mm_TALL_6p5mm
          - LED_Clip_12mm_MID_5mm
          - Cable_Clip_5mm_PETG
          - TEST_PLATE_4_variants_geometry_only
          - custom
      custom_path:
        description: "Если custom, путь к 3MF в папке input/"
        type: string
        required: false
        default: ""

permissions:
  contents: read

jobs:
  slice:
    runs-on: ubuntu-24.04
    timeout-minutes: 35
    steps:
      - uses: actions/checkout@v4

      - name: Prepare 3MF (free catalogue or private custom)
        shell: bash
        env:
          MODEL: ${{ inputs.model }}
          CUSTOM_PATH: ${{ inputs.custom_path }}
        run: |
          set -euo pipefail
          mkdir -p work output
          python3 - <<'PY3'
          import os, re, json, zipfile, urllib.request
          from pathlib import Path
          items = {
              'LED_Clip_10mm_MID_5mm': 'LED_Clip_10mm_MID_5mm_A1_PETG_PROJECT.3mf',
              'LED_Clip_10mm_LOW_3p5mm': 'LED_Clip_10mm_LOW_3p5mm_A1_PETG_PROJECT.3mf',
              'LED_Clip_10mm_TALL_6p5mm': 'LED_Clip_10mm_TALL_6p5mm_A1_PETG_PROJECT.3mf',
              'LED_Clip_12mm_MID_5mm': 'LED_Clip_12mm_MID_5mm_A1_PETG_PROJECT.3mf',
              'Cable_Clip_5mm_PETG': 'Cable_Clip_5mm_PETG_A1_PETG_PROJECT.3mf',
              'TEST_PLATE_4_variants_geometry_only': 'TEST_PLATE_4_variants_geometry_only_A1_PETG_PROJECT.3mf',
          }
          selected=os.environ['MODEL']
          dest=Path('work/for_slicing.3mf')
          if selected == 'custom':
              custom=os.environ.get('CUSTOM_PATH','')
              if not re.fullmatch(r'input/[A-Za-z0-9_.-]+\.3mf',custom):
                  raise SystemExit('Для custom укажи безопасный путь input/NAME.3mf')
              data=Path(custom).read_bytes()
              print('Using private file:',custom)
          else:
              if selected not in items:raise SystemExit('Unknown catalogue model')
              url='https://3d-masterskay.pages.dev/projects/'+items[selected]
              print('Downloading public project:',url)
              req=urllib.request.Request(url,headers={'User-Agent':'3D-Masterskaya-Free-Slicer/0.9'})
              with urllib.request.urlopen(req,timeout=45) as resp:
                  data=resp.read(25_000_001)
          if not 500 < len(data) <= 25_000_000:raise SystemExit('File missing or >25MB')
          dest.write_bytes(data)
          with zipfile.ZipFile(dest) as z:
              bad=z.testzip()
              if bad:raise SystemExit('Damaged file: '+bad)
              names=z.namelist()
              if not any(x.endswith('.model') for x in names):raise SystemExit('3MF geometry missing')
              if 'Metadata/project_settings.config' not in names:
                  raise SystemExit('Bambu print settings missing: need project 3MF, not bare geometry')
              cfg=json.loads(z.read('Metadata/project_settings.config'))
              machine=cfg.get('printer_settings_id','')
              nozzles=cfg.get('nozzle_diameter',[])
              filament=cfg.get('filament_type',[])
              if 'A1' not in machine or '0.4' not in str(nozzles) or 'PETG' not in str(filament):
                  raise SystemExit(f'Expected Bambu A1, 0.4mm, PETG: got {machine} / {nozzles} / {filament}')
              if any(x.lower().endswith('.gcode') for x in names):
                  raise SystemExit('Input already contains G-code; upload unsliced project')
          print('3MF project validated:',len(data),'bytes')
          PY3

      - name: Install Linux libraries for Bambu Studio
        shell: bash
        run: |
          set -euo pipefail
          sudo apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
            xvfb libgtk-3-0t64 libwebkit2gtk-4.1-0 libglu1-mesa libgl1 libfuse2t64 \
            libdbus-1-3 libgstreamer-plugins-base1.0-0 fonts-dejavu-core

      - name: Fetch OFFICIAL Bambu Studio (latest stable AppImage)
        shell: bash
        env:
          GITHUB_TOKEN: ${{ github.token }}
        run: |
          set -euo pipefail
          python3 - <<'PY3'
          import json,os,pathlib,urllib.request
          base='https://api.github.com/repos/bambulab/BambuStudio/releases/latest'
          headers={'User-Agent':'3D-Masterskaya-Free-Slicer/0.9','Accept':'application/vnd.github+json',
                   'Authorization':'Bearer '+os.environ['GITHUB_TOKEN']}
          with urllib.request.urlopen(urllib.request.Request(base,headers=headers),timeout=35) as resp:
              release=json.load(resp)
          assets=[a for a in release.get('assets',[]) if a.get('name','').lower().endswith('.appimage')
                  and ('ubuntu24.04' in a['name'].lower() or 'ubuntu-24.04' in a['name'].lower())]
          if not assets:raise SystemExit('No Ubuntu 24.04 Bambu Studio AppImage in latest STABLE GitHub release')
          asset=max(assets,key=lambda x:x.get('size',0))
          url=asset['browser_download_url']
          if not url.startswith('https://github.com/bambulab/BambuStudio/releases/download/'):
              raise SystemExit('Refusing third-party binary')
          print('Official Bambu Studio:',release['tag_name'],asset['name'],asset['size'])
          dest=pathlib.Path('work/BambuStudio.AppImage')
          with urllib.request.urlopen(urllib.request.Request(url,headers={'User-Agent':'3D-Masterskaya/0.9'}),timeout=120) as src, dest.open('wb') as out:
              size=0
              while True:
                  chunk=src.read(1024*1024)
                  if not chunk:break
                  size+=len(chunk)
                  if size>650_000_000:raise SystemExit('Download unexpectedly large')
                  out.write(chunk)
          if size!=asset['size'] or size<60_000_000:raise SystemExit('Downloaded binary incomplete')
          dest.chmod(0o755)
          PY3

      - name: Extract AppImage (without FUSE)
        shell: bash
        run: |
          set -euo pipefail
          cd work
          ./BambuStudio.AppImage --appimage-extract > /dev/null
          test -e squashfs-root/AppRun

      - name: Actual Bambu Studio CLI slicing
        shell: bash
        run: |
          set -euxo pipefail
          export NO_AT_BRIDGE=1
          timeout 720 xvfb-run -a work/squashfs-root/AppRun \
            --debug 2 --slice 0 \
            --export-3mf output/A1_PETG_sliced.gcode.3mf \
            work/for_slicing.3mf 2>&1 | tee work/slicer.log
          test -s output/A1_PETG_sliced.gcode.3mf

      - name: Verify actual G-code and metadata
        shell: bash
        run: |
          python3 - <<'PY3'
          import zipfile,json,pathlib,sys
          path=pathlib.Path('output/A1_PETG_sliced.gcode.3mf')
          try:
              with zipfile.ZipFile(path) as z:
                  if z.testzip():raise ValueError('ZIP corruption')
                  entries=[i for i in z.infolist() if i.filename.lower().endswith('.gcode') and i.file_size>100]
                  if not entries:raise ValueError('No nonempty G-code in 3MF')
                  if 'Metadata/project_settings.config' in z.namelist():
                      cfg=json.loads(z.read('Metadata/project_settings.config'))
                      if 'A1' not in str(cfg.get('printer_settings_id','')):
                          raise ValueError('Printer setting is not Bambu A1')
                      if 'PETG' not in str(cfg.get('filament_type',[])):
                          raise ValueError('Filament setting is not PETG')
                  print('VALIDATED G-CODE EXISTS:',[(e.filename,e.file_size) for e in entries])
                  print('CAUTION: not physically test-printed!')
          except Exception as e:
              sys.exit('REJECTED. Not safe to assume print ready: '+str(e))
          PY3

      - name: Save sliced 3MF (only if verification succeeded)
        uses: actions/upload-artifact@v4
        with:
          name: a1-petg-gcode-3mf
          path: output/A1_PETG_sliced.gcode.3mf
          if-no-files-found: error
          retention-days: 1

      - name: Save diagnostic log on failure
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: diagnostics
          path: work/slicer.log
          if-no-files-found: ignore
          retention-days: 1
