module BuildWise
  class BuildCommand
    attr_reader :builder, :success, :scm, :status, :setup_script_output

    DEFAULT_CONFIG = {:scm => {:type => 'svn'},
                      :log => {:enable => true},
                      :at_time => '* *',
    }

    attr_reader :build

    def initialize(application_name, cli_options = {})


      begin
        current_build_project_id_file = "#{BUILDWISE_HOME}/work/current_build_project_id"
        
        fio = File.open(current_build_project_id_file, "w")
        fio.write(application_name)
        fio.flush
        fio.close
      rescue => e
      end
      
      config_xml_file = "#{BUILDWISE_HOME}/config/#{application_name}.xml"
      unless File.exist?(config_xml_file)
        say "Project '#{application_name}' does not exist in BuildWise (#{config_xml_file})."
      end
      app_root = "#{BUILDWISE_HOME}/work/#{application_name}"

      @queued_build = cli_options[:build]

      def_options = {:application_root => app_root + '/sources', :application_name => application_name} #pseudo options that stored in config. Could not be set in any config file not through CLI
      @config = ::BuildWise::ProjectConfiguration.get(application_name)
      @config.application_root = app_root + "/sources"
      @config.application_name = application_name

      @config.force = cli_options[:force]




      @status = Status.read("#{app_root}/status.log")

      scm_type = @config.scm_type || "git"
      @scm = ::BuildWise::SCM.get(scm_type).new(@config.application_root, @config)
      raise "Client for SCM '#{scm_type}' does not installed" unless @scm.installed?



    end

    def run
      @notification_threads = [] # for sending notifications
      notification_step_execution  = nil
      log(:info, "Builder started: #{BUILDWISE_HOME}/work/#{@config.application_name}/.lock")
      begin
        Latch.lock("#{BUILDWISE_HOME}/work/#{@config.application_name}/.lock", :lock_ttl => 2 * LOCK_WAIT) do

          if @config.force
            if @queued_build.respond_to?("out_queue")
              @build ||= @queued_build.out_queue
              puts "#{Time.now} [INFO] get build from queue:  ##{@queued_build.id}"

            else

              puts "#{Time.now} [INFO] New Build ##{@queued_build.id}"
              @build = @queued_build
            end

            latest_build = Build.find_by_id(@build.id)
            @build.stage = "Updating from source control"
            @build.save
          end
          

          
          need_to_build = @config.force          
          build_start_time = Time.now
          
          if @build.nil? && @queued_build.class == Build
            @build = @queued_build
          end
          
          if need_to_build
            @build ||= @queued_build.out_queue  
            scm_update_step = BuildStepExecution.create(:task_name => Build::STEP_SCM_UPDATE, :build_id => @build.id, :start_time => build_start_time || Time.now)            
            update_build_stage(build, Build::STEP_SCM_UPDATE)                  
          end

          scm_output = determine_scm_changes          
          need_to_build ||= @scm.has_changes?
          
          log(:debug, "#about update scm: scm_changes => #{@scm.has_changes?}, :force =>  #{@config.force}, :last_build_ok => #{@status.previous_build_successful}|")
          
          if need_to_build

            @build ||= @queued_build.out_queue
            @build_config = @build.project.config.dup
            
            folder_name = "#{@build.id.to_s.rjust(5, '0')}-#{Time.now.strftime('%Y%m%d%H%M%S')}"
            @build.artifacts_dir = "#{BUILDWISE_HOME}/work/#{@config.application_name}/artifacts/#{folder_name}"
            FileUtils.mkdir_p(@build.artifacts_dir)
            
            check_out_dir = File.join(@config.application_root, @config.build_dir || '')
            if check_out_dir && !Dir.exist?(check_out_dir)
              FileUtils.mkdir_p(check_out_dir)
              @scm.initial_checkout              
            end
            
            Dir.chdir check_out_dir


            execute_update_source(scm_update_step, build_start_time)


            execute_prepare_command()


            archive_ui_tests(@build, @build_config.ui_tests_dir)
            
            log(:info, "Building ...")
            build_environment_variables = @config.build_environment_variables.dup
            build_environment_variables["ARTIFACT_DIR"] = ENV["ARTIFACT_DIR"] = @build.artifacts_dir
            build_environment_variables["BUILDWISE_PROJECT_IDENTIFIER"] = ENV["BUILDWISE_PROJECT_IDENTIFIER"] = @build.project.identifier            
            @build.update_column(:metadata, build_environment_variables.to_json)
            

            clear_test_reports_from_the_last_build()
            


            execute_build_steps()


            log(:info, "Parsing results and save to database ...")
            log(:debug, "About the parse results xml (#{@build.test_files.size} test files) and save to database")
            

            unless @build.is_parallel

              ui_test_report_dir = @build.determine_ui_test_report_dir
              log(:info, "Copy test reports to  #{ui_test_report_dir}")

              if !ui_test_report_dir.blank? && File.exist?(ui_test_report_dir)

                begin
                  if @build.test_syntax_framework == "Cucumber"
                    FileUtils.cp_r(ui_test_report_dir, @build.artifacts_dir)
                  else
                    FileUtils.cp_r(ui_test_report_dir, @build.artifacts_dir)
                  end
                rescue => e
                  error_msg = "[Warn] Failed to archive test reports '#{ui_test_report_dir}' to '#{@build.artifacts_dir}', #{e}, #{e.backtrace}"
                  puts "Error report : " + error_msg
                end

              end
            

              @build.analyse_ui_test_reports() # if @build.test_files.nil? || @build.test_files.empty?
              @build.test_files.reload

              log(:info, "Saved Build##{@build.id} after analyse Ok, framework: #{@build.test_syntax_framework},  #{@build.test_files.size} test files")
            end



                        

            if @config.files_to_archive
              files_or_dirs = @config.files_to_archive.split(";").collect{|x| x.strip }

              log(:info, "Archiving files: #{files_or_dirs.inspect}")
              files_or_dirs.each do |a_file|
              
                next if a_file.nil?
              
                if a_file.include?("*") # wildcard                  

                  Dir[a_file].each do |filename|                    
                    FileUtils.cp(filename, @build.artifacts_dir)
                  end
                
                else
                                
                  next unless File.exist?(a_file) 
              
                  if File.directory?(a_file)
                    FileUtils.cp_r(a_file, @build.artifacts_dir)

                    build_public_artifact_dir = File.join(BUILDWISE_DIR, "public", "artifacts", @build.id.to_s)
                    FileUtils.mkdir(build_public_artifact_dir) unless Dir.exist?(build_public_artifact_dir)
                    FileUtils.cp_r(a_file, build_public_artifact_dir)


                    begin
                      folder_base_name = File.basename(a_file)
                      the_zip_file = File.join(@build.artifacts_dir,  folder_base_name + ".tar.gz")
                      zip_command = "tar -zcvf #{the_zip_file} #{a_file}"
                      `#{zip_command}`

                    rescue => e
                      puts("Failed to zip the archive folder: #{e}")
                    end
                    
                  else

                    FileUtils.cp(a_file, @build.artifacts_dir)
                  
                  end
                  
                end

              end
            end
  
            

            notification_step_execution = send_build_notifications();















          end

        end #lock
      rescue Exception => e
        log(:warn, "Exception raised on execution: #{e}, #{e.backtrace}")

        if @build
          @build.status = "complete"
          @build.output = e
          @build.successful = false
          @build.backtrace = ([e.message]  + e.backtrace ).to_json
        end

        File.open("#{BUILDWISE_HOME}/error.log", File::WRONLY|File::APPEND|File::CREAT) do |f|
          f.puts Time.now.strftime("%a, %d %b %Y %H:%M:%S [#{@config.application_name}] --  #{e.class}")
          f.puts e.message unless e.message.empty?
          f.puts e.backtrace.collect { |line| ' '*5 + line }
          f.puts "\n"
        end

        raise e

      rescue => eee
        puts "#{Time.now} [WARN] Error => #{eee}"
      ensure
        
        log(:info, "Sending notifications ...")


        if @notification_threads && @notification_threads.any?
          @notification_threads.each do |nt|
            nt.join
          end
          if notification_step_execution
            notification_step_execution.end_time = Time.now
            notification_step_execution.save
          end
        end        
        

        this_build_log_file = File.expand_path(File.join(File.dirname(__FILE__), "../../log/builds/build_#{@build.id}.log"))
        log(:info, "Archiving the builder log for build #{@build.id}: #{this_build_log_file}")
        if File.exist?(this_build_log_file)
          log(:info, "Copy #{this_build_log_file} to #{@build.artifacts_dir}")
          FileUtils.cp(this_build_log_file, File.join(@build.artifacts_dir, "builder.log"))
        end
        log(:info, "The build #{@build.id} ends.")
        
        if @build && @build.class.name =~ /BuildQueue/
          @build.mark_as_processed

        elsif @build

          @build.finish_time = Time.now
          @build.duration = (@build.finish_time - @build.start_time) rescue nil
          @build.save

          @build.analyse_load_results # only meaning for load testing
          

          if @build.status.nil? || @build.status.empty? || @build.status == "building"
            @build.status = @scm.has_changes? ? "complete" : "unchanged"
            @build.save
          end

        end

        @build.parse_performance_results if @build.is_performance_testing

        execute_cleanup_command()

        log(:info, "The build #{@build.id} marked as complete")

      end

      
    end


    def update_build_stage(active_build, task_name)
      if active_build && task_name
        active_build.stage = task_name
        active_build.save
      end
    end
    
    def determine_scm_changes
      app_root = "#{BUILDWISE_HOME}/work/#{@config.application_name}"    
      unless File.exist?(File.join(app_root, "sources"))
        puts "No sources detected, update! shall clone it #{@scm}"

        @scm.initial_checkout
      else
        @scm.update!
      end
      return @scm.output                
    end
    

    def execute_update_source(scm_update_step, start_time = nil)      
      if scm_update_step.nil?
        execution = BuildStepExecution.create(:task_name => Build::STEP_SCM_UPDATE, :build_id => @build.id, :start_time => start_time || Time.now)
      else
        execution = scm_update_step
      end
      log_file = File.join(@build.artifacts_dir, "step_#{execution.id}.log")
      execution.update_column(:log_file, File.expand_path(log_file))
      
      begin              
        
        @scm.determine_changesets(File.join(@build.artifacts_dir, "changeset.log"))
    
        begin
          @build.last_commit_changed_files = @scm.last_commit_changed_files
        rescue => e3
          puts "#{Time.now} [Error] on get recent changed filenames in this build: #{e3}"
        end
    
        @build.increment_test_file_priority_for_recent_checkins(scm)
    
        @build.revision = @scm.current_revision(true)
        @build.last_author = @scm.last_author
        @build.last_commit_message = @scm.last_commit_message
        @build.last_commit_date = @scm.last_commit_date

        @build.label = @build.project.generate_label(@build.revision)
        @build.label ||= @build.id
        @build.save
        
        execution.successful = true 

      rescue => e_scm
        log(:warn, "Failed to update the source: #{e_scm}")
        
        log_io = File.open(log_file, "a")
        log_io.puts("execute the step: SCM failed: #{e_scm}\n#{e_scm.backtrace}")
        log_io.flush
        log_io.close
        
        execution.successful = false        
        execution.output ||= "" 
        execution.output += (e_scm + "\n" + e.backtrace) rescue ""
        raise "CheckOut Error"        

      ensure
        execution.end_time = Time.now
        execution.save        
      end      
      
    end
  
    
    

    def execute_prepare_command()
      build_prepare_command = @config.builder_prepare_command
      if build_prepare_command && !build_prepare_command.strip.empty? then
                
        builder = Builder.get("command").new(@config)        
        task_name = Build::STEP_PREPARE
        execution = BuildStepExecution.create(:task_name => task_name, :build_id => @build.id, :start_time => Time.now)
        log_file = File.join(@build.artifacts_dir, "step_#{execution.id}.log")
        execution.update_column(:log_file, File.expand_path(log_file))

        update_build_stage(build, task_name)        

        prepare_step = BuildStep.new(task_name)
        prepare_step.task = build_prepare_command
        task_successful = false
        begin
          task_successful = builder.run_task(prepare_step, log_file)
        rescue => te
          log_io = File.open(log_file, "a")
          log_io.puts("execute the step: #{task_name} failed: #{te}\n#{te.backtrace}")
          log_io.flush
          log_io.close
          
          raise te
        ensure
          execution.successful = task_successful
          execution.end_time = Time.now
          execution.safe_set_output(builder.output) if builder
          execution.save
        end
        
      end  
    end
    

    def execute_cleanup_command()
      build_cleanup_command = @config.builder_cleanup_command
      if build_cleanup_command && !build_cleanup_command.strip.empty? then
                
        builder = Builder.get("command").new(@config)        
        task_name = Build::STEP_CLEANUP
        execution = BuildStepExecution.create(:task_name => task_name, :build_id => @build.id, :start_time => Time.now)
        log_file = File.join(@build.artifacts_dir, "step_#{execution.id}.log")
        execution.update_column(:log_file, File.expand_path(log_file))

        update_build_stage(build, task_name)        

        cleanup_step = BuildStep.new(task_name)
        cleanup_step.task = build_cleanup_command
        task_successful = false
        begin
          task_successful = builder.run_task(cleanup_step, log_file)
        rescue => te
          log_io = File.open(log_file, "a")
          log_io.puts("execute the step: #{task_name} failed: #{te}\n#{te.backtrace}")
          log_io.flush
          log_io.close
          
          raise te
        ensure
          execution.successful = task_successful
          execution.end_time = Time.now
          execution.safe_set_output(builder.output) if builder
          execution.save
        end
        
      end  
    end

    




    def archive_ui_tests(the_build, ui_tests_dir)
      if ui_tests_dir.nil?
        ui_tests_dir = the_build.project.ui_test_dir        
      end
      
      ui_tests_dir = ui_tests_dir.first if ui_tests_dir.class == Array
      
      build_working_dir = the_build.project.working_dir
      ui_tests_dir = File.expand_path(File.join(build_working_dir, "sources", ui_tests_dir))
      if !Dir.exist?(ui_tests_dir)
        log(:info, "The ui_tests_dir '#{ui_tests_dir}' not exists, skip archiving.")        
        return 
      end
      
      begin
        log(:info, "Copy UI tests #{ui_tests_dir} to artifacts dir: #{the_build.artifacts_dir}.")        
        FileUtils.mkdir_p(File.join(the_build.artifacts_dir, "ui-tests"))

        require 'rake'      
        FileUtils.cp_r(FileList[ui_tests_dir + "/*"], the_build.artifacts_dir + "/ui-tests")         
      rescue => e
        log(:warn, "failed to copy acceptance test dir: #{ui_tests_dir} to #{the_build.artifacts_dir},  #{e}")
      end
        
    end
    


    def clear_test_reports_from_the_last_build
      ui_test_report_dir = @build.determine_ui_test_report_dir
      log(:info, "Clean reports from the last build at #{ui_test_report_dir}")            
      if ui_test_report_dir && Dir.exist?(ui_test_report_dir)
        result_dir = Dir.open(ui_test_report_dir)
        result_files = result_dir.entries.grep(/^*\.xml$/i)
        result_dir.close
      
        result_files.each do |result_file|

          result_full_file_path = File.join(ui_test_report_dir, result_file)
          FileUtils.rm(result_full_file_path) if File.exist?(result_full_file_path)
        end                
      end
    end
    
    def execute_build_steps()
      build_successful = true
      build_steps = build.project.build_steps
      brokeness = nil
      begin 
        build_steps.each do |step|        
          next unless step.enabled        
          task_name = step.name
          log(:info, "Build step: #{task_name}")
          


          execution = BuildStepExecution.create(:task_name => task_name, :build_id => @build.id, :start_time => Time.now, :is_ui_test => step.is_test_execution_step?)
          log_file = File.join(@build.artifacts_dir, "step_#{execution.id}.log")
          execution.update_column(:log_file, File.expand_path(log_file))
          
          update_build_stage(build, task_name)        

          builder = Builder.get(step.builder).new(@config)        
          task_successful = false
          begin
            task_successful = builder.run_task(step, log_file)
          rescue => te
            puts "#{Time.now} [ERROR] {Builder} run task error |#{te}|\n#{te.backtrace}"            
            brokeness = builder.brokeness
            raise te
          ensure
            execution.successful = task_successful
            execution.end_time = Time.now
            execution.safe_set_output(builder.output) if builder
            execution.save
          end
          
          build_successful &&= task_successful          
        end        
      rescue => e2
        build_successful = false
        build.status = "complete"
      ensure        
        puts "#{Time.now} [Build] successful? => #{build_successful} | current revision: #{@scm.current_revision}"
        @status.keep(build_successful, @scm.current_revision, brokeness)
      end

      begin
        



        if build && build.reload
          build.successful = build_successful
          build.output = @setup_script_output
          build.status = "complete"        
          build.save
        end
      
      rescue => e
        build.successful = false
        build.output = @setup_script_output
        build.status = "complete"        
        build.save
        log(:warn, "[Builder] failed to reload the build after all steps completed: #{e}")              
      end
      
      build.determine_load_build_successful  # if load testing, check success critera


      begin
        if @config.enable_log
          log_dir = "#{BUILDWISE_HOME}/work/#{@config.application_name}/logs/"
          FileUtils.mkpath(log_dir)

          time = Time.now.strftime("%Y%m%d%H%M%S")
          file_name = "#{log_dir}/#{time}-#{@status.current_state.to_s}.log"
          body = [@setup_script_output, scm.last_commit_message].join("\n\n")


          IO.write(file_name, body)
        end
      rescue => loge
        log(:warn, "[Builder] log output error: #{loge}")
      end
            
    end
    
    
    def send_build_notifications

      execution = BuildStepExecution.create(:task_name => Build::STEP_SEND_NOTIFICATIONS, :build_id => @build.id, :start_time => Time.now)
      log_file = File.join(@build.artifacts_dir, "notifications.log")
      execution.update_column(:log_file, File.expand_path(log_file))

      log_io = File.open(log_file, "a") 
      log_io.puts("Send #{@config.active_publishers.count} notifications: ")
      begin
        
        if @config.publishers      
          active_publishers = @config.active_publishers || []
          active_publishers.each do |pub|
            begin
              log_io.puts("publish to #{pub.name}")
          
              on_event = pub.on_event
              events = interpret_state(on_event || 'default')
              pub_opts = {}
              pub_opts[:application_name] = @config.application_name
              log_io.puts "[#{pub.name}] #{@status.current_state} | #{pub_opts.inspect}"
              @notification_threads << Thread.new {                    
                pub.publish(@status, self, @build, pub_opts) # if events.include?(@status.current_state)
              }
              log_io.puts("Notification sent: #{pub.name}")
            rescue => noti_e
              log_io.puts("[#{pub.name}] error: #{noti_e}")
              puts "[Builder] send notifications: #{noti_e}"
            end

          end # looping
      
        else
          log_io.puts("No notifications configured")          
        end
  
      ensure
        log_io.flush
        log_io.close
        
        execution.end_time = Time.now
        execution.save        
      end
      return  execution
    end
    
  
    def run_time?(time)
      minute, hour = @config[:at_time].split
      say "Run time is configured wrong." if minute.nil? or hour.nil?
      if hour.cron_match?(time.hour)
        return minute.cron_match?(time.min)
      end
      return false
    end

    def task_name_for_display(task_name)
      if task_name == "ui_test_task" then
        return "UI test"
      elsif task_name =~ /(.*)_task$/
        require 'active_support/core_ext'
        $1.humanize
      else
        return task_name
      end
    end
    
    

    def log(level, message)
      begin
        if (level.to_s == "info")
          logger.info(message)
        elsif (level.to_s == "warn")
          logger.warn(message)
        elsif (level.to_s == "debug")
          logger.debug(message)
        else
          logger.error(message)
        end
      rescue => e

        puts "#{Time.now} [#{level.to_s.upcase}] #{message}"
      end
    end
    
    private
    def get_configuration_option(hash, defining_key = nil, default_option = nil)
      if hash
        return hash[defining_key] if hash[defining_key]
        return hash.keys[0] if hash.size == 1
      end
      return default_option
    end
  end

end