name: CD

on:
  push:
    tags:
      - 'v*'
  workflow_dispatch:

permissions:
  contents: write

jobs:
  # ------------------------------------------------------------------
  # Windows Installer (MSYS2)
  # ------------------------------------------------------------------
  windows:
    runs-on: warp-windows-latest-x64-16x
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: true

      - name: Setup MSYS2
        uses: msys2/setup-msys2@v2
        with:
          msystem: UCRT64
          update: true
          install: >-
            mingw-w64-ucrt-x86_64-cmake
            mingw-w64-ucrt-x86_64-ninja
            mingw-w64-ucrt-x86_64-gcc
            mingw-w64-ucrt-x86_64-qt6-base
            mingw-w64-ucrt-x86_64-qt6-tools
            mingw-w64-ucrt-x86_64-ffmpeg
            mingw-w64-ucrt-x86_64-openimageio
            mingw-w64-ucrt-x86_64-opencolorio
            mingw-w64-ucrt-x86_64-openexr
            mingw-w64-ucrt-x86_64-fmt
            mingw-w64-ucrt-x86_64-expat
            mingw-w64-ucrt-x86_64-portaudio
            mingw-w64-ucrt-x86_64-vulkan-headers
            mingw-w64-ucrt-x86_64-vulkan-loader
            mingw-w64-ucrt-x86_64-nsis
            mingw-w64-ucrt-x86_64-ntldd

      - name: Configure
        shell: msys2 {0}
        run: |
          cmake -S . -B build -G Ninja \
            -DBUILD_QT6=ON \
            -DCMAKE_BUILD_TYPE=Release

      - name: Build
        shell: msys2 {0}
        run: cmake --build build --config Release

      - name: Deploy dependencies
        shell: msys2 {0}
        run: |
          mkdir -p app/packaging/windows/nsis/olive-editor
          cp build/app/olive-editor.exe app/packaging/windows/nsis/olive-editor/
          windeployqt6 app/packaging/windows/nsis/olive-editor/olive-editor.exe
          # Copy all non-Qt MSYS2 DLLs recursively
          cd app/packaging/windows/nsis/olive-editor
          for l in $(ntldd -R olive-editor.exe | grep -E 'mingw64|ucrt64|clang64' | sed 's/^[ \t]*//' | cut -d' ' -f3); do
            cp -v "$l" .
          done

      - name: Build installer
        shell: msys2 {0}
        run: |
          cd app/packaging/windows/nsis
          cp "${GITHUB_WORKSPACE}"/LICENSE .
          makensis olive.nsi
          mv setup.exe "${GITHUB_WORKSPACE}"/Oak-Video-Editor-Windows-x64.exe

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: oak-windows-installer
          path: Oak-Video-Editor-Windows-x64.exe

  # ------------------------------------------------------------------
  # macOS DMG
  # ------------------------------------------------------------------
  macos:
    runs-on: warp-macos-15-arm64-12x
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: true

      - name: Install dependencies
        run: |
          brew update
          brew install cmake ninja pkg-config qt@6 ffmpeg openimageio opencolorio openexr portaudio expat molten-vk vulkan-headers dylibbundler

      - name: Build OpenTimelineIO
        run: |
          git clone --depth 1 --branch v0.16.0 https://github.com/PixarAnimationStudios/OpenTimelineIO.git
          cmake -S OpenTimelineIO -B OpenTimelineIO/build -G Ninja \
            -DOTIO_SHARED_LIBS=ON \
            -DOTIO_PYTHON_BINDINGS=OFF \
            -DCMAKE_BUILD_TYPE=Release \
            -DCMAKE_INSTALL_PREFIX="${PWD}/otio-install"
          cmake --build OpenTimelineIO/build
          cmake --install OpenTimelineIO/build

      - name: Configure
        run: |
          export PATH="$(brew --prefix qt@6)/bin:$PATH"
          export CMAKE_PREFIX_PATH="$(brew --prefix qt@6)"
          cmake -S . -B build -G Ninja \
            -DBUILD_QT6=ON \
            -DCMAKE_BUILD_TYPE=Release \
            -DOTIO_LOCATION="${PWD}/otio-install" \
            -DOCIO_LOCATION="$(brew --prefix opencolorio)"

      - name: Build
        run: cmake --build build --config Release

      - name: Deploy Qt dependencies
        run: |
          export PATH="$(brew --prefix qt@6)/bin:$PATH"
          macdeployqt build/app/Olive.app
          # Re-sign after macdeployqt (it breaks signatures when modifying libs)
          codesign --force --deep --sign - build/app/Olive.app

      - name: Bundle non-Qt libraries
        run: |
          mkdir -p build/app/Olive.app/Contents/Frameworks
          python3 << 'PYEOF'
          import os, shutil, subprocess

          APP = "build/app/Olive.app"
          BINARY = f"{APP}/Contents/MacOS/Olive"
          DEST = f"{APP}/Contents/Frameworks"
          os.makedirs(DEST, exist_ok=True)

          def get_rpaths(binary):
              out = subprocess.run(["otool", "-l", binary], capture_output=True, text=True).stdout
              rpaths = []
              for i, line in enumerate(out.splitlines()):
                  if "cmd LC_RPATH" in line and i + 2 < len(out.splitlines()):
                      parts = out.splitlines()[i + 2].strip().split()
                      if len(parts) >= 2:
                          rpaths.append(parts[1])
              return rpaths

          def get_deps(binary):
              out = subprocess.run(["otool", "-L", binary], capture_output=True, text=True).stdout
              deps = []
              for line in out.splitlines()[1:]:
                  line = line.strip()
                  if not line:
                      continue
                  dep = line.split()[0]
                  if dep.startswith("/usr/lib/") or dep.startswith("/System/"):
                      continue
                  deps.append(dep)
              return deps

          def resolve(ref, rpaths, loader_dir=""):
              if ref.startswith("/"):
                  return ref if os.path.isfile(ref) else None
              if ref.startswith("@rpath/"):
                  for rp in rpaths:
                      p = os.path.join(rp, ref[7:])
                      if os.path.isfile(p):
                          return p
              if ref.startswith("@loader_path/") and loader_dir:
                  p = os.path.join(loader_dir, ref[13:])
                  if os.path.isfile(p):
                      return p
              if ref.startswith("@executable_path/"):
                  p = os.path.join(APP, "Contents/MacOS", ref[17:])
                  if os.path.isfile(p):
                      return p
              return None

          EXEC_RPATHS = get_rpaths(BINARY)
          PROCESSED = set()

          def process(target):
              tdir = os.path.dirname(target)
              print(f"Processing: {target}")
              for dep in get_deps(target):
                  resolved = resolve(dep, EXEC_RPATHS if target == BINARY else [], tdir)
                  if not resolved or not os.path.isfile(resolved):
                      continue
                  resolved = os.path.realpath(resolved)
                  if not (resolved.startswith("/opt/homebrew/") or resolved.startswith("/usr/local/") or resolved.startswith(os.path.realpath(os.getcwd()))):
                      continue
                  base = os.path.basename(resolved)
                  if base in PROCESSED:
                      subprocess.run(["install_name_tool", "-change", dep, f"@rpath/{base}", target], capture_output=True)
                      continue
                  PROCESSED.add(base)
                  dst = os.path.join(DEST, base)
                  if os.path.abspath(resolved) == os.path.abspath(dst):
                      # Already in Frameworks (likely copied by macdeployqt), just fix paths
                      subprocess.run(["install_name_tool", "-id", f"@rpath/{base}", dst], capture_output=True)
                      subprocess.run(["install_name_tool", "-change", dep, f"@rpath/{base}", target], capture_output=True)
                      process(dst)
                      continue
                  print(f"  Copying: {resolved} -> {dst}")
                  shutil.copy2(resolved, dst)
                  subprocess.run(["install_name_tool", "-id", f"@rpath/{base}", dst], capture_output=True)
                  subprocess.run(["install_name_tool", "-change", dep, f"@rpath/{base}", target], capture_output=True)
                  process(dst)

          process(BINARY)

          for rp in EXEC_RPATHS:
              subprocess.run(["install_name_tool", "-delete_rpath", rp, BINARY], capture_output=True)
          subprocess.run(["install_name_tool", "-add_rpath", "@executable_path/../Frameworks", BINARY], capture_output=True)

          subprocess.run(["codesign", "--force", "--deep", "--sign", "-", APP], check=True)
          print("Done. Frameworks:")
          for f in sorted(os.listdir(DEST)):
              print(f"  {f}")
          PYEOF

      - name: Debug bundle contents
        run: |
          echo "=== otool -L ==="
          otool -L build/app/Olive.app/Contents/MacOS/Olive
          echo "=== Frameworks dir ==="
          ls -la build/app/Olive.app/Contents/Frameworks/ || echo "(empty)"
          echo "=== App bundle size ==="
          du -sh build/app/Olive.app

      - name: Create DMG
        run: |
          hdiutil create \
            -srcfolder build/app/Olive.app \
            -volname "Oak Video Editor" \
            -fs HFS+ \
            -format UDZO \
            Oak-Video-Editor-macOS.dmg

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: oak-macos-dmg
          path: Oak-Video-Editor-macOS.dmg

  # ------------------------------------------------------------------
  # Linux AppImage (Ubuntu 24.04 — distro FFmpeg 6.x)
  # ------------------------------------------------------------------
  appimage:
    runs-on: warp-ubuntu-latest-x64-16x
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: true

      - name: Install dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            cmake ninja-build pkg-config \
            qt6-base-dev qt6-base-dev-tools qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \
            libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libavfilter-dev \
            libopencolorio-dev libopenimageio-dev libopenexr-dev libexpat1-dev \
            portaudio19-dev libgl1-mesa-dev libxkbcommon-dev \
            wget fuse3 desktop-file-utils

      - name: Build
        run: |
          cmake -S . -B build -G Ninja \
            -DCMAKE_BUILD_TYPE=Release \
            -DCMAKE_INSTALL_PREFIX=/usr \
            -DBUILD_QT6=ON \
            -DCMAKE_DISABLE_FIND_PACKAGE_Vulkan=ON
          cmake --build build

      - name: Install to AppDir
        run: |
          DESTDIR="${PWD}"/AppDir cmake --install build
          # Copy custom AppRun
          cp app/packaging/linux/AppRun AppDir/

      - name: Create AppImage
        env:
          APPIMAGE_EXTRACT_AND_RUN: 1
        run: |
          wget -q "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
          wget -q "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage"
          wget -q "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
          chmod +x linuxdeploy-x86_64.AppImage linuxdeploy-plugin-qt-x86_64.AppImage appimagetool-x86_64.AppImage
          export PATH="${PWD}:${PATH}"
          ./linuxdeploy-x86_64.AppImage \
            --appdir AppDir \
            --plugin qt \
            --desktop-file AppDir/usr/share/applications/org.oakvideoeditor.Oak.desktop \
            --icon-file AppDir/usr/share/icons/hicolor/512x512/apps/org.oakvideoeditor.Oak.png \
            --output appimage

      - name: Verify bundled libraries
        run: |
          # Ensure FFmpeg shared libraries are present inside the AppDir
          ls AppDir/usr/lib/libavcodec.so*    || (echo "Missing libavcodec" && exit 1)
          ls AppDir/usr/lib/libavformat.so*   || (echo "Missing libavformat" && exit 1)
          ls AppDir/usr/lib/libavutil.so*     || (echo "Missing libavutil" && exit 1)
          ls AppDir/usr/lib/libswscale.so*    || (echo "Missing libswscale" && exit 1)
          ls AppDir/usr/lib/libswresample.so* || (echo "Missing libswresample" && exit 1)
          # Ensure other runtime dependencies are present
          ls AppDir/usr/lib/libOpenImageIO.so*    || (echo "Missing libOpenImageIO" && exit 1)
          ls AppDir/usr/lib/libOpenColorIO.so*    || (echo "Missing libOpenColorIO" && exit 1)
          ls AppDir/usr/lib/libportaudio.so*      || (echo "Missing libportaudio" && exit 1)
          ls AppDir/usr/lib/libOpenEXR*.so*       || (echo "Missing libOpenEXR" && exit 1)

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: oak-linux-appimage
          path: Oak_Video_Editor-*.AppImage

  # ------------------------------------------------------------------
  # Create Draft Release
  # ------------------------------------------------------------------
  release:
    needs: [windows, macos, appimage]
    runs-on: ubuntu-latest
    steps:
      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts
          merge-multiple: true

      - name: Create Draft Release
        uses: softprops/action-gh-release@v2
        with:
          draft: true
          tag_name: ${{ github.ref_name }}
          name: ${{ github.ref_name }}
          files: artifacts/*
