module Backlog
  # Filesystem-only detection helpers for the `inspect` subcommand: does the
  # repo already have in-repo issue files. Deliberately narrow — network
  # checks (Planka, tea, gh) stay in bin/os-backlog where the rest of the
  # CLI's IO lives; this class exists so that piece is unit-testable without
  # touching the network or shelling out.
  class Inspector
    CANDIDATE_DIRS = ["docs/issues"].freeze
    CANDIDATE_FILES = ["ISSUES.md"].freeze

    # Labels that mark an issue as part of a dependency chain. Matched as
    # whole (hyphen-preserving) tokens, case-insensitively — "not-blocked"
    # does NOT match "blocked".
    CHAIN_LABELS = %w[blocked blocked-by depends depends-on prereq
                      prerequisite waiting needs].freeze

    # Dependency language that must appear near a cross-reference for it to
    # count as chain evidence — a plain "fixed in #12" does not.
    DEPENDENCY_LANGUAGE = /\b(blocked|depends|after|requires|prereq|follows)/i
    CROSS_REF_WINDOW = 40 # chars either side of the "#N" reference

    # Pure classifier for the shape of a repo's open git issues — no shell,
    # no network. Feeds the route skill's split-vs-migrate recommendation
    # (ADR-0033: git issues are specs; a chain of interdependent issues is
    # spec-shaped and should stay put).
    #
    # @param issues [Array<Hash>, nil] one hash per open issue:
    #   {number:, title:, labels: [String], body:}
    # @return [String, nil] "sequential-chain" | "flat-adhoc" | "mixed", or
    #   nil when there's no metadata or fewer than 3 issues (no evidence,
    #   which is distinct from evidence of flatness)
    def self.classify_issue_shape(issues)
      return nil if issues.nil? || issues.size < 3

      numbers = issues.map { |issue| issue[:number].to_i }
      with_evidence = issues.count { |issue| chain_evidence?(issue, numbers) }
      return "flat-adhoc" if with_evidence.zero?

      with_evidence * 3 >= issues.size * 2 ? "sequential-chain" : "mixed"
    end

    def self.chain_evidence?(issue, numbers)
      chain_label?(issue[:labels]) ||
        chain_cross_reference?(issue[:body], own_number: issue[:number].to_i, numbers: numbers)
    end

    def self.chain_label?(labels)
      Array(labels).any? do |label|
        tokens = label.to_s.downcase.scan(/[a-z][a-z-]*[a-z]|[a-z]/)
        tokens.any? { |token| CHAIN_LABELS.include?(token) }
      end
    end

    def self.chain_cross_reference?(body, own_number:, numbers:)
      text = body.to_s
      text.enum_for(:scan, /#(\d+)/).any? do
        match = Regexp.last_match
        referenced = match[1].to_i
        next false if referenced == own_number || !numbers.include?(referenced)

        window_start = [match.begin(0) - CROSS_REF_WINDOW, 0].max
        window = text[window_start, (match.begin(0) - window_start) + match[0].length + CROSS_REF_WINDOW]
        window.match?(DEPENDENCY_LANGUAGE)
      end
    end

    # @param repo_path [String] absolute path to the repo
    # @return [Array<String>] repo-relative paths of in-repo issue tracking
    #   found (empty if none)
    def self.issue_files(repo_path)
      found = []
      CANDIDATE_DIRS.each do |rel|
        found << rel if Dir.exist?(File.join(repo_path, rel))
      end
      CANDIDATE_FILES.each do |rel|
        found << rel if File.exist?(File.join(repo_path, rel))
      end
      found
    end
  end
end
