default_platform(:ios)

# Credentials: App Store Connect API key via `pass`.
# The api_key block uses the key inline — no session/2FA needed.

platform :ios do
  EXPORT_OPTIONS = {
    method: "app-store",
    signingStyle: "manual",
    manageAppVersionAndBuildNumber: false,
    provisioningProfiles: {
      "com.alexandrevazquez.mealmood" => "com.alexandrevazquez.mealmood AppStore",
      "com.alexandrevazquez.mealmood.widget" => "com.alexandrevazquez.mealmood.widget AppStore",
      "com.alexandrevazquez.mealmood.watchkitapp" => "com.alexandrevazquez.mealmood.watchkitapp AppStore",
      "com.alexandrevazquez.mealmood.watchkitapp.widget" => "com.alexandrevazquez.mealmood.watchkitapp.widget AppStore"
    }
  }

  def asc_api_key
    app_store_connect_api_key(
      key_id:      `pass show appstore/api-key-id`.strip,
      issuer_id:   `pass show appstore/issuer-id`.strip,
      key_content: `pass show appstore/api-key-p8`.strip,
      in_house:    false
    )
  end

  # A field missing from the CloudKit schema doesn't crash anything — it just
  # never syncs, silently. Nine of them drifted out of Production between 2.0
  # and 2.1.2 before anyone noticed (issue #38), so shipping now checks first.
  def check_cloudkit_schema(blocking:)
    script = File.expand_path("../scripts/check_cloudkit_schema.py", __dir__)
    ok = system("python3", script, "--environment", "production")
    return if ok

    if blocking
      UI.user_error!("CloudKit Production schema is behind the models — deploy it before shipping (see the output above).")
    else
      UI.important("CloudKit Production schema is behind the models. Fine for TestFlight, but deploy it before submitting to the App Store.")
    end
  end

  desc "Check the deployed CloudKit schema against the SwiftData models"
  lane :check_schema do
    check_cloudkit_schema(blocking: true)
  end

  desc "Push a new beta build to TestFlight"
  lane :beta do
    check_cloudkit_schema(blocking: false)
    increment_build_number(xcodeproj: "MealMood.xcodeproj")
    # No `sdk:` here: forcing one SDK globally breaks the watchOS targets,
    # which must build against watchos within the same archive.
    build_app(
      scheme: "MealMood",
      export_method: "app-store",
      destination: "generic/platform=iOS",
      export_options: EXPORT_OPTIONS
    )
    upload_to_testflight(
      api_key: asc_api_key,
      skip_waiting_for_build_processing: true
    )
  end

  # Firebase arrives via SPM, so upload-symbols sits in DerivedData instead of
  # the ./Pods path fastlane looks in by default.
  def crashlytics_upload_symbols_binary
    pattern = File.expand_path(
      "~/Library/Developer/Xcode/DerivedData/MealMood-*/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/upload-symbols"
    )
    found = Dir.glob(pattern)
    if found.empty?
      UI.user_error!("upload-symbols not found. Build the project once so SPM checks out firebase-ios-sdk.")
    end
    found.max_by { |path| File.mtime(path) }
  end

  # Every dSYM (app + widget) of the most recently archived build.
  def latest_archive_dsyms
    pattern = File.expand_path("~/Library/Developer/Xcode/Archives/*/MealMood*.xcarchive/dSYMs/*.dSYM")
    by_archive = Dir.glob(pattern).group_by { |path| path[/.*\.xcarchive/] }
    newest = by_archive.max_by { |archive, _| File.mtime(archive) }
    newest ? newest.last : []
  end

  # The shipped version lives in the Info.plist, not in MARKETING_VERSION.
  # Absolute path: the lane's working directory is not guaranteed to be the repo root.
  def shipped_app_version
    plist = File.expand_path("../MealMood/Resources/Info.plist", __dir__)
    value = `/usr/bin/plutil -extract CFBundleShortVersionString raw "#{plist}"`.strip
    UI.user_error!("Could not read the app version from #{plist}") if value.empty?
    value
  end

  # asc_api_key returns fastlane's option hash, not a Spaceship token.
  def connect_api_app
    require 'spaceship'
    Spaceship::ConnectAPI.token = Spaceship::ConnectAPI::Token.create(
      key_id:    `pass show appstore/api-key-id`.strip,
      issuer_id: `pass show appstore/issuer-id`.strip,
      key:       `pass show appstore/api-key-p8`.strip
    )
    Spaceship::ConnectAPI::App.find("com.alexandrevazquez.mealmood")
  end

  # `deliver` does not create the App Store version record — it fails with
  # "could not find an editable version for 'IOS'" when the previous version is
  # already READY_FOR_SALE and nothing is open for editing.
  def ensure_editable_version(version)
    app = connect_api_app
    editable = app.get_edit_app_store_version(platform: "IOS")

    if editable && editable.version_string != version
      # ensure_version! would silently renumber it — say so before it happens.
      UI.important("App Store Connect had #{editable.version_string} open for editing; renaming it to #{version}")
    end

    created = app.ensure_version!(version, platform: "IOS")
    UI.success(created ? "App Store version #{version} ready" : "App Store version #{version} already editable")
  end

  desc "Show the latest TestFlight beta feedback (tester reports and crashes)"
  lane :feedback do |options|
    app = connect_api_app
    # spaceship's get_beta_feedback hits v1/betaFeedbacks, a private endpoint
    # Apple has since removed. These are the current public ones.
    client = Spaceship::ConnectAPI.test_flight_request_client
    limit = (options[:limit] || 10).to_i

    ["v1/apps/#{app.id}/betaFeedbackCrashSubmissions", "v1/apps/#{app.id}/betaFeedbackScreenshotSubmissions"].each do |path|
      UI.header(path)
      begin
        resp = client.get(path, {
          "include" => "build,tester",
          "limit" => limit,
          "sort" => "-createdDate"
        })
        rows = resp.body["data"] || []
        # Resolve build ids so each report says which build it came from.
        builds = {}
        (resp.body["included"] || []).each do |inc|
          builds[inc["id"]] = inc.dig("attributes", "version") if inc["type"] == "builds"
        end
        UI.important("none") if rows.empty?
        rows.each do |row|
          a = row["attributes"] || {}
          build = builds[row.dig("relationships", "build", "data", "id")]
          UI.message("── #{a['createdDate']} — build #{build || '?'} — #{a['deviceModel']} — #{a['osVersion']} — id #{row['id']}")
          UI.message("   #{a['comment']}") if a["comment"]
        end
      rescue => e
        UI.error("#{path}: #{e.message[0, 200]}")
      end
    end
  end

  desc "Download the crash log of a TestFlight feedback submission (id from `feedback`)"
  lane :crashlog do |options|
    UI.user_error!("pass id:<submission id>") unless options[:id]
    connect_api_app
    client = Spaceship::ConnectAPI.test_flight_request_client
    resp = client.get("v1/betaFeedbackCrashSubmissions/#{options[:id]}/crashLog", {})
    # The log comes back inline as logText, not as a download URL.
    text = resp.body.dig("data", "attributes", "logText")
    UI.user_error!("No logText in response: #{resp.body.to_s[0, 300]}") unless text
    out = File.expand_path("../crashlog-#{options[:id]}.crash", __dir__)
    File.write(out, text)
    UI.success("Saved to #{out}")
  end

  desc "Show the App Store state of each version (review status, release type)"
  lane :status do
    app = connect_api_app
    app.get_app_store_versions(includes: "build").each do |v|
      UI.message("#{v.version_string} — #{v.app_store_state} — release: #{v.release_type} — build: #{v.build&.version}")
    end

    UI.header("Recent TestFlight builds")
    Spaceship::ConnectAPI::Build.all(app_id: app.id, limit: 5, sort: "-uploadedDate").each do |b|
      UI.message("build #{b.version} — #{b.processing_state} — expired: #{b.expired} — #{b.uploaded_date}")
    end
  end

  # For a build that is already on TestFlight: `release` would rebuild it and
  # `publish` never submits. Screenshots are left alone — they carry over.
  desc "Submit an already-uploaded build for App Store review (automatic release)"
  lane :submit do |options|
    UI.user_error!("pass build:<number>") unless options[:build]
    check_cloudkit_schema(blocking: true)
    version = options[:version] || shipped_app_version
    ensure_editable_version(version)
    upload_to_app_store(
      api_key: asc_api_key,
      app_version: version,
      build_number: options[:build],
      skip_binary_upload: true,
      skip_metadata: false,
      skip_screenshots: true,
      overwrite_screenshots: false,
      submit_for_review: true,
      automatic_release: true,
      force: true,
      precheck_include_in_app_purchases: false,
      run_precheck_before_submit: false
    )
  end

  # Safety net for the Xcode build phase, which only uploads the dSYM produced
  # locally. Run it once App Store Connect has finished processing the build —
  # `beta` uses skip_waiting_for_build_processing, so right after an upload the
  # dSYMs are not there yet and this would download nothing.
  desc "Download processed dSYMs from App Store Connect and send them to Crashlytics"
  lane :refresh_dsyms do |options|
    download_dsyms(
      api_key: asc_api_key,
      version: options[:version] || "latest",
      build_number: options[:build]
    )

    dsyms = lane_context[SharedValues::DSYM_PATHS] || []
    if dsyms.empty?
      # Expected for a post-bitcode app: Apple recompiles nothing, so it keeps
      # no dSYM of its own. Fall back to the ones the local archive produced.
      dsyms = latest_archive_dsyms
      UI.important("App Store Connect has no dSYMs for this build; using the latest local archive.")
    end

    if dsyms.empty?
      UI.important("No dSYMs found either — nothing to upload.")
      next
    end

    upload_symbols_to_crashlytics(
      gsp_path: "MealMood/Resources/GoogleService-Info.plist",
      binary_path: crashlytics_upload_symbols_binary,
      dsym_paths: dsyms
    )
    clean_build_artifacts
  end

  desc "Upload metadata + localized screenshots for an existing TestFlight build (no rebuild, no submit)"
  lane :publish do |options|
    version = options[:version] || "1.2.0"
    build   = options[:build]   || "55"
    upload_to_app_store(
      api_key: asc_api_key,
      app_version: version,
      build_number: build,
      skip_binary_upload: true,
      skip_metadata: false,
      skip_screenshots: false,
      overwrite_screenshots: true,
      submit_for_review: false,
      automatic_release: false,
      precheck_include_in_app_purchases: false,
      force: true,
      run_precheck_before_submit: false
    )
  end

  desc "Push a new release build to the App Store"
  lane :release do
    check_cloudkit_schema(blocking: true)
    increment_build_number(xcodeproj: "MealMood.xcodeproj")
    build_app(
      scheme: "MealMood",
      sdk: "iphoneos26.5",
      export_options: EXPORT_OPTIONS
    )
    upload_to_app_store(
      api_key: asc_api_key,
      automatic_release: true,
      force: true
    )
  end
end
