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 'local'
      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. However, exported policies contain only a single policy revision, so
      this policy group name is hardcoded to "local" and should not be changed.
    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)

def initialize(policyfile: nil, export_dir: nil, root_dir: nil, archive: false, force: false)
  @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
  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", "local.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
  p = Process.pid
  t = Time.new.utc.strftime("%Y%m%d%H%M%S")
  Dir.mktmpdir("chefcli-export-#{p}-#{t}") 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