require_relative 'common'
require_relative 'constants'

require 'digest'
require 'fileutils'
require 'json'
require 'net/http'
require 'openssl'
require 'shellwords'
require 'socket'
require 'timeout'
require 'tmpdir'
require 'uri'

module Utils

  # General

  def self.wait(cond = nil, timeout: 30, sleep_interval: 3)
    return Kernel.sleep(cond) if cond.is_a?(Integer)
    return Timeout.timeout(timeout) { yield } if block_given?
    return Kernel.sleep(timeout) if cond.nil?

    begin Timeout.timeout(timeout) do
        begin uri = URI.parse(cond.to_s); rescue; end
        loop do Logs.info("[wait] #{cond}")
          if uri.is_a?(URI::HTTP)
            http = Net::HTTP.new(uri.host, uri.port)
            http.use_ssl = (uri.scheme == 'https')
            http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl?
            begin break true if http.get(uri.path.empty? ? '/' : uri.path).code.to_i < 500
            rescue; end
          else
            host, port = cond.split(':', 2); port = (port || '80').to_i
            begin Socket.tcp(host, port, connect_timeout: sleep_interval) { |sock| sock.close }; break true
            rescue; end
          end
          sleep sleep_interval
        end
    end; rescue Timeout::Error; return false; end end

  # System

  def self.arch
    { 'x86_64'=>'amd64', 'aarch64'=>'arm64', 'arm64'=>'arm64', 'armv7l'=>'armv7' }.fetch(`uname -m`.strip, 'amd64')
  end

  def self.mapping(file)
    JSON.parse(File.read(File.expand_path(file, ENV['PWD'] || Dir.pwd)))
      .each_with_object({}) { |(k, v), hash| hash[k] = v.presence unless v.nil? || (v.respond_to?(:empty?) && v.empty?) }
  rescue Exception => e
    Logs.return("#{file}: #{e.message}", {}, level: :warn )
  end

  def self.snapshot(ctx, data_dir, name: ctx.cookbook_name, restore: false, user: Default.user, group: Default.group, snapshot_dir: Default.snapshot_dir, mode: 0o755)

    snapshot_dir = "#{snapshot_dir}/#{name}"
    snapshot = File.join(snapshot_dir, "#{name}-#{Time.now.strftime('%y%m%d-%H%M')}.tar.gz")

    md5_dir = ->(path) {
      entries = Dir.glob("#{path}/**/*", File::FNM_DOTMATCH)
      files = entries.reject { |f| File.directory?(f) || File.symlink?(f) || ['.', '..'].include?(File.basename(f)) || File.basename(f).start_with?('._') }
      Digest::MD5.new.tap { |md5| files.sort.each { |f| File.open(f, 'rb') { |io| md5.update(io.read) } } }.hexdigest  }

    verify = ->(archive, compare_dir) {
      Dir.mktmpdir do |tmp|
        Logs.try!("snapshot extraction", raise: true) do
          system("tar -xzf #{Shellwords.escape(archive)} -C #{Shellwords.escape(tmp)}") or raise("snapshot verification failed")
        end
        raise("verify snapshot failed") unless md5_dir.(tmp) == (Dir.exist?(compare_dir) ? md5_dir.(compare_dir) : '')
      end; true }

    if restore
      latest = Dir[File.join(snapshot_dir, "#{name}-*.tar.gz")].sort.reverse.first
      if latest && ::File.exist?(latest)
        FileUtils.rm_rf(data_dir)
        FileUtils.mkdir_p(data_dir)
        Logs.try!("snapshot restore", raise: true) do
          system("tar -xzf #{Shellwords.escape(latest)} -C #{Shellwords.escape(data_dir)}") or raise("tar extract failed")
        end
        FileUtils.chown_R(user, group, data_dir)
        FileUtils.chmod_R(mode, data_dir)
      end
      return true
    end
    return true unless Dir.exist?(data_dir) && !Dir.glob("#{data_dir}/*").empty? # true to be idempotent integrable before installation

    FileUtils.mkdir_p(File.dirname(snapshot))
    Logs.try!("snapshot creation", raise: true) do
      system("tar -czf #{Shellwords.escape(snapshot)} -C #{Shellwords.escape(data_dir)} .") or raise("tar compress failed")
    end

    return verify.(snapshot, data_dir)
  end

  # Remote

  def self.request(uri, method: Net::HTTP::Get, body: nil, headers: {}, user: nil, pass: nil, log: nil, expect: false, raise: true, sensitive: false, verify: OpenSSL::SSL::VERIFY_NONE)
    begin
      request = method.new(uri = URI(uri)); headers.each { |k, v| request[k] = v }
      request.basic_auth(user, pass) if user && pass
      if body and body.is_a?(Hash)
        Constants::HEADER_JSON.each { |k, v| request[k] = v }
        body = body.try(:json).or(body)
      end
      request.body = body

      response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', verify_mode: verify ) { |http| http.request(request) }
      if response.is_a?(Net::HTTPRedirection) && (location = response['location'])
        response = request(location.start_with?('/') ? "#{uri.scheme}://#{uri.host}#{location}" : URI.join("#{uri.scheme}://#{uri.host}#{uri.path}", location).to_s,
          user: user, pass: pass, headers: headers, method: method, body: body, expect: expect, log: log)
      end

      Logs.info(message = "#{method} [#{uri}] #{response&.code} #{response&.message} #{'(' + (sensitive ? body.try(:mask) : body) + ')' if body}") if log
      Logs.debug((sensitive ? response&.body.try(:mask) : response&.body), level: :debug) if log
      if expect
        result = (expect == true ? response.is_a?(Net::HTTPSuccess) : expect.include?(response.code.to_i))
        (raise and not result) ? raise(message) : result
      else return response end
    rescue Exception => e
      message = "[#{uri}] #{e.message}"; raise ? raise(message) : Logs.error(message)
    end
  end

  def self.proxmox(ctx, path)
    host    ||= Env.get_variable(ctx, 'proxmox_host')
    user    ||= Env.get_variable(ctx, 'proxmox_user')
    pass    ||= Env.get_variable(ctx, 'proxmox_password')
    token   ||= Env.get_variable(ctx, 'proxmox_token')
    secret  ||= Env.get_variable(ctx, 'proxmox_secret')
    node    ||= Env.get_variable(ctx, 'proxmox_node')

    if pass && !pass.empty?
      response = request(uri="https://#{host}:8006/api2/json/access/ticket", log: "Proxmox: Ticket", method: Net::HTTP::Post,
        body: URI.encode_www_form(username: user, password: pass), headers: Constants::HEADER_FORM, sensitive: true)
      headers = { 'Cookie' => "PVEAuthCookie=#{response.json['data']['ticket']}", 'CSRFPreventionToken' => response.json['data']['CSRFPreventionToken'] }
    else
      headers = { 'Authorization' => "PVEAPIToken=#{user}!#{token}=#{secret}" }
    end
    request("https://#{host}:8006/api2/json/nodes/#{node}/#{path}", headers: headers).json['data']
  end

  def self.install(ctx, owner:, repo:, app_dir:, name: nil, version: 'latest', user: Default.user, group: Default.group, extract: true)
    version_file = File.join(app_dir, '.version')
    version_installed = ::File.exist?(version_file) ? ::File.read(version_file).strip : nil
    release = nil

    FileUtils.mkdir_p(app_dir)
    assets = ->(a) { a[:name].match?(/linux[-_]#{Utils.arch}/i) && !a[:name].end_with?('.asc', '.sha265', '.pem') }

    if version == 'latest'
      release = Logs.blank!("check latest", latest(owner, repo))
      release = release.first if release.is_a?(Array)
      return false unless release
      version = release[:tag_name].to_s.gsub(/^v/, '')
    end

    if ( installation = ( if version_installed.nil?
        Logs.true("initial installation '#{version}'")
      elsif Gem::Version.new(version) > Gem::Version.new(version_installed)
        Logs.true("update from '#{version_installed}' to '#{version}'")
      else
        Logs.false("no update required '#{version_installed}' to '#{version}'")
    end ) )

      release = request(Constants::URI_GITHUB_TAG.call(owner, repo, version),
        headers: { 'Accept' => 'application/vnd.github+json' }, expect: true).json unless release
      download_url, filename = (if (asset = release[:assets].find(&assets))
        [asset[:browser_download_url], File.basename(URI.parse(asset[:browser_download_url]).path)]
      else [release[:tarball_url], "#{repo}-#{version}.tar.gz"] end)

      Dir.mktmpdir do |tmpdir|
        path = File.join(tmpdir, filename)
        Utils.download(ctx, path, download_url)

        if extract && path.end_with?('.tar.gz', '.tgz', '.zip')
          (system("tar -xzf #{Shellwords.escape(path)} --strip-components=1 -C #{Shellwords.escape(app_dir)}") or
            raise "tar extract failed for #{path}") if extract
        else # Binary
          FileUtils.mv(path, File.join(app_dir, name || repo))
        end
      end
    end

    FileUtils.chown_R(user, group, app_dir)
    FileUtils.chmod_R(0755, app_dir)

    Ctx.dsl(ctx).file version_file do
      content version.to_s
      owner Default.user
      group Default.group
      mode '0755'
      action :create
    end
    return version

  end

  def self.download(ctx, path, url, owner: Default.user, group: Default.group, mode: '0755', action: :create)
    Common.directories(Ctx.dsl(ctx), File.dirname(path), owner: owner, group: group, mode: mode)
    Ctx.dsl(ctx).remote_file path do
      source url.respond_to?(:call)? lazy { url.call } : url
      owner  owner
      group  group
      mode   mode
      action action
    end.run_action(action)
  end

  def self.latest(owner, repo)
    api_url = Constants::URI_GITHUB_LATEST.call(owner, repo)
    response = request(api_url, headers: { 'Accept' => 'application/vnd.github+json' })
    return false unless response.is_a?(Net::HTTPSuccess)
    response.json(symbolize_names: true)
  end

end