require_relative "../helpers/pretty_table"
require_relative "../helpers/git"
require_relative "../helpers/github"

module Fastlane
  module Actions
    class AnalyzeDependenciesAction < Action
      SYSTEM_FRAMEWORKS = [
        # Core system frameworks
        'Foundation', 'UIKit', 'SwiftUI', 'Combine', 'Swift', 'Darwin',

        # iOS system frameworks
        'AVFoundation', 'AVKit', 'CoreFoundation', 'CoreGraphics', 'CoreImage',
        'CoreData', 'CoreLocation', 'CoreMotion', 'CoreText', 'CoreVideo', 'CoreMedia',
        'CoreML', 'Vision', 'Photos', 'PhotosUI', 'Contacts', 'ContactsUI',
        'MessageUI', 'Messages', 'MapKit', 'WebKit', 'SafariServices', 'StoreKit',
        'GameplayKit', 'SpriteKit', 'SceneKit', 'ARKit', 'RealityKit',
        'Metal', 'MetalKit', 'MetalPerformanceShaders', 'Accelerate',
        'simd', 'ModelIO', 'GLKit', 'OpenGLES',

        # Network and security
        'Network', 'NetworkExtension', 'Security', 'CryptoKit', 'AuthenticationServices',

        # Media and audio
        'AudioToolbox', 'AVAudioSession', 'MediaPlayer', 'CoreAudio', 'CoreMIDI',

        # Device and sensors
        'CoreBluetooth', 'CoreNFC', 'HealthKit', 'HomeKit', 'EventKit',
        'EventKitUI', 'CoreSpotlight', 'Intents', 'IntentsUI',

        # Development and testing
        'XCTest', 'OSLog', 'os', 'Testing',

        # # Other common system modules
        'MobileCoreServices', 'UniformTypeIdentifiers',
        'LocalAuthentication', 'BiometricAuthentication', 'CallKit', 'PushKit',
        'UserNotifications', 'NotificationCenter', 'CloudKit', 'CoreServices',
        'AdSupport', 'AppTrackingTransparency',
        'SwiftData', 'CoreHaptics',
        'Charts', 'LinkPresentation',
      ]

      def self.run(params)
        unless params[:skip_resolve_dependencies]
          other_action.resolve_dependencies(build_flavor: params[:build_flavor])
        end
        @source_packages_path = params[:source_packages_path]

        package_path = File.expand_path(params[:package_path])
        package_dir = File.dirname(package_path)

        UI.message("Analyzing package...")
        targets = parse_packages(package_dir)
        UI.success("✅ Successfully analyzed package and found #{targets.count} targets!")

        if params[:summary]
          PrettyTable.print(
            data: targets.values,
            columns: [:package_name, :name, :type, :source_files, :dependencies, :implicit_dependencies, :exported_dependencies, :transitive_dependencies],
            title: "Targets",
          )
        end

        if params[:target]
          target = targets[params[:target]]
          UI.error("Target #{params[:target]} not found") if target.nil?
          PrettyTable.print(
            data: {
              name: target[:name],
              type: target[:type],
              path: target[:path],
              package_name: target[:package_name],
              package_type: target[:package_type],
              dependencies: target[:dependencies].map { |d| d[:name] },
              implicit_dependencies: target[:implicit_dependencies].map { |d| "#{d[:name]}~#{d[:transitive_from]}" },
              exported_dependencies: target[:exported_dependencies],
              transitive_dependencies: target[:transitive_dependencies].map { |d| "#{d[:name]}~#{d[:transitive_from]}" },
              source_files: target[:source_files],
              source_dependencies: target[:source_dependencies],
              testable_dependencies: target[:testable_dependencies],
            },
            title: "Target: #{target[:name]}",
          )
        end

        if params[:check_for_issues]
          UI.message("Analyzing targets...")
          issues = analyze_targets(targets).filter { |issue| params[:target].nil? || issue[:target] == params[:target] }
          if issues.empty?
            UI.success("✅ Dependency analysis found no issues!")
          else
            issues.map do |issue|
              file_info = issue[:file] ? "(#{[issue[:file], issue[:line]].compact.join(":")})" : ""
              Github.log_error("#{issue[:message]} #{file_info}")
            end
            UI.user_error!("❌ Dependency analysis found #{issues.count} issues!")
          end
        end

        targets
      end

      def self.parse_packages(root_dir)
        UI.verbose("Parsing packages...")
        targets = {}

        queue = [root_dir]
        packages = {
          root_dir => {
            transitive_from: nil
          }
        }

        while !queue.empty?
          package_dir = queue.shift
          package = parse_package(package_path: package_dir)
          package[:transitive_from] = packages[package_dir][:transitive_from]
          packages[package_dir] = package

          package[:targets].each do |target|
            targets[target[:name]] = target
          end

          package[:subpackages].each do |subpackage|
            next if packages.key?(subpackage[:path]) || !File.directory?(subpackage[:path])
            packages[subpackage[:path]] = subpackage
            queue << subpackage[:path]
          end
        end
        UI.verbose("✅ Found #{targets.count.to_s.yellow} targets across #{packages.count.to_s.yellow} packages!")

        UI.verbose("Resolving source files...")
        source_files = {}
        targets.values.sort_by { |target| target[:path] || '' }.reverse.each do |target|
          UI.verbose("Resolving source files for target #{target[:name].cyan}")
          target_source_files = if target[:path] && target[:package_type] != "external"
            UI.verbose("Computing source files for target: #{target[:name].cyan}")
            `git ls-files --cached --others --exclude-standard '**/*.swift' 2>/dev/null || echo ""`.strip.split("\n").map do |file|
              next unless file.start_with?(target[:path])

              if file.empty? || source_files.include?(file)
                nil
              else
                source_files[file] = target[:name]
                file
              end
            end.compact
          else
            []
          end
          imports_by_file = target_source_files.map do |file|
            imports = File.read(file).split("\n").map do |line|
              m = line.match(/^(?:@(?<type>\w+))?\s*import\s+(?<module>[A-Za-z][A-Za-z0-9_]+).*$/)
              if m
                UI.verbose("Found import of '#{m[:module]}'#{m[:type] ? " (#{m[:type]})" : ""} in #{file}")
              end
              m
            end.compact
            [file, imports]
          end.to_h
          target[:source_files] = imports_by_file.map { |file, imports| [file, imports.map { |import| import[:module] }.uniq] }.to_h
          target[:source_dependencies] = imports_by_file.values.flatten.map { |match| match[:module] }.uniq
          target[:exported_dependencies] = imports_by_file.values.flatten.select { |match| match[:type] == "_exported" }.map { |match| match[:module] }.uniq
          target[:testable_dependencies] = imports_by_file.values.flatten.select { |match| match[:type] == "testable" }.map { |match| match[:module] }.uniq
        end
        UI.verbose("✅ Found #{source_files.count.to_s.yellow} source files!")

        UI.verbose("Resolving transitive & implicit dependencies...")
        targets.values.each do |target|
          UI.verbose("Ensuring dependencies for target #{target[:name].cyan}")
          # Make all deps are present in the target hash
          (target[:dependencies] + target[:source_dependencies].map { |dep| { name: dep } }).each do |dep|
            targets[dep[:name]] ||= {
              name: dep[:name],
              type: "regular",
              path: target[:package_type] == "external" ? nil : "Sources/#{dep[:name]}",
              dependencies: [],
              transitive_from: dep[:transitive_from]
            }
          end

          UI.verbose("Resolving implicit dependencies for target #{target[:name].cyan}")
          target[:implicit_dependencies] = target[:dependencies].map do |dep|
            if targets[dep[:name]]
              (targets[dep[:name]][:exported_dependencies] || []).map { |export| { **targets[export], transitive_from: dep[:name] } }
            else
              []
            end
          end.flatten.uniq { |dep| dep[:name] }
          UI.verbose("Resolving transitive dependencies for target #{target[:name].cyan}")
          target[:transitive_dependencies] = resolve_transitive_dependencies(target[:name], targets, target[:package_type] == "external")
        end
        UI.verbose("✅ Successfully resolved transitive & implicit dependencies!")

        targets
      end

      def self.resolve_transitive_dependencies(target_name, targets, is_external)
        target = targets[target_name]
        if target[:dependencies].empty?
          return []
        end
        if target[:transitive_dependencies]
          return target[:transitive_dependencies]
        end

        UI.verbose("Computing transitive dependencies for target: #{target_name.cyan}")

        result = target[:dependencies].flat_map do |direct_dep|
          dep_name = direct_dep[:name]
          if targets[dep_name].nil?
            []
          else
            dep_target = targets[dep_name]

            direct_transitive = dep_target[:dependencies].map do |transitive_dep|
              { name: transitive_dep[:name], transitive_from: dep_name }
            end

            recursive_transitive = resolve_transitive_dependencies(dep_name, targets, is_external).map do |transitive_dep|
              { name: transitive_dep[:name], transitive_from: transitive_dep[:transitive_from] || dep_name }
            end

            direct_transitive + recursive_transitive
          end
        end.flatten.uniq { |dep| dep[:name] }

        target[:transitive_dependencies] = result
        return result
      end

      def self.parse_package(package_path:)
        UI.verbose("Fetching package info: #{package_path.cyan}")

        package = Dir.chdir(package_path) do
          Actions.sh "swift package dump-package 2>/dev/null", log: false do |status, result, _command|
            unless status.success?
              UI.crash!("swift package dump-package failed with exit code #{status.exitstatus}")
            end

            begin
              JSON.parse(result.strip)
            rescue JSON::ParserError => e
              UI.crash!("Failed to parse swift package dump-package output: #{e.message}")
            end
          end
        end

        package_type = if package_path.start_with?(@source_packages_path)
          "external"
        else
          "local"
        end

        targets = package["targets"].map do |target|
          target_path = case package_type
          when "external"
            nil
          when "local"
            if target["path"]
              File.join(package_path, target["path"])
            elsif target["type"] == "test"
              "Tests/#{target["name"]}"
            else
              "Sources/#{target["name"]}"
            end
          end

          {
            name: target["name"],
            type: target["type"],
            path: target_path,
            dependencies: (target["dependencies"] || []).map do |dep|
              name = dep["byName"]&.first || dep["product"]&.first || dep["target"]&.first
              unless name
                UI.user_error!("Invalid dependency format: #{dep}")
              end

              {
                name: name,
                package_name: dep["product"]&.first ? dep["product"][1] : nil,
              }
            end,
            package_name: package["name"],
            package_type: package_type,
          }
        end

        subpackages = package["dependencies"].map do |dep|
          subpackage_path = if package_type == "external" || dep["sourceControl"]&.first&.[]("identity")
            "#{@source_packages_path}/checkouts/#{dep["sourceControl"]&.first&.[]("identity")}"
          elsif dep["fileSystem"]&.first&.[]("path")
            dep["fileSystem"]&.first&.[]("path")
          else
            UI.crash!("Invalid package dependency: #{dep}")
          end

          { path: subpackage_path, transitive_from: package["name"] }
        end

        UI.verbose("Found #{targets.count.to_s.yellow} targets and #{subpackages.count.to_s.yellow} subpackages")

        {
          name: package["name"],
          type: package_type,
          path: package_path,
          targets: targets,
          subpackages: subpackages,
        }
      end


      def self.analyze_targets(targets)
        issues = []

        targets.values.each do |target|
          next if target[:package_type] != "root"
          deps = (target[:dependencies] + target[:implicit_dependencies]).map { |d| d[:name] }

          # Check for missing dependencies
          target[:missing_deps] = []
          target[:source_dependencies].each do |dep|
            next if dep == target[:name] || deps.include?(dep) || target[:missing_deps].include?(dep) || SYSTEM_FRAMEWORKS.include?(dep)

            file = target[:source_files].keys.find { |file| target[:source_files][file].include?(dep) }
            line = File.read(file).lines.find_index { |line| line.include?("import #{dep}") } + 1

            target[:missing_deps] << dep
            issues << {
              target: target[:name],
              message: "❌ ERROR[missing_dependency]: Target '#{target[:name]}' is missing dependency '#{dep}'",
              file: file,
              line: line,
            }
          end

          # Check for unnecessary dependencies
          target[:unnecessary_deps] = []
          target[:dependencies].each do |dep|
            # Skip if it's the target itself or if it's actually used in imports
            next if dep[:name] == target[:name] || target[:source_dependencies].include?(dep[:name]) || target[:unnecessary_deps].include?(dep[:name])

            target[:unnecessary_deps] << dep[:name]
            issues << {
              target: target[:name],
              message: "❌ ERROR[unnecessary_dependency]: Target '#{target[:name]}' has unnecessary dependency '#{dep[:name]}'",
            }
          end

          # File.open("build/deps.txt", "a") do |file|
          #   file.write("#{target[:name]} | MISSING DEPS: #{target[:missing_deps].join(", ")} \n") if target[:missing_deps].any?
          #   file.write("#{target[:name]} | UNNECESSARY DEPS: #{target[:unnecessary_deps].join(", ")} \n") if target[:unnecessary_deps].any?
          # end
        end

        issues.sort_by { |issue| [issue[:message]] }
      end

      def self.description
        "Analyze the dependency graph defined in a Swift Package.swift file"
      end

      def self.available_options
        [
          FastlaneCore::ConfigItem.new(
            key: :build_flavor,
            description: "The build flavor (#{BuildFlavor::ALL.join(', ')})",
            verify_block: proc do |value|
              UI.user_error!("Invalid flavor (must be one of #{BuildFlavor::ALL.join(', ')})") unless BuildFlavor::ALL.include?(value)
            end,
            default_value: BuildFlavor::PROD
          ),
          FastlaneCore::ConfigItem.new(
            key: :package_path,
            description: "Path to root of the package. Defaults to workspace root",
            optional: true,
            default_value: "Package.swift", # Current working directory
            verify_block: proc do |value|
              UI.user_error!("Invalid package path") unless File.exist?(value)
            end
          ),
          FastlaneCore::ConfigItem.new(
            key: :check_for_issues,
            description: "Fail on errors",
            type: Boolean,
            default_value: false
          ),
          FastlaneCore::ConfigItem.new(
            key: :summary,
            description: "Print summary of all targets",
            type: Boolean,
            default_value: false
          ),
          FastlaneCore::ConfigItem.new(
            key: :target,
            description: "Name of a specific target to inspect",
            optional: true,
            verify_block: proc do |value|
              UI.user_error!("Target name can not be empty") if value.to_s.empty?
            end
          ),
          FastlaneCore::ConfigItem.new(
            key: :skip_resolve_dependencies,
            description: "Skip resolving dependencies",
            type: Boolean,
            optional: true,
            default_value: false,
          ),
          *BuildConfig.default_options
        ]
      end

      def self.return_value
        "Hash of target information keyed by target name"
      end

      def self.authors
        ["Suno"]
      end

      def self.is_supported?(platform)
        [:ios, :mac].include?(platform)
      end
    end
  end
end
