require "json"
require "set"

module Adr
  # Deterministic-first ADR retrieval. Layers, in order:
  #   1. path/component match against affected-paths / affected-components
  #   2. status filter (Accepted only unless all_statuses)
  #   3. Graphify one-hop expansion of the query paths (when graphify-out/
  #      exists; degrades gracefully to layers 1-2 when absent or unreadable)
  # AI judgment (layer 4) happens in the `find` skill, only over what this
  # returns — never the full corpus.
  class Finder
    def initialize(repository:, graph_path: nil)
      @repository = repository
      @graph_path = graph_path
    end

    def find(paths: [], components: [], all_statuses: false)
      paths = normalize(paths)
      graph_paths = normalize(graph_neighbors(paths)) - paths
      @repository.entries.filter_map do |entry|
        record = entry.record
        next unless all_statuses || record.status == "Accepted"

        matched = match_reasons(record, paths, graph_paths, components)
        next if matched.empty?

        { "id" => record.id, "title" => record.title, "status" => record.status,
          "date" => record.date.to_s, "file" => entry.path, "matched_on" => matched }
      end
    end

    private

    def match_reasons(record, paths, graph_paths, components)
      affected_paths = Array(record.frontmatter["affected-paths"]).map { |p| p.to_s.chomp("/") }
      affected_components = Array(record.frontmatter["affected-components"]).map(&:to_s)
      reasons = []
      reasons << "path" if overlap?(affected_paths, paths)
      reasons << "graph" if reasons.empty? && overlap?(affected_paths, graph_paths)
      reasons << "component" if (affected_components & components).any?
      reasons
    end

    def overlap?(affected, queried)
      affected.any? { |a| queried.any? { |q| path_match?(a, q) } }
    end

    # Prefix containment either way: an ADR affecting `lib/storage` matches a
    # query for `lib/storage/adapter.rb`, and one affecting a specific file
    # matches a query for its directory.
    def path_match?(a, q)
      a == q || q.start_with?("#{a}/") || a.start_with?("#{q}/")
    end

    def normalize(paths)
      paths.map { |p| p.to_s.chomp("/") }.reject(&:empty?).uniq
    end

    # Files one graph hop from the queried files, via graphify-out/graph.json
    # node source_file attributes. Any failure degrades to no expansion.
    def graph_neighbors(paths)
      return [] unless @graph_path && File.exist?(@graph_path)

      graph = JSON.parse(File.read(@graph_path))
      nodes = graph.fetch("nodes", [])
      id_to_file = nodes.to_h { |n| [n["id"], n["source_file"]] }
      seeds = nodes.select do |n|
        n["source_file"] && paths.any? { |q| path_match?(n["source_file"].chomp("/"), q) }
      end.map { |n| n["id"] }.to_set
      return [] if seeds.empty?

      graph.fetch("links", []).flat_map do |link|
        if seeds.include?(link["source"]) then [link["target"]]
        elsif seeds.include?(link["target"]) then [link["source"]]
        else []
        end
      end.filter_map { |id| id_to_file[id] }.uniq
    rescue StandardError
      []
    end
  end
end
