# this file gets loaded in the CLI context, not the Rails boot context

require "thor"
require "bard/git"
require "bard/ci"
require "bard/copy"
require "bard/github"
require "bard/ping"
require "bard/config"
require "bard/command"
require "term/ansicolor"
require "open3"
require "uri"

module Bard
  class CLI < Thor
    include Term::ANSIColor

    class_option :verbose, type: :boolean, aliases: :v

    desc "data --from=production --to=local", "copy database and assets from from to to"
    option :from, default: "production"
    option :to, default: "local"
    def data
      from = config[options[:from]]
      to = config[options[:to]]

      if to.key == :production
        url = to.ping.first
        puts yellow "WARNING: You are about to push data to production, overwriting everything that is there!"
        answer = ask("If you really want to do this, please type in the full HTTPS url of the production server:")
        if answer != url
          puts red("!!! ") + "Failed! We expected #{url}. Is this really where you want to overwrite all the data?"
          exit 1
        end
      end

      puts "Dumping #{from.key} database to file..."
      from.run! "bin/rake db:dump"

      puts "Transfering file from #{from.key} to #{to.key}..."
      from.copy_file "db/data.sql.gz", to: to, verbose: true

      puts "Loading file into #{to.key} database..."
      to.run! "bin/rake db:load"

      config.data.each do |path|
        puts "Synchronizing files in #{path}..."
        from.copy_dir path, to: to, verbose: true
      end
    end

    desc "stage [branch=HEAD]", "pushes current branch, and stages it"
    def stage branch=Git.current_branch
      unless config.servers.key?(:production)
        raise Thor::Error.new("`bard stage` is disabled until a production server is defined. Until then, please use `bard deploy` to deploy to the staging server.")
      end

      run! "git push -u origin #{branch}", verbose: true
      config[:staging].run! "git fetch && git checkout -f origin/#{branch} && bin/setup"
      puts green("Stage Succeeded")

      ping :staging
    end

    option :"skip-ci", type: :boolean
    option :"local-ci", type: :boolean
    desc "deploy [TO=production]", "checks that current branch is a ff with master, checks with ci, merges into master, deploys to target, and then deletes branch."
    def deploy to=:production
      branch = Git.current_branch

      if branch == "master"
        if !Git.up_to_date_with_remote?(branch)
          run! "git push origin #{branch}:#{branch}"
        end
        invoke :ci, [branch], options.slice("local-ci") unless options["skip-ci"]

      else
        run! "git fetch origin master:master"

        unless Git.fast_forward_merge?("origin/master", branch)
          puts "The master branch has advanced. Attempting rebase..."
          run! "git rebase origin/master"
        end

        run! "git push -f origin #{branch}:#{branch}"

        invoke :ci, [branch], options.slice("local-ci") unless options["skip-ci"]

        run! "git push origin #{branch}:master"
        run! "git fetch origin master:master"
      end

      if `git remote` =~ /\bgithub\b/
        run! "git push github"
      end

      config[to].run! "git pull origin master && bin/setup"

      puts green("Deploy Succeeded")

      if branch != "master"
        puts "Deleting branch: #{branch}"
        run! "git push --delete origin #{branch}"

        if branch == Git.current_branch
          run! "git checkout master"
        end

        run! "git branch -D #{branch}"
      end

      ping to
    end

    option :"local-ci", type: :boolean
    option :status, type: :boolean
    desc "ci [branch=HEAD]", "runs ci against BRANCH"
    def ci branch=Git.current_branch
      ci = CI.new(project_name, branch, local: options["local-ci"])
      if ci.exists?
        return puts ci.status if options["status"]

        puts "Continuous integration: starting build on #{branch}..."

        success = ci.run do |elapsed_time, last_time|
          if last_time
            percentage = (elapsed_time.to_f / last_time.to_f * 100).to_i
            output = "  Estimated completion: #{percentage}%"
          else
            output = "  No estimated completion time. Elapsed time: #{elapsed_time} sec"
          end
          print "\x08" * output.length
          print output
          $stdout.flush
        end

        if success
          puts
          puts "Continuous integration: success!"
          puts "Deploying..."
        else
          puts
          puts ci.last_response
          puts ci.console
          puts red("Automated tests failed!")
          exit 1
        end

      else
        puts red("No CI found for #{project_name}!")
        puts "Re-run with --skip-ci to bypass CI, if you absolutely must, and know what you're doing."
        exit 1
      end
    end

    desc "open [server=production]", "opens the url in the web browser."
    def open server=:production
      exec "xdg-open #{config[server].ping.first}"
    end

    desc "hurt <command>", "reruns a command until it fails"
    def hurt *args
      1.upto(Float::INFINITY) do |count|
        puts "Running attempt #{count}"
        system *args
        unless $?.success?
          puts "Ran #{count-1} times before failing"
          break
        end
      end
    end

    option :home, type: :boolean
    desc "ssh [to=production]", "logs into the specified server via SSH"
    def ssh to=:production
      config[to].exec! "exec $SHELL -l", home: options[:home]
    end

    desc "install", "copies bin/setup and bin/ci scripts into current project."
    def install
      install_files_path = File.expand_path(File.join(__dir__, "../../install_files/*"))
      system "cp -R #{install_files_path} bin/"
      github_files_path = File.expand_path(File.join(__dir__, "../../install_files/.github"))
      system "cp -R #{github_files_path} ./"
    end

    desc "setup", "installs app in nginx"
    def setup
      path = "/etc/nginx/sites-available/#{project_name}"
      dest_path = path.sub("sites-available", "sites-enabled")
      server_name = "#{project_name}.localhost"

      system "sudo tee #{path} >/dev/null <<-EOF
server {
  listen 80;
  server_name #{server_name};

  root #{Dir.pwd}/public;
  passenger_enabled on;

  location ~* \\.(ico|css|js|gif|jp?g|png|webp) {
    access_log off;
    if (\\$request_filename ~ \"-[0-9a-f]{32}\\.\") {
      expires max;
      add_header Cache-Control public;
    }
  }
  gzip_static on;
}
EOF"
      system "sudo ln -sf #{path} #{dest_path}" if !File.exist?(dest_path)
      system "sudo service nginx restart"
    end

    desc "ping [server=production]", "hits the server over http to verify that its up."
    def ping server=:production
      server = config[server]
      down_urls = Bard::Ping.call(config[server])
      down_urls.each { |url| puts "#{url} is down!" }
      exit 1 if down_urls.any?
    end

    option :on, default: "production"
    desc "command <command> --on=production", "run the given command on the remote server"
    def command command
      server = config[options[:on]]
      server.run! remote_command, verbose: true
    end

    desc "master_key --from=production --to=local", "copy master key from from to to"
    option :from, default: "production"
    option :to, default: "local"
    def master_key
      from = config[options[:from]]
      to = config[options[:to]]
      from.copy_file "config/master.key", to:
    end

    desc "vim [branch=master]", "open all files that have changed since master"
    def vim branch="master"
      exec "vim -p `git diff #{branch} --name-only | grep -v sass$ | tac`"
    end

    def self.exit_on_failure? = true

    private

    def config
      @config ||= Bard::Config.new(project_name, path: "bard.rb")
    end

    def project_name
      @project_name ||= File.expand_path(".").split("/").last
    end

    def run!(...)
      Bard::Command.run!(...)
    end
  end
end