# frozen_string_literal: true

require "fileutils"
require "spaceship"

appfile_path = File.join( __dir__, "Appfile" )
unless File.exist?( appfile_path )
  UI.abort_with_message! <<~NO_APPFILE_ERROR.gsub( /\s+/, " " ).strip
    Could not find #{appfile_path}. Copy the example file in that directory to
    that path and fill in the relevant values to use Fastlane.
  NO_APPFILE_ERROR
end

forbidden_env_vars = [
  "MOCK_MODE"
]
forbidden_env_vars.each do | env_var |
  next unless ENV[env_var].to_s.size.positive?

  UI.abort_with_message! <<~NO_ENV_ERROR.gsub( /\s+/, " " ).strip
    ENV is set #{env_var}. Remove the value from your ENV before running fastlane.
  NO_ENV_ERROR
end

required_env_vars = [
  "IOS_PROVISIONING_PROFILE_NAME",
  "IOS_SHARE_BUNDLE_ID",
  "IOS_SHARE_PROVISIONING_PROFILE_NAME"
]
required_env_vars.each do | env_var |
  next unless ENV[env_var].to_s.empty?

  UI.abort_with_message! <<~NO_ENV_ERROR.gsub( /\s+/, " " ).strip
    ENV is missing #{env_var}. Add the value to .env or populate your ENV in
    another way.
  NO_ENV_ERROR
end

VERSION = File.open( "../package.json" ) {| f | JSON.parse( f.read )["version"] }
editor_cmd = [
  ENV.fetch( "EDITOR", nil ),
  `git config core.editor`,
  `which vi`
].map {| e | e.to_s.strip }.detect {| e | !e.empty? }
if editor_cmd.nil?
  UI.abort_with_message! <<~NO_EDITOR_ERROR
    Could not find an editor, not even vi. Set the EDITOR environmental
    variable or the core.editor git config"
  NO_EDITOR_ERROR
end
editor_cmd_needs_to_wait = ( editor_cmd =~ /^code / || editor_cmd =~ /^subl / ) && !editor_cmd.include?( "-w" )
EDITOR = editor_cmd_needs_to_wait ? "#{editor_cmd} -w" : editor_cmd
XCODEPROJ = "ios/iNaturalistReactNative.xcodeproj"
PACKAGE_ID = CredentialsManager::AppfileConfig.try_fetch_value( :package_name )
IOS_BUNDLE_ID = CredentialsManager::AppfileConfig.try_fetch_value( :app_identifier )
IOS_ITC_TEAM_ID = CredentialsManager::AppfileConfig.try_fetch_value( :itc_team_id )

# https://github.com/fastlane/fastlane/issues/20741#issuecomment-1280687500
# ENV["ITMSTRANSPORTER_FORCE_ITMS_PACKAGE_UPLOAD"] = "true"

def set_android_version_code( new_version_code )
  build_gradle_path = "../android/app/build.gradle"
  new_gradle = File.read( build_gradle_path ).sub( /versionCode\s+\d+/, "versionCode #{new_version_code}" )
  File.write( build_gradle_path, new_gradle )
end

def set_android_version_name( _new_version_name )
  build_gradle_path = "../android/app/build.gradle"
  new_gradle = File.read( build_gradle_path ).sub( /versionName\s+".+"/, "versionName \"#{VERSION}\"" )
  File.write( build_gradle_path, new_gradle )
end

def get_changelog_path( options = {} )
  build_number = options[:build_number] || get_build_number( xcodeproj: XCODEPROJ )
  changelog_dir_path = File.join( "metadata", "android", "en-US", "changelogs" )
  FileUtils.mkpath( changelog_dir_path )
  File.join( changelog_dir_path, "#{build_number}.txt" )
end

def app_store_release_notes
  build_number ||= get_build_number( xcodeproj: XCODEPROJ )
  data = {}
  Dir.glob( File.join( "metadata", "android", "*" ) ) do | locale_dir |
    changelog_path = File.join( locale_dir, "changelogs", "#{build_number}.txt" )
    locale = File.basename( locale_dir )
    unless File.exist?( changelog_path )
      UI.abort_with_message! <<~MSG
        No change log file exists at #{changelog_path}. That should have been
        created when you ran `fastlane tag`.
      MSG
    end
    data[locale] = File.read( changelog_path )
  end
  data
end

def get_aab_path( build_number = nil )
  build_number ||= get_build_number( xcodeproj: XCODEPROJ )
  File.expand_path( File.join(
    __dir__,
    "..",
    "android",
    "app",
    "build",
    "outputs",
    "bundle",
    "release",
    "#{PACKAGE_ID}-v#{VERSION}+#{build_number}-release.aab"
  ) )
end

def get_apk_path( build_number = nil )
  build_number ||= get_build_number( xcodeproj: XCODEPROJ )
  File.expand_path( File.join(
    __dir__,
    "..",
    "android",
    "app",
    "build",
    "outputs",
    "apk",
    "release",
    "#{PACKAGE_ID}-v#{VERSION}+#{build_number}-release.apk"
  ) )
end

def get_ipa_path( build_number = nil )
  build_number ||= get_build_number( xcodeproj: XCODEPROJ )
  File.expand_path( File.join(
    __dir__,
    "..",
    "ios",
    "build",
    "#{PACKAGE_ID}-v#{VERSION}+#{build_number}.ipa"
  ) )
end

def get_changelog( options = {} )
  build_number = options[:build_number] || nil
  changelog_path = get_changelog_path( build_number: build_number )
  unless File.exist?( changelog_path )
    return nil if options[:allow_blank]

    UI.abort_with_message! <<~MSG
      No change log file exists at #{changelog_path}. That should have been
      created when you ran `fastlane tag`.
    MSG
  end

  begin
    File.read( changelog_path )
  rescue StandardError
    nil
  end
end

def prompt_for_release_notes( options = {} )
  instructions = options[:instructions] || ""
  # Get release notes
  # Bit silly but takes advantage of existing syntax highlighting
  fname = "COMMIT_EDITMSG"
  File.open( fname, "w" ) do | f |
    f << instructions
  end
  system "#{EDITOR} #{fname}", exception: true
  release_notes = ""
  File.readlines( fname ).each do | line |
    release_notes += line unless line[0] == "#"
  end
  release_notes.strip!
  FileUtils.rm( fname )
  if release_notes.strip.empty?
    if options[:reset_git_repo_on_fail]
      reset_git_repo skip_clean: true
    end
    UI.abort_with_message! "You gotta enter release notes!"
  end
  if release_notes.strip.length > 500
    if options[:reset_git_repo_on_fail]
      reset_git_repo skip_clean: true
    end
    UI.abort_with_message! "Release notes must be 500 characters or less"
  end
  # Return the release notes
  release_notes
end

lane :tag do | options |
  desc "Add a new tag with an incremented version"
  ensure_git_status_clean( ignore_files: ["fastlane/Fastfile"] )
  last_tag = last_git_tag
  changes = changelog_from_git_commits( pretty: "# * %h %s (%an, %ai)" )
  if last_tag && changes.empty? && !options[:force]
    UI.abort_with_message! "Nothing has changed since the last tag (#{last_tag})"
  end

  # Increment the iOS build number
  increment_build_number( xcodeproj: XCODEPROJ )
  increment_version_number( xcodeproj: XCODEPROJ, version_number: VERSION )
  build_number = get_build_number( xcodeproj: XCODEPROJ )
  # set android/app/build.gradle versionCode to this build_number
  set_android_version_code( build_number )
  # set android/app/build.gradle versionName to VERSION
  set_android_version_name( VERSION )
  tag = "v#{VERSION}+#{build_number}"

  # Prompt for release notes
  instructions = <<~INSTRUCTIONS


    # Enter notes about what's new in #{tag}. Lines beginning with # will be ignored.
    # Keep notes within the 500 character limit required by Google Play.
    #
    # Here's what changed since the last tag:
    #{changes}
  INSTRUCTIONS
  # At the moment we are not bothered about translating those into other languages
  release_notes = prompt_for_release_notes( instructions: instructions, reset_git_repo_on_fail: true )

  # Write release notes to a place where they can be translated and add that file to git
  changelog_path = get_changelog_path( build_number: build_number )
  File.open( changelog_path, "w" ) do | f |
    f << "#{release_notes}\n"
  end
  changelog_git_path = File.join( "fastlane", changelog_path )
  git_add( path: changelog_git_path )

  # commit
  commit_version_bump(
    message: tag,
    xcodeproj: XCODEPROJ,
    include: [
      "android/app/build.gradle",
      changelog_git_path
    ]
  )
  push_to_git_remote

  # Create a tag for this release
  add_git_tag( tag: tag )
  push_git_tags
end

platform :android do
  desc "Upload description to Google Play"
  lane :upload_metadata do
    # Upload for all tracks
    tracks = [
      "internal",
      "alpha",
      "beta"
      # TODO: 20241105-we currently have no production track opened, so uploading will error out
      # "production"
    ]
    tracks.each do | track |
      upload_to_play_store(
        track: track,
        skip_upload_apk: true,
        skip_upload_aab: true,
        skip_upload_metadata:	false,
        skip_upload_changelogs:	true,
        skip_upload_images:	true,
        skip_upload_screenshots: true
      )
    end
  end

  lane :build do
    desc "Build release files for Android"
    Dir.children( "../android/app/src/main/assets/camera" ).each do | file |
      next if ENV.value?( file )

      UI.abort_with_message! <<~EXTRA_ANDROID_ASSETS_ERROR
        Android assets folder has extraneous file: #{file}, which is not listed
        as an environment variable. Remove any unreferenced files from
        android/app/src/main/assets/camera before running a release build
      EXTRA_ANDROID_ASSETS_ERROR
    end
    assets = File.open( "../android/link-assets-manifest.json" ) {| f | JSON.parse( f.read )["data"] }
    linked_assets = assets.map do | asset |
      asset["path"].split( "assets/fonts/" )[1]
    end
    Dir.children( "../assets/fonts" ).each do | file |
      next unless !linked_assets.include?( file ) && ( file != ".DS_Store" )

      UI.abort_with_message! <<~EXTRA_ASSETS_ERROR
        Assets folder has extraneous file: #{file}, which is not listed
        in link-assets-manifest.json. Remove any unreferenced assets before
        running a release build
      EXTRA_ASSETS_ERROR
    end
    keystore_properties_path = File.join(
      __dir__,
      "..",
      "android",
      "keystore.properties"
    )
    unless File.exist?( keystore_properties_path )
      UI.abort_with_message! <<~NO_KEYSTORE_PROPERTIES_ERROR.gsub( /\s+/, " " ).strip
        Could not find #{keystore_properties_path}. Copy the example file in that directory to
        that path and fill in the relevant values to build for Android.
      NO_KEYSTORE_PROPERTIES_ERROR
    end
    build_number = get_build_number( xcodeproj: XCODEPROJ )
    # Build AAB. This should write
    # android/app/build/outputs/bundle/release/PACKAGE_ID-vVERSION_NAME+VERSION_CODE-release.aab
    aab_path = get_aab_path( build_number )
    if File.exist?( aab_path )
      UI.important "AAB already exists at #{aab_path}"
    else
      gradle( task: "bundle", project_dir: "android" )
    end
    unless File.exist?( aab_path )
      UI.abort_with_message! "Failed to create AAB at #{aab_path}"
    end

    # Build APK. This should write
    # android/app/build/outputs/apk/release/PACKAGE_ID-vVERSION_NAME+VERSION_CODE-release.apk
    apk_path = get_apk_path( build_number )
    if File.exist?( apk_path )
      UI.important "APK already exists at #{apk_path}"
    else
      gradle( task: "build", project_dir: "android", flags: "-x lint" )
    end
    unless File.exist?( apk_path )
      UI.abort_with_message! "Failed to create APK at #{apk_path}"
    end
  end

  lane :clean do
    sh "rm -rf ../android/.gradle", step_name: "Deleting Android .gradle folder"
    sh "rm -rf ../android/build", step_name: "Deleting Android build folder"
    sh "(cd ../android && ./gradlew --stop && ./gradlew clean)", step_name: "Cleaning Android build environment"
    Dir.glob( File.join( File.dirname( get_aab_path ), "*.aab" ) ).each do | aab_path |
      UI.message "Deleting #{aab_path}"
      File.delete aab_path
    end
    Dir.glob( File.join( File.dirname( get_apk_path ), "*.apk" ) ).each do | apk_path |
      UI.message "Deleting #{apk_path}"
      File.delete apk_path
    end
  end
end

platform :ios do
  desc "Upload app metadata to App Store"
  # This only works while the new app version is in the "Prepare for Submission" state
  lane :upload_metadata do
    upload_to_app_store(
      metadata_path: "./fastlane/metadata/ios",
      force: true,
      skip_binary_upload: true,
      skip_metadata: false,
      skip_screenshots: true
    )
  end

  lane :build do
    desc "Build release files for iOS"
    # Build iOS app
    get_certificates
    get_provisioning_profile
    ipa_path = get_ipa_path
    if File.exist?( ipa_path )
      UI.important "IPA already exists at #{ipa_path}"
    else
      build_app(
        workspace: File.join( "ios", "iNaturalistReactNative.xcworkspace" ),
        scheme: "iNaturalistReactNative",
        output_directory: File.dirname( ipa_path ),
        output_name: File.basename( ipa_path ),
        # I'd rather not do it this way, but so far I haven't figured out a
        # way for fastlane to automatically detect both of the provisioning
        # profiles we need
        export_options: {
          signingStyle: "manual",
          provisioningProfiles: {
            IOS_BUNDLE_ID => ENV.fetch( "IOS_PROVISIONING_PROFILE_NAME", nil ),
            ENV.fetch( "IOS_SHARE_BUNDLE_ID", nil ) => ENV.fetch( "IOS_SHARE_PROVISIONING_PROFILE_NAME", nil )
          }
        }
      )
    end
  end

  lane :clean do
    sh "rm -rf ios/build", step_name: "Deleting iOS build folder"
    Dir.glob( File.join( File.dirname( get_ipa_path ), "*.ipa" ) ).each do | ipa_path |
      UI.message "Deleting #{ipa_path}"
      File.delete ipa_path
    end
  end
end

lane :build do
  desc "Build release files for all platforms"
  Fastlane::LaneManager.cruise_lane "ios", "build"
  Fastlane::LaneManager.cruise_lane "android", "build"
end

lane :clean do
  desc "Delete build artifacts"
  sh "rm -rf $TMPDIR/react-*", step_name: "Deleting React Native cache"
  sh "rm -rf $TMPDIR/metro-*", step_name: "Deleting Metro cache"
  sh "watchman watch-del-all", step_name: "Deleting watchman cache"
  Fastlane::LaneManager.cruise_lane "ios", "clean"
  Fastlane::LaneManager.cruise_lane "android", "clean"
end

lane :release do
  desc "Make github release for the latest tag and make builds"
  last_tag = last_git_tag
  if last_tag.nil? || last_tag.empty?
    UI.abort_with_message! "No tags have been added yet. Try starting with `fastlane tag`"
  end
  original_branch = git_branch
  system "git checkout #{last_tag}", exception: true
  build_number = get_build_number( xcodeproj: XCODEPROJ )
  if build_number.to_s != last_tag.split( "+" ).last
    UI.abort_with_message! <<~MSG
      The last tag doesn't match the current build number. Either make a new
      tag or check out the tag before releasing.
    MSG
  end
  changelog = get_changelog

  build

  apk_path = File.expand_path( get_apk_path( build_number ) )
  if File.exist?( apk_path )
    UI.success "Found APK at #{apk_path}"
  else
    UI.abort_with_message! "Failed to find APK at #{apk_path}"
  end

  github_release = get_github_release(
    url: "inaturalist/iNaturalistReactNative",
    version: last_tag,
    api_token: ENV.fetch( "GITHUB_TOKEN", nil )
  )
  if github_release
    UI.important "Release already exists at #{github_release['url']}. You need to manually upload any missing assets."
  else
    set_github_release(
      repository_name: "inaturalist/iNaturalistReactNative",
      api_token: ENV.fetch( "GITHUB_TOKEN", nil ),
      name: last_tag,
      tag_name: last_tag,
      description: changelog,
      # This is really just a fallback in case last_tag isn't really a tag
      commitish: "main",
      upload_assets: [apk_path],
      is_prerelease: true
    )
  end
  system "git checkout #{original_branch}", exception: true
end

lane :internal do
  desc "Push builds for the latest tag for internal testing"
  # Ensure build files exist for the latest tag
  aab_path = get_aab_path
  unless File.exist?( aab_path )
    UI.abort_with_message! <<~MSG
      AAB does not exist at #{aab_path}. You may need to run the release lane before doing this.
    MSG
  end
  last_tag = last_git_tag
  if last_tag.nil? || last_tag.empty?
    UI.abort_with_message! "No tags have been added yet. Try starting with `fastlane tag`"
  end
  changelog = get_changelog

  upload_to_play_store(
    aab: aab_path,
    track: "internal",
    version_name: last_tag
  )

  upload_to_testflight(
    ipa: get_ipa_path,
    changelog: changelog,
    # https://github.com/fastlane/fastlane/issues/20756
    itc_provider: CredentialsManager::AppfileConfig.try_fetch_value( :itc_provider ),
    # The following distributes the build to our invite-only external test group
    distribute_external: true,
    groups: [
      "iNat Friends"
    ]
  )

  slack_message = <<~MESSAGE
    🧪 NEW INTERNAL RELEASE
    Should be available to staff in TestFlight and Play Store soon. Please test and report any bugs, especially ones that should block release!

    #{changelog}
  MESSAGE
  slack(
    message: slack_message,
    default_payloads: [],
    payload: {
      version: last_tag,
      apk: "https://github.com/inaturalist/iNaturalistReactNative/releases/download/#{last_tag}/org.inaturalist.iNaturalistMobile-#{last_tag}-release.apk"
    }
  )
end

lane :beta do
  desc "Make latest builds available for public testing"

  build_number = get_build_number( xcodeproj: XCODEPROJ )

  # UI.message "Play Store: promoting build #{build_number} to beta track"
  upload_to_play_store(
    version_code: build_number,
    track: "internal",
    track_promote_to: "beta"
  )

  UI.message "TestFlight: adding build #{build_number} to Open Beta group"
  add_build_to_testflight_beta_group(
    version_code: build_number,
    beta_group: "Open Beta"
  )

  # We decided to send builds to internal and beta at the same time, so this is appearing twice in Slack
  # last_tag = last_git_tag
  # changelog = get_changelog
  # slack_message = <<~MESSAGE
  #   🚧 NEW BETA RELEASE
  #   Should be available to beta testers in TestFlight and Play Store soon.

  #   #{changelog}
  # MESSAGE
  # slack(
  #   message: slack_message,
  #   default_payloads: [],
  #   payload: {
  #     version: last_tag,
  #     apk: "https://github.com/inaturalist/iNaturalistReactNative/releases/download/#{last_tag}/org.inaturalist.iNaturalistMobile-#{last_tag}-release.apk"
  #   }
  # )
end

lane :download_app_store_reviews do
  desc "Download App Store reviews and save them to a file"

  UI.message "Logging into App Store Connect..."

  # Use credentials from the Appfile
  apple_id = CredentialsManager::AppfileConfig.try_fetch_value( :apple_id )
  itc_team_id = CredentialsManager::AppfileConfig.try_fetch_value( :itc_team_id )

  # Explicitly login with credentials
  Spaceship::Tunes.login( apple_id )
  Spaceship::Tunes.select_team( team_id: itc_team_id, team_name: nil )

  # Select the app using the bundle ID from the Appfile
  UI.message "Finding app with bundle ID: #{IOS_BUNDLE_ID}"
  app = Spaceship::Tunes::Application.find( IOS_BUNDLE_ID )

  unless app
    UI.abort_with_message! "Could not find app with bundle ID: #{IOS_BUNDLE_ID}"
  end

  # Fetch reviews
  UI.message "Fetching reviews for #{app.name}..."
  reviews = app.ratings.reviews

  # Create an array to hold processed reviews
  processed_reviews = []
  reviews.each do | review |
    # Convert review object to a hash with the fields you want
    processed_review = {
      rating: review.rating.to_i,
      title: review.title,
      review_content: review.review,
      nickname: review.nickname,
      country: review.store_front,
      last_modified: review.last_modified,
      edited: review.edited,
      responded: review.responded?,
      developer_response: review.developer_response.response
    }
    processed_reviews << processed_review
  end

  # Save reviews to a JSON file
  output_file = "app_store_reviews.json"
  File.write( output_file, JSON.pretty_generate( processed_reviews ) )

  UI.success "Downloaded #{reviews.count} reviews to #{output_file}"
end

lane :prod do
  desc "Set up App Store version and upload new metadata"

  version = get_version_number( xcodeproj: XCODEPROJ, target: "iNaturalistReactNative" )

  # Get all build numbers between the last released version and the current build number
  build_number = get_build_number( xcodeproj: XCODEPROJ )

  upload_to_app_store(
    build_number: build_number,
    app_version: version,
    automatic_release: false,
    submit_for_review: false,
    force: false,
    # We don't want to upload the current binary but promote it from TestFlight to AppStore
    # TODO: this does not work in conjunction with submit_for_review: false, so for now the new build
    # needs to be manually attached
    skip_binary_upload: true,
    skip_metadata: false,
    skip_screenshots: true,
    metadata_path: "./fastlane/metadata/ios",
    copyright: "#{Date.today.year} iNaturalist",
    precheck_include_in_app_purchases: false
  )

  slack_message = <<~MESSAGE
    🚀 NEW PROD RELEASE
    Staged on app stores with localized metadata, ready to submit for review.
  MESSAGE
  slack(
    message: slack_message,
    default_payloads: [],
    payload: {
      version: last_git_tag
    }
  )

  UI.important "This just sets up the version in the App Store. You need to go there " \
    "to attach a build and submit for review."
end
