require "fileutils"
require "ipaddr"
require "json"
require "net/http"
require "pathname"
require "uri"

require "cc/resolv"

module CC
  module CLI
    class Prepare < Command
      ARGUMENT_LIST = "[--allow-internal-ips]".freeze
      SHORT_HELP = "Run the commands in your prepare step.".freeze
      HELP = "#{SHORT_HELP}\n" \
        "\n" \
        "    --allow-internal-ips    Allow fetching from internal IPs.".freeze

      InternalHostError = Class.new(StandardError)
      FetchError = Class.new(StandardError)

      PRIVATE_ADDRESS_SUBNETS = [
        IPAddr.new("10.0.0.0/8"),
        IPAddr.new("172.16.0.0/12"),
        IPAddr.new("192.168.0.0/16"),
        IPAddr.new("fd00::/8"),
        IPAddr.new("127.0.0.1"),
        IPAddr.new("0:0:0:0:0:0:0:1"),
      ].freeze

      def run
        ::CC::Resolv.with_fixed_dns { fetch_all }
      rescue FetchError, InternalHostError => ex
        fatal(ex.message)
      end

      private

      def allow_internal_ips?
        @args.include?("--allow-internal-ips")
      end

      def fetches
        @fetches ||= config.prepare.fetch
      end

      def config
        @config ||= CC::Config.load
      end

      def fetch_all
        fetches.each do |entry|
          fetch(entry.url, entry.path)
        end
      end

      def fetch(url, target_path)
        ensure_external!(url) unless allow_internal_ips?

        uri = URI.parse(url)
        http = Net::HTTP.new(uri.host, uri.port)
        http.use_ssl = uri.scheme == "https"
        resp = http.get(uri)
        if resp.code == "200"
          write_file(target_path, resp.body)
          say("Wrote #{url} to #{target_path}")
        else
          raise FetchError, "Failed fetching #{url}: code=#{resp.code} body=#{resp.body}"
        end
      end

      def write_file(target_path, content)
        FileUtils.mkdir_p(Pathname.new(target_path).parent.to_s)
        File.write(target_path, content)
      end

      def ensure_external!(url)
        uri = URI.parse(url)

        if internal?(uri.host)
          raise InternalHostError, "Won't fetch #{url.inspect}: it maps to an internal address"
        end
      end

      # rubocop:disable Style/CaseEquality
      def internal?(host)
        address = ::Resolv.getaddress(host)

        PRIVATE_ADDRESS_SUBNETS.any? do |subnet|
          subnet === IPAddr.new(address.to_s)
        end
      rescue ::Resolv::ResolvError
        true # localhost
      end
      # rubocop:enable Style/CaseEquality
    end
  end
end
