require "json"
require "fileutils"

default_platform(:ios)

# Fastlane reuses the same named simulator across language runs. The app's
# snapshot harness writes launch-time locale and preview state into simulator
# preferences, so a later locale can inherit a poisoned process/data state and
# never reach the first screenshot. Erase only the simulator instances that
# this lane selected before each language, keeping each locale independent.
def reset_screenshot_simulators(device_names)
  data = JSON.parse(
    sh("xcrun", "simctl", "list", "devices", "available", "-j", log: false)
  )
  udids = data.fetch("devices", {}).values.flatten.filter_map do |device|
    device["udid"] if device["isAvailable"] != false && device_names.include?(device["name"])
  end.uniq
  udids.each do |udid|
    # simctl reports status 149 when the device is already shut down. `system`
    # keeps that expected state non-fatal while the following erase remains a
    # checked Fastlane command.
    system("xcrun", "simctl", "shutdown", udid, out: File::NULL, err: File::NULL)
    sh("xcrun", "simctl", "erase", udid, log: false)
  end
  UI.message("Reset #{udids.length} screenshot simulator(s) for a clean locale run")
end

# Fastlane accepts [language, locale] pairs. Use Apple's language identifier for
# the first value and keep the App Store identifier for the output directory.
# Script variants remain explicit (zh-Hant/zh-Hans); regional App Store IDs use
# their base language for AppleLanguages (de-DE -> de).
def snapshot_language_code(app_store_locale)
  parts = app_store_locale.tr("_", "-").split("-")
  language = parts.first.to_s
  script = parts.drop(1).find { |part| part.match?(/\A[A-Za-z]{4}\z/) }
  script ? "#{language}-#{script}" : language
end

# cmux iOS fastlane lanes. Scope is App Store screenshots for now; build/upload
# of the binary lives in ios/scripts/upload-testflight.sh (--lane appstore).

platform :ios do
  desc "Capture App Store screenshots for all nine app languages (iPhone 6.9\" + iPad 13\")"
  desc "Uses a DEBUG build with CMUX_UITEST_MOCK_DATA; no Apple signing required."
  desc "CI overrides device names per Xcode version via SNAPSHOT_DEVICES (the model"
  desc "names in Snapfile drift between Xcode releases, so CI resolves them at runtime)."
  lane :screenshots do
    # Devices/languages are the single source here (NOT the Snapfile) so the
    # action params always win. CI sets SNAPSHOT_DEVICES to the exact simulators
    # available on the runner (model names drift across Xcode versions); the
    # local defaults below cover a typical current Xcode and can be overridden
    # via env.
    devices = (ENV["SNAPSHOT_DEVICES"] || "iPhone 16 Pro Max,iPad Pro 13-inch (M4)")
              .split(",").map(&:strip).reject(&:empty?)
    languages = (ENV["SNAPSHOT_LANGUAGES"] ||
                 "en-US,de-DE,fr-FR,ar-SA,es-ES,zh-Hant,zh-Hans,ko,ja")
                .split(",").map(&:strip).reject(&:empty?)
    # Snapshot's `clear_previous_screenshots` option is useful for one capture,
    # but this lane invokes Snapshot once per locale. Clear the root exactly
    # once, then preserve each locale directory across serial invocations.
    screenshot_root = File.expand_path("screenshots", __dir__)
    FileUtils.rm_rf(screenshot_root)
    FileUtils.mkdir_p(screenshot_root)
    languages.each do |language|
      reset_screenshot_simulators(devices)
      captured_before = Dir.glob(File.join(screenshot_root, "**", "*.png")).length
      capture_screenshots(
        scheme: "cmux-ios",
        # NOTE: do not pass `testplan:` here. With -testPlan, the UI test runs
        # and passes but fastlane collects zero screenshots (verified on CI runs
        # 33479247651 vs the green 32219546960); the known-good path is the plain
        # scheme invocation, which requires every test target in the scheme's
        # default plan to at least compile.
        devices: devices,
        # Keep AppleLanguages valid while retaining the App Store locale as
        # the screenshot directory name.
        languages: [[snapshot_language_code(language), language]],
        output_directory: screenshot_root,
        clear_previous_screenshots: false,
        # Clean, deterministic status bar (9:41, full battery/wifi) on every shot.
        override_status_bar: true,
        # Capture in dark mode.
        dark_mode: true
      )
      # capture_screenshots does not raise when the scheme's test build fails.
      # It records the failure and returns, so a compile error in any test
      # target of the cmux-ios scheme -- which the plain invocation above
      # requires to build -- silently produces nothing, burns every remaining
      # language, and then surfaces from the frame lane as "no localized raws
      # ... run capture first", which points at the wrong thing. Release run
      # 35225939389 spent 47 minutes that way on a test file missing its
      # @testable import. Stop at the first language that captures nothing and
      # name the real cause.
      if Dir.glob(File.join(screenshot_root, "**", "*.png")).length == captured_before
        UI.user_error!(
          "No screenshots were captured for #{language}. This lane passes no " \
          "testplan:, so every test target in the cmux-ios scheme has to " \
          "compile before any screenshot is taken. Look for the first " \
          "compiler error in the xcodebuild output above: exit status 65 is a " \
          "build failure, not a flaky simulator."
        )
      end
    end
    UI.message("Screenshots written to ios/fastlane/screenshots/<lang>/")
    frame unless ENV["SNAPSHOT_SKIP_FRAME"] == "1"
  end

  desc "Frame captured screenshots with the device shell, tranquil background, and"
  desc "localized titles (fastlane frameit). Requires imagemagick."
  lane :frame do
    ss = File.expand_path("screenshots", __dir__)
    # Capture the nine localized app languages, then fan them out to every App
    # Store locale before framing. Listing locales without a binary translation
    # reuse the English raw; each locale still receives its own title.
    sh("python3", File.expand_path("frame_assets/propagate_locales.py", __dir__), ss)
    # Premium composition: stitch the screenshot into the real iPhone device
    # frame (frameit's frame PNG) and place it large under a bold header; iPad is
    # a large bezel-less landscape screen with a bold header. (Plain frameit
    # shrinks the tall device under the title and leaves big dead margins.)
    sh("python3", File.expand_path("frame_assets/compose_shots.py", __dir__), ss)
    # Build the HTML gallery (screenshots/index.html) for review; CI uploads it
    # inside the screenshots artifact.
    sh("python3", File.expand_path("frame_assets/generate_preview.py", __dir__), ss)
    UI.message("Framed screenshots + gallery written")
  end

  desc "Upload the captured screenshots to the com.cmux.app App Store listing"
  desc "Screenshots only: does not touch the binary, metadata, or version state."
  lane :upload_screenshots do
    deliver(
      api_key: asc_api_key,
      app_identifier: "com.cmux.app",
      screenshots_path: "./fastlane/screenshots",
      skip_binary_upload: true,
      skip_metadata: true,
      skip_app_version_update: true,
      overwrite_screenshots: true,
      run_precheck_before_submit: false,
      force: true # non-interactive
    )
  end
end

# Resolve an App Store Connect API key from env (CI / agents) or the repo-local
# plist used by ios/scripts. Mirrors the resolution in upload-testflight.sh.
def asc_api_key
  # Accept both the fastlane-style names (ASC_API_*) and the names used by the
  # machine-wide secret store ~/.secrets/cmux-appstoreconnect.env
  # (ASC_KEY_ID / ASC_ISSUER_ID / ASC_PRIVATE_KEY_PATH).
  key_id = ENV["ASC_API_KEY_ID"] || ENV["ASC_KEY_ID"]
  issuer_id = ENV["ASC_API_ISSUER_ID"] || ENV["ASC_ISSUER_ID"]
  key_path = ENV["ASC_API_KEY_PATH"] || ENV["ASC_PRIVATE_KEY_PATH"]

  if key_id.nil? || issuer_id.nil? || key_path.nil?
    plist = File.expand_path("../Config/AppStoreConnect.local.plist", __dir__)
    if File.exist?(plist)
      require "plist"
      data = Plist.parse_xml(plist) || {}
      key_id ||= data["ASC_API_KEY_ID"]
      issuer_id ||= data["ASC_API_ISSUER_ID"]
      key_path ||= data["ASC_API_KEY_PATH"]
    end
  end

  key_path ||= File.expand_path("~/.appstoreconnect/private_keys/AuthKey_#{key_id}.p8") if key_id
  UI.user_error!("App Store Connect API key not found (set ASC_API_KEY_ID/ASC_API_ISSUER_ID/ASC_API_KEY_PATH)") unless key_id && issuer_id && key_path && File.exist?(key_path)

  app_store_connect_api_key(
    key_id: key_id,
    issuer_id: issuer_id,
    key_filepath: key_path,
    in_house: false
  )
end
