require 'xcodeproj'

require_relative 'module'

module Gym
  class CodeSigningMapping
    attr_accessor :project

    def initialize(project: nil)
      self.project = project
    end

    # @param primary_mapping [Hash] The preferred mapping (e.g. whatever the user provided)
    # @param secondary_mapping [Hash] (optional) The secondary mapping (e.g. whatever is detected from the Xcode project)
    # @param export_method [String] The method that should be preferred in case there is a conflict
    def merge_profile_mapping(primary_mapping: nil, secondary_mapping: nil, export_method: nil)
      final_mapping = (primary_mapping || {}).dup # for verbose output at the end of the method
      secondary_mapping ||= self.detect_project_profile_mapping # default to Xcode project

      final_mapping = final_mapping.transform_keys(&:to_sym)
      secondary_mapping = secondary_mapping.transform_keys(&:to_sym)

      # Now it's time to merge the (potentially) existing mapping
      #   (e.g. coming from `provisioningProfiles` of the `export_options` or from previous match calls)
      # with the secondary hash we just created (or was provided as parameter).
      # Both might include information about what profile to use
      # This is important as it might not be clear for the user that they have to call match for each app target
      # before adding this code, we'd only either use whatever we get from match, or what's defined in the Xcode project
      # With the code below, we'll make sure to take the best of it:
      #
      #   1) A provisioning profile is defined in the `primary_mapping`
      #   2) A provisioning profile is defined in the `secondary_mapping`
      #   3) On a conflict (app identifier assigned both in xcode and match)
      #     3.1) we'll choose whatever matches what's defined as the `export_method`
      #     3.2) If both include the right `export_method`, we'll prefer the one from `primary_mapping`
      #     3.3) If none has the right export_method, we'll use whatever is defined in the Xcode project
      #
      # To get a better sense of this, check out code_signing_spec.rb for some test cases

      secondary_mapping.each do |bundle_identifier, provisioning_profile|
        if final_mapping[bundle_identifier].nil?
          final_mapping[bundle_identifier] = provisioning_profile
        else
          if self.app_identifier_contains?(final_mapping[bundle_identifier], export_method) # 3.1 + 3.2 nothing to do in this case
          elsif self.app_identifier_contains?(provisioning_profile, export_method)
            # Also 3.1 (3.1 is "implemented" twice, as it could be either the primary, or the secondary being the one that matches)
            final_mapping[bundle_identifier] = provisioning_profile
          else
            # 3.3
            final_mapping[bundle_identifier] = provisioning_profile
          end
        end
      end

      UI.verbose("Merging provisioning profile mappings")
      UI.verbose("-------------------------------------")
      UI.verbose("Primary provisioning profile mapping:")
      UI.verbose(primary_mapping)
      UI.verbose("Secondary provisioning profile mapping:")
      UI.verbose(secondary_mapping)
      UI.verbose("Resulting in the following mapping:")
      UI.verbose(final_mapping)

      return final_mapping
    end

    # Helper method to remove "-" and " " and downcase app identifier
    # and compare if an app identifier includes a certain string
    # We do some `gsub`bing, because we can't really know the profile type, so we'll just look at the name and see if it includes
    # the export method (which it usually does, but with different notations)
    def app_identifier_contains?(str, contains)
      return str.to_s.gsub("-", "").gsub(" ", "").gsub("InHouse", "enterprise").downcase.include?(contains.to_s.gsub("-", "").gsub(" ", "").downcase)
    end

    def test_target?(build_settings)
      return (!build_settings["TEST_TARGET_NAME"].nil? || !build_settings["TEST_HOST"].nil?)
    end

    def same_platform?(sdkroot)
      destination = Gym.config[:destination].dup
      destination.slice!("generic/platform=")
      destination_sdkroot = []
      case destination
      when "macosx"
        destination_sdkroot = ["macosx"]
      when "iOS"
        destination_sdkroot = ["iphoneos", "watchos"]
      when "tvOS"
        destination_sdkroot = ["appletvos"]
      end

      # Catalyst projects will always have an "iphoneos" sdkroot
      # Need to force a same platform when trying to build as macos
      if Gym.building_mac_catalyst_for_mac?
        return true
      end

      return destination_sdkroot.include?(sdkroot)
    end

    def detect_configuration_for_archive
      extract_from_scheme = lambda do
        if self.project.workspace?
          available_schemes = self.project.workspace.schemes.reject { |k, v| v.include?("Pods/Pods.xcodeproj") }
          project_path = available_schemes[Gym.config[:scheme]]
        else
          project_path = self.project.path
        end

        if project_path
          scheme_path = File.join(project_path, "xcshareddata", "xcschemes", "#{Gym.config[:scheme]}.xcscheme")
          Xcodeproj::XCScheme.new(scheme_path).archive_action.build_configuration if File.exist?(scheme_path)
        end
      end

      configuration = Gym.config[:configuration]
      configuration ||= extract_from_scheme.call if Gym.config[:scheme]
      configuration ||= self.project.default_build_settings(key: "CONFIGURATION")
      return configuration
    end

    def detect_project_profile_mapping
      provisioning_profile_mapping = {}
      specified_configuration = detect_configuration_for_archive

      self.project.project_paths.each do |project_path|
        UI.verbose("Parsing project file '#{project_path}' to find selected provisioning profiles")
        UI.verbose("Finding provision profiles for '#{specified_configuration}'") if specified_configuration

        begin
          # Storing bundle identifiers with duplicate profiles
          # for informing user later on
          bundle_identifiers_with_duplicates = []

          project = Xcodeproj::Project.open(project_path)
          project.targets.each do |target|
            target.build_configuration_list.build_configurations.each do |build_configuration|
              current = build_configuration.build_settings
              next if test_target?(current)
              sdkroot = build_configuration.resolve_build_setting("SDKROOT", target)
              next unless same_platform?(sdkroot)
              next unless specified_configuration == build_configuration.name

              # Catalyst apps will have some build settings that will have a configuration
              # that is specific for macos so going to do our best to capture those
              #
              # There are other platform filters besides "[sdk=macosx*]" that we could use but
              # this is the default that Xcode will use so this will also be our default
              sdk_specifier = Gym.building_mac_catalyst_for_mac? ? "[sdk=macosx*]" : ""

              # Look for sdk specific bundle identifier (if set) and fallback to general configuration if none
              bundle_identifier = build_configuration.resolve_build_setting("PRODUCT_BUNDLE_IDENTIFIER#{sdk_specifier}", target)
              bundle_identifier ||= build_configuration.resolve_build_setting("PRODUCT_BUNDLE_IDENTIFIER", target)
              next unless bundle_identifier

              # Xcode prefixes "maccatalyst." if building a Catalyst app for mac and
              # if DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER is set to YES
              if Gym.building_mac_catalyst_for_mac? && build_configuration.resolve_build_setting("DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER", target) == "YES"
                bundle_identifier = "maccatalyst.#{bundle_identifier}"
              end

              # Look for sdk specific provisioning profile specifier (if set) and fallback to general configuration if none
              provisioning_profile_specifier = build_configuration.resolve_build_setting("PROVISIONING_PROFILE_SPECIFIER#{sdk_specifier}", target)
              provisioning_profile_specifier ||= build_configuration.resolve_build_setting("PROVISIONING_PROFILE_SPECIFIER", target)

              # Look for sdk specific provisioning profile uuid (if set) and fallback to general configuration if none
              provisioning_profile_uuid = build_configuration.resolve_build_setting("PROVISIONING_PROFILE#{sdk_specifier}", target)
              provisioning_profile_uuid ||= build_configuration.resolve_build_setting("PROVISIONING_PROFILE", target)

              has_profile_specifier = provisioning_profile_specifier.to_s.length > 0
              has_profile_uuid = provisioning_profile_uuid.to_s.length > 0

              # Stores bundle identifiers that have already been mapped to inform user
              if provisioning_profile_mapping[bundle_identifier] && (has_profile_specifier || has_profile_uuid)
                bundle_identifiers_with_duplicates << bundle_identifier
              end

              # Creates the mapping for a bundle identifier and profile specifier/uuid
              if has_profile_specifier
                provisioning_profile_mapping[bundle_identifier] = provisioning_profile_specifier
              elsif has_profile_uuid
                provisioning_profile_mapping[bundle_identifier] = provisioning_profile_uuid
              end
            end

            # Alerting user to explicitly specify a mapping if cannot be determined
            next if bundle_identifiers_with_duplicates.empty?
            UI.error("Couldn't automatically detect the provisioning profile mapping")
            UI.error("There were multiple profiles for bundle identifier(s): #{bundle_identifiers_with_duplicates.uniq.join(', ')}")
            UI.error("You need to provide an explicit mapping of what provisioning")
            UI.error("profile to use for each bundle identifier of your app")
          end
        rescue => ex
          # We catch errors here, as we might run into an exception on one included project
          # But maybe the next project actually contains the information we need
          if Helper.xcode_at_least?("9.0")
            UI.error("Couldn't automatically detect the provisioning profile mapping")
            UI.error("Since Xcode 9 you need to provide an explicit mapping of what")
            UI.error("provisioning profile to use for each target of your app")
            UI.error(ex)
            UI.verbose(ex.backtrace.join("\n"))
          end
        end
      end

      return provisioning_profile_mapping
    end
    # rubocop:enable Metrics/PerceivedComplexity
  end
end
