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"
    }
  }

  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

  desc "Push a new beta build to TestFlight"
  lane :beta do
    increment_build_number(xcodeproj: "MealMood.xcodeproj")
    build_app(
      scheme: "MealMood",
      export_method: "app-store",
      sdk: "iphoneos26.5",
      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]
    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
    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
