#!/usr/bin/env ruby

class DevHelper
  # Support dashes in command names
  COMMAND_TO_METHOD = {
    "ts-node" => :ts_node,
    "check-types" => :check_types,
    "bash-completions" => :bash_completions,
    "plantuml-to-png" => :plantuml_to_png,
  }
  METHOD_TO_COMMAND = COMMAND_TO_METHOD.invert

  REPLACE_PROCESS = "replace_process"
  WAIT_FOR_PROCESS = "wait_for_process"

  # External Interface
  def self.call(*args)
    new.call(*args)
  end

  # Core logic
  def call(*args, **kwargs)
    command = args[0]
    method = COMMAND_TO_METHOD.fetch(command, command)
    if args.length.positive? && respond_to?(method)
      public_send(method, *args.drop(1), **kwargs)
    else
      compose(*args, **kwargs)
    end
  end

  def compose(*args, **kwargs)
    command = compose_command(*args, **kwargs)
    puts "Running: #{command}" unless kwargs[:slient]

    case kwargs[:execution_mode]
    when WAIT_FOR_PROCESS
      wait_for_process_with_logging(command)
    else
      exec(command)
    end
  end

  # Primary command wrappers
  def build(*args, **kwargs)
    compose(%w[build], *args, **kwargs)
  end

  def compile(*args, **kwargs)
    run(*%w[api npm run build], execution_mode: WAIT_FOR_PROCESS)
    exit($?.exitstatus) unless $?.success?
    run(*%w[web npm run build])
  end

  def up(*args, **kwargs)
    compose(*%w[up --remove-orphans], *args, **kwargs)
  end

  def down(*args, **kwargs)
    compose(*%w[down --remove-orphans], *args, **kwargs)
  end

  def logs(*args, **kwargs)
    compose(*%w[logs -f], *args, **kwargs)
  end

  def run(*args, **kwargs)
    compose(*%w[run --rm], *args, **kwargs)
  end

  def ps(*args, **kwargs)
    compose(*%w[ps], *args, **kwargs)
  end

  # Custom helpers
  def api(*args, **kwargs)
    run(*%w[api], *args, **kwargs)
  end

  def web(*args, **kwargs)
    run(*%w[web], *args, **kwargs)
  end

  def check_types(*args, **kwargs)
    run(*%w[api npm run check-types], *args, **kwargs)
  end

  def test(*args, **kwargs)
    service = args[0]
    if service == "api"
      test_api(*args.drop(1), **kwargs)
    elsif service == "web"
      test_web(*args.drop(1), **kwargs)
    else
      test_api(*args, **kwargs)
    end
  end

  def test_api(*args, **kwargs)
    reformat_project_relative_path_filter_for_vitest!(args, "api/")
    run(*%w[test_api npm run test], *args, **kwargs)
  end

  def test_web(*args, **kwargs)
    reformat_project_relative_path_filter_for_vitest!(args, "web/")
    run(*%w[test_web npm run test], *args, **kwargs)
  end

  def sqlcmd(*args, **kwargs)
    db_host = ENV.fetch('DB_HOST', 'localhost')
    db_user = ENV.fetch('DB_USER', 'sa')
    db_pass = ENV.fetch('DB_PASS', '1m5ecure!')
    db_name = ENV.fetch('DB_NAME', 'YHSI')
    compose(
      *%w[exec db /opt/mssql-tools/bin/sqlcmd],
      *%W[-U #{db_user}],
      *%W[-P #{db_pass}],
      *%W[-H #{db_host}],
      *%W[-d #{db_name}],
      '-I', # enable quoted identifiers, e.g. "table"."column"
      *args,
      **kwargs
    )
  end

  def db(*args, **kwargs)
    compose(*%w[exec db], *args, **kwargs)
  end

  def debug
    api_container_id = container_id("api")
    puts "Waiting for breakpoint to trigger..."
    puts "'ctrl-c' to exit."
    command = "docker attach --detach-keys ctrl-c #{api_container_id}"
    puts "Running: #{command}"
    exec(command)
    exit 0
  end

  def npm(*args, **kwargs)
    run(*%w[api npm], *args, **kwargs)
  end

  def ts_node(*args, **kwargs)
    run(*%w[api npm run ts-node], *args, **kwargs)
  end

  def knex(*args, **kwargs)
    if RUBY_PLATFORM =~ /linux/
      run(*%w[api npm run knex], *args, execution_mode: WAIT_FOR_PROCESS, **kwargs)

      file_or_directory = "#{project_root}/api/src/db/migrations"
      exit(0) unless take_over_needed?(file_or_directory)

      ownit file_or_directory
    else
      run(*%w[api npm run knex], *args, **kwargs)
    end
  end

  def migrate(*args, **kwargs)
    action = args[0]
    knex("migrate:#{action}", *args.drop(1), **kwargs)
  end

  def seed(*args, **kwargs)
    action = args[0]
    knex("seed:#{action}", *args.drop(1), **kwargs)
  end

  def ownit(*args, **kwargs)
    file_or_directory = args[0]
    raise ScriptError, "Must provide a file or directory path." if file_or_directory.nil?

    if RUBY_PLATFORM =~ /linux/
      puts "Take ownership of the file or directory? #{file_or_directory}"
      exec("sudo chown -R #{user_id}:#{group_id} #{file_or_directory}")
    else
      raise NotImplementedError, "Not implement for platform #{RUBY_PLATFORM}"
    end
  end

  def plantuml_to_png(*args, **kwargs)
    file_path = args.pop
    raise ScriptError, "Must provide a file path." if file_path.nil?

    png_path = file_path.gsub(/\.(wsd|pu|puml|plantuml|uml)$/, ".png")

    command = <<~BASH
      curl #{args.join(" ")} \
        --data-binary @'#{file_path}' \
        http://localhost:9999/png > '#{png_path}'
    BASH

    puts "Running: #{command}"
    exec(command)
  end

  def bash_completions
    completions =
      public_methods(false)
        .reject { |word| %i[call].include?(word) }
        .map { |word| METHOD_TO_COMMAND.fetch(word, word) }
    puts completions
  end

  private

  def wait_for_process_with_logging(command)
    IO.popen("#{command} 2>&1") do |io|
      until io.eof?
        line = io.gets
        puts line
      end
    end
  end

  def container_id(container_name, *args, **kwargs)
    command = compose_command(*%w[ps -q], container_name, *args, **kwargs)
    puts "Running: #{command}"
    id_of_container = `#{command}`.chomp
    puts "Container id is: #{id_of_container}"
    id_of_container
  end

  def service_running?(container_name)
    ps(*%w[-q --status=running], execution_mode: WAIT_FOR_PROCESS, slient: true) != ""
  end

  def compose_command(*args, **kwargs)
    environment = kwargs.fetch(:environment, "development")
    "cd #{project_root} && docker compose -f docker-compose.#{environment}.yml #{args.join(" ")}"
  end

  def project_root
    @project_root ||= File.absolute_path("#{__dir__}/..")
  end

  def take_over_needed?(file_or_directory)
    files_owned_by_others =
      system("find #{file_or_directory} -not -user #{user_id} -print -quit | grep -q .")
    files_owned_by_others
  end

  def user_id
    unless RUBY_PLATFORM =~ /linux/
      raise NotImplementedError, "Not implement for platform #{RUBY_PLATFORM}"
    end

    `id -u`.strip
  end

  def group_id
    unless RUBY_PLATFORM =~ /linux/
      raise NotImplementedError, "Not implement for platform #{RUBY_PLATFORM}"
    end

    `id -g`.strip
  end

  def reformat_project_relative_path_filter_for_vitest!(args, prefix)
    if args.length.positive? && args[0].start_with?(prefix)
      src_path_prefix = "#{prefix}src/"
      test_path_regex = Regexp.escape(prefix)
      src_path_regex = Regexp.escape(src_path_prefix)

      if args[0].start_with?(src_path_prefix)
        # TODO: handle other file types
        args[0] = args[0].gsub(/^#{src_path_regex}/, "tests/").gsub(/\.ts$/, ".test.ts")
      else
        args[0] = args[0].gsub(/^#{test_path_regex}/, "")
      end

      puts "Reformatted path filter from project relative to service relative for vitest."
    end
  end
end

# Only execute main function when file is executed
DevHelper.call(*ARGV) if $PROGRAM_NAME == __FILE__

## Dev completions
# https://iridakos.com/programming/2018/03/01/bash-programmable-completion-tutorial
# _dev_completions () {
#   local dev_command_path="$(which dev)"
#   local dev_function_names
#   dev_function_names="$(ruby "$dev_command_path" bash_completions)"
#   # COMP_WORDS: an array of all the words typed after the name of the program the compspec belongs to
#   # COMP_CWORD: an index of the COMP_WORDS array pointing to the word the current cursor is at - in other words, the index of the word the cursor was when the tab key was pressed
#   # COMP_LINE: the current command line
#   COMPREPLY=($(compgen -W "$dev_function_names" "${COMP_WORDS[$COMP_CWORD]}"))
# }

# complete -F _dev_completions dev
# complete -W "allow" direnv
