class ChefCLI::PolicyfileServices::ExportRepo

def archive?

def archive?
  @archive
end

def archive_file_location

def archive_file_location
  return nil unless archive?
  filename = "#{policyfile_lock.name}-#{policyfile_lock.revision_id}.tgz"
  File.join(export_dir, filename)
end

def assert_export_dir_clean!

def assert_export_dir_clean!
  if !force_export? && !conflicting_fs_entries.empty? && !archive?
    msg = "Export dir (#{export_dir}) not clean. Refusing to export. (Conflicting files: #{conflicting_fs_entries.join(", ")})"
    raise ExportDirNotEmpty, msg
  end
end

def assert_lockfile_exists!

def assert_lockfile_exists!
  unless File.exist?(policyfile_lock_expanded_path)
    raise LockfileNotFound, "No lockfile at #{policyfile_lock_expanded_path} - you need to run `install` before `push`"
  end
end

def chefignore_for(cookbook_path)

def chefignore_for(cookbook_path)
  Chef::Cookbook::Chefignore.new(File.join(cookbook_path, "chefignore"))
end

def client_rb_staging_path

def client_rb_staging_path
  File.join(dot_chef_staging_dir, "config.rb")
end

def conflicting_fs_entries

def conflicting_fs_entries
  Dir.glob(File.join(cookbook_artifacts_dir, "*")) +
    Dir.glob(File.join(policies_dir, "*")) +
    Dir.glob(File.join(policy_groups_dir, "*")) +
    Dir.glob(File.join(export_dir, "Policyfile.lock.json"))
end

def cookbook_artifacts_dir

def cookbook_artifacts_dir
  File.join(export_dir, "cookbook_artifacts")
end

def cookbook_artifacts_staging_dir

def cookbook_artifacts_staging_dir
  File.join(staging_dir, "cookbook_artifacts")
end

def cookbook_files_to_copy(cookbook_path)

def cookbook_files_to_copy(cookbook_path)
  cookbook = cookbook_loader_for(cookbook_path).cookbook_version
  root = Pathname.new(cookbook.root_dir)
  cookbook.all_files.map do |full_path|
    Pathname.new(full_path).relative_path_from(root).to_s
  end
end

def cookbook_loader_for(cookbook_path)

def cookbook_loader_for(cookbook_path)
  loader = Chef::Cookbook::CookbookVersionLoader.new(cookbook_path, chefignore_for(cookbook_path))
  loader.load!
  loader
end

def copy_cookbook(lock)

def copy_cookbook(lock)
  dirname = "#{lock.name}-#{lock.identifier}"
  export_path = File.join(staging_dir, "cookbook_artifacts", dirname)
  metadata_rb_path = File.join(export_path, "metadata.rb")
  FileUtils.mkdir(export_path) unless File.directory?(export_path)
  copy_unignored_cookbook_files(lock, export_path)
  FileUtils.rm_f(metadata_rb_path)
  if lock.cookbook_version.nil?
    ui.msg "Unable to get the cookbook version/metadata for #{lock}"
  end
  metadata = lock.cookbook_version.metadata
  metadata_json_path = File.join(export_path, "metadata.json")
  File.open(metadata_json_path, "wb+") do |f|
    f.print(FFI_Yajl::Encoder.encode(metadata.to_hash, pretty: true ))
  end
end

def copy_cookbooks

def copy_cookbooks
  policyfile_lock.cookbook_locks.each do |name, lock|
    copy_cookbook(lock)
  end
end

def copy_policyfile_lock

def copy_policyfile_lock
  File.open(lockfile_staging_path, "wb+") do |f|
    f.print(FFI_Yajl::Encoder.encode(policyfile_lock.to_lock, pretty: true ))
  end
end

def copy_unignored_cookbook_files(lock, export_path)

def copy_unignored_cookbook_files(lock, export_path)
  cookbook_files_to_copy(lock.cookbook_path).each do |rel_path|
    full_source_path = File.join(lock.cookbook_path, rel_path)
    full_dest_path = File.join(export_path, rel_path)
    dest_dirname = File.dirname(full_dest_path)
    FileUtils.mkdir_p(dest_dirname) unless File.directory?(dest_dirname)
    FileUtils.cp(full_source_path, full_dest_path)
  end
end

def create_archive

def create_archive
  Dir.chdir(staging_dir) do
    targets = Find.find(".").collect { |e| e }
    Mixlib::Archive.new(archive_file_location).create(targets, gzip: true)
  end
end

def create_client_rb

def create_client_rb
  File.open(client_rb_staging_path, "wb+") do |f|
    f.print( <<~CONFIG )
      ### Chef Infra Client Configuration ###
      # The settings in this file will configure chef to apply the exported policy in
      # this directory. To use it, run:
      #
      # chef-client -z
      #
      policy_name '#{policy_name}'
      policy_group '#{policy_group}'
      use_policyfile true
      policy_document_native_api true
      # In order to use this repo, you need a version of Chef Infra Client and Chef Zero
      # that supports policyfile "native mode" APIs:
      current_version = Gem::Version.new(Chef::VERSION)
      unless Gem::Requirement.new(">= 12.7").satisfied_by?(current_version)
        puts("!" * 80)
        puts(<<-MESSAGE)
      This Chef Repo requires features introduced in Chef Infra Client 12.7, but you are using
      Chef \#{Chef::VERSION}. Please upgrade to Chef Infra Client 12.7 or later.
      MESSAGE
        puts("!" * 80)
        exit!(1)
      end
    CONFIG
  end
end

def create_policy_group_repo_item

def create_policy_group_repo_item
  data = {
    "policies" => {
      policyfile_lock.name => {
        "revision_id" => policyfile_lock.revision_id,
      },
    },
  }
  File.open(policy_group_repo_item_path, "wb+") do |f|
    f.print(FFI_Yajl::Encoder.encode(data, pretty: true ))
  end
end

def create_policyfile_repo_item

def create_policyfile_repo_item
  File.open(policyfile_repo_item_path, "wb+") do |f|
    f.print(FFI_Yajl::Encoder.encode(policyfile_lock.to_lock, pretty: true ))
  end
end

def create_readme_md

def create_readme_md
  File.open(readme_staging_path, "wb+") do |f|
    f.print( <<~README )
      # Exported Chef Infra Repository for Policy '#{policy_name}'
      Policy revision: #{policyfile_lock.revision_id}
      This directory contains all the cookbooks and configuration necessary for Chef
      to converge a system using this exported policy. To converge a system with the
      exported policy, use a privileged account to run `chef-client -z` from the
      directory containing the exported policy.
      ## Contents:
      ### Policyfile.lock.json
      A copy of the exported policy, used by the `chef push-archive` command.
      ### .chef/config.rb
      A configuration file for Chef Infra Client. This file configures Chef Infra Client to
      use the correct `policy_name` and `policy_group` for this exported repository. Chef
      Infra Client will use this configuration automatically if you've set your working
      directory properly.
      ### cookbook_artifacts/
      All of the cookbooks required by the policy will be stored in this directory.
      ### policies/
      A different copy of the exported policy, used by the `chef-client` command.
      ### policy_groups/
      Policy groups are used by Chef Infra Server to manage multiple revisions of the same
      policy. The default "local" policy is recommended for export use since there can be
      no different revisions when not utilizing a server.
    README
  end
end

def create_repo_structure

def create_repo_structure
  FileUtils.mkdir_p(export_dir)
  FileUtils.mkdir_p(dot_chef_staging_dir)
  FileUtils.mkdir_p(cookbook_artifacts_staging_dir)
  FileUtils.mkdir_p(policies_staging_dir)
  FileUtils.mkdir_p(policy_groups_staging_dir)
end

def dot_chef_dir

def dot_chef_dir
  File.join(export_dir, ".chef")
end

def dot_chef_staging_dir

def dot_chef_staging_dir
  File.join(staging_dir, ".chef")
end

def export

def export
  with_staging_dir do
    create_repo_structure
    copy_cookbooks
    create_policyfile_repo_item
    create_policy_group_repo_item
    copy_policyfile_lock
    create_client_rb
    create_readme_md
    if archive?
      create_archive
    else
      mv_staged_repo
    end
  end
rescue => error
  msg = "Failed to export policy (in #{policyfile_filename}) to #{export_dir}"
  raise PolicyfileExportRepoError.new(msg, error)
end

def force_export?

def force_export?
  @force_export
end

def initialize(policyfile: nil, export_dir: nil, root_dir: nil, archive: false, force: false, policy_group: nil)

def initialize(policyfile: nil, export_dir: nil, root_dir: nil, archive: false, force: false, policy_group: nil)
  @root_dir = root_dir
  @export_dir = File.expand_path(export_dir)
  @archive = archive
  @force_export = force
  @ui = UI.new
  @policy_data = nil
  @policyfile_lock = nil
  @policy_group = policy_group
  @policy_group ||= "local".freeze
  policyfile_rel_path = policyfile || "Policyfile.rb"
  policyfile_full_path = File.expand_path(policyfile_rel_path, root_dir)
  @storage_config = Policyfile::StorageConfig.new.use_policyfile(policyfile_full_path)
  @staging_dir = nil
end

def lockfile_staging_path

def lockfile_staging_path
  File.join(staging_dir, "Policyfile.lock.json")
end

def mv_staged_repo

def mv_staged_repo
  # If we got here, either these dirs are empty/don't exist or force is
  # set to true.
  FileUtils.rm_rf(cookbook_artifacts_dir)
  FileUtils.rm_rf(policies_dir)
  FileUtils.rm_rf(policy_groups_dir)
  FileUtils.rm_rf(dot_chef_dir)
  FileUtils.mv(cookbook_artifacts_staging_dir, export_dir)
  FileUtils.mv(policies_staging_dir, export_dir)
  FileUtils.mv(policy_groups_staging_dir, export_dir)
  FileUtils.mv(lockfile_staging_path, export_dir)
  FileUtils.mv(dot_chef_staging_dir, export_dir)
  FileUtils.mv(readme_staging_path, export_dir)
end

def policies_dir

def policies_dir
  File.join(export_dir, "policies")
end

def policies_staging_dir

def policies_staging_dir
  File.join(staging_dir, "policies")
end

def policy_data

def policy_data
  @policy_data ||= FFI_Yajl::Parser.parse(IO.read(policyfile_lock_expanded_path))
rescue => error
  raise PolicyfileExportRepoError.new("Error reading lockfile #{policyfile_lock_expanded_path}", error)
end

def policy_group_repo_item_path

def policy_group_repo_item_path
  File.join(staging_dir, "policy_groups", "#{policy_group}.json")
end

def policy_groups_dir

def policy_groups_dir
  File.join(export_dir, "policy_groups")
end

def policy_groups_staging_dir

def policy_groups_staging_dir
  File.join(staging_dir, "policy_groups")
end

def policy_name

def policy_name
  policyfile_lock.name
end

def policyfile_lock

def policyfile_lock
  @policyfile_lock || validate_lockfile
end

def policyfile_repo_item_path

def policyfile_repo_item_path
  basename = "#{policyfile_lock.name}-#{policyfile_lock.revision_id}"
  File.join(staging_dir, "policies", "#{basename}.json")
end

def readme_staging_path

def readme_staging_path
  File.join(staging_dir, "README.md")
end

def run

def run
  assert_lockfile_exists!
  assert_export_dir_clean!
  validate_lockfile
  write_updated_lockfile
  export
end

def staging_dir

def staging_dir
  @staging_dir
end

def validate_lockfile

def validate_lockfile
  return @policyfile_lock if @policyfile_lock
  @policyfile_lock = ChefCLI::PolicyfileLock.new(storage_config).build_from_lock_data(policy_data)
  # TODO: enumerate any cookbook that have been updated
  @policyfile_lock.validate_cookbooks!
  @policyfile_lock
rescue PolicyfileExportRepoError
  raise
rescue => error
  raise PolicyfileExportRepoError.new("Invalid lockfile data", error)
end

def with_staging_dir

def with_staging_dir
  require "securerandom" unless defined?(SecureRandom)
  random_string = SecureRandom.hex(2)
  path = "chef-export-#{random_string}"
  Dir.mktmpdir(path) do |d|
    begin
      @staging_dir = d
      yield
    ensure
      @staging_dir = nil
    end
  end
end

def write_updated_lockfile

def write_updated_lockfile
  File.open(policyfile_lock_expanded_path, "wb+") do |f|
    f.print(FFI_Yajl::Encoder.encode(policyfile_lock.to_lock, pretty: true ))
  end
end