# typed: strict
# frozen_string_literal: true

require "cache_store"
require "linkage_checker"

module OS
  module Mac
    module FormulaCellarChecks
      extend T::Helpers

      requires_ancestor { Homebrew::FormulaAuditor }
      requires_ancestor { ::FormulaCellarChecks }

      sig { returns(T.nilable(String)) }
      def check_shadowed_headers
        return if ["libtool", "subversion", "berkeley-db"].any? do |formula_name|
          formula.name.start_with?(formula_name)
        end

        return if formula.name.match?(Version.formula_optionally_versioned_regex(:php))
        return if formula.keg_only? || !formula.include.directory?

        files  = relative_glob(formula.include, "**/*.h")
        files &= relative_glob("#{MacOS.sdk_path}/usr/include", "**/*.h")
        files.map! { |p| File.join(formula.include, p) }

        return if files.empty?

        <<~EOS
          Header files that shadow system header files were installed to "#{formula.include}"
          The offending files are:
            #{files * "\n  "}
        EOS
      end

      sig { returns(T.nilable(String)) }
      def check_openssl_links
        return unless formula.prefix.directory?

        keg = ::Keg.new(formula.prefix)
        system_openssl = keg.mach_o_files.select do |obj|
          dlls = obj.dynamically_linked_libraries
          dlls.any? { |dll| %r{/usr/lib/lib(crypto|ssl|tls)\..*dylib}.match? dll }
        end
        return if system_openssl.empty?

        <<~EOS
          object files were linked against system openssl
          These object files were linked against the deprecated system OpenSSL or
          the system's private LibreSSL.
          Adding `depends_on "openssl"` to the formula may help.
            #{system_openssl * "\n  "}
        EOS
      end

      sig { params(lib: ::Pathname).returns(T.nilable(String)) }
      def check_python_framework_links(lib)
        python_modules = ::Pathname.glob lib/"python*/site-packages/**/*.so"
        framework_links = python_modules.select do |obj|
          obj = MachOPathname.wrap(obj)
          dlls = obj.dynamically_linked_libraries
          dlls.any? { |dll| dll.include?("Python.framework") }
        end
        return if framework_links.empty?

        <<~EOS
          python modules have explicit framework links
          These python extension modules were linked directly to a Python
          framework binary. They should be linked with -undefined dynamic_lookup
          instead of -lpython or -framework Python.
            #{framework_links * "\n  "}
        EOS
      end

      sig { void }
      def check_linkage
        return unless formula.prefix.directory?

        keg = ::Keg.new(formula.prefix)

        CacheStoreDatabase.use(:linkage) do |db|
          typed_db = T.cast(db, CacheStoreDatabase[String, T::Hash[T.any(String, Symbol), T.anything]])
          checker = ::LinkageChecker.new(keg, formula, cache_db: typed_db)
          next unless checker.broken_library_linkage?

          output = <<~EOS
            #{formula} has broken dynamic library links:
              #{checker.display_test_output}
          EOS

          tab = keg.tab
          if tab.poured_from_bottle
            output += <<~EOS
              Rebuild this from source with:
                brew reinstall --build-from-source #{formula}
              If that's successful, file an issue#{formula.tap ? " here:\n  #{formula.tap!.issues_url}" : "."}
            EOS
          end
          problem_if_output output
        end
      end

      sig { params(formula: ::Formula).returns(T.nilable(String)) }
      def check_flat_namespace(formula)
        return unless formula.prefix.directory?
        return if formula.tap&.audit_exception(:flat_namespace_allowlist, formula.name)

        keg = ::Keg.new(formula.prefix)
        flat_namespace_files = keg.mach_o_files.reject do |file|
          next true unless file.dylib?

          macho = MachO.open(file)
          if MachO::Utils.fat_magic?(macho.magic)
            macho.machos.map(&:header).all? { |h| h.flag? :MH_TWOLEVEL }
          else
            macho.header.flag? :MH_TWOLEVEL
          end
        end
        return if flat_namespace_files.empty?

        <<~EOS
          Libraries were compiled with a flat namespace.
          This can cause linker errors due to name collisions and
          is often due to a bug in detecting the macOS version.
            #{flat_namespace_files * "\n  "}
           Learn more about this in:
            #{Formatter.url("https://developer.apple.com/forums/thread/689991?answerId=687895022#687895022")}
        EOS
      end

      sig { void }
      def audit_installed
        super
        problem_if_output(check_shadowed_headers)
        problem_if_output(check_openssl_links)
        problem_if_output(check_python_framework_links(formula.lib))
        check_linkage
        problem_if_output(check_flat_namespace(formula))
      end

      MACOS_LIB_EXTENSIONS = %w[.dylib .framework].freeze

      sig { params(filename: ::Pathname).returns(T::Boolean) }
      def valid_library_extension?(filename)
        super || MACOS_LIB_EXTENSIONS.include?(filename.extname)
      end
    end
  end
end

FormulaCellarChecks.prepend(OS::Mac::FormulaCellarChecks)
