#!/usr/bin/env ruby
# frozen_string_literal: true
#
# os-backlog CLI: resolve, inspect, config-write, projects, issue-create,
# issues. Post-ADR-0042 (Planka retired): git issues (forgejo/github/repo)
# are the single tracker for both state and specs.
#
# Usage:
#   os-backlog resolve < input.json
#     input.json: {"repo_path": "...", "config": "..."|null}
#   os-backlog inspect                         (detect existing tracking for cwd repo, JSON)
#   os-backlog config-write <tracker-value>    (write tracker key to .cc-os/config
#                                                + upsert the global project index)
#   os-backlog projects [NAME-FILTER]           (print ~/.cc-os/projects.json as JSON)
#   os-backlog issue-create --title "..." --body "..." [--priority P0-P3]
#     (creates an issue on the cwd repo's configured tracker; never applies
#     a state label)
#   os-backlog issues                          (open issues on the cwd repo's
#                                                configured tracker, grouped by
#                                                state label, JSON)
#
# resolve, config-write are pure — no network access at all. inspect,
# issue-create, and issues touch the network/CLIs (tea/gh) but fail soft
# field-by-field (inspect) or with a one-line error (issue-create/issues)
# rather than leaving a stack trace.
#
# Fails soft: never destructive.

require "json"
require "fileutils"
require "shellwords"
require_relative "../lib/backlog"

def fail_soft(message)
  warn "os-backlog: #{message}"
  exit 1
end

def parse_flag(args, flag)
  idx = args.index(flag)
  return nil unless idx

  args.delete_at(idx)
  args.delete_at(idx)
end

def require_flag(args, flag, command)
  parse_flag(args, flag) || fail_soft("#{command} requires #{flag} <value>")
end

# Best-effort open-issue count + lightweight per-issue metadata via tea
# (Forgejo) or gh (GitHub), whichever the detected remote implies. Fails
# soft per-field: any fetch/parse failure means metadata nil. Returns
# [issue_cli_hash, metadata_or_nil] — the metadata feeds the pure
# Backlog::Inspector.classify_issue_shape and never appears in the JSON.
def inspect_issue_cli(remote_info)
  unless remote_info && %i[forgejo github].include?(remote_info[:kind])
    return [{ "tool" => nil, "open_count" => nil, "reason" => "no forgejo/github remote detected" }, nil]
  end

  repo_slug = "#{remote_info[:owner]}/#{remote_info[:repo]}"
  if remote_info[:kind] == :forgejo
    tea_issues(repo_slug)
  else
    gh_issues(repo_slug)
  end
end

def tea_issues(repo_slug)
  return [{ "tool" => "tea", "open_count" => nil, "reason" => "tea CLI not found" }, nil] unless system("which tea > /dev/null 2>&1")

  out = `tea issues list --repo #{repo_slug} --state open --fields index,title,labels,body --output json 2>/dev/null`
  parsed = JSON.parse(out)
  issues = parsed.map do |issue|
    { number: issue["index"].to_i, title: issue["title"],
      labels: issue["labels"].to_s.split(",").map(&:strip).reject(&:empty?),
      body: issue["body"] }
  end
  [{ "tool" => "tea", "open_count" => issues.size }, issues]
rescue StandardError
  # JSON mode unavailable/unparsable: fall back to the plain line count.
  out = `tea issues list --repo #{repo_slug} --state open 2>/dev/null`
  lines = out.to_s.lines.map(&:strip).reject(&:empty?)
  [{ "tool" => "tea", "open_count" => lines.size }, nil]
end

def gh_issues(repo_slug)
  return [{ "tool" => "gh", "open_count" => nil, "reason" => "gh CLI not found" }, nil] unless system("which gh > /dev/null 2>&1")

  out = `gh issue list --repo #{repo_slug} --state open --json number,title,labels,body 2>/dev/null`
  parsed = JSON.parse(out)
  issues = parsed.map do |issue|
    { number: issue["number"].to_i, title: issue["title"],
      labels: Array(issue["labels"]).map { |label| label["name"] },
      body: issue["body"] }
  end
  [{ "tool" => "gh", "open_count" => issues.size }, issues]
rescue StandardError
  [{ "tool" => "gh", "open_count" => nil, "reason" => "could not parse gh output" }, nil]
end

# Canonical path for the global project index: the realpath of cwd — the
# directory config-write just wrote .cc-os/config into, i.e. the project
# the row describes. Deliberately NOT the git toplevel: in an umbrella
# repo a subdirectory's config is its own project, and keying on the
# toplevel would make every subproject overwrite the umbrella's row.
# Never raises.
def canonical_repo_path
  File.realpath(Dir.pwd)
rescue StandardError
  Dir.pwd
end

# Best-effort upsert of the global project index row after a successful
# config-write. Index failures must never fail config-write — warn at most.
def update_project_index(tracker)
  repo_path = canonical_repo_path
  remote = `git remote get-url origin 2>/dev/null`.strip
  remote = nil if remote.empty?
  Backlog::ProjectIndex.new.upsert(path: repo_path, name: File.basename(repo_path),
                                   tracker: tracker, remote: remote)
rescue StandardError => e
  warn "os-backlog: (non-fatal) could not update project index: #{e.message}"
end

# The cwd repo's configured tracker, or nil (no .cc-os/config, or no
# tracker key set). Shared by issue-create/issues.
def cwd_tracker
  config_path = File.join(Dir.pwd, ".cc-os", "config")
  contents = File.exist?(config_path) ? File.read(config_path) : nil
  Backlog::Config.new(contents).tracker
end

def require_cwd_tracker(command)
  tracker = cwd_tracker
  return tracker if tracker && Backlog::Tracker.valid?(tracker)

  fail_soft("#{command}: no valid tracker configured for #{Dir.pwd} — run /os-backlog:route first")
end

command, *rest = ARGV

begin
case command
when "resolve"
  input = JSON.parse($stdin.read)
  resolver = Backlog::Resolver.new(
    repo_path: input.fetch("repo_path"),
    config_contents: input["config"]
  )
  puts resolver.resolve_string
when "inspect"
  repo_path = Dir.pwd
  config_path = File.join(repo_path, ".cc-os", "config")
  config_contents = File.exist?(config_path) ? File.read(config_path) : nil
  config = Backlog::Config.new(config_contents)

  remote_output = `git -C #{Shellwords.escape(repo_path)} remote -v 2>/dev/null`
  remote_info = Backlog::Tracker.parse_remote(remote_output)

  issue_cli, issue_metadata = inspect_issue_cli(remote_info)
  findings = {
    "repo_path" => repo_path,
    "tracker_configured" => config.tracker,
    "git_remote" => remote_info ? remote_info.transform_keys(&:to_s) : nil,
    "issue_cli" => issue_cli,
    "issue_shape" => Backlog::Inspector.classify_issue_shape(issue_metadata),
    "in_repo_issue_files" => Backlog::Inspector.issue_files(repo_path)
  }
  puts JSON.pretty_generate(findings)
when "config-write"
  value = rest.shift
  fail_soft("config-write requires a tracker value, e.g. forgejo:owner/repo") unless value
  if value.start_with?("planka:")
    fail_soft("planka: trackers are retired (ADR-0042) — run /os-backlog:route to " \
              "choose forgejo:/github:/repo: instead")
  end
  unless Backlog::Tracker.valid?(value)
    fail_soft("invalid tracker value #{value.inspect}; expected one of " \
              "forgejo:<owner>/<repo> | github:<owner>/<repo> | repo:<path>")
  end

  config_dir = File.join(Dir.pwd, ".cc-os")
  config_path = File.join(config_dir, "config")
  existing = File.exist?(config_path) ? File.read(config_path) : nil
  updated = Backlog::Config.merge(existing, "tracker", value)
  FileUtils.mkdir_p(config_dir)
  File.write(config_path, updated)
  update_project_index(value)
  puts "tracker set to #{value} in #{config_path}"
when "projects"
  filter = rest.shift
  projects = Backlog::ProjectIndex.new.all
  if filter
    projects = projects.select do |path, row|
      [row["name"], row["tracker"], path].any? { |field| field.to_s.include?(filter) }
    end
  end
  puts JSON.pretty_generate({ "projects" => projects })
when "issue-create"
  title = require_flag(rest, "--title", "issue-create")
  body = require_flag(rest, "--body", "issue-create") || ""
  priority = parse_flag(rest, "--priority")
  tracker = require_cwd_tracker("issue-create")
  issues = Backlog::Issues.new(runner: Backlog::Issues::ShellRunner.new, repo_root: Dir.pwd)
  begin
    result = issues.create(tracker: tracker, title: title, body: body, priority: priority)
  rescue ArgumentError, Backlog::Issues::CommandFailed => e
    fail_soft("issue-create: #{e.message}")
  end
  puts JSON.pretty_generate(result.transform_keys(&:to_s))
when "issues"
  tracker = require_cwd_tracker("issues")
  issues = Backlog::Issues.new(runner: Backlog::Issues::ShellRunner.new, repo_root: Dir.pwd)
  begin
    grouped = issues.list(tracker: tracker)
  rescue ArgumentError, Backlog::Issues::CommandFailed => e
    fail_soft("issues: #{e.message}")
  end
  puts JSON.pretty_generate(grouped.transform_keys(&:to_s).transform_values { |v| v.map { |i| i.transform_keys(&:to_s) } })
when nil, "-h", "--help"
  puts <<~USAGE
    usage: os-backlog <command> [options]

    commands:
      resolve   (reads {"repo_path","config"} JSON from stdin)
      inspect                       (detect existing tracking for cwd repo, JSON)
      config-write <tracker-value>  (write tracker key to .cc-os/config + global project index)
      projects [NAME-FILTER]        (print the global project index at ~/.cc-os/projects.json as JSON)
      issue-create --title "..." --body "..." [--priority P0-P3]
                                     (create an issue on the cwd repo's configured tracker)
      issues                        (list open issues on the cwd repo's configured tracker,
                                      grouped by state label, JSON)
  USAGE
  exit(command.nil? ? 1 : 0)
else
  fail_soft("unknown command #{command.inspect}")
end
rescue SystemExit
  raise
rescue StandardError => e
  fail_soft(e.message)
end
