module Terraspace::Terraform::RemoteState
  class Fetcher
    extend Memoist
    include Terraspace::Compiler::CommandsConcern
    include Terraspace::Compiler::DirsConcern
    include Terraspace::Util::Logging

    def initialize(parent, identifier, options={})
      @parent, @identifier, @options = parent, identifier, options
      child_name, @output_key = identifier.split('.')
      @child = Terraspace::Mod.new(child_name)
      @child.resolved = @parent.resolved
    end

    def run
      validate! # check child stack exists
      pull
      load
    end

    # Returns OutputProxy
    def output
      run
      if pull_success?
        pull_success_output
      else
        @error_type ||= :state_not_found # could be set to :bucket_not_found by bucket_not_found_error
        error = output_error(@error_type)
        OutputProxy.new(@child, nil, @options.merge(error: error))
      end
    end

    def pull_success_output
      if @outputs.key?(@output_key)
        OutputProxy.new(@child, output_value, @options)
      else
        error = output_error(:key_not_found)
        OutputProxy.new(@child, nil, @options.merge(error: error))
      end
    end

    def output_value
      return unless @outputs.key?(@output_key)
      result = @outputs.dig(@output_key)
      result.dig("value") if result
    end

    def output_error(type)
      msg = case type
      when :key_not_found
        "Output #{@output_key} was not found for the #{@parent.name} tfvars file. Either #{@child.name} stack has not been deployed yet or it does not have this output: #{@output_key}. Also, if local backend is being used and has been removed/cleaned, then it will also result zero-byte state.json with the 'terraform state pull' used to download the terraform state and output will not be found."
      when :state_not_found
        "Output #{@output_key} could not be looked up for the #{@parent.name} tfvars file. #{@child.name} stack needs to be deployed"
      when :bucket_not_found
        "The bucket for the backend could not be found"
      end
      msg = "(#{msg})"
      log_message(msg)
      msg
    end

    @@pull_successes = {}
    @@download_shown = false
    def pull
      return if @@pull_successes[cache_key]
      logger.debug "Downloading tfstate files for dependencies defined in tfvars..." unless @@download_shown || @options[:quiet]
      @@download_shown = true
      logger.debug "Downloading tfstate for stack: #{@child.name}"

      success = init # init not yet run. only run .init directly, not .run. init can completely error and early exit.
      return unless success

      FileUtils.mkdir_p(File.dirname(state_path))
      command = "cd #{@child.cache_dir} && #{Terraspace.terraform_bin} state pull > #{state_path}"
      logger.debug "=> #{command}"
      success = system(command)
      # Can error if using a old terraform version and the statefile was created with a newer version of terraform
      # IE: Failed to refresh state: state snapshot was created by Terraform v0.13.2, which is newer than current v0.12.29;
      #     upgrade to Terraform v0.13.2 or greater to work with this state
      unless success
        logger.info "Error running: #{command}".color(:red)
        logger.info "Please fix the error before continuing"
      end
      @@pull_successes[cache_key] = success
    end

    def init
      Terraspace::CLI::Init.new(mod: @child.name, quiet: true, suppress_error_color: true).init
      true
    rescue Terraspace::BucketNotFoundError # from Terraspace::Shell
      bucket_not_found_error
      false
    end

    # mimic pull error
    def bucket_not_found_error
      @@pull_successes[cache_key] = false
      @error_type = :bucket_not_found
    end

    def load
      return self unless pull_success?

      # use or set cache
      if @@cache[cache_key]
        @outputs = @@cache[cache_key]
      else
        @outputs = @@cache[cache_key] = read_statefile_outputs
      end

      self
    end
    memoize :load

    def cache_key
      @child.name
    end

    def read_statefile_outputs
      data = JSON.load(IO.read(state_path))
      data ? data['outputs'] : {}
    end

    def pull_success?
      @@pull_successes[cache_key]
    end

    def state_path
      "#{Terraspace.tmp_root}/remote_state/#{Process.pid}/#{@child.build_dir}/state.json"
    end

    # Note we already validate mod exist at the terraform_output helper. This is just in case that logic changes.
    def validate!
      unless @child.exist?
        logger.error "ERROR: stack #{@child.name} not found".color(:red)
        exit 1
      end
      select = Terraspace::Compiler::Select.new(@child.name)
      unless select.selected?
        logger.error "ERROR: stack #{@child.name} is configured to not be included. IE: config.all.include_stacks or config.all.exclude_stacks".color(:red)
        exit 1
      end
    end

    # Using debug level because all the tfvar files always get evaluated.
    # So dont want these messages to show up and be noisy unless debugging.
    def log_message(msg)
      logger.debug "DEBUG: #{msg}".color(:yellow)
    end

    cattr_accessor :cache, default: {}
    class << self
      def flush!
        @@pull_successes = {}
        @@cache = {}
      end
    end
  end
end
