require 'fileutils'

require 'cerberus/utils'
require 'cerberus/constants'
require 'cerberus/config'
require 'cerberus/status'
require 'cerberus/latch'
require 'cerberus/component_lazy_loader'

module Cerberus
  class AddCommand
    EXAMPLE_CONFIG = File.expand_path(File.dirname(__FILE__) + '/config.example.yml')

    def initialize(path, cli_options = {})
      @path, @cli_options = path, HashWithIndifferentAccess.new(cli_options)
    end

    def run
      scm_type = @cli_options[:scm] || Cerberus::SCM.guess_type(@path) || 'svn'
      scm = Cerberus::SCM.get(scm_type).new(@path, Config.new(nil, @cli_options))
      say "Can't find any #{scm_type} application under #{@path}" unless scm.url

      application_name = @cli_options[:application_name] || extract_project_name(@path)

      create_example_config

      config_name = "#{HOME}/config/#{application_name}.yml"
      say "Application #{application_name} already present in Cerberus" if File.exist?(config_name)

      app_config = {'scm' => {
        'url' => scm.url,
        'type' => scm_type,
      },
                    'publisher' => {'mail' => {'recipients' => @cli_options[:recipients]}}}
      app_config['builder'] = {'type' => @cli_options[:builder]} if @cli_options[:builder]
      dump_yml(config_name, app_config)
      puts "Application '#{application_name}' has been added to Cerberus successfully" unless @cli_options[:quiet]
    end

    private

    def extract_project_name(path)
      path = File.expand_path(path) if test(?d, path)
      File.basename(path).strip.gsub(/\.git$/, '')
    end

    def create_example_config
      FileUtils.mkpath(HOME) unless test(?d, HOME)
      FileUtils.cp(EXAMPLE_CONFIG, CONFIG_FILE) unless test(?f, CONFIG_FILE)
    end
  end

  class RemoveCommand
    # DRY: this is ugly and needs refactoring. It duplicates functionality from other
    # classes a lot.

    def initialize(application_name, cli_options = {})
      unless File.exist?("#{HOME}/config/#{application_name}.yml")
        say "Project '#{application_name}' does not exist in Cerberus. Type 'cerberus list' to see the list of all active projects."
      end
      @app_root = "#{HOME}/work/#{application_name}"

      def_options = {:application_root => @app_root, :application_name => application_name}
      @config = Config.new(application_name, cli_options.merge(def_options))
    end

    def run
      application_name = @config[:application_name]

      config_name = "#{HOME}/config/#{application_name}.yml"

      if not File.exist?(config_name)
        say "Unknown application #{application_name}"
        exit(1)
      end
      FileUtils.rm_rf @config[:application_root]
      File.unlink config_name
      puts "Application '#{application_name}' removed." unless @config[:quiet]
    end
  end

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

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

    def initialize(application_name, cli_options = {})
      unless File.exist?("#{HOME}/config/#{application_name}.yml")
        say "Project '#{application_name}' does not exist in Cerberus. Type 'cerberus list' to see the list of all active projects."
      end

      app_root = "#{HOME}/work/#{application_name}"

      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 = Config.new(application_name, cli_options.merge(def_options))
      @config.merge!(DEFAULT_CONFIG, false)

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

      scm_type = @config[:scm, :type]
      @scm = SCM.get(scm_type).new(@config[:application_root], @config)
      say "Client for SCM '#{scm_type}' does not installed" unless @scm.installed?

      builder_type = get_configuration_option(@config[:builder], :type, :rake)
      @builder = Builder.get(builder_type).new(@config)
    end

    def run
      begin
        Latch.lock("#{HOME}/work/#{@config[:application_name]}/.lock", :lock_ttl => @config[:max_wait_time]) do
          @scm.update!
          if (@config[:force] || @scm.has_changes? || @status.previous_build_successful.nil? || (@status.previous_build_failed? && !@config[:require_revision_change]))
            Dir.chdir File.join(@config[:application_root], @config[:build_dir] || '')
            @setup_script_output = `#{@config[:setup_script]}` if @config[:setup_script]

            build_successful = @builder.run
            @status.keep(build_successful, @scm.current_revision, @builder.brokeness)

            #Save logs to directory
            if @config[:log, :enable]
              log_dir = "#{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, builder.output].join("\n\n")
              IO.write(file_name, body)
            end

            #send notifications
            active_publishers = get_configuration_option(@config[:publisher], :active, 'mail')
            active_publishers.split(/\W+/).each do |pub|
              publisher_config = @config[:publisher, pub]
              raise "Publisher have no configuration: #{pub}" unless publisher_config

              events = interpret_state(publisher_config[:on_event] || @config[:publisher, :on_event] || 'default')
              Publisher.get(pub, publisher_config).publish(@status, self, @config) if events.include?(@status.current_state)
            end

            #Process hooks
            hooks = @config[:hook]
            hooks.each_pair { |name, hook|
              events = interpret_state(hook[:on_event] || 'all', false)
              if events.include?(@status.current_state)
                `#{hook[:action]}`
              end
            } if hooks
          end
        end #lock
      rescue Exception => e
        if ENV['CERBERUS_ENV'] == 'TEST'
          raise e
        else
          File.open("#{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
        end
      end
    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

    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

  class BuildAllCommand
    def initialize(cli_options = {})
      @cli_options = cli_options
    end

    def run
      threads = []
      Dir["#{HOME}/config/*.yml"].each do |fn|
        fn =~ %r{#{HOME}/config/(.*).yml}
        application_name = $1

        command = Cerberus::BuildCommand.new(application_name, @cli_options)
        threads << Thread.new { command.run } if command.run_time?(Time.now)
      end

      @already_waited = false
      threads.each do |t|
        if @already_waited or not t.join(LOCK_WAIT)
          t.kill
          @already_waited = true
        end
      end
    end
  end

  class ListCommand
    def initialize(cli_options = {})
    end

    def run
      projects = Dir["#{HOME}/config/*.yml"]
      if projects.empty?
        puts "There are no active projects"
      else
        puts "List of active projects:"

        projects.sort.each do |fn|
          fn =~ %r{#{HOME}/config/(.*).yml}

          puts "  * #{$1}"
        end

        puts "\nType 'cerberus build PROJECT_NAME' to build any of these projects"
      end
    end
  end

  class StatusCommand
    def initialize(cli_options = {})
    end

    def run
      projects = Dir["#{HOME}/config/*.yml"].sort.map { |fn| fn.gsub(/.*\/(.*).yml$/, '\1') }
      if projects.empty?
        puts "There are not any active projects"
      else
        delim = ' | '
        cols = [
          ['Project Name', 30, lambda { |p, s| p }],
          ['Revision', 10, lambda { |p, s| "#{s.revision.to_s.slice(0, 8)}" }],
          ['Status', 10, lambda { |p, s| s.previous_build_successful ? 'Pass' : 'Fail' }],
          ['Last Success', 10, lambda { |p, s| "#{s.successful_build_revision.to_s.slice(0, 8)}" }],
        ]
        header = cols.map { |head, size, lamb| head.ljust(size) }.join(delim)
        puts '-' * header.length
        puts header
        puts '-' * header.length
        projects.each do |proj|
          status = Status.read("#{HOME}/work/#{proj}/status.log")
          row = cols.map { |head, size, lamb| lamb.call(proj, status).to_s.ljust(size) }.join(delim)
          puts status.previous_build_successful ? ansi_green(row) : ansi_red(row)
        end
        puts '-' * header.length
      end
    end

    def ansi_green(str)
      ansi_escape('32m', str)
    end

    def ansi_red(str)
      ansi_escape('31m', str)
    end

    def ansi_escape(code, str)
      "\033[#{code}" + str + "\033[0m"
    end
  end
end
