#!/usr/bin/env ruby

require 'sinatra/base'

require 'flapjack/data/entity'
require 'flapjack/data/entity_check'

require 'flapjack/gateways/jsonapi/entity_presenter'
require 'flapjack/gateways/jsonapi/entity_check_presenter'

module Flapjack

  module Gateways

    class JSONAPI < Sinatra::Base

      module EntityMethods

        module Helpers

          def find_entity(entity_name)
            entity = Flapjack::Data::Entity.find_by_name(entity_name, :redis => redis)
            raise Flapjack::Gateways::JSONAPI::EntityNotFound.new(entity_name) if entity.nil?
            entity
          end

          def find_entity_check(entity, check)
            entity_check = Flapjack::Data::EntityCheck.for_entity(entity,
              check, :redis => redis)
            raise Flapjack::Gateways::JSONAPI::EntityCheckNotFound.new(entity, check) if entity_check.nil?
            entity_check
          end

          def find_tags(tags)
            halt err(403, "no tags") if tags.nil? || tags.empty?
            tags
          end

          def entities_and_checks(entity_name, check)
            if entity_name
              # backwards-compatible, single entity or entity&check from route
              entities = check ? nil : [entity_name]
              checks   = check ? {entity_name => check} : nil
            else
              # new and improved bulk API queries
              entities = params[:entity]
              checks   = params[:check]
              entities = [entities] unless entities.nil? || entities.is_a?(Array)
              # TODO err if checks isn't a Hash (similar rules as in flapjack-diner)
            end
            [entities, checks]
          end

          def bulk_api_check_action(entities, entity_checks, action, params = {})
            unless entities.nil? || entities.empty?
              entities.each do |entity_name|
                entity = find_entity(entity_name)
                checks = entity.check_list.sort
                checks.each do |check|
                  action.call( find_entity_check(entity, check) )
                end
              end
            end

            unless entity_checks.nil? || entity_checks.empty?
              entity_checks.each_pair do |entity_name, checks|
                entity = find_entity(entity_name)
                checks = [checks] unless checks.is_a?(Array)
                checks.each do |check|
                  action.call( find_entity_check(entity, check) )
                end
              end
            end
          end

          def present_api_results(entities, entity_checks, result_type, &block)
            result = []

            unless entities.nil? || entities.empty?
              result += entities.collect {|entity_name|
                entity = find_entity(entity_name)
                yield(Flapjack::Gateways::JSONAPI::EntityPresenter.new(entity, :redis => redis))
              }.flatten(1)
            end

            unless entity_checks.nil? || entity_checks.empty?
              result += entity_checks.inject([]) {|memo, (entity_name, checks)|
                checks = [checks] unless checks.is_a?(Array)
                entity = find_entity(entity_name)
                memo += checks.collect {|check|
                  entity_check = find_entity_check(entity, check)
                  {:entity => entity_name,
                   :check => check,
                   result_type.to_sym => yield(Flapjack::Gateways::JSONAPI::EntityCheckPresenter.new(entity_check))
                  }
                }
              }.flatten(1)
            end

            result
          end

          # NB: casts to UTC before converting to a timestamp
          def validate_and_parsetime(value)
            return unless value
            Time.iso8601(value).getutc.to_i
          rescue ArgumentError => e
            logger.error "Couldn't parse time from '#{value}'"
            nil
          end

        end

        # used for backwards-compatible route matching below
        ENTITY_CHECK_FRAGMENT = '(?:/([a-zA-Z0-9][a-zA-Z0-9\.\-]*[a-zA-Z0-9])(?:/(.+))?)?'

        def self.registered(app)

          app.helpers Flapjack::Gateways::JSONAPI::EntityMethods::Helpers

          app.get '/entities' do
            content_type :json
            cors_headers

            entities_json = Flapjack::Data::Entity.all(:redis => redis).collect {|e|
              presenter = Flapjack::Gateways::JSONAPI::EntityPresenter.new(e, :redis => redis)
              id = (e.id.respond_to?(:length) && e.id.length > 0) ? e.id : e.name
              {'id' => id, 'name' => e.name, 'checks' => presenter.status }.to_json
            }.join(',')

            '{"entities":[' + entities_json + ']}'
          end


          app.get %r{/status#{ENTITY_CHECK_FRAGMENT}} do
            content_type :json

            captures    = params[:captures] || []
            entity_name = captures[0]
            check       = captures[1]

            entities, checks = entities_and_checks(entity_name, check)

            results = present_api_results(entities, checks, 'status') {|presenter|
              presenter.status
            }

            if entity_name
              # compatible with previous data format
              results = results.collect {|status_h| status_h[:status]}
              check ? results.first.to_json : "[" + results.map {|r| r.to_json }.join(',') + "]"
            else
              # new and improved data format which reflects the request param structure
              "[" + results.map {|r| r.to_json }.join(',') + "]"
            end
          end

          app.get %r{/((?:outages|(?:un)?scheduled_maintenances|downtime))#{ENTITY_CHECK_FRAGMENT}} do
            action      = params[:captures][0].to_sym
            entity_name = params[:captures][1]
            check       = params[:captures][2]

            entities, checks = entities_and_checks(entity_name, check)

            start_time = validate_and_parsetime(params[:start_time])
            end_time   = validate_and_parsetime(params[:end_time])

            results = present_api_results(entities, checks, action) {|presenter|
              presenter.send(action, start_time, end_time)
            }

            if check
              # compatible with previous data format
              results.first[action].to_json
            elsif entity_name
              # compatible with previous data format
              rename = {:unscheduled_maintenances => :unscheduled_maintenance,
                        :scheduled_maintenances   => :scheduled_maintenance}
              drop   = [:entity]
              results.collect{|r|
                r.inject({}) {|memo, (k, v)|
                  if new_k = rename[k]
                    memo[new_k] = v
                  elsif !drop.include?(k)
                    memo[k] = v
                  end
                 memo
                }
              }.to_json
            else
              # new and improved data format which reflects the request param structure
              results.to_json
            end
          end

          # create a scheduled maintenance period for a check on an entity
          app.post %r{/scheduled_maintenances#{ENTITY_CHECK_FRAGMENT}} do

            captures    = params[:captures] || []
            entity_name = captures[0]
            check       = captures[1]

            entities, checks = entities_and_checks(entity_name, check)

            start_time = validate_and_parsetime(params[:start_time])
            halt( err(403, "start time must be provided") ) unless start_time

            act_proc = proc {|entity_check|
              entity_check.create_scheduled_maintenance(start_time,
                params[:duration].to_i, :summary => params[:summary])
            }

            bulk_api_check_action(entities, checks, act_proc)
            status 204
          end

          # create an acknowledgement for a service on an entity
          # NB currently, this does not acknowledge a specific failure event, just
          # the entity-check as a whole
          app.post %r{/acknowledgements#{ENTITY_CHECK_FRAGMENT}} do
            captures    = params[:captures] || []
            entity_name = captures[0]
            check       = captures[1]

            entities, checks = entities_and_checks(entity_name, check)

            dur = params[:duration] ? params[:duration].to_i : nil
            duration = (dur.nil? || (dur <= 0)) ? (4 * 60 * 60) : dur
            summary = params[:summary]

            opts = {'duration' => duration}
            opts['summary'] = summary if summary

            act_proc = proc {|entity_check|
              Flapjack::Data::Event.create_acknowledgement(
                entity_check.entity_name, entity_check.check,
                :summary => params[:summary],
                :duration => duration,
                :redis => redis)
            }

            bulk_api_check_action(entities, checks, act_proc)
            status 204
          end

          app.delete %r{/((?:un)?scheduled_maintenances)} do
            action = params[:captures][0]

            # no backwards-compatible mode here, it's a new method
            entities, checks = entities_and_checks(nil, nil)

            act_proc = case action
            when 'scheduled_maintenances'
              start_time = validate_and_parsetime(params[:start_time])
              halt( err(403, "start time must be provided") ) unless start_time
              opts = {}
              proc {|entity_check| entity_check.end_scheduled_maintenance(start_time.to_i) }
            when 'unscheduled_maintenances'
              end_time = validate_and_parsetime(params[:end_time]) || Time.now
              proc {|entity_check| entity_check.end_unscheduled_maintenance(end_time.to_i) }
            end

            bulk_api_check_action(entities, checks, act_proc)
            status 204
          end

          app.post %r{/test_notifications#{ENTITY_CHECK_FRAGMENT}} do
            captures    = params[:captures] || []
            entity_name = captures[0]
            check       = captures[1]

            entities, checks = entities_and_checks(entity_name, check)

            act_proc = proc {|entity_check|
              summary = params[:summary] ||
                        "Testing notifications to all contacts interested in entity #{entity_check.entity.name}"
              Flapjack::Data::Event.test_notifications(
                entity_check.entity_name, entity_check.check,
                :summary => summary,
                :redis => redis)
            }

            bulk_api_check_action(entities, checks, act_proc)
            status 204
          end

          app.post '/entities' do
            pass unless Flapjack::Gateways::JSONAPI::JSON_REQUEST_MIME_TYPES.include?(request.content_type)

            errors = []
            ret = nil

            # FIXME should scan for invalid records before making any changes, fail early

            entities = params[:entities]
            unless entities
              logger.debug("no entities object found in the following supplied JSON:")
              logger.debug(request.body)
              return err(403, "No entities object received")
            end
            return err(403, "The received entities object is not an Enumerable") unless entities.is_a?(Enumerable)
            return err(403, "Entity with a nil id detected") unless entities.any? {|e| !e['id'].nil?}

            entities.each do |entity|
              unless entity['id']
                errors << "Entity not imported as it has no id: #{entity.inspect}"
                next
              end
              Flapjack::Data::Entity.add(entity, :redis => redis)
            end
            errors.empty? ? 204 : err(403, *errors)
          end

        end

      end

    end

  end

end