require "json"
require "fileutils"

module Backlog
  # Pure logic for the issue-triggered wakeup poller (ADR-0035): which
  # projects are polled, which issues count as new, what autonomy tier a
  # label set maps to, and the tmux command a wakeup would run (ADR-0036).
  #
  # All methods are pure over injected data — the bin/wakeup-poll glue does
  # the shelling out (tea/gh, tmux) and file IO. Read paths never raise.
  module Wakeup
    module_function

    # Autonomy tiers, from ADR-0029 label vocabulary (ADR-0035 mapping):
    # afk-ready -> acting session; semi -> triage-only session;
    # anything else (hitl / unlabeled) -> notify-only.
    def tier(labels)
      names = Array(labels).flat_map { |l| l.to_s.split(",") }.map(&:strip)
      return :afk_ready if names.include?("afk-ready")
      return :semi if names.include?("semi")

      :notify
    end

    # Filter global-index rows (ADR-0034 shape: path -> row) down to
    # wakeup-eligible projects: tracker forgejo:/github: AND the per-project
    # .cc-os/config carries wakeup=true (config is the authority; the
    # config_reader is injected so this stays pure).
    #
    # @return [Array<Hash>] rows as {path:, name:, tracker_kind:, repo_slug:}
    def eligible_projects(projects, config_reader:)
      projects.filter_map do |path, row|
        tracker = row["tracker"].to_s
        kind, slug = tracker.split(":", 2)
        next unless %w[forgejo github].include?(kind) && !slug.to_s.empty?
        next unless opted_in?(config_reader.call(path))

        { path: path, name: row["name"] || File.basename(path),
          tracker_kind: kind, repo_slug: slug }
      end
    end

    def opted_in?(config_text)
      config_text.to_s.each_line.any? { |l| l.strip == "wakeup=true" }
    end

    # Issues newer than the last-seen index for this repo. A nil last_seen
    # means the repo has never been scanned: baseline without waking
    # anything (ADR-0035 — no bulk backfill of pre-existing issues).
    def new_issues(issues, last_seen)
      return [] if last_seen.nil?

      issues.select { |i| i["index"].to_i > last_seen.to_i }
    end

    def highest_index(issues, floor = 0)
      ([floor.to_i] + issues.map { |i| i["index"].to_i }).max
    end

    # The tmux command a wakeup runs (ADR-0036 naming: cc-<project>-issue<N>).
    def tmux_command(project:, path:, issue_index:, prompt:)
      [
        "tmux", "new-session", "-d",
        "-s", "cc-#{project}-issue#{issue_index}",
        "-c", path,
        "claude", prompt
      ]
    end

    # Initial prompt for the wakened session: issue coordinates + autonomy
    # tier + the standing guardrails (ADR-0035 context handoff). The issue
    # body (with any Discoverer block) is passed through verbatim.
    def session_prompt(issue:, tier:, url:)
      action = tier == :afk_ready ? AFK_READY_ACTION : SEMI_ACTION
      <<~PROMPT
        Wakeup: new issue ##{issue['index']} — #{issue['title']}
        #{url}

        #{action}
        File-don't-fix still applies (ADR-0034): never edit other projects.
        Before exiting, post your triage/summary as a comment on the issue —
        that comment is the durable record (ADR-0036 lifecycle).

        Issue body:
        #{issue['body']}
      PROMPT
    end

    AFK_READY_ACTION =
      "This issue is labeled afk-ready: triage it and, if it is actionable " \
      "within this repo, do the work."
    SEMI_ACTION =
      "This issue is labeled semi: TRIAGE ONLY — label, comment, and " \
      "propose an approach, but stop at proposing. Do not implement."

    # Last-seen state (~/.cc-os/wakeup-state.json): repo_slug -> highest
    # issue index seen. Missing/corrupt reads as empty; writes are atomic.
    class State
      def self.default_path(env: ENV)
        base = env["CC_OS_HOME"] || File.join(env.fetch("HOME", Dir.home), ".cc-os")
        File.join(base, "wakeup-state.json")
      end

      def initialize(path: self.class.default_path)
        @path = path
      end

      def last_seen(repo_slug)
        read[repo_slug]
      end

      def record(repo_slug, index)
        data = read
        data[repo_slug] = index
        write(data)
      end

      private

      def read
        return {} unless File.exist?(@path)

        parsed = JSON.parse(File.read(@path))
        parsed.is_a?(Hash) ? parsed : {}
      rescue StandardError
        {}
      end

      def write(data)
        dir = File.dirname(@path)
        FileUtils.mkdir_p(dir)
        temp = File.join(dir, ".wakeup-state.json.tmp.#{Process.pid}")
        File.write(temp, JSON.pretty_generate(data) + "\n")
        File.rename(temp, @path)
      end
    end
  end
end
