module Adr
  # Mechanical classification of existing ADR-like content into the surveyed
  # shapes, enumerating migration units. Report-only for unrecognized content —
  # nothing here writes anything.
  class Detector
    Unit = Struct.new(:shape, :source_path, :anchor, :title, :body, keyword_init: true) do
      def source_ref
        anchor ? "#{source_path}##{anchor}" : source_path
      end
    end

    CANDIDATE_DIRS = [%w[docs adr], %w[docs decisions], %w[docs decision]].freeze
    GENERATED_BASENAMES = %w[README.md migration-report.md].freeze
    UNIT_HEADING = /^\#{1,4}\s*(?:ADR[-\s]?\d+|D-\d+)\b/i
    PROSE_DECISION = /^(?:\#{1,4}\s*|\*\*)?Decision\s+\d+\s*[:—–-]/i
    DECISION_MARKERS = /^(?:\#{1,4}\s*|\*\*)?(?:Status|Decision|Decided|Context)\b/i

    def initialize(root:)
      @root = root
    end

    def units
      candidate_files.flat_map { |path| classify_file(path) }
    end

    def candidate_files
      files = CANDIDATE_DIRS.flat_map do |parts|
        Dir.glob(File.join(@root, *parts, "*.md"))
      end
      files += Dir.glob(File.join(@root, "DECISIONS.md"))
      files.uniq.sort.reject { |p| generated?(p) || already_migrated?(p) }
    end

    private

    def generated?(path)
      GENERATED_BASENAMES.include?(File.basename(path))
    end

    # A file already in the os-adr format (frontmatter with an id) is not a
    # migration source.
    def already_migrated?(path)
      text = File.read(path)
      return false unless text.start_with?("---\n")
      !Record.parse(text).id.nil?
    rescue Record::ParseError, StandardError
      false
    end

    def classify_file(path)
      text = File.read(path)
      base = File.basename(path)
      relative = relative(path)

      if text.scan(UNIT_HEADING).size >= 2
        split_units(relative, text, :monolithic, UNIT_HEADING)
      elsif text.scan(PROSE_DECISION).size >= 2
        split_units(relative, text, :prose_embedded, PROSE_DECISION)
      elsif text.scan(/^\**Status\**\s*[:：]/i).size >= 2
        # Multiple decisions but no mechanically splittable unit headings:
        # report-only, never auto-converted.
        [Unit.new(shape: :unrecognized, source_path: relative, anchor: nil,
                  title: base, body: text)]
      elsif text.match?(DECISION_MARKERS)
        shape =
          if base.match?(/\A\d{4}-\d{2}(-\d{2})?-/) then :dated_single_file
          elsif base.match?(/\A\d+-/)               then :numbered_per_file
          else                                           :topic_single_file
          end
        [Unit.new(shape: shape, source_path: relative, anchor: nil,
                  title: single_title(text, base), body: text)]
      else
        [Unit.new(shape: :unrecognized, source_path: relative, anchor: nil,
                  title: base, body: text)]
      end
    end

    def split_units(relative, text, shape, pattern)
      lines = text.lines
      starts = lines.each_index.select { |i| lines[i].match?(pattern) }
      starts.each_with_index.map do |start, n|
        stop = starts[n + 1] || lines.size
        heading = lines[start].strip
        Unit.new(shape: shape, source_path: relative, anchor: heading,
                 title: unit_title(heading), body: lines[start...stop].join)
      end
    end

    def unit_title(heading)
      heading
        .sub(/\A\#{1,4}\s*/, "")
        .gsub("**", "")
        .sub(/\A(?:ADR[-\s]?\d+|D-\d+|Decision\s+\d+)\s*[:—–-]?\s*/i, "")
        .strip
    end

    def single_title(text, base)
      h1 = text[/^# +(.+)$/, 1]
      return h1.strip.sub(/\A(?:ADR[-\s]?\d+|D-\d+|\d{1,4})\s*[:—–-]\s*/i, "") if h1

      base.sub(/\.md\z/, "").sub(/\A[\d-]+/, "").tr("-", " ").strip.capitalize
    end

    def relative(path)
      path.sub(%r{\A#{Regexp.escape(@root)}/?}, "")
    end
  end
end
