# frozen_string_literal: true

require 'fileutils'
require 'open3'

require_relative 'opened_docs'
require_relative 'preview'
require_relative 'sh'
require_relative 'shell_repo'

##
# Helper class for initiating a conversion and dealing with the results.
class Dest
  include Sh

  ##
  # Stdout and stderr of running the conversions in convert_single
  # or convert_all.
  attr_reader :convert_outputs

  ##
  # Status of the conversions. If any conversion fails it'll raise an error
  # unless called with expect_failure. If it is called with expect_failure
  # then it'll fail if there *isn't* a failure.
  attr_reader :convert_statuses

  def initialize(tmp)
    @repos_cache = File.expand_path 'repos', tmp
    @bare_dest = File.expand_path 'dest.git', tmp
    @dest = File.expand_path 'dest', tmp
    Dir.mkdir @dest
    @initialized_bare_repo = false
    @convert_outputs = []
    @convert_statuses = []
    @init_from_shell = true
  end

  ##
  # Expands a path relative to the destination.
  def path(relative_path)
    File.expand_path relative_path, @dest
  end

  ##
  # Create the fluent builder that you can use to convert a single book.
  def prepare_convert_single(from, to)
    ConvertSingle.new from, to, self
  end

  def prepare_convert_all(conf)
    ConvertAll.new conf, @repos_cache, bare_repo, self
  end

  ##
  # Checks out the results of the last call to convert_all
  def checkout_conversion(branch: nil)
    branch_cmd = ''
    branch_cmd = "--branch #{branch} " if branch
    sh "git clone #{branch_cmd}#{bare_repo} #{@dest}"
  end

  ##
  # Executes `git show`.
  def commit_info
    Dir.chdir bare_repo do
      sh 'git show'
    end
  end

  ##
  # Executes `git show` for a single file.
  def commit_info_for_file(file)
    Dir.chdir bare_repo do
      sh "git show -- #{file}"
    end
  end

  ##
  # Start the preview service.
  def start_preview
    Preview.new(bare_repo)
  end

  ##
  # Start the preview service in air gapped mode.
  def start_air_gapped
    # The air gapped build expects the built docs to be *exactly* where the
    # Dockerfile puts them. So we put them there too.
    FileUtils.rm_rf '/docs_build/.repos/target_repo.git'
    FileUtils.mkdir_p '/docs_build/.repos'
    FileUtils.cp_r bare_repo, '/docs_build/.repos/target_repo.git'
    Preview.new(bare_repo, air_gapped: true)
  end

  def remove_target_brach(branch_name)
    Dir.chdir bare_repo do
      sh "git branch -D #{branch_name}"
    end
  end

  ##
  # The location of the bare repository. The first time this is called in a
  # given context the bare repository is initialized
  def bare_repo
    unless @initialized_bare_repo
      if @init_from_shell
        ShellRepo.build!
        sh "git clone --bare /tmp/shell #{@bare_dest}"
      else
        sh "git init --bare #{@bare_dest}"
      end
      @initialized_bare_repo = true
    end
    @bare_dest
  end

  ##
  # Should the bare repository that holds the destination docs be initailized
  # from a "shell" repository for speed or not to actually test adding
  # everything to an empty repository
  def init_from_shell=(init_from_shell)
    if @initialized_bare_repo
      raise "can't change initialization after initialized"
    end

    @init_from_shell = init_from_shell
  end

  def run_convert(env, cmd, expect_failure)
    cmd.unshift '/docs_build/build_docs.pl', '--in_standard_docker'
    # Use popen here instead of capture to keep stdin open to appease the
    # docker-image-always-removed paranoia in build_docs.pl
    _stdin, out, wait_thr = Open3.popen2e(env, *cmd)
    status = wait_thr.value
    out = out.read.force_encoding 'UTF-8'
    ok = status.success?
    ok = !ok if expect_failure
    raise_status cmd, out, status unless ok
    check_for_perl_warnings out
    save_conversion out, status.exitstatus
  end

  def check_for_perl_warnings(out)
    raise "Perl warnings:\n#{out}" if out.include? 'Use of uninitialized value'
    raise "Perl warnings:\n#{out}" if out.include? 'masks earlier declaration'
  end

  def save_conversion(out, status)
    @convert_outputs << out
    @convert_statuses << status
  end

  def run_convert_and_open(cmd, uses_preview)
    cmd.unshift '/docs_build/build_docs.pl', '--in_standard_docker'
    cmd += ['--open']
    OpenedDocs.new cmd, uses_preview
  end

  class CmdBuilder
    def initialize
      @env = {}
    end

    def open
      raise 'env unsupported' unless @env.empty?

      @dest.run_convert_and_open @cmd, uses_preview
    end

    def node_name(node_name)
      @env['NODE_NAME'] = node_name
      self
    end

    def convert(expect_failure: false)
      @dest.run_convert @env, @cmd, expect_failure
    end
  end

  class ConvertSingle < CmdBuilder
    def initialize(from, to, dest)
      super()
      @cmd = %W[
        --doc #{from}
        --out #{dest.path(to)}
      ]
      @dest = dest
    end

    def suppress_migration_warnings
      @cmd += ['--suppress_migration_warnings']
      self
    end

    def alternatives(source_lang, dest_lang, dir)
      @cmd += ['--alternatives', "#{source_lang}:#{dest_lang}:#{dir}"]
      self
    end

    def single
      @cmd += ['--single']
      self
    end

    def uses_preview
      true
    end
  end

  class ConvertAll < CmdBuilder
    def initialize(conf, repos_cache, target_repo, dest)
      super()
      @cmd = %W[
        --all
        --push
        --reposcache #{repos_cache}
        --target_repo #{target_repo}
        --conf #{conf}
      ]
      @dest = dest
    end

    def target_branch(target_branch)
      @cmd += ['--target_branch', target_branch]
      self
    end

    def announce_preview(preview_location)
      @cmd += ['--announce_preview', preview_location]
      self
    end

    def skip_link_check
      @cmd += ['--skiplinkcheck']
      self
    end

    def keep_hash
      @cmd += ['--keep_hash']
      self
    end

    def sub_dir(repo, branch)
      @cmd += ['--sub_dir', "#{repo.name}:#{branch}:#{repo.root}"]
      self
    end

    def uses_preview
      false
    end
  end
end
