#!/usr/bin/env ruby
# frozen_string_literal: true
#
# os-backlog CLI: board-ensure, activate, archive, resolve, card-add,
# cards, snapshot, inspect, config-write.
#
# Usage:
#   os-backlog board-ensure NAME --project Dev|Clients
#   os-backlog activate NAME
#   os-backlog archive NAME --yes
#   os-backlog resolve < input.json
#     input.json: {"repo_path": "...", "config": "..."|null, "boards": ["...", ...]}
#   os-backlog card-add --board NAME --title "..." [--description "..."]
#   os-backlog card-move --board NAME --card ID --to COLUMN
#   os-backlog cards --board NAME              (human-readable per-list card listing)
#   os-backlog snapshot --board NAME           (full board state as JSON; read-only)
#   os-backlog inspect                         (detect existing tracking for cwd repo, JSON)
#   os-backlog config-write <tracker-value>    (write tracker key to .cc-os/config)
#
# board-ensure/activate/archive talk to Planka over the network (the
# installed planka-api gem, credentials from PLANKA_BASE_URL/USERNAME/
# PASSWORD). resolve, config-write are pure — no network access at all.
# inspect touches the network/CLIs opportunistically but fails soft
# field-by-field rather than aborting.
#
# Fails soft: if the gem or Planka credentials are unavailable, prints one
# clear error and exits nonzero. Never destructive.

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

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

def build_client
  require "planka_api"
  client = Planka::Client.new
  client.login
  client
rescue LoadError
  fail_soft("the planka-api gem is not installed/loadable; cannot reach Planka")
rescue StandardError => e
  fail_soft("could not authenticate to Planka: #{e.message}")
end

def parse_project_flag(args)
  idx = args.index("--project")
  return "Dev" unless idx

  args.delete_at(idx)
  project = args.delete_at(idx)
  project || fail_soft("--project requires a value (Dev or Clients)")
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 Planka check for `inspect`: does an existing board already
# match this repo per the resolver's own conventions. Never fails the whole
# `inspect` command — reports unavailability instead.
def inspect_planka(repo_path)
  require "planka_api"
  client = Planka::Client.new
  client.login
  boards = client.projects.list.flat_map { |project| client.boards.list(project.id).map(&:name) }
  resolver = Backlog::Resolver.new(repo_path: repo_path, config_contents: nil, boards: boards)
  { "checked" => true, "decision" => resolver.resolve_string }
rescue LoadError
  { "checked" => false, "reason" => "planka-api gem not installed" }
rescue StandardError => e
  { "checked" => false, "reason" => e.message }
end

# Best-effort open-issue count via tea (Forgejo) or gh (GitHub), whichever
# the detected remote implies. Fails soft per-field.
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" }
  end

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

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

  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 }
end

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

  out = `gh issue list --repo #{repo_slug} --state open --json number 2>/dev/null`
  parsed = JSON.parse(out)
  { "tool" => "gh", "open_count" => parsed.size }
rescue JSON::ParserError
  { "tool" => "gh", "open_count" => nil, "reason" => "could not parse gh output" }
end

command, *rest = ARGV

begin
case command
when "board-ensure"
  name = rest.shift
  fail_soft("board-ensure requires a board name") unless name
  project = parse_project_flag(rest)
  client = build_client
  result = Backlog::BoardEnsurer.new(client: client).ensure(name, project_name: project)
  puts "board #{result.board.name} (#{result.created ? 'created' : 'existing'}) " \
       "in project #{project}; lists created: #{result.lists_created.join(', ')}; " \
       "labels created: #{result.labels_created.join(', ')}"
when "activate"
  name = rest.shift
  fail_soft("activate requires a board name") unless name
  client = build_client
  board = Backlog::BoardEnsurer.new(client: client).activate(name)
  puts "activated: #{board.name}"
when "archive"
  name = rest.shift
  fail_soft("archive requires a board name") unless name
  fail_soft("archive requires explicit --yes (human go-ahead only)") unless rest.include?("--yes")

  client = build_client
  board = Backlog::BoardEnsurer.new(client: client).archive(name)
  puts "archived: #{board.name}"
when "resolve"
  input = JSON.parse($stdin.read)
  resolver = Backlog::Resolver.new(
    repo_path: input.fetch("repo_path"),
    config_contents: input["config"],
    boards: input.fetch("boards", [])
  )
  puts resolver.resolve_string
when "card-add"
  board = require_flag(rest, "--board", "card-add")
  title = require_flag(rest, "--title", "card-add")
  description = parse_flag(rest, "--description")
  client = build_client
  card = Backlog::Cards.new(client: client).add(board_name: board, title: title, description: description)
  puts "created card ##{card.id} at Backlog on #{board}: #{card.name}"
when "card-move"
  board = require_flag(rest, "--board", "card-move")
  card_id = require_flag(rest, "--card", "card-move")
  to = require_flag(rest, "--to", "card-move")
  client = build_client
  card = Backlog::Cards.new(client: client).move(board_name: board, card_id: card_id, to: to)
  puts "moved card ##{card.id} to #{to} on #{board}"
when "cards"
  board = require_flag(rest, "--board", "cards")
  client = build_client
  snapshot = Backlog::Cards.new(client: client).snapshot(board_name: board)
  snapshot["lists"].each do |list|
    puts "#{list['name']} (#{list['cards'].size})"
    list["cards"].each do |card|
      labels = card["labels"].empty? ? "" : " [#{card['labels'].join(', ')}]"
      puts "  - #{card['name']}#{labels}"
    end
  end
when "snapshot"
  board = require_flag(rest, "--board", "snapshot")
  client = build_client
  puts JSON.pretty_generate(Backlog::Cards.new(client: client).snapshot(board_name: board))
when "card-label"
  board = require_flag(rest, "--board", "card-label")
  card_id = require_flag(rest, "--card", "card-label")
  label = require_flag(rest, "--label", "card-label")
  client = build_client
  Backlog::Cards.new(client: client).attach_label(board_name: board, card_id: card_id, label: label)
  puts "labeled card ##{card_id} with #{label}"
when "card-comment"
  card_id = require_flag(rest, "--card", "card-comment")
  text = require_flag(rest, "--text", "card-comment")
  client = build_client
  comment = Backlog::Cards.new(client: client).comment(card_id: card_id, text: text)
  puts "commented on card ##{card_id} (comment ##{comment.id})"
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)

  findings = {
    "repo_path" => repo_path,
    "tracker_configured" => config.tracker,
    "planka" => inspect_planka(repo_path),
    "git_remote" => remote_info ? remote_info.transform_keys(&:to_s) : nil,
    "issue_cli" => inspect_issue_cli(remote_info),
    "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
  unless Backlog::Tracker.valid?(value)
    fail_soft("invalid tracker value #{value.inspect}; expected one of " \
              "planka:<board> | 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)
  puts "tracker set to #{value} in #{config_path}"
when nil, "-h", "--help"
  puts <<~USAGE
    usage: os-backlog <command> [options]

    commands:
      board-ensure NAME --project Dev|Clients
      activate NAME
      archive NAME --yes
      resolve   (reads {"repo_path","config","boards"} JSON from stdin)
      card-add --board NAME --title "..." [--description "..."]
      card-move --board NAME --card ID --to COLUMN
      cards --board NAME
      snapshot --board NAME
      card-label --board NAME --card ID --label NAME
      card-comment --card ID --text "..."
      inspect                       (detect existing tracking for cwd repo, JSON)
      config-write <tracker-value>  (write tracker key to .cc-os/config)
  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
