# fastlane ios release \
#   key_id:77HLG2C29P \
#   issuer_id:a1ead579-73d7-4227-b9d6-1aeccf17edb4 \
#   key_filepath:AuthKey_77HLG2C29P.p8 \
#   app_identifier:com.lazycoders.fluentcommunity \
#   team_id:785R8UTSWS

# install fastlane plugin
# fastlane add_plugin versioning

default_platform(:ios)
platform :ios do
  before_all do
    ENV['MATCH_PASSWORD'] = ENV['FASTLANE_MATCH_PASSWORD'] || '123456'
  end

  # print given options
  desc "Print given options"
  lane :print_options do |options|
    UI.message("Options: #{options}")
  end

  # Check app state without building
  desc "Check app state without building"
  lane :check_state do |options|
    validate_required_options(options)
    api_key = setup_app_store_connect_api_key(options)

    verify_app_state(api_key, options[:app_identifier])

    new_version, app_state = determine_app_version(api_key, options[:app_identifier])

    use_timestamp = options[:use_timestamp_builds] || false
    new_build_number = get_next_build_number(api_key, options[:app_identifier], new_version, use_timestamp)

    UI.message("\n🎯 Summary:")
    UI.message("Current state: #{app_state || 'NEW APP'}")
    UI.message("Will use version: #{new_version}")
    UI.message("Will use build number: #{new_build_number}")
    UI.message("Build number type: #{use_timestamp ? 'Timestamp-based' : 'Sequential'}")

    if app_state
      testflight_only_states = ["READY_FOR_SALE", "PENDING_DEVELOPER_RELEASE", "READY_FOR_DISTRIBUTION"]
      upload_method = testflight_only_states.include?(app_state) ? "TestFlight Only" : "App Store Connect"
      UI.message("Upload method: #{upload_method}")
    end
  end

  desc "Release a new version"
  lane :release do |options|
    # Group 1: Input validation and setup
    validate_required_options(options)
    verify_p8_file_exists(options[:key_filepath])
    api_key = setup_app_store_connect_api_key(options)

    # Group 2: App and bundle verification
    verify_bundle_id_exists(api_key, options[:app_identifier])
    verify_app_exists(api_key, options[:app_identifier])

    # Show current app state for visibility
    verify_app_state(api_key, options[:app_identifier])

    # Group 3: Version management
    new_version, app_state = determine_app_version(api_key, options[:app_identifier])

    # Check if timestamp-based build numbers should be used
    use_timestamp = options[:use_timestamp_builds] || false
    new_build_number = get_next_build_number(api_key, options[:app_identifier], new_version, use_timestamp)

    # Skip verification for timestamp builds (they're always unique)
    unless use_timestamp
      # Verify build number is actually available
      unless verify_build_number_available(api_key, options[:app_identifier], new_version, new_build_number)
        # If not available, find the next available build number
        new_build_number = find_next_available_build_number(api_key, options[:app_identifier], new_version)
        UI.important("🔄 Using corrected build number: #{new_build_number}")
      end
    end

    update_version_and_build_number(new_version, new_build_number)

    # Group 3.5: iOS deployment target validation
    validate_and_fix_deployment_target

    # Group 4: Certificate and signing setup
    keychain_name, keychain_password = setup_keychain(options[:app_identifier])
    cleanup_certificate_branch(options[:app_identifier])
    cleanup_apple_signing_artifacts(api_key, options)
    ensure_push_capability(api_key, options) if options[:has_push_notification].to_s == 'true'
    setup_code_signing(api_key, options, keychain_name, keychain_password)

    # Group 5: Build and upload
    build_app(options[:app_identifier])
    upload_build(api_key, options[:team_id], options[:app_identifier], app_state)

    # Cleanup
    delete_keychain(name: "#{keychain_name}-db")
  end

  # IMPROVED: Get next build number - checks ALL builds including failed ones
  # Set use_timestamp_builds: true in options to use timestamp-based build numbers
  def get_next_build_number(api_key, app_identifier, version, use_timestamp = false)
    # If timestamp mode is enabled, use timestamp-based build numbers
    if use_timestamp
      return get_timestamp_build_number
    end

    begin
      UI.message("🔍 Checking for existing builds for version #{version}...")

      # Method 1: Try to get all builds including failed/processing ones
      app = Spaceship::ConnectAPI::App.find(app_identifier)

      begin
        all_builds = Spaceship::ConnectAPI::Build.all(
          app_id: app.id,
          version: version,
          sort: '-uploadedDate'
        )

        if all_builds && !all_builds.empty?
          # Get all build numbers (including processing, failed, and invalid)
          build_numbers = all_builds.map do |build|
            build.version.to_i
          end.compact.reject { |n| n == 0 }

          if !build_numbers.empty?
            latest_build = build_numbers.max
            UI.message("📦 Found existing builds: #{build_numbers.sort.join(', ')}")
            UI.message("🔢 Latest build number: #{latest_build}")

            next_build = latest_build + 1
            UI.success("✅ Will use build number: #{next_build}")
            return next_build
          end
        end
      rescue => e
        UI.message("⚠️  Could not fetch all builds: #{e.message}")
      end

      # Method 2: Fallback to latest_testflight_build_number (only processed builds)
      UI.message("Trying alternative method to find builds...")
      begin
        latest_build = latest_testflight_build_number(
          api_key: api_key,
          app_identifier: app_identifier,
          version: version
        )

        if latest_build && latest_build > 0
          UI.message("📦 Found latest processed build: #{latest_build}")
          UI.important("⚠️  Note: This may not include failed or processing builds")
          next_build = latest_build + 1
          UI.success("✅ Will use build number: #{next_build}")
          return next_build
        end
      rescue => e
        UI.message("⚠️  Fallback method failed: #{e.message}")
      end

      # Method 3: No builds found
      UI.message("✅ No builds found for version #{version}. Using build number 1.")
      return 1

    rescue => e
      UI.error("⚠️  Error in get_next_build_number: #{e.message}")
      UI.message("Defaulting to build number 1")
      return 1
    end
  end

  # NEW: Generate timestamp-based build number
  def get_timestamp_build_number
    # Format: YYYYMMDDHHmm (e.g., 202510220945 for Oct 22, 2025 9:45 AM)
    timestamp = Time.now.strftime("%Y%m%d%H%M").to_i
    UI.message("🕐 Using timestamp-based build number: #{timestamp}")
    UI.message("📅 Timestamp: #{Time.now.strftime('%Y-%m-%d %H:%M')}")
    return timestamp
  end

  # NEW: Verify build number is actually available
  def verify_build_number_available(api_key, app_identifier, version, build_number)
    begin
      UI.message("🔍 Verifying build number #{build_number} is available for version #{version}...")

      app = Spaceship::ConnectAPI::App.find(app_identifier)
      all_builds = Spaceship::ConnectAPI::Build.all(
        app_id: app.id,
        version: version
      )

      return true if all_builds.nil? || all_builds.empty?

      existing_build_numbers = all_builds.map { |b| b.version.to_i }.compact.reject { |n| n == 0 }

      if existing_build_numbers.include?(build_number)
        UI.error("❌ Build number #{build_number} already exists for version #{version}!")
        UI.message("📦 Existing builds: #{existing_build_numbers.sort.join(', ')}")
        return false
      else
        UI.success("✅ Build number #{build_number} is available")
        return true
      end

    rescue => e
      UI.message("⚠️  Could not verify build number: #{e.message}")
      UI.message("⚠️  Proceeding anyway - will rely on conflict detection")
      return true
    end
  end

  # NEW: Find next available build number when conflict detected
  def find_next_available_build_number(api_key, app_identifier, version)
    begin
      app = Spaceship::ConnectAPI::App.find(app_identifier)
      all_builds = Spaceship::ConnectAPI::Build.all(
        app_id: app.id,
        version: version
      )

      if all_builds.nil? || all_builds.empty?
        return 1
      end

      existing_build_numbers = all_builds.map { |b| b.version.to_i }.compact.reject { |n| n == 0 }
      max_build = existing_build_numbers.max || 0

      next_build = max_build + 1
      UI.success("✅ Next available build number: #{next_build}")
      return next_build

    rescue => e
      UI.error("Error finding next available build: #{e.message}")
      return 1
    end
  end

  # Helper methods for improved readability
  def validate_required_options(options)
    required_options = %i[key_id issuer_id key_filepath app_identifier team_id]
    missing_options = required_options.select { |option| options[option].nil? }
    UI.user_error!("Missing required options: #{missing_options.join(', ')}") unless missing_options.empty?
  end

  def verify_p8_file_exists(key_filepath)
    unless File.exist?("../#{key_filepath}")
      UI.user_error!("The .p8 file does not exist in the parent folder (should be in the ios folder)")
    end
  end

  def setup_app_store_connect_api_key(options)
    return app_store_connect_api_key(
      key_id: options[:key_id],
      issuer_id: options[:issuer_id],
      key_filepath: options[:key_filepath],
      in_house: false
    )
  end

  def verify_bundle_id_exists(api_key, app_identifier)
    begin
      bundle_id = Spaceship::ConnectAPI::BundleId.find(app_identifier)

      if bundle_id.nil?
        UI.important("😕 Bundle ID verification failed")
        UI.user_error!("Bundle ID '#{app_identifier}' not found in developer account. Please create it first.")
      else
        UI.success("✅ Bundle ID '#{app_identifier}' verified successfully")
      end
    rescue => e
      UI.error("❌ Bundle ID verification error")
      UI.user_error!("Failed to verify bundle ID: #{e.message}")
    end
  end

  def cleanup_apple_signing_artifacts(api_key, options)
    app_id = options[:app_identifier]
    UI.message("🧹 Cleaning up Apple signing artifacts for '#{app_id}'...")

    # 1) Prune one distribution cert if there are 2 or more non-expired ones.
    now = Time.now
    dist_certs = Spaceship::ConnectAPI::Certificate.all.select do |c|
      c.certificate_type == "DISTRIBUTION" &&
        (c.expiration_date.nil? || Time.parse(c.expiration_date) > now)
    end
    UI.message("Found #{dist_certs.count} non-expired DISTRIBUTION certificate(s)")
    if dist_certs.count >= 2
      target = dist_certs.first
      UI.important("Deleting distribution cert '#{target.name}' (id: #{target.id})")
      target.delete!
    else
      UI.success("✅ Cert count under threshold — no deletion needed")
    end

    # 2) Delete the existing match-generated AppStore profile so match regenerates a fresh one.
    profile_name = "match AppStore #{app_id}"
    profile = Spaceship::ConnectAPI::Profile.all.find { |p| p.name == profile_name }
    if profile
      UI.important("Deleting provisioning profile '#{profile_name}' (id: #{profile.id})")
      profile.delete!
    else
      UI.success("✅ Provisioning profile '#{profile_name}' not found — nothing to delete")
    end
  end

  def ensure_push_capability(api_key, options)
    app_id = options[:app_identifier]
    UI.message("🔔 Ensuring Push Notifications capability on App ID '#{app_id}'...")
    bundle_id = Spaceship::ConnectAPI::BundleId.find(app_id)
    UI.user_error!("Bundle ID '#{app_id}' not found") if bundle_id.nil?

    existing = bundle_id.get_capabilities.map { |c| c.capability_type }
    if existing.include?(Spaceship::ConnectAPI::BundleIdCapability::Type::PUSH_NOTIFICATIONS)
      UI.success("✅ Push Notifications already enabled — skipping")
      return
    end

    # Direct POST: spaceship 2.228 sends an outdated 'capability' relationship that Apple rejects.
    body = {
      data: {
        type: "bundleIdCapabilities",
        attributes: { capabilityType: "PUSH_NOTIFICATIONS" },
        relationships: {
          bundleId: { data: { type: "bundleIds", id: bundle_id.id } }
        }
      }
    }
    Spaceship::ConnectAPI.provisioning_request_client.post("v1/bundleIdCapabilities", body)
    UI.success("✅ Push Notifications enabled on App ID")
  end

  def verify_app_exists(api_key, app_identifier)
    begin
      app = Spaceship::ConnectAPI::App.find(app_identifier)

      if app.nil?
        UI.important("😕 App verification failed")
        UI.user_error!("App '#{app_identifier}' not found in developer account. Please create it first.")
      else
        UI.success("✅ App '#{app_identifier}' verified successfully")
      end
    rescue => e
      UI.error("❌ App verification error")
      UI.user_error!("Failed to verify app: #{e.message}")
    end
  end

  def verify_app_state(api_key, app_identifier)
    begin
      app = Spaceship::ConnectAPI::App.find(app_identifier)
      all_versions = app.get_app_store_versions

      UI.message("\n📊 App Store Version Status:")
      UI.message("=" * 60)

      if all_versions.empty?
        UI.message("No versions found - this appears to be a new app")
      else
        all_versions.each do |version|
          UI.message("Version: #{version.version_string} | State: #{version.app_store_state}")
        end
      end

      UI.message("=" * 60 + "\n")
    rescue => e
      UI.message("Could not fetch app state details: #{e.message}")
    end
  end

  def determine_app_version(api_key, app_identifier)
    begin
      # Get all app store versions to find the latest one
      app = Spaceship::ConnectAPI::App.find(app_identifier)
      all_versions = app.get_app_store_versions

      # Find the version with the highest version number
      latest_version_info = all_versions.max_by do |version|
        version.version_string.split('.').map(&:to_i)
      end

      if latest_version_info.nil?
        UI.message("No versions found in App Store. Using initial version 1.0.0")
        return ["1.0.0", nil]
      end

      current_version = latest_version_info.version_string
      app_state = latest_version_info.app_store_state

      UI.message("Latest App Store version: #{current_version}")
      UI.message("Version state: #{app_state}")

      # States that REQUIRE a new version (cannot modify existing)
      states_requiring_new_version = [
        "READY_FOR_SALE",              # App is live on App Store
        "PENDING_DEVELOPER_RELEASE",   # Approved, waiting for you to release
        "READY_FOR_DISTRIBUTION"       # Approved, ready to distribute manually
      ]

      # States where you CAN reuse the same version
      states_allowing_same_version = [
        "IN_REVIEW",
        "WAITING_FOR_REVIEW",
        "PREPARE_FOR_SUBMISSION",
        "REJECTED",
        "DEVELOPER_REMOVED_FROM_SALE",
        "METADATA_REJECTED",
        "PENDING_CONTRACT",
        "INVALID_BINARY",
        "REMOVED_FROM_SALE",
        "WAITING_FOR_EXPORT_COMPLIANCE",
        "DEVELOPER_REJECTED"
      ]

      if states_requiring_new_version.include?(app_state)
        # If version is approved/released or pending release, increment to a new version
        UI.important("⚠️  App is in '#{app_state}' state. Creating new version...")

        version_components = current_version.split('.')
        if version_components.length >= 3
          # Increment patch version (1.2.3 -> 1.2.4)
          version_components[-1] = (version_components[-1].to_i + 1).to_s
        elsif version_components.length == 2
          # If only major.minor, add patch version (1.2 -> 1.2.1)
          version_components << "1"
        else
          # Fallback - just add .1
          version_components = [current_version, "1"]
        end

        new_version = version_components.join('.')
        UI.success("✅ Incrementing version from #{current_version} to #{new_version}")
        return [new_version, app_state]

      elsif states_allowing_same_version.include?(app_state)
        # Keep the same version for states like IN_REVIEW, WAITING_FOR_REVIEW, etc.
        UI.message("✅ App is in '#{app_state}' state. Can reuse version: #{current_version}")
        return [current_version, app_state]

      else
        # Unknown state - be conservative and increment
        UI.important("⚠️  Unknown state '#{app_state}'. Being conservative - incrementing version...")
        version_components = current_version.split('.')
        version_components[-1] = (version_components[-1].to_i + 1).to_s
        new_version = version_components.join('.')
        UI.message("Incrementing version from #{current_version} to #{new_version}")
        return [new_version, app_state]
      end
    rescue => e
      # If app is not in App Store yet, use default version 1.0.0
      UI.message("Error determining app version: #{e.message}. Using initial version 1.0.0")
      return ["1.0.0", nil]
    end
  end

  def update_version_and_build_number(new_version, new_build_number)
    UI.message("Using version: #{new_version} with build number: #{new_build_number}")

    increment_version_number_in_plist(
      version_number: new_version,
      target: 'Runner',
      xcodeproj: './Runner.xcodeproj'
    )

    increment_build_number_in_plist(
      build_number: new_build_number.to_s,
      target: 'Runner',
      xcodeproj: './Runner.xcodeproj'
    )
  end

  def validate_and_fix_deployment_target
    UI.message("🔍 Validating iOS deployment target format...")

    podfile_path = "./Podfile"
    if File.exist?(podfile_path)
      content = File.read(podfile_path)

      # Check for invalid 3-digit version format (e.g., 15.5.0)
      if content.match?(/platform :ios, ['"](\d+\.\d+\.\d+)['"]/)
        invalid_version = content.match(/platform :ios, ['"](\d+\.\d+\.\d+)['"]/)[1]
        valid_version = invalid_version.split('.')[0..1].join('.')

        UI.important("⚠️  Found invalid iOS version format: #{invalid_version}")
        UI.message("Fixing to: #{valid_version}")

        # Fix platform line
        content.gsub!(/platform :ios, ['"]#{Regexp.escape(invalid_version)}['"]/,
                      "platform :ios, '#{valid_version}'")

        # Fix post_install deployment target
        content.gsub!(/IPHONEOS_DEPLOYMENT_TARGET['"] = ['"]#{Regexp.escape(invalid_version)}['"]/,
                      "IPHONEOS_DEPLOYMENT_TARGET' = '#{valid_version}'")

        File.write(podfile_path, content)
        UI.success("✅ Fixed Podfile deployment target")

        # Reinstall pods with corrected version
        UI.message("Running pod install to apply changes...")
        sh "pod install"
      else
        UI.success("✅ Deployment target format is valid")
      end
    end
  end

  def setup_keychain(app_identifier)
    keychain_name = app_identifier
    keychain_password = "123456"
    keychain_path = File.expand_path("~/Library/Keychains/#{keychain_name}-db")

    unless File.exist?(keychain_path)
      create_keychain(
        name: keychain_name,
        password: keychain_password,
        default_keychain: false,
        unlock: true,
        timeout: 0,
        lock_when_sleeps: false,
        add_to_search_list: true
      )
    end

    sh "security unlock-keychain -p #{keychain_password} #{keychain_path}"
    return [keychain_name, keychain_password]
  end

  def cleanup_certificate_branch(branch_name)
    # WARNING: This contains hardcoded credentials - should use ENV variables
    repo_url = ENV['GIT_CERTIFICATES_URL'] || "https://eamon831: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@github.com/eamon831/certificates.git"

    sh <<-SHELL
      REPO_URL="#{repo_url}"
      if git ls-remote --heads $REPO_URL #{branch_name} | grep -q "refs/heads/#{branch_name}"; then
          echo "#{branch_name} branch exists. Deleting..."
          git clone $REPO_URL
          cd certificates
          git push origin --delete #{branch_name}
          cd ..
          rm -rf certificates
      else
         echo "#{branch_name} branch does not exist. Skipping deletion."
      fi
    SHELL
  end

  def setup_code_signing(api_key, options, keychain_name, keychain_password)
    branch_name = options[:app_identifier]

    match(
      type: "appstore",
      api_key: api_key,
      git_url: "https://github.com/eamon831/certificates.git",
      app_identifier: options[:app_identifier],
      force: true,
      keychain_name: "#{keychain_name}-db",
      keychain_password: keychain_password,
      git_branch: branch_name
    )

    update_code_signing_settings(
      use_automatic_signing: false,
      path: "Runner.xcodeproj",
      team_id: options[:team_id],
      bundle_identifier: options[:app_identifier],
      code_sign_identity: "iPhone Distribution",
      sdk: "iphoneos*",
      profile_name: "match AppStore #{options[:app_identifier]}"
    )
  end

  def build_app(app_identifier)
    gym(
      workspace: "Runner.xcworkspace",
      scheme: "Runner",
      export_method: "app-store",
      export_options: {
        provisioningProfiles: {
          app_identifier => "match AppStore #{app_identifier}"
        }
      },
      derived_data_path: "build",
      buildlog_path: "build",
    )
  end

  def upload_build(api_key, team_id, app_identifier, app_state, depth = 0)
    max_retries = 3

    # Prevent potential infinite recursion
    if depth >= max_retries
      UI.error("Maximum retry depth (#{max_retries}) reached. Please manually increment your build number and try again.")
      return false
    end

    begin
      current_build = get_build_number_from_plist(xcodeproj: 'Runner.xcodeproj')
      UI.message("Starting app upload with build number: #{current_build}")

      # States where app is approved/released - can ONLY upload to TestFlight
      testflight_only_states = [
        "READY_FOR_SALE",              # Live on App Store
        "PENDING_DEVELOPER_RELEASE",   # Approved, waiting for release
        "READY_FOR_DISTRIBUTION"       # Approved, ready to distribute
      ]

      if testflight_only_states.include?(app_state)
        # Use TestFlight upload because we cannot create a new App Store version
        UI.important("🚀 App is in '#{app_state}' state. Uploading to TestFlight only...")

        # Provide more context based on specific state
        case app_state
        when "READY_FOR_SALE"
          UI.message("📱 App is currently LIVE on the App Store")
        when "PENDING_DEVELOPER_RELEASE"
          UI.message("⏳ App is approved and waiting for you to release it")
        when "READY_FOR_DISTRIBUTION"
          UI.message("✅ App is approved and ready for manual distribution")
        end

        upload_to_testflight(
          api_key: api_key,
          ipa: "Runner.ipa",
          team_id: team_id,
          app_identifier: app_identifier,
          skip_waiting_for_build_processing: true,
          distribute_external: false
        )

        UI.success("📱 App successfully uploaded to TestFlight")
        UI.important("ℹ️  Note: This build was uploaded to TestFlight only.")

        if app_state == "READY_FOR_DISTRIBUTION" || app_state == "PENDING_DEVELOPER_RELEASE"
          UI.important("ℹ️  To submit to App Store, you must first release the pending version.")
        end
      else
        # For other states (IN_REVIEW, WAITING_FOR_REVIEW, PREPARE_FOR_SUBMISSION,
        # REJECTED, DEVELOPER_REMOVED_FROM_SALE, etc.), use normal App Store upload
        UI.message("📤 Uploading to App Store Connect...")
        UI.message("Current state '#{app_state || 'NEW'}' allows creating/updating App Store version")

        deliver(
          api_key: api_key,
          ipa: "Runner.ipa",
          team_id: team_id,
          app_identifier: app_identifier,
          precheck_include_in_app_purchases: false,
          submit_for_review: false,
          automatic_release: false,
          skip_metadata: true,
          skip_screenshots: true,
          skip_binary_upload: false
        )

        UI.success("📱 App successfully uploaded to App Store Connect")
      end

      return true
    rescue FastlaneCore::Interface::FastlaneError => e
      UI.error("Upload error: #{e.message}")

      if e.message.include?("already been used") ||
         e.message.include?("must be higher") ||
         e.message.include?("higher than the previously uploaded version") ||
         e.message.include?("value that has already been used") ||
         e.message.include?("already exists")

        # Get current build number
        current_build = get_build_number_from_plist(xcodeproj: 'Runner.xcodeproj').to_i

        # Calculate new build number with exponential backoff strategy
        increment_amount = 5 * (2 ** depth)  # 5, 10, 20 for retries 0,1,2
        new_build = current_build + increment_amount

        UI.important("Build number conflict detected (attempt #{depth+1}/#{max_retries}).")
        UI.important("Incrementing build number from #{current_build} to #{new_build} and rebuilding...")

        # Update build number
        increment_build_number_in_plist(build_number: new_build.to_s)

        # Rebuild the app with new build number
        begin
          build_app(app_identifier)
        rescue => build_error
          UI.error("Failed to rebuild app: #{build_error.message}")
          return false
        end

        # Try uploading again with incremented depth parameter to track recursion
        UI.message("Retrying upload with new build number: #{new_build}")
        return upload_build(api_key, team_id, app_identifier, app_state, depth + 1)
      else
        UI.error("Non-conflict error occurred. Upload failed: #{e.message}")
        return false
      end
    rescue => general_error
      UI.error("Unexpected error during upload: #{general_error.message}")
      UI.error(general_error.backtrace.join("\n"))
      return false
    end
  end
end