# typed: strict
# frozen_string_literal: true

require "cask/audit"
require "utils/output"

module Cask
  # Helper class for auditing all available languages of a cask.
  class Auditor
    include ::Utils::Output::Mixin

    # TODO: use argument forwarding (...) when Sorbet supports it in strict mode
    sig {
      params(
        cask: ::Cask::Cask, audit_download: T::Boolean, audit_online: T.nilable(T::Boolean),
        audit_strict: T.nilable(T::Boolean), audit_signing: T.nilable(T::Boolean),
        audit_new_cask: T.nilable(T::Boolean),
        any_named_args: T::Boolean, language: T.nilable(String), only: T::Array[String], except: T::Array[String]
      ).returns(T::Set[Audit::Error])
    }
    def self.audit(
      cask, audit_download: false, audit_online: nil, audit_strict: nil, audit_signing: nil,
      audit_new_cask: nil, any_named_args: false, language: nil,
      only: [], except: []
    )
      new(
        cask, audit_download:, audit_online:, audit_strict:, audit_signing:,
        audit_new_cask:, any_named_args:, language:, only:, except:
      ).audit
    end

    sig { returns(::Cask::Cask) }
    attr_reader :cask

    sig { returns(T.nilable(String)) }
    attr_reader :language

    sig {
      params(
        cask: ::Cask::Cask, audit_download: T::Boolean, audit_online: T.nilable(T::Boolean),
        audit_strict: T.nilable(T::Boolean), audit_signing: T.nilable(T::Boolean),
        audit_new_cask: T.nilable(T::Boolean),
        any_named_args: T::Boolean, language: T.nilable(String), only: T::Array[String], except: T::Array[String]
      ).void
    }
    def initialize(
      cask,
      audit_download: false,
      audit_online: nil,
      audit_strict: nil,
      audit_signing: nil,
      audit_new_cask: nil,
      any_named_args: false,
      language: nil,
      only: [],
      except: []
    )
      @cask = cask
      @audit_download = audit_download
      @audit_online = audit_online
      @audit_new_cask = audit_new_cask
      @audit_strict = audit_strict
      @audit_signing = audit_signing
      @any_named_args = any_named_args
      @language = language
      @only = only
      @except = except
    end

    LANGUAGE_BLOCK_LIMIT = 10

    sig { returns(T::Set[Audit::Error]) }
    def audit
      errors = Set.new

      if !language && !(blocks = language_blocks).empty?
        sample_languages = if blocks.length > LANGUAGE_BLOCK_LIMIT && !@audit_new_cask
          sample_keys = T.must(blocks.keys.sample(LANGUAGE_BLOCK_LIMIT))
          ohai "Auditing a sample of available languages for #{cask}: " \
               "#{sample_keys.map { |lang| lang[0].to_s }.to_sentence}"
          blocks.select { |k| sample_keys.include?(k) }
        else
          blocks
        end

        sample_languages.each_key do |l|
          audit = audit_languages(l)
          if audit.summary.present? && output_summary?(audit)
            ohai "Auditing language: #{l.map { |lang| "'#{lang}'" }.to_sentence}" if output_summary?
            puts audit.summary
          end
          errors += audit.errors
        end
      else
        audit = audit_cask_instance(cask)
        puts audit.summary if audit.summary.present? && output_summary?(audit)
        errors += audit.errors
      end

      errors
    end

    sig { params(audit: T.nilable(Audit)).returns(T::Boolean) }
    def output_summary?(audit = nil)
      return true if @any_named_args
      return true if @audit_strict
      return false if audit.nil?

      audit.errors?
    end

    private

    sig { params(languages: T::Array[String]).returns(::Cask::Audit) }
    def audit_languages(languages)
      original_config = cask.config
      begin
        localized_config = original_config.merge(Config.new(explicit: { languages: }))
        cask.config = localized_config

        audit_cask_instance(cask)
      ensure
        cask.config = original_config
      end
    end

    sig { params(cask: ::Cask::Cask).returns(::Cask::Audit) }
    def audit_cask_instance(cask)
      audit = Audit.new(
        cask,
        online:   @audit_online,
        strict:   @audit_strict,
        signing:  @audit_signing,
        new_cask: @audit_new_cask,
        download: @audit_download,
        only:     @only,
        except:   @except,
      )
      audit.run!
    end

    sig { returns(T::Hash[T::Array[String], T.proc.returns(T.untyped)]) }
    def language_blocks
      cask.instance_variable_get(:@dsl).instance_variable_get(:@language_blocks)
    end
  end
end
