require 'fastlane_core/command_executor'

require_relative '../module'
require_relative './interface'

module Match
  module Storage
    # Store the code signing identities in a git repo
    class GitStorage < Interface
      # User provided values
      attr_accessor :git_url
      attr_accessor :shallow_clone
      attr_accessor :skip_docs
      attr_accessor :branch
      attr_accessor :git_full_name
      attr_accessor :git_user_email
      attr_accessor :clone_branch_directly
      attr_accessor :type
      attr_accessor :platform
      attr_accessor :git_basic_authorization
      attr_accessor :git_bearer_authorization
      attr_accessor :git_private_key

      def self.configure(params)
        return self.new(
          type: params[:type].to_s,
          platform: params[:platform].to_s,
          git_url: params[:git_url],
          shallow_clone: params[:shallow_clone],
          skip_docs: params[:skip_docs],
          branch: params[:git_branch],
          git_full_name: params[:git_full_name],
          git_user_email: params[:git_user_email],
          clone_branch_directly: params[:clone_branch_directly],
          git_basic_authorization: params[:git_basic_authorization],
          git_bearer_authorization: params[:git_bearer_authorization],
          git_private_key: params[:git_private_key]
        )
      end

      def initialize(type: nil,
                     platform: nil,
                     git_url: nil,
                     shallow_clone: nil,
                     skip_docs: false,
                     branch: "master",
                     git_full_name: nil,
                     git_user_email: nil,
                     clone_branch_directly: false,
                     git_basic_authorization: nil,
                     git_bearer_authorization: nil,
                     git_private_key: nil)
        self.git_url = git_url
        self.shallow_clone = shallow_clone
        self.skip_docs = skip_docs
        self.branch = branch
        self.git_full_name = git_full_name
        self.git_user_email = git_user_email
        self.clone_branch_directly = clone_branch_directly
        self.git_basic_authorization = git_basic_authorization
        self.git_bearer_authorization = git_bearer_authorization
        self.git_private_key = convert_private_key_path_to_absolute(git_private_key)

        self.type = type if type
        self.platform = platform if platform
      end

      def convert_private_key_path_to_absolute(git_private_key)
        if !git_private_key.nil? && File.file?(File.expand_path(git_private_key))
          File.expand_path(git_private_key).shellescape.to_s
        else
          git_private_key
        end
      end

      def prefixed_working_directory
        return working_directory
      end

      def download
        # Check if we already have a functional working_directory
        return if @working_directory

        # No existing working directory, creating a new one now
        self.working_directory = Dir.mktmpdir

        command = "git clone #{self.git_url.shellescape} #{self.working_directory.shellescape}"
        # HTTP headers are supposed to be case-insensitive but
        # Bitbucket requires `Authorization: Basic` and `Authorization Bearer` to work
        # https://github.com/fastlane/fastlane/pull/15928
        command << " -c http.extraheader='Authorization: Basic #{self.git_basic_authorization}'" unless self.git_basic_authorization.nil?
        command << " -c http.extraheader='Authorization: Bearer #{self.git_bearer_authorization}'" unless self.git_bearer_authorization.nil?

        if self.shallow_clone
          command << " --depth 1"
        end

        if self.clone_branch_directly
          command += " -b #{self.branch.shellescape} --single-branch"
        elsif self.shallow_clone
          # shallow clone all branches if not cloning branch directly
          command += " --no-single-branch"
        end

        command = command_from_private_key(command) unless self.git_private_key.nil?

        UI.message("Cloning remote git repo...")
        if self.branch && !self.clone_branch_directly
          UI.message("If cloning the repo takes too long, you can use the `clone_branch_directly` option in match.")
        end

        begin
          # GIT_TERMINAL_PROMPT will fail the `git clone` command if user credentials are missing
          Helper.with_env_values('GIT_TERMINAL_PROMPT' => '0') do
            FastlaneCore::CommandExecutor.execute(command: command,
                                                print_all: FastlaneCore::Globals.verbose?,
                                            print_command: FastlaneCore::Globals.verbose?)
          end
        rescue
          UI.error("Error cloning certificates repo, please make sure you have read access to the repository you want to use")
          if self.branch && self.clone_branch_directly
            UI.error("You passed '#{self.branch}' as branch in combination with the `clone_branch_directly` flag. Please remove `clone_branch_directly` flag on the first run for _match_ to create the branch.")
          end
          UI.error("Run the following command manually to make sure you're properly authenticated:")
          UI.command(command)
          UI.user_error!("Error cloning certificates git repo, please make sure you have access to the repository - see instructions above")
        end

        add_user_config(self.git_full_name, self.git_user_email)

        unless File.directory?(self.working_directory)
          UI.user_error!("Error cloning repo, make sure you have access to it '#{self.git_url}'")
        end

        checkout_branch
      end

      def human_readable_description
        "Git Repo [#{self.git_url}]"
      end

      def delete_files(files_to_delete: [], custom_message: nil)
        if files_to_delete.count > 0
          commands = files_to_delete.map { |filename|  "git --literal-pathspecs rm -- #{filename.shellescape}" }
          git_push(commands: commands, commit_message: custom_message)
        end
      end

      def upload_files(files_to_upload: [], custom_message: nil)
        commands = files_to_upload.map do |current_file|
          "git add #{current_file.shellescape}"
        end

        git_push(commands: commands, commit_message: custom_message)
      end

      # Generate the commit message based on the user's parameters
      def generate_commit_message
        [
          "[fastlane]",
          "Updated",
          self.type,
          "and platform",
          self.platform
        ].join(" ")
      end

      def generate_matchfile_content
        UI.important("Please create a new, private git repository to store the certificates and profiles there")
        url = UI.input("URL of the Git Repo: ")

        return "git_url(\"#{url}\")"
      end

      def list_files(file_name: "", file_ext: "")
        Dir[File.join(working_directory, "**", file_name, "*.#{file_ext}")]
      end

      def command_from_private_key(command)
        if File.file?(self.git_private_key)
          ssh_add = File.expand_path(self.git_private_key).shellescape.to_s
        else
          UI.message("Private key file does not exist, will continue by using it as a raw key.")
          ssh_add = "- <<< \"#{self.git_private_key}\""
        end
        return "ssh-agent bash -c 'ssh-add #{ssh_add}; #{command}'"
      end

      private

      # Create and checkout an specific branch in the git repo
      def checkout_branch
        return unless self.working_directory

        commands = []
        if branch_exists?(self.branch)
          # Checkout the branch if it already exists
          commands << "git checkout #{self.branch.shellescape}"
        else
          # If a new branch is being created, we create it as an 'orphan' to not inherit changes from the master branch.
          commands << "git checkout --orphan #{self.branch.shellescape}"
          # We also need to reset the working directory to not transfer any uncommitted changes to the new branch.
          commands << "git reset --hard"
        end

        UI.message("Checking out branch #{self.branch}...")

        Dir.chdir(self.working_directory) do
          commands.each do |command|
            FastlaneCore::CommandExecutor.execute(command: command,
                                                  print_all: FastlaneCore::Globals.verbose?,
                                                  print_command: FastlaneCore::Globals.verbose?)
          end
        end
      end

      # Checks if a specific branch exists in the git repo
      def branch_exists?(branch)
        return unless self.working_directory

        result = Dir.chdir(self.working_directory) do
          FastlaneCore::CommandExecutor.execute(command: "git --no-pager branch --list origin/#{branch.shellescape} --no-color -r",
                                                print_all: FastlaneCore::Globals.verbose?,
                                                print_command: FastlaneCore::Globals.verbose?)
        end
        return !result.empty?
      end

      def add_user_config(user_name, user_email)
        # Add git config if needed
        commands = []
        commands << "git config user.name \"#{user_name}\"" unless user_name.nil?
        commands << "git config user.email \"#{user_email}\"" unless user_email.nil?

        return if commands.empty?

        UI.message("Add git user config to local git repo...")
        Dir.chdir(self.working_directory) do
          commands.each do |command|
            FastlaneCore::CommandExecutor.execute(command: command,
                                                  print_all: FastlaneCore::Globals.verbose?,
                                                  print_command: FastlaneCore::Globals.verbose?)
          end
        end
      end

      def git_push(commands: [], commit_message: nil)
        commit_message ||= generate_commit_message
        commands << "git commit -m #{commit_message.shellescape}"
        git_push_command = "git push origin #{self.branch.shellescape}"
        git_push_command = command_from_private_key(git_push_command) unless self.git_private_key.nil?
        commands << git_push_command

        UI.message("Pushing changes to remote git repo...")
        Helper.with_env_values('GIT_TERMINAL_PROMPT' => '0') do
          commands.each do |command|
            FastlaneCore::CommandExecutor.execute(command: command,
                                                print_all: FastlaneCore::Globals.verbose?,
                                            print_command: FastlaneCore::Globals.verbose?)
          end
        end
      rescue => ex
        UI.error("Couldn't commit or push changes back to git...")
        UI.error(ex)
      end
    end
  end
end
