#!/usr/bin/env ruby
# encoding: UTF-8
# frozen_string_literal: true

# Usage: write-config <key> <value>
#        write-config <key> --push <value>
# Writes a value into the GLOBAL ~/.plastic/config.yml at a dot-notation key,
# creating intermediate hashes as needed. read-config's missing write-side
# counterpart. --push appends into an array at that key instead of overwriting
# it (deduped, creating the array if absent) -- used for dismissal lists like
# config_asks_dismissed.
#
# Concurrency: this is the sole intended writer of the owner's live
# config.yml, and it can be invoked by more than one agent session at once.
# The read-modify-write is guarded by an exclusive flock on a sibling lock
# file held across the whole critical section, and the write itself is a
# write-to-temp-then-rename in the same directory, so a reader never observes
# a half-written file and two concurrent writers never lose one another's key.
#
# Safety: if config.yml exists but cannot be parsed, or parses into something
# that is not a key/value mapping, this script refuses to write at all and
# exits non-zero. Silently treating a broken config.yml as an
# empty hash (the way read-config, a read-only script, safely can) would
# rewrite the owner's config.yml from nothing and destroy whatever was in it;
# a guard must fail milder than the bug it guards against, not worse.
#
# Environment:
#   PLASTIC_HOME -- override ~/.plastic (for testing)

require "yaml"
require "json"
require "fileutils"

def usage_abort
  $stderr.puts "Usage: write-config <key> <value>\n       write-config <key> --push <value>"
  exit 1
end

argv = ARGV.dup
push = false
if (idx = argv.index("--push"))
  push = true
  argv.delete_at(idx)
end

key, raw_value = argv
usage_abort if key.nil? || key.empty? || raw_value.nil?

def coerce(raw)
  JSON.parse(raw)
rescue JSON::ParserError
  raw
end

value = coerce(raw_value)

global_root = ENV.fetch("PLASTIC_HOME", File.expand_path("~/.plastic"))
config_path = File.join(global_root, "config.yml")
lock_path = "#{config_path}.lock"

FileUtils.mkdir_p(global_root)

File.open(lock_path, File::CREAT | File::RDWR, 0o644) do |lock_file|
  lock_file.flock(File::LOCK_EX)

  config =
    if File.exist?(config_path)
      begin
        YAML.safe_load(File.read(config_path)) || {}
      rescue StandardError => e
        $stderr.puts "write-config: refusing to overwrite #{config_path}: could not parse it (#{e.message})"
        exit 1
      end
    else
      {}
    end

  # A config.yml that parses cleanly but is not a mapping (a bare string, a
  # top-level list) would otherwise reach the assignment below and die with a
  # raw IndexError or TypeError backtrace. Same refusal, same clean message:
  # an unusable config.yml is never written over, and never merely confusing.
  unless config.is_a?(Hash)
    $stderr.puts "write-config: refusing to overwrite #{config_path}: it does not hold a key/value mapping (found #{config.class})"
    exit 1
  end

  keys = key.split(".")
  leaf = keys.pop
  node = keys.reduce(config) { |acc, k| acc[k] ||= {} }

  if push
    current = node[leaf]
    current = [] unless current.is_a?(Array)
    current << value unless current.include?(value)
    node[leaf] = current
  else
    node[leaf] = value
  end

  tmp_path = "#{config_path}.tmp.#{Process.pid}.#{Time.now.to_f}"
  File.write(tmp_path, YAML.dump(config))
  File.rename(tmp_path, config_path)

  puts "#{key} = #{node[leaf].inspect}"
end
