# typed: strict
# frozen_string_literal: true

require "pathname"

module Packwerk
  class OffenseCollection
    extend T::Sig
    extend T::Helpers

    sig do
      params(
        root_path: String,
        package_todos: T::Hash[Packwerk::Package, Packwerk::PackageTodo]
      ).void
    end
    def initialize(root_path, package_todos = {})
      @root_path = root_path
      @package_todos = T.let(package_todos, T::Hash[Packwerk::Package, Packwerk::PackageTodo])
      @new_violations = T.let([], T::Array[Packwerk::ReferenceOffense])
      @strict_mode_violations = T.let([], T::Array[Packwerk::ReferenceOffense])
      @errors = T.let([], T::Array[Packwerk::Offense])
    end

    sig { returns(T::Array[Packwerk::ReferenceOffense]) }
    attr_reader :new_violations

    sig { returns(T::Array[Packwerk::Offense]) }
    attr_reader :errors

    sig { returns(T::Array[Packwerk::ReferenceOffense]) }
    attr_reader :strict_mode_violations

    sig do
      params(offense: Packwerk::Offense)
        .returns(T::Boolean)
    end
    def listed?(offense)
      return false unless offense.is_a?(ReferenceOffense)

      already_listed?(offense)
    end

    sig { params(offenses: T::Array[Offense]).void }
    def add_offenses(offenses)
      offenses.each { |offense| add_offense(offense) }
    end

    sig do
      params(offense: Packwerk::Offense).void
    end
    def add_offense(offense)
      unless offense.is_a?(ReferenceOffense)
        @errors << offense
        return
      end

      already_listed = already_listed?(offense)

      new_violations << offense unless already_listed

      if strict_mode_violation?(offense)
        add_to_package_todo(offense) if already_listed
        strict_mode_violations << offense
      else
        add_to_package_todo(offense)
      end
    end

    sig { params(for_files: T::Set[String]).returns(T::Boolean) }
    def stale_violations?(for_files)
      @package_todos.values.any? do |package_todo|
        package_todo.stale_violations?(for_files)
      end
    end

    sig { params(package_set: Packwerk::PackageSet).void }
    def persist_package_todo_files(package_set)
      dump_package_todo_files
      cleanup_extra_package_todo_files(package_set)
    end

    sig { returns(T::Array[Packwerk::Offense]) }
    def outstanding_offenses
      errors + new_violations
    end

    sig { returns(T::Array[Packwerk::ReferenceOffense]) }
    def unlisted_strict_mode_violations
      strict_mode_violations.reject { |offense| already_listed?(offense) }
    end

    private

    sig { params(offense: ReferenceOffense).returns(T::Boolean) }
    def already_listed?(offense)
      package_todo_for(offense.reference.package).listed?(offense.reference,
        violation_type: offense.violation_type)
    end

    sig { params(offense: ReferenceOffense).returns(T::Boolean) }
    def add_to_package_todo(offense)
      package_todo_for(offense.reference.package).add_entries(offense.reference,
        offense.violation_type)
    end

    sig { params(offense: ReferenceOffense).returns(T::Boolean) }
    def strict_mode_violation?(offense)
      checker = Checker.find(offense.violation_type)
      checker.strict_mode_violation?(offense)
    end

    sig { params(package_set: Packwerk::PackageSet).void }
    def cleanup_extra_package_todo_files(package_set)
      packages_without_todos = (package_set.packages.values - @package_todos.keys)

      packages_without_todos.each do |package|
        Packwerk::PackageTodo.new(
          package,
          package_todo_file_for(package),
        ).delete_if_exists
      end
    end

    sig { void }
    def dump_package_todo_files
      @package_todos.each_value(&:dump)
    end

    sig { params(package: Packwerk::Package).returns(Packwerk::PackageTodo) }
    def package_todo_for(package)
      @package_todos[package] ||= Packwerk::PackageTodo.new(
        package,
        package_todo_file_for(package),
      )
    end

    sig { params(package: Packwerk::Package).returns(String) }
    def package_todo_file_for(package)
      File.join(@root_path, package.name, "package_todo.yml")
    end
  end
end