require 'fastlane_core/core_ext/cfpropertylist'
require 'fastlane_core/project'
require_relative 'module'
require_relative 'code_signing_mapping'

module Gym
  # This class detects all kinds of default values
  class DetectValues
    # This is needed as these are more complex default values
    # Returns the finished config object
    def self.set_additional_default_values
      config = Gym.config

      # First, try loading the Gymfile from the current directory
      config.load_configuration_file(Gym.gymfile_name)

      # Detect the project
      FastlaneCore::Project.detect_projects(config)
      Gym.project = FastlaneCore::Project.new(config)

      # Go into the project's folder, as there might be a Gymfile there
      project_path = File.expand_path("..", Gym.project.path)
      unless File.expand_path(".") == project_path
        Dir.chdir(project_path) do
          config.load_configuration_file(Gym.gymfile_name)
        end
      end

      ensure_export_options_is_hash

      detect_scheme
      detect_platform # we can only do that *after* we have the scheme
      detect_selected_provisioning_profiles # we can only do that *after* we have the platform
      detect_configuration
      detect_toolchain
      detect_third_party_installer

      config[:output_name] ||= Gym.project.app_name

      config[:build_path] ||= archive_path_from_local_xcode_preferences

      # Make sure the output name is valid and remove a trailing `.ipa` extension
      # as it will be added by gym for free
      config[:output_name].gsub!(".ipa", "")
      config[:output_name].gsub!(File::SEPARATOR, "_")

      return config
    end

    def self.archive_path_from_local_xcode_preferences
      day = Time.now.strftime("%F") # e.g. 2015-08-07
      archive_path = File.expand_path("~/Library/Developer/Xcode/Archives/#{day}/")

      return archive_path unless has_xcode_preferences_plist?

      custom_archive_path = xcode_preferences_dictionary['IDECustomDistributionArchivesLocation']
      return archive_path if custom_archive_path.to_s.length == 0

      return File.join(custom_archive_path, day)
    end

    # Helper Methods

    # this file only exists when you edit the Xcode preferences to set custom values
    def self.has_xcode_preferences_plist?
      File.exist?(xcode_preference_plist_path)
    end

    def self.xcode_preference_plist_path
      File.expand_path("~/Library/Preferences/com.apple.dt.Xcode.plist")
    end

    def self.xcode_preferences_dictionary(path = xcode_preference_plist_path)
      CFPropertyList.native_types(CFPropertyList::List.new(file: path).value)
    end

    # Since Xcode 9 you need to provide the explicit mapping of what provisioning profile to use for
    # each target of your app
    def self.detect_selected_provisioning_profiles
      Gym.config[:export_options] ||= {}
      hash_to_use = (Gym.config[:export_options][:provisioningProfiles] || {}).dup || {} # dup so we can show the original values in `verbose` mode

      unless Gym.config[:skip_profile_detection]
        mapping_object = CodeSigningMapping.new(project: Gym.project)
        hash_to_use = mapping_object.merge_profile_mapping(primary_mapping: hash_to_use,
                                                           export_method: Gym.config[:export_method])
      end

      return if hash_to_use.count == 0 # We don't want to set a mapping if we don't have one
      Gym.config[:export_options][:provisioningProfiles] = hash_to_use
      UI.message("Detected provisioning profile mapping: #{hash_to_use}")
    rescue => ex
      # We don't want to fail the build if the automatic detection doesn't work
      # especially since the mapping is optional for pre Xcode 9 setups
      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

    # Detects name of a "3rd Party Mac Developer Installer" cert for the configured team id
    def self.detect_third_party_installer
      return if Gym.config[:installer_cert_name]

      team_id = Gym.config[:export_team_id] || Gym.project.build_settings(key: "DEVELOPMENT_TEAM")
      return if team_id.nil?

      case Gym.config[:export_method]
      when "app-store"
        prefix = "3rd Party Mac Developer Installer: "
      when "developer-id"
        prefix = "Developer ID Installer: "
      else
        return
      end

      output = Helper.backticks("security find-certificate -a -c \"#{prefix}\"", print: false)

      # Find matches, filter by team_id, prepend prefix for full cert name
      certs = output.scan(/"(?:#{prefix})(.*)"/)
      certs = certs.flatten.uniq.select do |cert|
        cert.include?(team_id)
      end.map do |cert|
        prefix + cert
      end

      if certs.first
        UI.verbose("Detected installer certificate to use: #{certs.first}")
        Gym.config[:installer_cert_name] = certs.first
      end
    end

    def self.detect_scheme
      Gym.project.select_scheme
    end

    def self.min_xcode8?
      Helper.xcode_at_least?("8.0")
    end

    # Is it an iOS device or a Mac?
    def self.detect_platform
      return if Gym.config[:destination]

      platform = if Gym.project.tvos?
                   "tvOS"
                 elsif Gym.project.visionos?
                   "visionOS"
                 elsif Gym.building_for_ios?
                   "iOS"
                 elsif Gym.building_for_mac?
                   min_xcode8? ? "macOS" : "OS X"
                 else
                   "iOS"
                 end
      Gym.config[:destination] = "generic/platform=#{platform}"
    end

    # Detects the available configurations (e.g. Debug, Release)
    def self.detect_configuration
      config = Gym.config
      configurations = Gym.project.configurations
      return if configurations.count == 0 # this is an optional value anyway

      if config[:configuration]
        # Verify the configuration is available
        unless configurations.include?(config[:configuration])
          UI.error("Couldn't find specified configuration '#{config[:configuration]}'.")
          config[:configuration] = nil
        end
      end
    end

    # The toolchain parameter is used if you don't use the default toolchain of Xcode (e.g. Swift 2.3 with Xcode 8)
    def self.detect_toolchain
      return unless Gym.config[:toolchain]

      # Convert the aliases to the full string to make it easier for the user #justfastlanethings
      if Gym.config[:toolchain].to_s == "swift_2_3"
        Gym.config[:toolchain] = "com.apple.dt.toolchain.Swift_2_3"
      end
    end

    def self.ensure_export_options_is_hash
      return if Gym.config[:export_options].nil? || Gym.config[:export_options].kind_of?(Hash)

      # Reads options from file
      plist_file_path = Gym.config[:export_options]
      UI.user_error!("Couldn't find plist file at path #{File.expand_path(plist_file_path)}") unless File.exist?(plist_file_path)
      hash = Plist.parse_xml(plist_file_path)
      UI.user_error!("Couldn't read provided plist at path #{File.expand_path(plist_file_path)}") if hash.nil?
      # Convert keys to symbols
      Gym.config[:export_options] = keys_to_symbols(hash)
    end

    def self.keys_to_symbols(hash)
      # Convert keys to symbols
      hash = hash.each_with_object({}) do |(k, v), memo|
        memo[k.b.to_s.to_sym] = v
        memo
      end
      hash
    end
  end
end
