#!/usr/bin/env ruby
require 'rake'
require 'yaml'
require 'set'
require 'pp'

$LOAD_PATH.unshift(File.dirname(__FILE__))

class JobManager
  ALL = "all"
  NATS = "nats_server"
  ROUTER = "router"
  STAGER = "stager"
  CC = "cloud_controller"
  CCDB = "ccdb"
  CF = "cloudfoundry"
  HM = "health_manager"
  DEA = "dea"
  UAA = "uaa"
  UAADB = "uaadb"
  ACM = "acm"
  ACMDB = "acmdb"
  SERVICES_REDIS= "services_redis"

  SERVICE_LIFECYCLE = ["serialization_data_server"]

  SERVICES = ["redis", "mysql", "mongodb", "neo4j", "rabbitmq", "postgresql", "vblob", "memcached", "elasticsearch", "couchdb", "echo"]
  SERVICES_LIFECYCLE_ENABLED = ["redis", "mysql", "mongodb", "postgresql"]
  SERVICES_AUXILIARY = ["service_broker"]
  SERVICES_NODE = SERVICES.map do |service|
     "#{service}_node"
  end
  SERVICES_GATEWAY = SERVICES.map do |service|
    "#{service}_gateway"
  end
  SERVICES_WORKER = SERVICES_LIFECYCLE_ENABLED.map do |service|
    "#{service}_worker"
  end
  SERVICES_GATEWAY << "filesystem_gateway"
  SERVICES_NODE.each do |node|
    # Service name constant e.g. REDIS_NODE -> "redis_node"
    const_set(node.upcase, node)
  end

  SERVICE_TOOLS = ["backup_manager", "snapshot_manager"]

  # All supported jobs
  JOBS = [ALL, NATS, ROUTER, STAGER, CF, CC, HM, DEA, CCDB, UAA, UAADB, SERVICES_REDIS] + SERVICE_LIFECYCLE + SERVICES_NODE + SERVICES_GATEWAY + SERVICES_AUXILIARY + SERVICE_TOOLS + SERVICES_WORKER
  SYSTEM_JOB = [CF]

  # List of the required properties for jobs
  INSTALLED_JOB_PROPERTIES = {NATS => ["host"], CC => ["service_api_uri"],
                              CCDB => ["host"]}

  INSTALL_JOB_PROPERTIES = {CC => ["builtin_services"], MYSQL_NODE => ["index"], MONGODB_NODE => ["index"], REDIS_NODE => ["index"], NEO4J_NODE => ["index"], POSTGRESQL_NODE => ["index"], RABBITMQ_NODE => ["index"], VBLOB_NODE => ["index"], MEMCACHED_NODE => ["index"], ELASTICSEARCH_NODE => ["index"], COUCHDB_NODE => ["index"], ECHO_NODE => ["index"]}

  # Dependency between JOBS and  components that are consumed by "vcap_dev" when cf is started or
  # stopped
  SERVICE_LIFECYCLE_RUN_COMPONENTS = Hash.new
  SERVICE_LIFECYCLE.each do |lifecycle|
    SERVICE_LIFECYCLE_RUN_COMPONENTS[lifecycle] = lifecycle
  end

  SERVICE_NODE_RUN_COMPONENTS = Hash.new
  SERVICES_NODE.each do |node|
    SERVICE_NODE_RUN_COMPONENTS[node] = node
  end

  SERVICE_GATEWAY_RUN_COMPONENTS = Hash.new
  SERVICES_GATEWAY.each do |gateway|
    SERVICE_GATEWAY_RUN_COMPONENTS[gateway] = gateway
  end

  SERVICE_WORKER_RUN_COMPONENTS = Hash.new
  SERVICES_WORKER.each do |worker|
    SERVICE_WORKER_RUN_COMPONENTS[worker] = worker
  end

  SERVICE_AUXILIARY_RUN_COMPONENTS = Hash.new
  SERVICES_AUXILIARY.each do |service|
    SERVICE_AUXILIARY_RUN_COMPONENTS[service] = service
  end

  SERVICE_TOOL_RUN_COMPONENTS = Hash.new
  SERVICE_TOOLS.each do |tool|
    SERVICE_TOOL_RUN_COMPONENTS[tool] = tool
  end

  RUN_COMPONENTS = {ROUTER => ROUTER, STAGER => STAGER, CC => CC, HM => HM, DEA => DEA, UAA => UAA, SERVICES_REDIS => SERVICES_REDIS}.update(SERVICE_LIFECYCLE_RUN_COMPONENTS).update(SERVICE_NODE_RUN_COMPONENTS).update(SERVICE_GATEWAY_RUN_COMPONENTS).update(SERVICE_AUXILIARY_RUN_COMPONENTS).update(SERVICE_TOOL_RUN_COMPONENTS).update(SERVICE_WORKER_RUN_COMPONENTS)

  class << self
    if defined?(Rake::DSL)
      include Rake::DSL
    end
    # Take user input and create a Hash that has the job name as the key and its
    # properties as value.
    #
    # Allows the user to specify jobs as either
    #   jobs:
    #     install:
    #       - redis
    #       - mysql
    #   OR
    #   jobs:
    #     install:
    #       redis:
    #       mysql:
    #
    def sanitize_jobs(type)
      return nil if @config["jobs"][type].nil?

      jobs = {}
      config_jobs = @config["jobs"][type]
      config_jobs.each do |element|
        case element
        when String
          jobs[element] = nil
        when Hash
          if element.length > 1
           puts "Bad input, #{element.pretty_inspect} should have only one key, please fix your yaml file."
           exit 1
          end
          element.each do |job, properties|
            jobs[job] = properties.nil? ? nil : properties.dup
          end
        else
          puts "Unsupported type for Installed or Install job #{element}"
          exit 1
        end
      end

      # validate jobs
      given_jobs = Set.new(jobs.keys)
      if (intersect = @valid_jobs.intersection(given_jobs)) != given_jobs
        puts "Input Error: Please provide valid #{type} jobs, following jobs are not recognized\n#{(given_jobs - intersect).pretty_inspect}"
        exit 1
      end

      jobs
    end

    def detect_duplicate_jobs
      if !@config["jobs"]["install"].nil? && !@config["jobs"]["installed"].nil?
        install_jobs = Set.new(@config["jobs"]["install"].keys)
        installed_jobs = Set.new(@config["jobs"]["installed"].keys)
        common = install_jobs.intersection(installed_jobs)
        unless common.empty?
          puts "Input error, The following jobs are specified in both the install and installed list.\n#{common.pretty_inspect}"
          exit 1
        end
      end
    end

    def validate_properties(jobs, required_properties)
      return if jobs.nil?

      missing_keys = {}
      jobs.each do |job, properties|
        # Check if this job needs properties
        next if required_properties[job].nil?

        expected = Set.new(required_properties[job])
        given = properties.nil? ? Set.new : Set.new(properties.keys)

        # Check if all the required properties are given
        if !expected.subset?(given)
          missing_keys[job] ||= []
          missing_keys[job] << (expected - given).to_a
        end
      end

      if !missing_keys.empty?
        puts "Input Error: The following mandatory job properties are missing #{missing_keys.pretty_inspect}"
        exit 1
      end
    end

    # Gets called by rake tasks for each job.
    # The possibilities for each job are as follows.
    # 1. It is already installed
    # 2. It is in the install list
    # 3. It is not on the install list or the installed list
    #
    # Case 1, propogate the properties of the installed job to the spec
    # so dependent jobs can use these properties.
    # Case 2, add the required chef role to the chef run list, add default/given
    # properties to the spec
    # Case 3, is a dependecy failure.
    def install(job)
      unless @all_install
        if !@config["jobs"]["installed"].nil? && !@config["jobs"]["installed"][job].nil?
          @spec[job] = @config["jobs"]["installed"][job].dup
          return
        end

        unless @config["jobs"]["install"].has_key?(job) || SYSTEM_JOB.include?(job)
          puts "Dependecy check error: job #{job} is needed by one of the jobs in the install list, please add job #{job} to the install or installed list"
          exit 1
        end

        if !@config["jobs"]["install"][job].nil?
          @spec[job] = @config["jobs"]["install"][job].dup
        end
      end

      # Prepare the run list for this job
      if RUN_COMPONENTS.has_key?(job)
        case RUN_COMPONENTS[job]
        when String
          @run_list << RUN_COMPONENTS[job]
        when Array
          RUN_COMPONENTS[job].each do |component|
            @run_list << component
          end
        end
      end

      if job == "snapshot_manager"
        @spec["service_lifecycle"] = { :enable => true }
      end

      @roles << job
    end

    def process_jobs
      # Default to all jobs
      if @config["jobs"].nil?
        # Install all jobs
        @all_install = true
        Rake.application[ALL].invoke
        return
      end

      # Make sure that the "install" and "installed" jobs specified are valid
      @config["jobs"]["install"] = sanitize_jobs("install")
      @config["jobs"]["installed"] = sanitize_jobs("installed")

      if @config["jobs"]["install"].nil?
        puts "You have not selected any jobs to install."
        exit 0
      end

      # Make sure that the "install" and "installed" jobs do not intersect
      detect_duplicate_jobs

      if @config["jobs"]["install"].include?("all")
        # Install all jobs
        if !@config["jobs"]["installed"].nil?
          puts "Please correct your config file. You are trying to install all jobs, but you have also specified an 'installed' section"
          exit 1
        end

        if @config["jobs"]["install"].length != 1
          puts "Please correct your config file. You are trying to install all jobs, remove all other jobs from the 'install' list"
          exit 1
        end

        @all_install = true
        Rake.application[ALL].invoke
        return
      end

      # Sanity check the given properties
      validate_properties(@config["jobs"]["installed"], INSTALLED_JOB_PROPERTIES)
      validate_properties(@config["jobs"]["install"], INSTALL_JOB_PROPERTIES)

      # Let the install rake task do the dependency management
      @config["jobs"]["install"].keys.each do |job|
        Rake.application[job].invoke
      end
    end

    def go(config)
      @spec = {}
      @roles = []
      @run_list = Set.new
      @valid_jobs = Set.new(JOBS)
      @config = config.dup
      @all_install = false

      # Load the job dependecies
      Rake.application.rake_require("job_dependency")

      process_jobs

      # All dependencies are resolved, return the job property spec, chef
      # roles and the vcap run list
      return @spec, @roles, @run_list.to_a
    end
  end
end
