# frozen_string_literal: true

require 'site_health/url_map'
require 'site_health/event_emitter'
require 'site_health/timer'

module SiteHealth
  # Holds page analysis data
  class Nurse
    attr_reader :config, :failures, :checkers

    # @return [Array<Issue>] found issues
    attr_reader :issues

    def initialize(config: SiteHealth.config)
      @config = config
      @checkers = config.checkers
      @pages_journal = UrlMap.new { {} }
      @failures = []
      @issues = []
      @clerk = nil
      @punched_out = false
    end

    # @return [Nurse] returns self
    def punch_out!
      post_shift_analysis unless @punched_out

      @punched_out = true
      self
    end

    # @return [Hash] check results
    def journal
      {
        checked_urls: @pages_journal.to_h,
        internal_server_error_urls: failures,
      }
    end

    # @return [Array] all URL that have failed
    def check_failed_url(url)
      clerk.emit_failed_url(url)
      @failures << url
    end

    # @return [Object] the event emitter
    # @yieldparam [Object] the event emiiter
    def clerk
      @clerk ||= begin
        events = %w[journal failed_url check page issue].concat(checkers.map(&:name))
        EventEmitter.define(*events).new.tap { |e| yield(e) if block_given? }
      end
    end

    # @return [Hash] result data
    def check_page(page)
      @pages_journal[page.url].tap do |journal|
        timer = Timer.start
        clerk.emit_page(page)

        journal[:started_at] = timer.started_at
        journal[:checked] = true
        journal[:url] = page.url
        journal[:content_type] = page.content_type
        journal[:http_status] = page.code
        journal[:redirect] = page.redirect?
        journal[:title] = page.title
        journal[:links_to] = page.each_url.map do |url|
          (@pages_journal[url][:links_from] ||= []) << page.url
          url.to_s
        end

        journal[:checks] = lab_results(page)

        timer.finish

        journal[:finished_at] = timer.finished_at
        journal[:runtime_in_seconds] = timer.diff.round(1)

        clerk.emit_journal(journal, page)
      end
    end

    # @return [Hash] results of all checkers for page
    def lab_results(page)
      journal = {}
      checkers.each do |checker_klass|
        checker = checker_klass.new(page, config: config)
        next unless checker.should_check?

        checker.call

        issues = checker.issues
        @issues.concat(issues.to_a)

        clerk.emit_check(checker)
        clerk.emit(checker.name, checker)
        clerk.emit_each_issue(issues)

        journal[checker.name.to_sym] = checker.to_h
      end
      journal
    end

    # Provides transparent access to the methods in {#clerk}.
    # @param [Symbol] name
    #   The name of the missing method.
    # @param [Array] arguments
    #   Additional arguments for the missing method.
    # @raise [NoMethodError]
    #   The missing method did not map to a method in {#clerk}.
    # @see #clerk
    def method_missing(method, *args, &block)
      if clerk.respond_to?(method)
        return clerk.public_send(method, *args, &block)
      end

      super
    end

    # @param [Symbol] name
    #   The name of the missing method.
    # @param [Boolean] include_private optional (default: false)
    #   Whether to include private methods
    # @return [Boolean]
    #   true if it can respond to method name, false otherwise
    def respond_to_missing?(method, include_private = false)
      clerk.respond_to?(method, include_private) || super
    end

    private

    def post_shift_analysis
      issues = links_to_page_not_found_issues
      clerk.emit_each_issue(issues)
      @issues.concat(issues)
    end

    def links_to_page_not_found_issues
      issues = []
      not_found = @issues.
                  select { |issue| issue.code == :not_found }.
                  map { |issue| issue.url.to_s }

      not_found.each do |url|
        (@pages_journal[url][:links_from] || []).each do |link_from_url|
          issues << build_links_to_not_found_issue(link_from_url, url)
        end
      end

      issues
    end

    def build_links_to_not_found_issue(url, not_found_url)
      Issue.new(
        name: 'links_to_page_not_found',
        code: :links_to_not_found,
        title: 'Links to page not found',
        detail: "Links to #{not_found_url} that is 404 page not found",
        severity: :major,
        priority: :high,
        url: url
      )
    end
  end
end