# frozen_string_literal: true

class CellManager
  include GDK::CoreHelper::DeepHash

  PORT_BASE_OFFSET = 12_000
  PORT_OFFSET = 150
  LEGACY_CELL_ID = 1
  LEGACY_CELL_SEQUENCE_MAXVAL = (2**63) - 1
  DEFAULT_SEQUENCE_RANGE = 100_000_000_000

  AUTO_GENERATED_HEADER = <<~TEXT
    # This configuration was generated by `gdk cells up` on %<date>s.
    #
    # Do not change this file!
    # Change `%<main_gdk>s` instead and rerun the command.
  TEXT

  Result = Struct.new('Result', :success?)

  def up
    return true unless enabled?

    cells.all? do |cell|
      break unless gdk_build(cell)

      sh = run_in_cell(cell[:id], %w[reconfigure], skip_directory_check: true)
      if sh.success?
        GDK::Output.success("Cell #{cell[:id]} is ready.")
      else
        GDK::Output.error("Cell #{cell[:id]} failed to reconfigure.")
      end

      sh.success?
    end
  end

  def update
    return true unless enabled?

    cells.all? do |cell|
      GDK::Output.info("Updating cell #{cell[:id]}")
      run_in_cell(cell[:id], %w[update]).success?
    end
  end

  def start
    return true unless enabled?

    cells.all? do |cell|
      run_in_cell(cell[:id], %w[start]).success?
    end
  end

  def stop
    return true unless enabled?

    cells.all? do |cell|
      run_in_cell(cell[:id], %w[stop]).success?
    end
  end

  def restart
    return true unless enabled?

    cells.all? do |cell|
      run_in_cell(cell[:id], %w[restart]).success?
    end
  end

  def status
    return true unless enabled?

    cells.all? do |cell|
      GDK::Output.info("cell-#{cell[:id]}")
      run_in_cell(cell[:id], %w[status]).success?
    end
  end

  def run_in_cell(cell_id, args, quiet: false, skip_directory_check: false)
    return Result.new(false) unless enabled?

    cell, _index = find_cell(cell_id)
    unless cell
      GDK::Output.error("Cell #{cell_id} not found. #{help_cell_list}")
      return Result.new(false)
    end

    dir = directory_for_cell(cell)
    unless skip_directory_check || Dir.exist?(dir)
      GDK::Output.error("Cell #{cell[:id]} doesn’t exist yet, run `gdk cells up` first.")
      return Result.new(false)
    end

    opts = { chdir: dir }
    sh = GDK::Shellout.new("gdk #{args.join ' '};", **opts)
    sh.execute(display_output: !quiet)

    sh
  end

  def enabled?
    GDK.config.cells.enabled
  end

  def cell_exist?(id)
    !!find_cell(id)
  end

  def get_config_for(id)
    cell = find_cell(id)
    raise "No config for cell `#{id}` found" unless cell

    cell_gdk_root = Pathname.new(directory_for_cell(cell))
    cell_gdk_config_path = cell_gdk_root.join('gdk.yml')

    merged_cell_config(cell_gdk_config_path, cell)
  end

  private

  def find_cell(id)
    cells.find { |cell| cell[:id] == id }
  end

  def port_offset_for(index)
    PORT_BASE_OFFSET + (index * PORT_OFFSET)
  end

  def gdk_build(cell)
    cell_gdk_root = directory_for_cell(cell)

    if Dir.exist?(cell_gdk_root)
      write_cell_config(cell_gdk_root, cell)
    else
      GDK::Output.info("Cloning into GDK for #{cell[:id]}")
      sh = GDK::Shellout.new(*%W[git clone #{GDK.root} #{cell_gdk_root}])
      sh.execute
      return false unless sh.success?

      sh = GDK::Shellout.new(*%w[git remote get-url origin], chdir: GDK.root)
      sh.execute
      original_origin = sh.success? ? sh.read_stdout.chomp : 'https://gitlab.com/gitlab-org/gitlab-development-kit.git'

      sh = GDK::Shellout.new(*%W[git remote set-url origin #{original_origin}], chdir: cell_gdk_root)
      sh.execute
      return false unless sh.success?

      write_cell_config(cell_gdk_root, cell)

      return false unless gdk_install(cell)
    end

    true
  end

  def write_cell_config(cell_gdk_root, cell)
    cell_gdk_config_path = "#{cell_gdk_root}/gdk.yml"
    # Always start with an empty file
    FileUtils.rm_f(cell_gdk_config_path)

    config = merged_cell_config(cell_gdk_config_path, cell)
    config.save_yaml!

    header = format(AUTO_GENERATED_HEADER, date: Time.now, main_gdk: GDK::Config::FILE)
    File.write(cell_gdk_config_path, header + File.read(cell_gdk_config_path))
  end

  def merged_cell_config(cell_gdk_config_path, cell)
    yaml = deep_merge(main_gdk_config_yaml, cell_instance_config(cell))

    config = cell_config_for(cell_gdk_config_path, yaml)
    config.validate!

    config
  end

  def main_gdk_config_yaml
    yaml = GDK.config.yaml.except('cells')
    # deep dup to avoid side-effects
    JSON.parse(JSON.dump(yaml))
  end

  # Returns a config value:
  # - Cell-specific value if it's defined in cell's gdk.yml
  # - Main's value otherwise if +fallback_value+ is a GDK::Config
  # - Passed +fallback_value+ otherwise
  def value_for(slug, config, fallback_value)
    if config.user_defined?(slug)
      config.dig(slug) # rubocop:disable Style/SingleArgumentDig -- That's a different dig
    else
      fallback_value.dig(slug) # rubocop:disable Style/SingleArgumentDig -- That's a different dig
    end
  end

  def cell_instance_config(cell)
    yaml = cell.config || {}

    # Ensure that old/non-synced cells GDKs still work as expected
    # See https://gitlab.com/gitlab-org/gitlab-development-kit/-/issues/2309
    yaml['port_offset'] = port_offset_for(cell.__index)
    yaml['cells'] ||= {}
    yaml['cells']['port_offset'] = yaml['port_offset']

    config = cell.root.class.new(yaml: yaml).tap(&:validate!)
    config.dump!(user_only: true)
  end

  def gdk_install(cell)
    pathname = directory_for_cell(cell)
    return if Dir.exist?("#{pathname}/gitlab")

    gitlab_repo = "#{GDK.root}/gitlab"
    # Force shell invocation by adding ';'
    # This is necessary to make `gdk` command work in the right directory.
    # TODO use YAML value, or default
    cmd = "gdk install gitlab_repo=#{gitlab_repo};"
    sh = GDK::Shellout.new(cmd, chdir: pathname)
    sh.execute(display_output: true)

    raise 'Failed to gdk install' unless sh.success?

    true
  end

  def help_cell_list
    return 'Check doc/howto/cells.md on how to add local cell instances.' if cells.empty?

    "Found: #{cells.map(&:id).join(', ')}."
  end

  def cells
    GDK.config.cells.instances.elems
  end

  def directory_for_cell(cell)
    "#{GDK.root}/gitlab-cells/cell-#{cell[:id]}"
  end

  def cell_config_for(gdk_config_path, yaml)
    # Currently, there's no good way of passing GDK root and config path to config
    # so we override constants and assign YAML just for Cells.
    # See https://gitlab.com/gitlab-org/gitlab-development-kit/-/merge_requests/4027#note_2148267092
    klass = Class.new(GDK::Config)
    klass.const_set(:FILE, gdk_config_path)
    # Avoid messing up the superclass (i.e. `GDK::Config`)
    klass.attributes = GDK::Config.attributes.dup

    config = klass.load_from_file
    config.instance_variable_set(:@yaml, yaml)
    config
  end
end
