# frozen_string_literal: true

#
# Copyright (C) 2022 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.

module HealthChecks
  extend ApplicationHelper

  class << self
    def process_readiness_checks(is_deep_check)
      readiness_checks
        .transform_values { |v| component_check(v, is_deep_check) }
    end

    def process_deep_checks
      critical = critical_checks
                 .transform_values { |v| execute_deep_check(v) }
                 .transform_values { |v| component_check(v, true) }

      secondary = secondary_checks
                  .transform_values { |v| execute_deep_check(v) }
                  .transform_values { |v| component_check(v, true) }

      { critical:, secondary: }
    end

    def send_to_statsd(result = nil, additional_tags = {})
      result ||= process_deep_checks.merge({ readiness: process_readiness_checks(true) })

      result.each do |check_type, check_values|
        check_values.each do |check_name, check_results|
          tags = { type: check_type, key: check_name, **additional_tags }

          InstStatsd::Statsd.timing("canvas.health_checks.response_time_ms", check_results[:time], tags:)
          InstStatsd::Statsd.gauge("canvas.health_checks.status", check_results[:status] ? 1 : 0, tags:)
        end
      end
    end

    private

    def readiness_checks
      {
        # ensures brandable_css_bundles_with_deps exists, returns a string (path), treated as truthy
        common_css: -> { !!css_url_for("common") },
        # ensures webpack worked; returns a string, treated as truthy
        common_js: -> { Canvas::Cdn.registry.scripts_available? },
        # returns a PrefixProxy instance, treated as truthy
        consul: -> { DynamicSettings.find(tree: :private)[:readiness, failsafe: nil].nil? },
        # returns the value of the block <integer>, treated as truthy
        filesystem: lambda do
          !!Tempfile.open("readiness", ENV["TMPDIR"] || Dir.tmpdir) { |f| f.write("readiness") }
        end,
        # returns a boolean
        jobs: lambda do
                Delayed::Job.connection.verify!
                true
              end,
        # returns a boolean
        postgresql: lambda do
                      Account.connection.verify!
                      GuardRail.activate(:secondary) { Account.connection.verify! }
                      true
                    end,
        # nil response treated as truthy
        ha_cache: -> { MultiCache.cache.fetch("readiness").nil? },
        # ensures `gulp rev` has ran; returns a string, treated as truthy
        rev_manifest: -> { Canvas::Cdn.registry.statics_available? },
        # ensures we retrieved something back from Vault; returns a boolean
        vault: lambda do
          path = "#{Canvas::Vault.kv_mount}/data/secrets"

          # First try to check if it's cached without reaching out to vault because
          # Canvas can operate without vault if the secrets are cached
          # If it's not cached, we'll reach out to vault and cache the response
          Canvas::Vault.cached?(path) || !Canvas::Vault.read(path).nil?
        end
      }
    end

    def critical_checks
      ret = {
        default_shard: lambda do
                         Shard.connection.verify!
                         true
                       end
      }

      if InstFS.enabled?
        ret[:inst_fs] = lambda do
          CanvasHttp
            .get(URI.join(InstFS.app_host, "/readiness").to_s)
            .is_a?(Net::HTTPSuccess)
        end
      end

      if Canvas.redis_enabled?
        ret[:redis] = lambda do
          nodes = Canvas.redis.try(:ring)&.nodes || Array.wrap(Canvas.redis)
          nodes.all? { |node| node.get("deep_check").nil? }
        end
      end

      if Services::RichContent.send(:service_settings)[:RICH_CONTENT_APP_HOST]
        ret[:rich_content_service] = lambda do
          CanvasHttp
            .get(
              URI::HTTPS.build(
                host: Services::RichContent.send(:service_settings)[:RICH_CONTENT_APP_HOST],
                path: "/readiness"
              ).to_s
            ).is_a?(Net::HTTPSuccess)
        end
      end

      if MathMan.use_for_svg?
        ret[:mathman] = lambda do
          CanvasHttp
            .get(MathMan.url_for(latex: "x", target: :svg))
            .is_a?(Net::HTTPSuccess)
        end
      end

      if LiveEvents::Client.config
        ret[:live_events] = lambda do
          !LiveEvents.send(:client).stream_client.put_records(
            records: [
              {
                data: {
                  attributes: {
                    event_name: "noop",
                    event_time: Time.now.utc.iso8601(3)
                  },
                  body: {}
                }.to_json,
                partition_key: rand(1000).to_s
              }
            ],
            stream_name: LiveEvents.send(:client).stream_name
          ).nil?
        end
      end

      if DynamicSettings.config.present?
        ret[:consul] = lambda do
          DynamicSettings.find(tree: :private)["health_check", ttl: 0.1]
          true
        end
      end

      ret
    end

    def secondary_checks
      ret = {}
      if PageView.pv4?
        ret[:pv4] = lambda do
          CanvasHttp
            .get(URI.join(ConfigFile.load("pv4")["uri"], "/health_check").to_s)
            .is_a?(Net::HTTPSuccess)
        end
      end

      if Canvadocs.enabled?
        ret[:canvadocs] = lambda do
          CanvasHttp
            .get(URI.join(Canvadocs.config["base_url"], "/readiness").to_s)
            .is_a?(Net::HTTPSuccess)
        end
      end

      if CutyCapt.enabled? && CutyCapt.screencap_service
        ret[:screencap] = lambda do
          Tempfile.create("example.png", encoding: "ascii-8bit") do |f|
            CutyCapt.screencap_service.snapshot_url_to_file("about:blank", f)
          end
        end
      end

      if Account.site_admin.feature_enabled?(:notification_service)
        ret[:notification_queue] = lambda do
          !Services::NotificationService.process(Account.site_admin.global_id, nil, "noop", "nobody").nil?
        end
      end

      if ReleaseNote.enabled?
        ret[:release_notes] = lambda do
          !ReleaseNote.ddb_client.update_item(
            table_name: ReleaseNote.ddb_table_name,
            key: { "PartitionKey" => "healthcheck",
                   "RangeKey" => "canvas" }
          ).nil?
        end
      end

      if IncomingMailProcessor::IncomingMessageProcessor.run_periodically?
        ret[:incoming_mail] = lambda do
          IncomingMailProcessor::IncomingMessageProcessor.healthy?
        end
      end

      ret
    end

    def execute_deep_check(proc)
      Thread.new do
        Thread.current.report_on_exception = false
        proc.call
      end
    end

    def component_check(component, is_deep_check)
      status = false
      message = "service is up"
      exception_type = is_deep_check ? :deep_health_check : :readiness_health_check
      timeout = Setting.get("healthcheck_timelimit", 5.seconds.to_s).to_f
      response_time_ms =
        Benchmark.ms do
          Timeout.timeout(timeout, Timeout::Error) do
            status = component.is_a?(Thread) ? component.value : component.call
          end
        rescue Timeout::Error => e
          message = e.message
          Canvas::Errors.capture_exception(exception_type, e.message, :warn)
        rescue => e
          message = e.message
          Canvas::Errors.capture_exception(exception_type, e, :error)
        end

      { status:, message:, time: response_time_ms }
    end
  end
end
